@tstdl/base 0.87.13 → 0.87.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.87.13",
3
+ "version": "0.87.15",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -5,13 +5,12 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.io/license
7
7
  */
8
- import type { ReactiveNode } from './graph.js';
9
8
  /**
10
9
  * Symbol used to tell `Signal`s apart from other functions.
11
10
  *
12
11
  * This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values.
13
12
  */
14
- declare const SIGNAL: unique symbol;
13
+ export declare const SIGNAL: unique symbol;
15
14
  /**
16
15
  * A reactive value which notifies consumers of any changes.
17
16
  *
@@ -28,21 +27,6 @@ export interface Signal<T> {
28
27
  * Checks if the given `value` is a reactive `Signal`.
29
28
  */
30
29
  export declare function isSignal(value: unknown): value is Signal<unknown>;
31
- /**
32
- * Converts `fn` into a marked signal function (where `isSignal(fn)` will be `true`).
33
- *
34
- * @param fn A zero-argument function which will be converted into a `Signal`.
35
- */
36
- export declare function createSignalFromFunction<T>(node: ReactiveNode, fn: () => T): Signal<T>;
37
- /**
38
- * Converts `fn` into a marked signal function (where `isSignal(fn)` will be `true`), and
39
- * potentially add some set of extra properties (passed as an object record `extraApi`).
40
- *
41
- * @param fn A zero-argument function which will be converted into a `Signal`.
42
- * @param extraApi An object whose properties will be copied onto `fn` in order to create a specific
43
- * desired interface for the `Signal`.
44
- */
45
- export declare function createSignalFromFunction<T, U extends Record<string, unknown>>(node: ReactiveNode, fn: () => T, extraApi: U): Signal<T> & U;
46
30
  /**
47
31
  * A comparison function which can determine if two values are equal.
48
32
  */
@@ -55,4 +39,3 @@ export type ValueEqualityFn<T> = (a: T, b: T) => boolean;
55
39
  * propagate change notification upon explicit mutation without identity change.
56
40
  */
57
41
  export declare function defaultEquals<T>(a: T, b: T): boolean;
58
- export {};
@@ -18,7 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var api_exports = {};
20
20
  __export(api_exports, {
21
- createSignalFromFunction: () => createSignalFromFunction,
21
+ SIGNAL: () => SIGNAL,
22
22
  defaultEquals: () => defaultEquals,
23
23
  isSignal: () => isSignal
24
24
  });
@@ -34,10 +34,6 @@ const SIGNAL = Symbol("SIGNAL");
34
34
  function isSignal(value) {
35
35
  return typeof value === "function" && value[SIGNAL] !== void 0;
36
36
  }
37
- function createSignalFromFunction(node, fn, extraApi = {}) {
38
- fn[SIGNAL] = node;
39
- return Object.assign(fn, extraApi);
40
- }
41
37
  function defaultEquals(a, b) {
42
38
  return (a === null || typeof a !== "object") && Object.is(a, b);
43
39
  }
@@ -31,88 +31,53 @@ var import_graph = require("./graph.js");
31
31
  * found in the LICENSE file at https://angular.io/license
32
32
  */
33
33
  function computed(computation, options) {
34
- const node = new ComputedImpl(computation, options?.equal ?? import_api.defaultEquals);
35
- return (0, import_api.createSignalFromFunction)(node, node.signal.bind(node));
34
+ const node = Object.create(COMPUTED_NODE);
35
+ node.computation = computation;
36
+ options?.equal && (node.equal = options.equal);
37
+ const computed2 = () => {
38
+ (0, import_graph.producerUpdateValueVersion)(node);
39
+ (0, import_graph.producerAccessed)(node);
40
+ if (node.value === ERRORED) {
41
+ throw node.error;
42
+ }
43
+ return node.value;
44
+ };
45
+ computed2[import_api.SIGNAL] = node;
46
+ return computed2;
36
47
  }
37
48
  const UNSET = Symbol("UNSET");
38
49
  const COMPUTING = Symbol("COMPUTING");
39
50
  const ERRORED = Symbol("ERRORED");
40
- class ComputedImpl extends import_graph.ReactiveNode {
41
- computation;
42
- equal;
43
- constructor(computation, equal) {
44
- super();
45
- this.computation = computation;
46
- this.equal = equal;
47
- }
48
- /**
49
- * Current value of the computation.
50
- *
51
- * This can also be one of the special values `UNSET`, `COMPUTING`, or `ERRORED`.
52
- */
53
- value = UNSET;
54
- /**
55
- * If `value` is `ERRORED`, the error caught from the last computation attempt which will
56
- * be re-thrown.
57
- */
58
- error = null;
59
- /**
60
- * Flag indicating that the computation is currently stale, meaning that one of the
61
- * dependencies has notified of a potential change.
62
- *
63
- * It's possible that no dependency has _actually_ changed, in which case the `stale`
64
- * state can be resolved without recomputing the value.
65
- */
66
- stale = true;
67
- consumerAllowSignalWrites = false;
68
- onConsumerDependencyMayHaveChanged() {
69
- if (this.stale) {
70
- return;
71
- }
72
- this.stale = true;
73
- this.producerMayHaveChanged();
74
- }
75
- onProducerUpdateValueVersion() {
76
- if (!this.stale) {
77
- return;
78
- }
79
- if (this.value !== UNSET && this.value !== COMPUTING && !this.consumerPollProducersForChange()) {
80
- this.stale = false;
81
- return;
82
- }
83
- this.recomputeValue();
84
- }
85
- recomputeValue() {
86
- if (this.value === COMPUTING) {
51
+ const COMPUTED_NODE = {
52
+ ...import_graph.REACTIVE_NODE,
53
+ value: UNSET,
54
+ dirty: true,
55
+ error: null,
56
+ equal: import_api.defaultEquals,
57
+ producerMustRecompute(node) {
58
+ return node.value === UNSET || node.value === COMPUTING;
59
+ },
60
+ producerRecomputeValue(node) {
61
+ if (node.value === COMPUTING) {
87
62
  throw new Error("Detected cycle in computations.");
88
63
  }
89
- const oldValue = this.value;
90
- this.value = COMPUTING;
91
- this.trackingVersion++;
92
- const prevConsumer = (0, import_graph.setActiveConsumer)(this);
64
+ const oldValue = node.value;
65
+ node.value = COMPUTING;
66
+ const prevConsumer = (0, import_graph.consumerBeforeComputation)(node);
93
67
  let newValue;
94
68
  try {
95
- newValue = this.computation();
69
+ newValue = node.computation();
96
70
  } catch (err) {
97
71
  newValue = ERRORED;
98
- this.error = err;
72
+ node.error = err;
99
73
  } finally {
100
- (0, import_graph.setActiveConsumer)(prevConsumer);
74
+ (0, import_graph.consumerAfterComputation)(node, prevConsumer);
101
75
  }
102
- this.stale = false;
103
- if (oldValue !== UNSET && oldValue !== ERRORED && newValue !== ERRORED && this.equal(oldValue, newValue)) {
104
- this.value = oldValue;
76
+ if (oldValue !== UNSET && oldValue !== ERRORED && newValue !== ERRORED && node.equal(oldValue, newValue)) {
77
+ node.value = oldValue;
105
78
  return;
106
79
  }
107
- this.value = newValue;
108
- this.valueVersion++;
80
+ node.value = newValue;
81
+ node.version++;
109
82
  }
110
- signal() {
111
- this.onProducerUpdateValueVersion();
112
- this.producerAccessed();
113
- if (this.value === ERRORED) {
114
- throw this.error;
115
- }
116
- return this.value;
117
- }
118
- }
83
+ };
@@ -22,7 +22,7 @@ export declare class EffectManager {
22
22
  private readonly all;
23
23
  private readonly queue;
24
24
  private pendingFlush;
25
- create(effectFn: (onCleanup: (cleanupFn: EffectCleanupFn) => void) => void, { allowSignalWrites }?: CreateEffectOptions): EffectRef;
25
+ create(effectFn: (onCleanup: (cleanupFn: EffectCleanupFn) => void) => void, allowSignalWrites: boolean): EffectRef;
26
26
  flush(): void;
27
27
  get isQueueEmpty(): boolean;
28
28
  }
@@ -34,12 +34,12 @@ class EffectManager {
34
34
  all = /* @__PURE__ */ new Set();
35
35
  queue = /* @__PURE__ */ new Set();
36
36
  pendingFlush;
37
- create(effectFn, { allowSignalWrites = false } = {}) {
38
- const watch = new import_watch.Watch(effectFn, (watch2) => {
39
- if (!this.all.has(watch2)) {
37
+ create(effectFn, allowSignalWrites) {
38
+ const w = (0, import_watch.watch)(effectFn, () => {
39
+ if (!this.all.has(w)) {
40
40
  return;
41
41
  }
42
- this.queue.add(watch2);
42
+ this.queue.add(w);
43
43
  if (!this.pendingFlush) {
44
44
  this.pendingFlush = true;
45
45
  queueMicrotask(() => {
@@ -48,24 +48,24 @@ class EffectManager {
48
48
  });
49
49
  }
50
50
  }, allowSignalWrites);
51
- this.all.add(watch);
52
- watch.notify();
51
+ this.all.add(w);
52
+ w.notify();
53
53
  const destroy = () => {
54
- watch.cleanup();
55
- this.all.delete(watch);
56
- this.queue.delete(watch);
54
+ w.cleanup();
55
+ this.all.delete(w);
56
+ this.queue.delete(w);
57
57
  };
58
58
  return {
59
59
  destroy
60
60
  };
61
61
  }
62
62
  flush() {
63
- if (this.queue.size == 0) {
63
+ if (this.queue.size === 0) {
64
64
  return;
65
65
  }
66
- for (const watch of this.queue) {
67
- this.queue.delete(watch);
68
- watch.run();
66
+ for (const w of this.queue) {
67
+ this.queue.delete(w);
68
+ w.run();
69
69
  }
70
70
  }
71
71
  get isQueueEmpty() {
@@ -74,5 +74,5 @@ class EffectManager {
74
74
  }
75
75
  const effectManager = new EffectManager();
76
76
  function effect(effectFn, options) {
77
- return effectManager.create(effectFn, options);
77
+ return effectManager.create(effectFn, options?.allowSignalWrites ?? false);
78
78
  }
@@ -5,104 +5,149 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.io/license
7
7
  */
8
+ type Version = number & {
9
+ __brand: 'Version';
10
+ };
8
11
  export declare function setActiveConsumer(consumer: ReactiveNode | null): ReactiveNode | null;
12
+ export declare const REACTIVE_NODE: {
13
+ version: Version;
14
+ dirty: boolean;
15
+ producerNode: undefined;
16
+ producerLastReadVersion: undefined;
17
+ producerIndexOfThis: undefined;
18
+ nextProducerIndex: number;
19
+ liveConsumerNode: undefined;
20
+ liveConsumerIndexOfThis: undefined;
21
+ consumerAllowSignalWrites: boolean;
22
+ consumerIsAlwaysLive: boolean;
23
+ producerMustRecompute: () => boolean;
24
+ producerRecomputeValue: () => void;
25
+ consumerMarkedDirty: () => void;
26
+ };
9
27
  /**
10
- * A node in the reactive graph.
28
+ * A producer and/or consumer which participates in the reactive graph.
11
29
  *
12
- * Nodes can be producers of reactive values, consumers of other reactive values, or both.
30
+ * Producer `ReactiveNode`s which are accessed when a consumer `ReactiveNode` is the
31
+ * `activeConsumer` are tracked as dependencies of that consumer.
13
32
  *
14
- * Producers are nodes that produce values, and can be depended upon by consumer nodes.
33
+ * Certain consumers are also tracked as "live" consumers and create edges in the other direction,
34
+ * from producer to consumer. These edges are used to propagate change notifications when a
35
+ * producer's value is updated.
15
36
  *
16
- * Producers expose a monotonic `valueVersion` counter, and are responsible for incrementing this
17
- * version when their value semantically changes. Some producers may produce their values lazily and
18
- * thus at times need to be polled for potential updates to their value (and by extension their
19
- * `valueVersion`). This is accomplished via the `onProducerUpdateValueVersion` method for
20
- * implemented by producers, which should perform whatever calculations are necessary to ensure
21
- * `valueVersion` is up to date.
22
- *
23
- * Consumers are nodes that depend on the values of producers and are notified when those values
24
- * might have changed.
25
- *
26
- * Consumers do not wrap the reads they consume themselves, but rather can be set as the active
27
- * reader via `setActiveConsumer`. Reads of producers that happen while a consumer is active will
28
- * result in those producers being added as dependencies of that consumer node.
29
- *
30
- * The set of dependencies of a consumer is dynamic. Implementers expose a monotonically increasing
31
- * `trackingVersion` counter, which increments whenever the consumer is about to re-run any reactive
32
- * reads it needs and establish a new set of dependencies as a result.
33
- *
34
- * Producers store the last `trackingVersion` they've seen from `Consumer`s which have read them.
35
- * This allows a producer to identify whether its record of the dependency is current or stale, by
36
- * comparing the consumer's `trackingVersion` to the version at which the dependency was
37
- * last observed.
37
+ * A `ReactiveNode` may be both a producer and consumer.
38
38
  */
39
- export declare abstract class ReactiveNode {
40
- private readonly id;
41
- /**
42
- * A cached weak reference to this node, which will be used in `ReactiveEdge`s.
43
- */
44
- private readonly ref;
45
- /**
46
- * Edges to producers on which this node depends (in its consumer capacity).
47
- */
48
- private readonly producers;
49
- /**
50
- * Edges to consumers on which this node depends (in its producer capacity).
51
- */
52
- private readonly consumers;
39
+ export interface ReactiveNode {
53
40
  /**
54
- * Monotonically increasing counter representing a version of this `Consumer`'s
55
- * dependencies.
56
- */
57
- protected trackingVersion: number;
58
- /**
59
- * Monotonically increasing counter which increases when the value of this `Producer`
60
- * semantically changes.
41
+ * Version of the value that this node produces.
42
+ *
43
+ * This is incremented whenever a new value is produced by this node which is not equal to the
44
+ * previous value (by whatever definition of equality is in use).
61
45
  */
62
- protected valueVersion: number;
46
+ version: Version;
63
47
  /**
64
- * Whether signal writes should be allowed while this `ReactiveNode` is the current consumer.
48
+ * Whether this node (in its consumer capacity) is dirty.
49
+ *
50
+ * Only live consumers become dirty, when receiving a change notification from a dependency
51
+ * producer.
65
52
  */
66
- protected abstract readonly consumerAllowSignalWrites: boolean;
53
+ dirty: boolean;
67
54
  /**
68
- * Called for consumers whenever one of their dependencies notifies that it might have a new
69
- * value.
55
+ * Producers which are dependencies of this consumer.
56
+ *
57
+ * Uses the same indices as the `producerLastReadVersion` and `producerIndexOfThis` arrays.
70
58
  */
71
- protected abstract onConsumerDependencyMayHaveChanged(): void;
59
+ producerNode: ReactiveNode[] | undefined;
72
60
  /**
73
- * Called for producers when a dependent consumer is checking if the producer's value has actually
74
- * changed.
61
+ * `Version` of the value last read by a given producer.
62
+ *
63
+ * Uses the same indices as the `producerNode` and `producerIndexOfThis` arrays.
75
64
  */
76
- protected abstract onProducerUpdateValueVersion(): void;
65
+ producerLastReadVersion: Version[] | undefined;
77
66
  /**
78
- * Polls dependencies of a consumer to determine if they have actually changed.
67
+ * Index of `this` (consumer) in each producer's `liveConsumers` array.
68
+ *
69
+ * This value is only meaningful if this node is live (`liveConsumers.length > 0`). Otherwise
70
+ * these indices are stale.
79
71
  *
80
- * If this returns `false`, then even though the consumer may have previously been notified of a
81
- * change, the values of its dependencies have not actually changed and the consumer should not
82
- * rerun any reactions.
72
+ * Uses the same indices as the `producerNode` and `producerLastReadVersion` arrays.
83
73
  */
84
- protected consumerPollProducersForChange(): boolean;
74
+ producerIndexOfThis: number[] | undefined;
85
75
  /**
86
- * Notify all consumers of this producer that its value may have changed.
76
+ * Index into the producer arrays that the next dependency of this node as a consumer will use.
77
+ *
78
+ * This index is zeroed before this node as a consumer begins executing. When a producer is read,
79
+ * it gets inserted into the producers arrays at this index. There may be an existing dependency
80
+ * in this location which may or may not match the incoming producer, depending on whether the
81
+ * same producers were read in the same order as the last computation.
87
82
  */
88
- protected producerMayHaveChanged(): void;
83
+ nextProducerIndex: number;
89
84
  /**
90
- * Mark that this producer node has been accessed in the current reactive context.
85
+ * Array of consumers of this producer that are "live" (they require push notifications).
86
+ *
87
+ * `liveConsumerNode.length` is effectively our reference count for this node.
91
88
  */
92
- protected producerAccessed(): void;
89
+ liveConsumerNode: ReactiveNode[] | undefined;
93
90
  /**
94
- * Whether this consumer currently has any producers registered.
91
+ * Index of `this` (producer) in each consumer's `producerNode` array.
92
+ *
93
+ * Uses the same indices as the `liveConsumerNode` array.
95
94
  */
96
- protected get hasProducers(): boolean;
95
+ liveConsumerIndexOfThis: number[] | undefined;
97
96
  /**
98
- * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
99
- * based on the current consumer context.
97
+ * Whether writes to signals are allowed when this consumer is the `activeConsumer`.
98
+ *
99
+ * This is used to enforce guardrails such as preventing writes to writable signals in the
100
+ * computation function of computed signals, which is supposed to be pure.
100
101
  */
101
- protected get producerUpdatesAllowed(): boolean;
102
+ consumerAllowSignalWrites: boolean;
103
+ readonly consumerIsAlwaysLive: boolean;
102
104
  /**
103
- * Checks if a `Producer` has a current value which is different than the value
104
- * last seen at a specific version by a `Consumer` which recorded a dependency on
105
- * this `Producer`.
105
+ * Tracks whether producers need to recompute their value independently of the reactive graph (for
106
+ * example, if no initial value has been computed).
106
107
  */
107
- private producerPollStatus;
108
+ producerMustRecompute(node: unknown): boolean;
109
+ producerRecomputeValue(node: unknown): void;
110
+ consumerMarkedDirty(node: unknown): void;
108
111
  }
112
+ /**
113
+ * Called by implementations when a producer's signal is read.
114
+ */
115
+ export declare function producerAccessed(node: ReactiveNode): void;
116
+ /**
117
+ * Ensure this producer's `version` is up-to-date.
118
+ */
119
+ export declare function producerUpdateValueVersion(node: ReactiveNode): void;
120
+ /**
121
+ * Propagate a dirty notification to live consumers of this producer.
122
+ */
123
+ export declare function producerNotifyConsumers(node: ReactiveNode): void;
124
+ /**
125
+ * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
126
+ * based on the current consumer context.
127
+ */
128
+ export declare function producerUpdatesAllowed(): boolean;
129
+ export declare function consumerMarkDirty(node: ReactiveNode): void;
130
+ /**
131
+ * Prepare this consumer to run a computation in its reactive context.
132
+ *
133
+ * Must be called by subclasses which represent reactive computations, before those computations
134
+ * begin.
135
+ */
136
+ export declare function consumerBeforeComputation(node: ReactiveNode | null): ReactiveNode | null;
137
+ /**
138
+ * Finalize this consumer's state after a reactive computation has run.
139
+ *
140
+ * Must be called by subclasses which represent reactive computations, after those computations
141
+ * have finished.
142
+ */
143
+ export declare function consumerAfterComputation(node: ReactiveNode | null, prevConsumer: ReactiveNode | null): void;
144
+ /**
145
+ * Determine whether this consumer has any dependencies which have changed since the last time
146
+ * they were read.
147
+ */
148
+ export declare function consumerPollProducersForChange(node: ReactiveNode): boolean;
149
+ /**
150
+ * Disconnect this consumer from the graph.
151
+ */
152
+ export declare function consumerDestroy(node: ReactiveNode): void;
153
+ export {};
@@ -18,7 +18,16 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var graph_exports = {};
20
20
  __export(graph_exports, {
21
- ReactiveNode: () => ReactiveNode,
21
+ REACTIVE_NODE: () => REACTIVE_NODE,
22
+ consumerAfterComputation: () => consumerAfterComputation,
23
+ consumerBeforeComputation: () => consumerBeforeComputation,
24
+ consumerDestroy: () => consumerDestroy,
25
+ consumerMarkDirty: () => consumerMarkDirty,
26
+ consumerPollProducersForChange: () => consumerPollProducersForChange,
27
+ producerAccessed: () => producerAccessed,
28
+ producerNotifyConsumers: () => producerNotifyConsumers,
29
+ producerUpdateValueVersion: () => producerUpdateValueVersion,
30
+ producerUpdatesAllowed: () => producerUpdatesAllowed,
22
31
  setActiveConsumer: () => setActiveConsumer
23
32
  });
24
33
  module.exports = __toCommonJS(graph_exports);
@@ -29,7 +38,6 @@ module.exports = __toCommonJS(graph_exports);
29
38
  * Use of this source code is governed by an MIT-style license that can be
30
39
  * found in the LICENSE file at https://angular.io/license
31
40
  */
32
- let _nextReactiveId = 0;
33
41
  let activeConsumer = null;
34
42
  let inNotificationPhase = false;
35
43
  function setActiveConsumer(consumer) {
@@ -37,119 +45,166 @@ function setActiveConsumer(consumer) {
37
45
  activeConsumer = consumer;
38
46
  return prev;
39
47
  }
40
- class ReactiveNode {
41
- id = _nextReactiveId++;
42
- /**
43
- * A cached weak reference to this node, which will be used in `ReactiveEdge`s.
44
- */
45
- ref = new WeakRef(this);
46
- /**
47
- * Edges to producers on which this node depends (in its consumer capacity).
48
- */
49
- producers = /* @__PURE__ */ new Map();
50
- /**
51
- * Edges to consumers on which this node depends (in its producer capacity).
52
- */
53
- consumers = /* @__PURE__ */ new Map();
54
- /**
55
- * Monotonically increasing counter representing a version of this `Consumer`'s
56
- * dependencies.
57
- */
58
- trackingVersion = 0;
59
- /**
60
- * Monotonically increasing counter which increases when the value of this `Producer`
61
- * semantically changes.
62
- */
63
- valueVersion = 0;
64
- /**
65
- * Polls dependencies of a consumer to determine if they have actually changed.
66
- *
67
- * If this returns `false`, then even though the consumer may have previously been notified of a
68
- * change, the values of its dependencies have not actually changed and the consumer should not
69
- * rerun any reactions.
70
- */
71
- consumerPollProducersForChange() {
72
- for (const [producerId, edge] of this.producers) {
73
- const producer = edge.producerNode.deref();
74
- if (producer == null || edge.atTrackingVersion !== this.trackingVersion) {
75
- this.producers.delete(producerId);
76
- producer?.consumers.delete(this.id);
77
- continue;
78
- }
79
- if (producer.producerPollStatus(edge.seenValueVersion)) {
80
- return true;
81
- }
48
+ const REACTIVE_NODE = {
49
+ version: 0,
50
+ dirty: false,
51
+ producerNode: void 0,
52
+ producerLastReadVersion: void 0,
53
+ producerIndexOfThis: void 0,
54
+ nextProducerIndex: 0,
55
+ liveConsumerNode: void 0,
56
+ liveConsumerIndexOfThis: void 0,
57
+ consumerAllowSignalWrites: false,
58
+ consumerIsAlwaysLive: false,
59
+ producerMustRecompute: () => false,
60
+ producerRecomputeValue: () => {
61
+ },
62
+ consumerMarkedDirty: () => {
63
+ }
64
+ };
65
+ function producerAccessed(node) {
66
+ if (inNotificationPhase) {
67
+ throw new Error("Assertion error: signal read during notification phase");
68
+ }
69
+ if (activeConsumer === null) {
70
+ return;
71
+ }
72
+ const idx = activeConsumer.nextProducerIndex++;
73
+ assertConsumerNode(activeConsumer);
74
+ if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
75
+ if (consumerIsLive(activeConsumer)) {
76
+ const staleProducer = activeConsumer.producerNode[idx];
77
+ producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
82
78
  }
83
- return false;
84
- }
85
- /**
86
- * Notify all consumers of this producer that its value may have changed.
87
- */
88
- producerMayHaveChanged() {
89
- const prev = inNotificationPhase;
90
- inNotificationPhase = true;
91
- try {
92
- for (const [consumerId, edge] of this.consumers) {
93
- const consumer = edge.consumerNode.deref();
94
- if (consumer == null || consumer.trackingVersion !== edge.atTrackingVersion) {
95
- this.consumers.delete(consumerId);
96
- consumer?.producers.delete(this.id);
97
- continue;
98
- }
99
- consumer.onConsumerDependencyMayHaveChanged();
79
+ }
80
+ if (activeConsumer.producerNode[idx] !== node) {
81
+ activeConsumer.producerNode[idx] = node;
82
+ activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0;
83
+ }
84
+ activeConsumer.producerLastReadVersion[idx] = node.version;
85
+ }
86
+ function producerUpdateValueVersion(node) {
87
+ if (consumerIsLive(node) && !node.dirty) {
88
+ return;
89
+ }
90
+ if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
91
+ node.dirty = false;
92
+ return;
93
+ }
94
+ node.producerRecomputeValue(node);
95
+ node.dirty = false;
96
+ }
97
+ function producerNotifyConsumers(node) {
98
+ if (node.liveConsumerNode === void 0) {
99
+ return;
100
+ }
101
+ const prev = inNotificationPhase;
102
+ inNotificationPhase = true;
103
+ try {
104
+ for (const consumer of node.liveConsumerNode) {
105
+ if (!consumer.dirty) {
106
+ consumerMarkDirty(consumer);
100
107
  }
101
- } finally {
102
- inNotificationPhase = prev;
103
108
  }
109
+ } finally {
110
+ inNotificationPhase = prev;
111
+ }
112
+ }
113
+ function producerUpdatesAllowed() {
114
+ return activeConsumer?.consumerAllowSignalWrites !== false;
115
+ }
116
+ function consumerMarkDirty(node) {
117
+ node.dirty = true;
118
+ producerNotifyConsumers(node);
119
+ node.consumerMarkedDirty?.(node);
120
+ }
121
+ function consumerBeforeComputation(node) {
122
+ node && (node.nextProducerIndex = 0);
123
+ return setActiveConsumer(node);
124
+ }
125
+ function consumerAfterComputation(node, prevConsumer) {
126
+ setActiveConsumer(prevConsumer);
127
+ if (!node || node.producerNode === void 0 || node.producerIndexOfThis === void 0 || node.producerLastReadVersion === void 0) {
128
+ return;
129
+ }
130
+ if (consumerIsLive(node)) {
131
+ for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
132
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
133
+ }
134
+ }
135
+ for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
136
+ node.producerNode.pop();
137
+ node.producerLastReadVersion.pop();
138
+ node.producerIndexOfThis.pop();
104
139
  }
105
- /**
106
- * Mark that this producer node has been accessed in the current reactive context.
107
- */
108
- producerAccessed() {
109
- if (inNotificationPhase) {
110
- throw new Error(`Assertion error: signal read during notification phase`);
140
+ }
141
+ function consumerPollProducersForChange(node) {
142
+ assertConsumerNode(node);
143
+ for (let i = 0; i < node.producerNode.length; i++) {
144
+ const producer = node.producerNode[i];
145
+ const seenVersion = node.producerLastReadVersion[i];
146
+ if (seenVersion !== producer.version) {
147
+ return true;
111
148
  }
112
- if (activeConsumer === null) {
113
- return;
149
+ producerUpdateValueVersion(producer);
150
+ if (seenVersion !== producer.version) {
151
+ return true;
114
152
  }
115
- let edge = activeConsumer.producers.get(this.id);
116
- if (edge === void 0) {
117
- edge = {
118
- consumerNode: activeConsumer.ref,
119
- producerNode: this.ref,
120
- seenValueVersion: this.valueVersion,
121
- atTrackingVersion: activeConsumer.trackingVersion
122
- };
123
- activeConsumer.producers.set(this.id, edge);
124
- this.consumers.set(activeConsumer.id, edge);
125
- } else {
126
- edge.seenValueVersion = this.valueVersion;
127
- edge.atTrackingVersion = activeConsumer.trackingVersion;
153
+ }
154
+ return false;
155
+ }
156
+ function consumerDestroy(node) {
157
+ assertConsumerNode(node);
158
+ if (consumerIsLive(node)) {
159
+ for (let i = 0; i < node.producerNode.length; i++) {
160
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
128
161
  }
129
162
  }
130
- /**
131
- * Whether this consumer currently has any producers registered.
132
- */
133
- get hasProducers() {
134
- return this.producers.size > 0;
135
- }
136
- /**
137
- * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
138
- * based on the current consumer context.
139
- */
140
- get producerUpdatesAllowed() {
141
- return activeConsumer?.consumerAllowSignalWrites !== false;
142
- }
143
- /**
144
- * Checks if a `Producer` has a current value which is different than the value
145
- * last seen at a specific version by a `Consumer` which recorded a dependency on
146
- * this `Producer`.
147
- */
148
- producerPollStatus(lastSeenValueVersion) {
149
- if (this.valueVersion !== lastSeenValueVersion) {
150
- return true;
163
+ node.producerNode.length = node.producerLastReadVersion.length = node.producerIndexOfThis.length = 0;
164
+ if (node.liveConsumerNode) {
165
+ node.liveConsumerNode.length = node.liveConsumerIndexOfThis.length = 0;
166
+ }
167
+ }
168
+ function producerAddLiveConsumer(node, consumer, indexOfThis) {
169
+ assertProducerNode(node);
170
+ assertConsumerNode(node);
171
+ if (node.liveConsumerNode.length === 0) {
172
+ for (let i = 0; i < node.producerNode.length; i++) {
173
+ node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
174
+ }
175
+ }
176
+ node.liveConsumerIndexOfThis.push(indexOfThis);
177
+ return node.liveConsumerNode.push(consumer) - 1;
178
+ }
179
+ function producerRemoveLiveConsumerAtIndex(node, idx) {
180
+ assertProducerNode(node);
181
+ assertConsumerNode(node);
182
+ if (node.liveConsumerNode.length === 1) {
183
+ for (let i = 0; i < node.producerNode.length; i++) {
184
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
151
185
  }
152
- this.onProducerUpdateValueVersion();
153
- return this.valueVersion !== lastSeenValueVersion;
154
186
  }
187
+ const lastIdx = node.liveConsumerNode.length - 1;
188
+ node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
189
+ node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
190
+ node.liveConsumerNode.length--;
191
+ node.liveConsumerIndexOfThis.length--;
192
+ if (idx < node.liveConsumerNode.length) {
193
+ const idxProducer = node.liveConsumerIndexOfThis[idx];
194
+ const consumer = node.liveConsumerNode[idx];
195
+ assertConsumerNode(consumer);
196
+ consumer.producerIndexOfThis[idxProducer] = idx;
197
+ }
198
+ }
199
+ function consumerIsLive(node) {
200
+ return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0;
201
+ }
202
+ function assertConsumerNode(node) {
203
+ node.producerNode ??= [];
204
+ node.producerIndexOfThis ??= [];
205
+ node.producerLastReadVersion ??= [];
206
+ }
207
+ function assertProducerNode(node) {
208
+ node.liveConsumerNode ??= [];
209
+ node.liveConsumerIndexOfThis ??= [];
155
210
  }
@@ -33,81 +33,19 @@ var import_graph = require("./graph.js");
33
33
  * found in the LICENSE file at https://angular.io/license
34
34
  */
35
35
  let postSignalSetFn = null;
36
- class WritableSignalImpl extends import_graph.ReactiveNode {
37
- value;
38
- equal;
39
- readonlySignal;
40
- consumerAllowSignalWrites = false;
41
- constructor(value, equal) {
42
- super();
43
- this.value = value;
44
- this.equal = equal;
45
- }
46
- onConsumerDependencyMayHaveChanged() {
47
- }
48
- onProducerUpdateValueVersion() {
49
- }
50
- /**
51
- * Directly update the value of the signal to a new value, which may or may not be
52
- * equal to the previous.
53
- *
54
- * In the event that `newValue` is semantically equal to the current value, `set` is
55
- * a no-op.
56
- */
57
- set(newValue) {
58
- if (!this.producerUpdatesAllowed) {
59
- (0, import_errors.throwInvalidWriteToSignalError)();
60
- }
61
- if (!this.equal(this.value, newValue)) {
62
- this.value = newValue;
63
- this.valueVersion++;
64
- this.producerMayHaveChanged();
65
- postSignalSetFn?.();
66
- }
67
- }
68
- /**
69
- * Derive a new value for the signal from its current value using the `updater` function.
70
- *
71
- * This is equivalent to calling `set` on the result of running `updater` on the current
72
- * value.
73
- */
74
- update(updater) {
75
- if (!this.producerUpdatesAllowed) {
76
- (0, import_errors.throwInvalidWriteToSignalError)();
77
- }
78
- this.set(updater(this.value));
79
- }
80
- /**
81
- * Calls `mutator` on the current value and assumes that it has been mutated.
82
- */
83
- mutate(mutator) {
84
- if (!this.producerUpdatesAllowed) {
85
- (0, import_errors.throwInvalidWriteToSignalError)();
86
- }
87
- mutator(this.value);
88
- this.valueVersion++;
89
- this.producerMayHaveChanged();
90
- postSignalSetFn?.();
91
- }
92
- asReadonly() {
93
- if (this.readonlySignal === void 0) {
94
- this.readonlySignal = (0, import_api.createSignalFromFunction)(this, () => this.signal());
95
- }
96
- return this.readonlySignal;
97
- }
98
- signal() {
99
- this.producerAccessed();
100
- return this.value;
101
- }
102
- }
103
36
  function signal(initialValue, options) {
104
- const signalNode = new WritableSignalImpl(initialValue, options?.equal ?? import_api.defaultEquals);
105
- const signalFn = (0, import_api.createSignalFromFunction)(signalNode, signalNode.signal.bind(signalNode), {
106
- set: signalNode.set.bind(signalNode),
107
- update: signalNode.update.bind(signalNode),
108
- mutate: signalNode.mutate.bind(signalNode),
109
- asReadonly: signalNode.asReadonly.bind(signalNode)
110
- });
37
+ const node = Object.create(SIGNAL_NODE);
38
+ node.value = initialValue;
39
+ options?.equal && (node.equal = options.equal);
40
+ function signalFn() {
41
+ (0, import_graph.producerAccessed)(node);
42
+ return node.value;
43
+ }
44
+ signalFn.set = signalSetFn;
45
+ signalFn.update = signalUpdateFn;
46
+ signalFn.mutate = signalMutateFn;
47
+ signalFn.asReadonly = signalAsReadonlyFn;
48
+ signalFn[import_api.SIGNAL] = node;
111
49
  return signalFn;
112
50
  }
113
51
  function setPostSignalSetFn(fn) {
@@ -115,3 +53,46 @@ function setPostSignalSetFn(fn) {
115
53
  postSignalSetFn = fn;
116
54
  return prev;
117
55
  }
56
+ const SIGNAL_NODE = {
57
+ ...import_graph.REACTIVE_NODE,
58
+ equal: import_api.defaultEquals,
59
+ readonlyFn: void 0
60
+ };
61
+ function signalValueChanged(node) {
62
+ node.version++;
63
+ (0, import_graph.producerNotifyConsumers)(node);
64
+ postSignalSetFn?.();
65
+ }
66
+ function signalSetFn(newValue) {
67
+ const node = this[import_api.SIGNAL];
68
+ if (!(0, import_graph.producerUpdatesAllowed)()) {
69
+ (0, import_errors.throwInvalidWriteToSignalError)();
70
+ }
71
+ if (!node.equal(node.value, newValue)) {
72
+ node.value = newValue;
73
+ signalValueChanged(node);
74
+ }
75
+ }
76
+ function signalUpdateFn(updater) {
77
+ if (!(0, import_graph.producerUpdatesAllowed)()) {
78
+ (0, import_errors.throwInvalidWriteToSignalError)();
79
+ }
80
+ signalSetFn.call(this, updater(this[import_api.SIGNAL].value));
81
+ }
82
+ function signalMutateFn(mutator) {
83
+ const node = this[import_api.SIGNAL];
84
+ if (!(0, import_graph.producerUpdatesAllowed)()) {
85
+ (0, import_errors.throwInvalidWriteToSignalError)();
86
+ }
87
+ mutator(node.value);
88
+ signalValueChanged(node);
89
+ }
90
+ function signalAsReadonlyFn() {
91
+ const node = this[import_api.SIGNAL];
92
+ if (node.readonlyFn === void 0) {
93
+ const readonlyFn = () => this();
94
+ readonlyFn[import_api.SIGNAL] = node;
95
+ node.readonlyFn = readonlyFn;
96
+ }
97
+ return node.readonlyFn;
98
+ }
@@ -5,7 +5,6 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.io/license
7
7
  */
8
- import { ReactiveNode } from './graph.js';
9
8
  /**
10
9
  * A cleanup function that can be optionally registered from the watch logic. If registered, the
11
10
  * cleanup logic runs before the next watch execution.
@@ -15,24 +14,8 @@ export type WatchCleanupFn = () => void;
15
14
  * A callback passed to the watch function that makes it possible to register cleanup logic.
16
15
  */
17
16
  export type WatchCleanupRegisterFn = (cleanupFn: WatchCleanupFn) => void;
18
- /**
19
- * Watches a reactive expression and allows it to be scheduled to re-run
20
- * when any dependencies notify of a change.
21
- *
22
- * `Watch` doesn't run reactive expressions itself, but relies on a consumer-
23
- * provided scheduling operation to coordinate calling `Watch.run()`.
24
- */
25
- export declare class Watch extends ReactiveNode {
26
- private watch;
27
- private schedule;
28
- protected readonly consumerAllowSignalWrites: boolean;
29
- private dirty;
30
- private cleanupFn;
31
- private registerOnCleanup;
32
- constructor(watch: (onCleanup: WatchCleanupRegisterFn) => void, schedule: (watch: Watch) => void, allowSignalWrites: boolean);
17
+ export interface Watch {
33
18
  notify(): void;
34
- protected onConsumerDependencyMayHaveChanged(): void;
35
- protected onProducerUpdateValueVersion(): void;
36
19
  /**
37
20
  * Execute the reactive expression in the context of this `Watch` consumer.
38
21
  *
@@ -42,3 +25,4 @@ export declare class Watch extends ReactiveNode {
42
25
  run(): void;
43
26
  cleanup(): void;
44
27
  }
28
+ export declare function watch(fn: (onCleanup: WatchCleanupRegisterFn) => void, schedule: (watch: Watch) => void, allowSignalWrites: boolean): Watch;
@@ -18,7 +18,7 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
  var watch_exports = {};
20
20
  __export(watch_exports, {
21
- Watch: () => Watch
21
+ watch: () => watch
22
22
  });
23
23
  module.exports = __toCommonJS(watch_exports);
24
24
  var import_graph = require("./graph.js");
@@ -29,56 +29,47 @@ var import_graph = require("./graph.js");
29
29
  * Use of this source code is governed by an MIT-style license that can be
30
30
  * found in the LICENSE file at https://angular.io/license
31
31
  */
32
- const NOOP_CLEANUP_FN = () => {
33
- };
34
- class Watch extends import_graph.ReactiveNode {
35
- watch;
36
- schedule;
37
- consumerAllowSignalWrites;
38
- dirty = false;
39
- cleanupFn = NOOP_CLEANUP_FN;
40
- registerOnCleanup = (cleanupFn) => {
41
- this.cleanupFn = cleanupFn;
42
- };
43
- constructor(watch, schedule, allowSignalWrites) {
44
- super();
45
- this.watch = watch;
46
- this.schedule = schedule;
47
- this.consumerAllowSignalWrites = allowSignalWrites;
48
- }
49
- notify() {
50
- if (!this.dirty) {
51
- this.schedule(this);
52
- }
53
- this.dirty = true;
54
- }
55
- onConsumerDependencyMayHaveChanged() {
56
- this.notify();
32
+ function watch(fn, schedule, allowSignalWrites) {
33
+ const node = Object.create(WATCH_NODE);
34
+ if (allowSignalWrites) {
35
+ node.consumerAllowSignalWrites = true;
57
36
  }
58
- onProducerUpdateValueVersion() {
59
- }
60
- /**
61
- * Execute the reactive expression in the context of this `Watch` consumer.
62
- *
63
- * Should be called by the user scheduling algorithm when the provided
64
- * `schedule` hook is called by `Watch`.
65
- */
66
- run() {
67
- this.dirty = false;
68
- if (this.trackingVersion !== 0 && !this.consumerPollProducersForChange()) {
37
+ node.fn = fn;
38
+ node.schedule = schedule;
39
+ const registerOnCleanup = (cleanupFn) => {
40
+ node.cleanupFn = cleanupFn;
41
+ };
42
+ const run = () => {
43
+ node.dirty = false;
44
+ if (node.hasRun && !(0, import_graph.consumerPollProducersForChange)(node)) {
69
45
  return;
70
46
  }
71
- const prevConsumer = (0, import_graph.setActiveConsumer)(this);
72
- this.trackingVersion++;
47
+ node.hasRun = true;
48
+ const prevConsumer = (0, import_graph.consumerBeforeComputation)(node);
73
49
  try {
74
- this.cleanupFn();
75
- this.cleanupFn = NOOP_CLEANUP_FN;
76
- this.watch(this.registerOnCleanup);
50
+ node.cleanupFn();
51
+ node.cleanupFn = NOOP_CLEANUP_FN;
52
+ node.fn(registerOnCleanup);
77
53
  } finally {
78
- (0, import_graph.setActiveConsumer)(prevConsumer);
54
+ (0, import_graph.consumerAfterComputation)(node, prevConsumer);
79
55
  }
80
- }
81
- cleanup() {
82
- this.cleanupFn();
83
- }
56
+ };
57
+ node.ref = {
58
+ notify: () => (0, import_graph.consumerMarkDirty)(node),
59
+ run,
60
+ cleanup: () => node.cleanupFn()
61
+ };
62
+ return node.ref;
84
63
  }
64
+ const NOOP_CLEANUP_FN = () => {
65
+ };
66
+ const WATCH_NODE = {
67
+ ...import_graph.REACTIVE_NODE,
68
+ consumerIsAlwaysLive: true,
69
+ consumerAllowSignalWrites: false,
70
+ consumerMarkedDirty: (node) => {
71
+ node.schedule(node.ref);
72
+ },
73
+ hasRun: false,
74
+ cleanupFn: NOOP_CLEANUP_FN
75
+ };
@@ -30,7 +30,6 @@ __export(dynamic_text_model_exports, {
30
30
  });
31
31
  module.exports = __toCommonJS(dynamic_text_model_exports);
32
32
  var import_rxjs = require("rxjs");
33
- var import_core = require("../core.js");
34
33
  var import_inject = require("../injector/inject.js");
35
34
  var import_api = require("../signals/api.js");
36
35
  var import_switch_map = require("../signals/switch-map.js");
@@ -38,12 +37,11 @@ var import_untracked_operator = require("../signals/untracked-operator.js");
38
37
  var import_type_guards = require("../utils/type-guards.js");
39
38
  var import_localization_service = require("./localization.service.js");
40
39
  const missingLocalizationKeyText = "[MISSING LOCALIZATION KEY]";
41
- function resolveDynamicText(text, localizationService) {
42
- const resolvedLocalizationService = localizationService ?? ((0, import_inject.isInInjectionContext)() ? (0, import_inject.inject)(import_localization_service.LocalizationService) : (0, import_core.getGlobalInjector)().resolve(import_localization_service.LocalizationService));
40
+ function resolveDynamicText(text, localizationService = (0, import_inject.inject)(import_localization_service.LocalizationService)) {
43
41
  const localizableTextSignal = (0, import_api.isSignal)(text) ? text : (0, import_rxjs.isObservable)(text) ? (0, import_api.toSignal)(text.pipe((0, import_untracked_operator.runInUntracked)()), { initialValue: missingLocalizationKeyText }) : (0, import_api.computed)(() => text);
44
42
  return (0, import_switch_map.switchMap)(() => {
45
43
  const localizableText = localizableTextSignal();
46
- return (0, import_type_guards.isString)(localizableText) ? (0, import_api.computed)(() => localizableText) : resolvedLocalizationService.localize(localizableText);
44
+ return (0, import_type_guards.isString)(localizableText) ? (0, import_api.computed)(() => localizableText) : localizationService.localize(localizableText);
47
45
  });
48
46
  }
49
47
  function resolveDynamicText$(text, localizationService) {