Repository: angular
Issue: require explicit toSignal new param in order to reduce risk of env injectors' destroyref resulting in no cleanup (more strict toSignal API)
Labels: area: core,core: reactivity,cross-cutting: signals

### Which @angular/* package(s) are relevant/related to the feature request?

core

### Description

It's more of a **CHANGE request**.

Currently when using `toSignal` passing the `injector` via options it **OPTIONAL**. On one hand it makes sense, though in practice I see a huge risk of developers accidentally introducing too many immediate subscriptions bound to Env Injectors which will never get cleaned up:

```ts
toSignal(myObservable$)
```
Now if the dev doesn't know all the quirks around injector hierarchy, they might not know that e.g. a service provided-in-root might get env injector which doesn't trigger `DestroyRef`. In fact, moving that code (since it depends on `inject`/`DestroyRef`) changes the semantics significantly!

Relates to #51290.



### Proposed solution

## Usage

```ts
// within a component/directive OR service provided in component/directive
const signal = toSignal(observable$) // ✅ no runtime error
// no additional params needed
// runtime check makes sure that a NodeInjector spits out a NodeInjectorDestroyRef

// within a global service
const signal = toSignal(observable$) // ❌ safety error: Env Injector was used when only node injectors are allowed (whatever message...)
// expected: toSignal(observable$, { onlyInNodeInjectors: false })

// within a global service
const signal = toSignal(observable$, { onlyInNodeInjectors: false }) // ✅ no runtime error
// the developer is doing it ON THEIR OWN RISK
```

## How

This _proposal_ aims to make potentially unsafe usage explicit (like with requireSync default is `false`). I.e. revert the current API (thankfully, toSignal is still within `developerPreview`) whenever **ENV injector** would get passed: from "_implicit env injector_" into "_explicit env injector_" (doesn't matter if node injector gets used).

Slightly change the toSignal function signature and/or overloads.

Current options:
```ts
export interface ToSignalOptions {
  initialValue?: unknown;
  requireSync?: boolean;
  injector?: Injector;
  manualCleanup?: boolean;
  rejectErrors?: boolean;
}
```

Note that specific `onlyInNodeInjectors` name of the option is less relevant at the moment... 😉

Proposed:
```ts
export interface ToSignalOptions {
  ...
  onlyInNodeInjectors?: boolean // default true
}
```

Runtime check:
- angular has internal access to all classes that represent injectors (env, node) or destroy refs (node)
- depending on what is simpler (checking the injectors OR destroyRefs - ultimately it's all about destroyRefs anyway), use the `instanceOf` operator to find out whether the allowed

```ts
// either injectors:
- class NodeInjector
- abstract class EnvironmentInjector / class R3Injector extends EnvironmentInjector

// or:
- class NodeInjectorDestroyRef
```

Mechanics:
- in compile-time we don't know whether the injector is going to be **node** or **env** one
- the default (true) allows to only use node injectors, and since node injectors get removed along with components, the risk of memory leaks drops significantly
- if devs want to go with env injectors, they shall do that explicitly.



### Alternatives considered

# 1. enum

change `manualCleanup`'s boolean with an `cleanup` **enum** with:
- `NODE_ONLY` (default) -> destroyRef can be taken ONLY from node injectors
- `MANUAL_CLEANUP` -> no destroyRef
- `ALL_INJECTORS` -> (unrecommended) although both node and env injectors might be used to fetch destroyRef

again, better names could be provided.

# 2. toSignal overloads

update the overloads:

```ts
// Base case: no options -> `undefined` in the result type.
export function toSignal<T>(source: Observable<T>|Subscribable<T>): Signal<T|undefined>;
// Options with `undefined` initial value and no `requiredSync` -> `undefined`.
export function toSignal<T>(
    source: Observable<T>|Subscribable<T>,
    options: ToSignalOptions&{initialValue?: undefined, requireSync?: false}): Signal<T|undefined>;
// Options with `null` initial value -> `null`.
export function toSignal<T>(
    source: Observable<T>|Subscribable<T>,
    options: ToSignalOptions&{initialValue?: null, requireSync?: false}): Signal<T|null>;
// Options with `undefined` initial value and `requiredSync` -> strict result type.
export function toSignal<T>(
    source: Observable<T>|Subscribable<T>,
    options: ToSignalOptions&{initialValue?: undefined, requireSync: true}): Signal<T>;
// Options with a more specific initial value type.
export function toSignal<T, const U extends T>(
    source: Observable<T>|Subscribable<T>,
    options: ToSignalOptions&{initialValue: U, requireSync?: false}): Signal<T|U>;
```

to enforce passing the injector explicitly. But **I don't like this idea** since it 1. worsens the DX and 2. doesn't guarantee that people would not just simply `injector = inject(Injector)` and YOLO, which would NOT remove the issue in the first place.