Implement \`toggleWatchSignal(id)\` in \`signal_debug.ts\` to enable toggling reactive watch listeners on individual signal graph nodes.
- Create reactive \`Watch\` instances using \`createWatch\` to log signal value/state changes to the console when active.
- Use \`WeakRef\` mapping and \`FinalizationRegistry\` (\`watchCleanupRegistry\`) so active watches do not retain strong references to signal nodes or prevent garbage collection.
- Expose \`watched\` status on \`DebugSignalGraphNode\` and publish \`toggleWatchSignal\` onto \`window.ng\` in development mode.
- Add acceptance tests covering watch activation, signal mutation logging, manual disposal, GC cleanup, and safe invalid ID handling.
This commit addresses a scaling issue in the signal dependency graph where
the detection of duplicate dependency links would perform a linear scan across
all consumer links of all producers. The linear scan is replaced with a version
comparison of the dependency edge against the current epoch; if they are
equal the existing dependency edge is known to be valid in this epoch. This means
that the link won't be eligible for removal and therefore doesn't have to be
recreated.
When `producerAccessed` creates a new link for a non-live consumer (e.g.
a computed signal with no readers), it eagerly sets `prevConsumer` to the
producer's current `consumersTail`. However, because the consumer is not
live, `producerAddLiveConsumer` is skipped and the link is never inserted
into the producer's consumer doubly-linked list.
This means the link holds a reference *into* the producer's consumer list
without being *part* of it. When the node that `prevConsumer` points to is
later removed via `producerRemoveLiveConsumerLink`, the dangling link is
not patched because it isn't traversable from the list.
The result is that the removed consumer link — and everything it
references — is kept alive by the dangling `prevConsumer` pointer on the
non-live link, which itself is kept alive through the computed signal's
`producers` linked list.
In practice this causes multi-MB memory leaks in Angular apps: a
root-provided service with a computed signal (e.g. `AttachmentApiService.urls`)
holds a producer link to `ApplicationEnvironmentService.environmentSignal`.
That link's `prevConsumer` captures a stale reference to a destroyed view's
`ReactiveLViewConsumer` link, retaining the entire LView hierarchy —
components, QueryLists, ElementRefs, and detached DOM — after the view is
destroyed.
The fix initializes `prevConsumer` to `undefined` at link creation time.
This is safe because `producerAddLiveConsumer` unconditionally sets
`link.prevConsumer = consumersTail` (line 513) when the link is actually
inserted into the consumer list. The value set in `producerAccessed` was
always overwritten for live consumers, and was never correct for non-live
consumers.
Made-with: Cursor
PR Close#68681
These type annotations allow TS to associate the object's properties
with their corresponding declaration in the interfaces, enabling
much better code navigation. For example, "Find all implementations"
for `ReactiveNode.producerRecomputeValue` now finds the implementation
in `COMPUTED_NODE` and `LINKED_SIGNAL_NODE`.
This commit ports the changes in #55818 from `computed` to `linkedSignal`,
which duplicates the core logic to recompute the downstream value for an
upstream change.
Explicitly adding an `export {}` to modules containing `declare global` fixes an issue where Rollup would incorrectly claim that the `global` variable is not defined in the emitted `.d.ts` files.
Needed to land the latest `rules_angular`.
The `toString()` implementations in the primitives package intended to include
the debug name, yet the debug name was evaluated during construction before it
could ever have been assigned. This commit fixes that.
The Angular wrappers override the `toString()` representation to evaluate signals
ad-hoc instead of showing their internal state, and this commit aligns their
behavior to include the debug name in `toString` as well.
Unlike a normal `signal()`, a `linkedSignal()` can be in an error state when
its computation fails. Currently, there's a bug where `linkedSignal.update`
does not account for this error state, and will pass the internal `ERRORED`
`Symbol` as the current value to the updater function.
This commit fixes the issue by having `update()` check and throw the error
instead of calling the updater function.
If the computed's `toString` is called and `node.value` is a Symbol, the browser will throw an exception `ERROR TypeError: Cannot convert a Symbol value to a string`
Symbols cannot be implicitly changed to strings. This change changes the conversion to be explicit by wrapping `node.value` with `String()`
This can be reproduced if you create a computed with `createComputed(computation, equal)` and call `toString()` while `node.value` is something like `Symbol(UNSET)`
If the computed's `toString` is called and `node.value` is a Symbol, the browser will throw an exception `ERROR TypeError: Cannot convert a Symbol value to a string`
Symbols cannot be implicitly changed to strings. This change changes the conversion to be explicit by wrapping `node.value` with `String()`
This can be reproduced if you create a computed with `createComputed(computation, equal)` and call `toString()` while `node.value` is something like `Symbol(UNSET)`
The flag `skipFormatting` got renamed to `ngSkipFormatting` during review of https://github.com/angular/angular/pull/64000, but a couple usages got missed, causing some unfortunate UI recursion.
This makes it possible to batch effects, where we can "reopen" consumers
during initial render and then finalize them after we are finally done
adding all the effects to a batch:
```
function createBatch() {
const effect = // ... create effect node
resetConsumerBeforeComputation(effect);
return effect;
}
// pseudo-code
function appendEffect(effectBatch, updater) {
if (value is a signal) {
const prevConsumer = setActiveConsumer(effectBatch.node);
const output = value();
setActiveConsumer(prevConsumer);
effectBatch.push({ signal, updater });
return output;
}
}
function finalizeBatch(effectBatch) {
if (effectBatch.length > 0) {
finalizeConsumerAfterComputation(effectBatch.node);
}
}
const effectBatch = createBatchEffectNode();
appendEffect(signal1, (newValue) => /* something */);
appendEffect(signal2, (newValue) => /* something different */);
finalizeBatch(effectBatch);
```
PR Close#62549
Refactor createSignal to return a tuple instead of a signal getter. createSignalTuple will be removed in a follow up pr once createSignalTuple usages in google3 are migrated to createSignal.
PR Close#61705
As part of the Bazel toolchain migration we noticed that implicit types
generated by the TypeScript compiler sometimes end up referencing types
from other packages (i.e. cross-package imports).
These imports currently work just because the Bazel `ts_library` and
`ng_module` rules automatically inserted a `<amd-module
name="@angular/x" />` into `.d.ts` of packages. This helped TS figure
out how to import a given file. Notably this is custom logic that is not
occuring in vanilla TS or Angular compilations—so we will drop this
magic as part of the toolchain cleanup!
To improve code quality and keep the existing behavior working, we are
doing the following:
- adding a lint rule that reduces the risk of such imports breaking. The
failure scenario without the rule is that API goldens show unexpected
diffs, and types might be duplicated in a different package!
- keeping the `<amd-module` headers, but we manually insert them into
the package entry-points. This should ensure we don't regress
anywhere; while we also improved general safety around this above.
Long-term, isolated declarations or a lint rule from eslint-typescript
can make this even more robust.
PR Close#61312
Add createSignalTuple function to match Wiz array destructuring signal return. This will be the implementation for createSignal once createSignal usages in google3 are migrated to createSignalTuple.
PR Close#60903
This updates `createSignal` to use `signalGetFn` to define the signal getter and exports `signalGetFn` from the shared signal primitives.
PR Close#60497
This change pushes the toString implementation of signal getters
down to the primitives package so it can be shared with other
frameworks.
Closes#59990
PR Close#60365
Adds a hook in the same style as `postSignalSetFn` for running side effects when a producer has been created. This hook will be passed the reactive node being created.
PR Close#60333
This change moves more logic to the primitives package by pushing
the equal configuration on a reactive node to the signal and
computed creation utilities.
PR Close#60300
Updates the signature of the `throwInvalidWriteToSignalError` to take the signal node in question and pass it along to the throwInvalidWriteToSignalErrorFn handler function. This allows the handler to e.g. include the signal name in error messaging.
PR Close#59600
Prevent leaking signal reads and exceptions from a custom `equal`
function of a producer `computed()` to a consumer.
Upstream https://github.com/tc39/proposal-signals/pull/90 with a notable
change: Angular does **not** track reactive reads in custom `equal`
implementations.
PR Close#55818
This commit exposes the new producerMarkClean utility that
makes it possible to mark a reactive node as clean following
changes to the node's value. In practice it resets the dirty
flag and all other internal flags as need. Prior to this change
the cleanup code was duplicated.
The new utility will be used in the linkedSignal implementation.
PR Close#58228
Export `producerIncrementEpoch` which was missing before, as well as a new
`runPostSignalSetFn` helper. These changes make it easier to write `signal`-
like utilities which don't use the `createSignal` abstraction.
PR Close#56759