@angular/core 16.2.3 → 16.2.4

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.
Files changed (35) hide show
  1. package/esm2022/src/linker/view_container_ref.mjs +35 -1
  2. package/esm2022/src/render3/component.mjs +4 -3
  3. package/esm2022/src/render3/di.mjs +1 -1
  4. package/esm2022/src/render3/instructions/change_detection.mjs +4 -4
  5. package/esm2022/src/render3/instructions/shared.mjs +20 -14
  6. package/esm2022/src/render3/interfaces/injector.mjs +1 -1
  7. package/esm2022/src/render3/interfaces/styling.mjs +4 -7
  8. package/esm2022/src/render3/node_manipulation.mjs +4 -3
  9. package/esm2022/src/render3/reactive_lview_consumer.mjs +25 -45
  10. package/esm2022/src/render3/reactivity/effect.mjs +8 -8
  11. package/esm2022/src/render3/util/injector_utils.mjs +1 -1
  12. package/esm2022/src/signals/index.mjs +4 -4
  13. package/esm2022/src/signals/src/api.mjs +2 -11
  14. package/esm2022/src/signals/src/computed.mjs +43 -93
  15. package/esm2022/src/signals/src/graph.mjs +238 -162
  16. package/esm2022/src/signals/src/signal.mjs +59 -79
  17. package/esm2022/src/signals/src/watch.mjs +38 -52
  18. package/esm2022/src/signals/src/weak_ref.mjs +2 -29
  19. package/esm2022/src/util/security/trusted_type_defs.mjs +1 -1
  20. package/esm2022/src/util/security/trusted_types.mjs +1 -1
  21. package/esm2022/src/version.mjs +1 -1
  22. package/esm2022/src/zone/ng_zone.mjs +16 -1
  23. package/esm2022/testing/src/logger.mjs +3 -3
  24. package/fesm2022/core.mjs +476 -483
  25. package/fesm2022/core.mjs.map +1 -1
  26. package/fesm2022/rxjs-interop.mjs +373 -413
  27. package/fesm2022/rxjs-interop.mjs.map +1 -1
  28. package/fesm2022/testing.mjs +474 -482
  29. package/fesm2022/testing.mjs.map +1 -1
  30. package/index.d.ts +112 -102
  31. package/package.json +1 -1
  32. package/rxjs-interop/index.d.ts +1 -1
  33. package/schematics/ng-generate/standalone-migration/bundle.js +9 -9
  34. package/schematics/ng-generate/standalone-migration/bundle.js.map +1 -1
  35. package/testing/index.d.ts +1 -1
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @license Angular v16.2.3
2
+ * @license Angular v16.2.4
3
3
  * (c) 2010-2022 Google LLC. https://angular.io/
4
4
  * License: MIT
5
5
  */
@@ -131,15 +131,6 @@ const SIGNAL = Symbol('SIGNAL');
131
131
  function isSignal(value) {
132
132
  return typeof value === 'function' && value[SIGNAL] !== undefined;
133
133
  }
134
- /**
135
- * Converts `fn` into a marked signal function (where `isSignal(fn)` will be `true`), and
136
- * potentially add some set of extra properties (passed as an object record `extraApi`).
137
- */
138
- function createSignalFromFunction(node, fn, extraApi = {}) {
139
- fn[SIGNAL] = node;
140
- // Copy properties from `extraApi` to `fn` to complete the desired API of the `Signal`.
141
- return Object.assign(fn, extraApi);
142
- }
143
134
  /**
144
135
  * The default equality function used for `signal` and `computed`, which treats objects and arrays
145
136
  * as never equal, and all other primitive values using identity semantics.
@@ -234,216 +225,265 @@ function initNgDevMode() {
234
225
 
235
226
  // Required as the signals library is in a separate package, so we need to explicitly ensure the
236
227
  /**
237
- * A `WeakRef`-compatible reference that fakes the API with a strong reference
238
- * internally.
228
+ * The currently active consumer `ReactiveNode`, if running code in a reactive context.
229
+ *
230
+ * Change this via `setActiveConsumer`.
239
231
  */
240
- class LeakyRef {
241
- constructor(ref) {
242
- this.ref = ref;
243
- }
244
- deref() {
245
- return this.ref;
246
- }
232
+ let activeConsumer = null;
233
+ let inNotificationPhase = false;
234
+ function setActiveConsumer(consumer) {
235
+ const prev = activeConsumer;
236
+ activeConsumer = consumer;
237
+ return prev;
247
238
  }
248
- // `WeakRef` is not always defined in every TS environment where Angular is compiled. Instead,
249
- // read it off of the global context if available.
250
- // tslint:disable-next-line: no-toplevel-property-access
251
- let WeakRefImpl = _global['WeakRef'] ?? LeakyRef;
252
- function newWeakRef(value) {
253
- if (typeof ngDevMode !== 'undefined' && ngDevMode && WeakRefImpl === undefined) {
254
- throw new Error(`Angular requires a browser which supports the 'WeakRef' API`);
239
+ const REACTIVE_NODE = {
240
+ version: 0,
241
+ dirty: false,
242
+ producerNode: undefined,
243
+ producerLastReadVersion: undefined,
244
+ producerIndexOfThis: undefined,
245
+ nextProducerIndex: 0,
246
+ liveConsumerNode: undefined,
247
+ liveConsumerIndexOfThis: undefined,
248
+ consumerAllowSignalWrites: false,
249
+ consumerIsAlwaysLive: false,
250
+ producerMustRecompute: () => false,
251
+ producerRecomputeValue: () => { },
252
+ consumerMarkedDirty: () => { },
253
+ };
254
+ /**
255
+ * Called by implementations when a producer's signal is read.
256
+ */
257
+ function producerAccessed(node) {
258
+ if (inNotificationPhase) {
259
+ throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode ?
260
+ `Assertion error: signal read during notification phase` :
261
+ '');
262
+ }
263
+ if (activeConsumer === null) {
264
+ // Accessed outside of a reactive context, so nothing to record.
265
+ return;
266
+ }
267
+ // This producer is the `idx`th dependency of `activeConsumer`.
268
+ const idx = activeConsumer.nextProducerIndex++;
269
+ assertConsumerNode(activeConsumer);
270
+ if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
271
+ // There's been a change in producers since the last execution of `activeConsumer`.
272
+ // `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and
273
+ // replaced with `this`.
274
+ //
275
+ // If `activeConsumer` isn't live, then this is a no-op, since we can replace the producer in
276
+ // `activeConsumer.producerNode` directly. However, if `activeConsumer` is live, then we need
277
+ // to remove it from the stale producer's `liveConsumer`s.
278
+ if (consumerIsLive(activeConsumer)) {
279
+ const staleProducer = activeConsumer.producerNode[idx];
280
+ producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
281
+ // At this point, the only record of `staleProducer` is the reference at
282
+ // `activeConsumer.producerNode[idx]` which will be overwritten below.
283
+ }
255
284
  }
256
- return new WeakRefImpl(value);
257
- }
258
- function setAlternateWeakRefImpl(impl) {
259
- // no-op since the alternate impl is included by default by the framework. Remove once internal
260
- // migration is complete.
285
+ if (activeConsumer.producerNode[idx] !== node) {
286
+ // We're a new dependency of the consumer (at `idx`).
287
+ activeConsumer.producerNode[idx] = node;
288
+ // If the active consumer is live, then add it as a live consumer. If not, then use 0 as a
289
+ // placeholder value.
290
+ activeConsumer.producerIndexOfThis[idx] =
291
+ consumerIsLive(activeConsumer) ? producerAddLiveConsumer(node, activeConsumer, idx) : 0;
292
+ }
293
+ activeConsumer.producerLastReadVersion[idx] = node.version;
261
294
  }
262
-
263
- // Required as the signals library is in a separate package, so we need to explicitly ensure the
264
295
  /**
265
- * Counter tracking the next `ProducerId` or `ConsumerId`.
296
+ * Ensure this producer's `version` is up-to-date.
266
297
  */
267
- let _nextReactiveId = 0;
298
+ function producerUpdateValueVersion(node) {
299
+ if (consumerIsLive(node) && !node.dirty) {
300
+ // A live consumer will be marked dirty by producers, so a clean state means that its version
301
+ // is guaranteed to be up-to-date.
302
+ return;
303
+ }
304
+ if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
305
+ // None of our producers report a change since the last time they were read, so no
306
+ // recomputation of our value is necessary, and we can consider ourselves clean.
307
+ node.dirty = false;
308
+ return;
309
+ }
310
+ node.producerRecomputeValue(node);
311
+ // After recomputing the value, we're no longer dirty.
312
+ node.dirty = false;
313
+ }
268
314
  /**
269
- * Tracks the currently active reactive consumer (or `null` if there is no active
270
- * consumer).
315
+ * Propagate a dirty notification to live consumers of this producer.
271
316
  */
272
- let activeConsumer = null;
317
+ function producerNotifyConsumers(node) {
318
+ if (node.liveConsumerNode === undefined) {
319
+ return;
320
+ }
321
+ // Prevent signal reads when we're updating the graph
322
+ const prev = inNotificationPhase;
323
+ inNotificationPhase = true;
324
+ try {
325
+ for (const consumer of node.liveConsumerNode) {
326
+ if (!consumer.dirty) {
327
+ consumerMarkDirty(consumer);
328
+ }
329
+ }
330
+ }
331
+ finally {
332
+ inNotificationPhase = prev;
333
+ }
334
+ }
273
335
  /**
274
- * Whether the graph is currently propagating change notifications.
336
+ * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
337
+ * based on the current consumer context.
275
338
  */
276
- let inNotificationPhase = false;
277
- function setActiveConsumer(consumer) {
278
- const prev = activeConsumer;
279
- activeConsumer = consumer;
280
- return prev;
339
+ function producerUpdatesAllowed() {
340
+ return activeConsumer?.consumerAllowSignalWrites !== false;
341
+ }
342
+ function consumerMarkDirty(node) {
343
+ node.dirty = true;
344
+ producerNotifyConsumers(node);
345
+ node.consumerMarkedDirty?.(node);
281
346
  }
282
347
  /**
283
- * A node in the reactive graph.
284
- *
285
- * Nodes can be producers of reactive values, consumers of other reactive values, or both.
286
- *
287
- * Producers are nodes that produce values, and can be depended upon by consumer nodes.
288
- *
289
- * Producers expose a monotonic `valueVersion` counter, and are responsible for incrementing this
290
- * version when their value semantically changes. Some producers may produce their values lazily and
291
- * thus at times need to be polled for potential updates to their value (and by extension their
292
- * `valueVersion`). This is accomplished via the `onProducerUpdateValueVersion` method for
293
- * implemented by producers, which should perform whatever calculations are necessary to ensure
294
- * `valueVersion` is up to date.
295
- *
296
- * Consumers are nodes that depend on the values of producers and are notified when those values
297
- * might have changed.
298
- *
299
- * Consumers do not wrap the reads they consume themselves, but rather can be set as the active
300
- * reader via `setActiveConsumer`. Reads of producers that happen while a consumer is active will
301
- * result in those producers being added as dependencies of that consumer node.
348
+ * Prepare this consumer to run a computation in its reactive context.
302
349
  *
303
- * The set of dependencies of a consumer is dynamic. Implementers expose a monotonically increasing
304
- * `trackingVersion` counter, which increments whenever the consumer is about to re-run any reactive
305
- * reads it needs and establish a new set of dependencies as a result.
350
+ * Must be called by subclasses which represent reactive computations, before those computations
351
+ * begin.
352
+ */
353
+ function consumerBeforeComputation(node) {
354
+ node && (node.nextProducerIndex = 0);
355
+ return setActiveConsumer(node);
356
+ }
357
+ /**
358
+ * Finalize this consumer's state after a reactive computation has run.
306
359
  *
307
- * Producers store the last `trackingVersion` they've seen from `Consumer`s which have read them.
308
- * This allows a producer to identify whether its record of the dependency is current or stale, by
309
- * comparing the consumer's `trackingVersion` to the version at which the dependency was
310
- * last observed.
360
+ * Must be called by subclasses which represent reactive computations, after those computations
361
+ * have finished.
311
362
  */
312
- class ReactiveNode {
313
- constructor() {
314
- this.id = _nextReactiveId++;
315
- /**
316
- * A cached weak reference to this node, which will be used in `ReactiveEdge`s.
317
- */
318
- this.ref = newWeakRef(this);
319
- /**
320
- * Edges to producers on which this node depends (in its consumer capacity).
321
- */
322
- this.producers = new Map();
323
- /**
324
- * Edges to consumers on which this node depends (in its producer capacity).
325
- */
326
- this.consumers = new Map();
327
- /**
328
- * Monotonically increasing counter representing a version of this `Consumer`'s
329
- * dependencies.
330
- */
331
- this.trackingVersion = 0;
332
- /**
333
- * Monotonically increasing counter which increases when the value of this `Producer`
334
- * semantically changes.
335
- */
336
- this.valueVersion = 0;
337
- }
338
- /**
339
- * Polls dependencies of a consumer to determine if they have actually changed.
340
- *
341
- * If this returns `false`, then even though the consumer may have previously been notified of a
342
- * change, the values of its dependencies have not actually changed and the consumer should not
343
- * rerun any reactions.
344
- */
345
- consumerPollProducersForChange() {
346
- for (const [producerId, edge] of this.producers) {
347
- const producer = edge.producerNode.deref();
348
- // On Safari < 16.1 deref can return null, we need to check for null also.
349
- // See https://github.com/WebKit/WebKit/commit/44c15ba58912faab38b534fef909dd9e13e095e0
350
- if (producer == null || edge.atTrackingVersion !== this.trackingVersion) {
351
- // This dependency edge is stale, so remove it.
352
- this.producers.delete(producerId);
353
- producer?.consumers.delete(this.id);
354
- continue;
355
- }
356
- if (producer.producerPollStatus(edge.seenValueVersion)) {
357
- // One of the dependencies reports a real value change.
358
- return true;
359
- }
360
- }
361
- // No dependency reported a real value change, so the `Consumer` has also not been
362
- // impacted.
363
- return false;
364
- }
365
- /**
366
- * Notify all consumers of this producer that its value may have changed.
367
- */
368
- producerMayHaveChanged() {
369
- // Prevent signal reads when we're updating the graph
370
- const prev = inNotificationPhase;
371
- inNotificationPhase = true;
372
- try {
373
- for (const [consumerId, edge] of this.consumers) {
374
- const consumer = edge.consumerNode.deref();
375
- // On Safari < 16.1 deref can return null, we need to check for null also.
376
- // See https://github.com/WebKit/WebKit/commit/44c15ba58912faab38b534fef909dd9e13e095e0
377
- if (consumer == null || consumer.trackingVersion !== edge.atTrackingVersion) {
378
- this.consumers.delete(consumerId);
379
- consumer?.producers.delete(this.id);
380
- continue;
381
- }
382
- consumer.onConsumerDependencyMayHaveChanged();
383
- }
384
- }
385
- finally {
386
- inNotificationPhase = prev;
363
+ function consumerAfterComputation(node, prevConsumer) {
364
+ setActiveConsumer(prevConsumer);
365
+ if (!node || node.producerNode === undefined || node.producerIndexOfThis === undefined ||
366
+ node.producerLastReadVersion === undefined) {
367
+ return;
368
+ }
369
+ if (consumerIsLive(node)) {
370
+ // For live consumers, we need to remove the producer -> consumer edge for any stale producers
371
+ // which weren't dependencies after the recomputation.
372
+ for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
373
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
387
374
  }
388
375
  }
389
- /**
390
- * Mark that this producer node has been accessed in the current reactive context.
391
- */
392
- producerAccessed() {
393
- if (inNotificationPhase) {
394
- throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode ?
395
- `Assertion error: signal read during notification phase` :
396
- '');
376
+ // Truncate the producer tracking arrays.
377
+ for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
378
+ node.producerNode.pop();
379
+ node.producerLastReadVersion.pop();
380
+ node.producerIndexOfThis.pop();
381
+ }
382
+ }
383
+ /**
384
+ * Determine whether this consumer has any dependencies which have changed since the last time
385
+ * they were read.
386
+ */
387
+ function consumerPollProducersForChange(node) {
388
+ assertConsumerNode(node);
389
+ // Poll producers for change.
390
+ for (let i = 0; i < node.producerNode.length; i++) {
391
+ const producer = node.producerNode[i];
392
+ const seenVersion = node.producerLastReadVersion[i];
393
+ // First check the versions. A mismatch means that the producer's value is known to have
394
+ // changed since the last time we read it.
395
+ if (seenVersion !== producer.version) {
396
+ return true;
397
397
  }
398
- if (activeConsumer === null) {
399
- return;
398
+ // The producer's version is the same as the last time we read it, but it might itself be
399
+ // stale. Force the producer to recompute its version (calculating a new value if necessary).
400
+ producerUpdateValueVersion(producer);
401
+ // Now when we do this check, `producer.version` is guaranteed to be up to date, so if the
402
+ // versions still match then it has not changed since the last time we read it.
403
+ if (seenVersion !== producer.version) {
404
+ return true;
400
405
  }
401
- // Either create or update the dependency `Edge` in both directions.
402
- let edge = activeConsumer.producers.get(this.id);
403
- if (edge === undefined) {
404
- edge = {
405
- consumerNode: activeConsumer.ref,
406
- producerNode: this.ref,
407
- seenValueVersion: this.valueVersion,
408
- atTrackingVersion: activeConsumer.trackingVersion,
409
- };
410
- activeConsumer.producers.set(this.id, edge);
411
- this.consumers.set(activeConsumer.id, edge);
406
+ }
407
+ return false;
408
+ }
409
+ /**
410
+ * Disconnect this consumer from the graph.
411
+ */
412
+ function consumerDestroy(node) {
413
+ assertConsumerNode(node);
414
+ if (consumerIsLive(node)) {
415
+ // Drop all connections from the graph to this node.
416
+ for (let i = 0; i < node.producerNode.length; i++) {
417
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
412
418
  }
413
- else {
414
- edge.seenValueVersion = this.valueVersion;
415
- edge.atTrackingVersion = activeConsumer.trackingVersion;
419
+ }
420
+ // Truncate all the arrays to drop all connection from this node to the graph.
421
+ node.producerNode.length = node.producerLastReadVersion.length = node.producerIndexOfThis.length =
422
+ 0;
423
+ if (node.liveConsumerNode) {
424
+ node.liveConsumerNode.length = node.liveConsumerIndexOfThis.length = 0;
425
+ }
426
+ }
427
+ /**
428
+ * Add `consumer` as a live consumer of this node.
429
+ *
430
+ * Note that this operation is potentially transitive. If this node becomes live, then it becomes
431
+ * a live consumer of all of its current producers.
432
+ */
433
+ function producerAddLiveConsumer(node, consumer, indexOfThis) {
434
+ assertProducerNode(node);
435
+ assertConsumerNode(node);
436
+ if (node.liveConsumerNode.length === 0) {
437
+ // When going from 0 to 1 live consumers, we become a live consumer to our producers.
438
+ for (let i = 0; i < node.producerNode.length; i++) {
439
+ node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
416
440
  }
417
441
  }
418
- /**
419
- * Whether this consumer currently has any producers registered.
420
- */
421
- get hasProducers() {
422
- return this.producers.size > 0;
423
- }
424
- /**
425
- * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
426
- * based on the current consumer context.
427
- */
428
- get producerUpdatesAllowed() {
429
- return activeConsumer?.consumerAllowSignalWrites !== false;
430
- }
431
- /**
432
- * Checks if a `Producer` has a current value which is different than the value
433
- * last seen at a specific version by a `Consumer` which recorded a dependency on
434
- * this `Producer`.
435
- */
436
- producerPollStatus(lastSeenValueVersion) {
437
- // `producer.valueVersion` may be stale, but a mismatch still means that the value
438
- // last seen by the `Consumer` is also stale.
439
- if (this.valueVersion !== lastSeenValueVersion) {
440
- return true;
442
+ node.liveConsumerIndexOfThis.push(indexOfThis);
443
+ return node.liveConsumerNode.push(consumer) - 1;
444
+ }
445
+ /**
446
+ * Remove the live consumer at `idx`.
447
+ */
448
+ function producerRemoveLiveConsumerAtIndex(node, idx) {
449
+ assertProducerNode(node);
450
+ assertConsumerNode(node);
451
+ if (node.liveConsumerNode.length === 1) {
452
+ // When removing the last live consumer, we will no longer be live. We need to remove
453
+ // ourselves from our producers' tracking (which may cause consumer-producers to lose
454
+ // liveness as well).
455
+ for (let i = 0; i < node.producerNode.length; i++) {
456
+ producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
441
457
  }
442
- // Trigger the `Producer` to update its `valueVersion` if necessary.
443
- this.onProducerUpdateValueVersion();
444
- // At this point, we can trust `producer.valueVersion`.
445
- return this.valueVersion !== lastSeenValueVersion;
446
458
  }
459
+ // Move the last value of `liveConsumers` into `idx`. Note that if there's only a single
460
+ // live consumer, this is a no-op.
461
+ const lastIdx = node.liveConsumerNode.length - 1;
462
+ node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
463
+ node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
464
+ // Truncate the array.
465
+ node.liveConsumerNode.length--;
466
+ node.liveConsumerIndexOfThis.length--;
467
+ // If the index is still valid, then we need to fix the index pointer from the producer to this
468
+ // consumer, and update it from `lastIdx` to `idx` (accounting for the move above).
469
+ if (idx < node.liveConsumerNode.length) {
470
+ const idxProducer = node.liveConsumerIndexOfThis[idx];
471
+ const consumer = node.liveConsumerNode[idx];
472
+ assertConsumerNode(consumer);
473
+ consumer.producerIndexOfThis[idxProducer] = idx;
474
+ }
475
+ }
476
+ function consumerIsLive(node) {
477
+ return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0;
478
+ }
479
+ function assertConsumerNode(node) {
480
+ node.producerNode ??= [];
481
+ node.producerIndexOfThis ??= [];
482
+ node.producerLastReadVersion ??= [];
483
+ }
484
+ function assertProducerNode(node) {
485
+ node.liveConsumerNode ??= [];
486
+ node.liveConsumerIndexOfThis ??= [];
447
487
  }
448
488
 
449
489
  /**
@@ -452,10 +492,21 @@ class ReactiveNode {
452
492
  * @developerPreview
453
493
  */
454
494
  function computed(computation, options) {
455
- const node = new ComputedImpl(computation, options?.equal ?? defaultEquals);
456
- // Casting here is required for g3, as TS inference behavior is slightly different between our
457
- // version/options and g3's.
458
- return createSignalFromFunction(node, node.signal.bind(node));
495
+ const node = Object.create(COMPUTED_NODE);
496
+ node.computation = computation;
497
+ options?.equal && (node.equal = options.equal);
498
+ const computed = () => {
499
+ // Check if the value needs updating before returning it.
500
+ producerUpdateValueVersion(node);
501
+ // Record that someone looked at this signal.
502
+ producerAccessed(node);
503
+ if (node.value === ERRORED) {
504
+ throw node.error;
505
+ }
506
+ return node.value;
507
+ };
508
+ computed[SIGNAL] = node;
509
+ return computed;
459
510
  }
460
511
  /**
461
512
  * A dedicated symbol used before a computed value has been calculated for the first time.
@@ -474,108 +525,47 @@ const COMPUTING = Symbol('COMPUTING');
474
525
  * Explicitly typed as `any` so we can use it as signal's value.
475
526
  */
476
527
  const ERRORED = Symbol('ERRORED');
477
- /**
478
- * A computation, which derives a value from a declarative reactive expression.
479
- *
480
- * `Computed`s are both producers and consumers of reactivity.
481
- */
482
- class ComputedImpl extends ReactiveNode {
483
- constructor(computation, equal) {
484
- super();
485
- this.computation = computation;
486
- this.equal = equal;
487
- /**
488
- * Current value of the computation.
489
- *
490
- * This can also be one of the special values `UNSET`, `COMPUTING`, or `ERRORED`.
491
- */
492
- this.value = UNSET;
493
- /**
494
- * If `value` is `ERRORED`, the error caught from the last computation attempt which will
495
- * be re-thrown.
496
- */
497
- this.error = null;
498
- /**
499
- * Flag indicating that the computation is currently stale, meaning that one of the
500
- * dependencies has notified of a potential change.
501
- *
502
- * It's possible that no dependency has _actually_ changed, in which case the `stale`
503
- * state can be resolved without recomputing the value.
504
- */
505
- this.stale = true;
506
- this.consumerAllowSignalWrites = false;
507
- }
508
- onConsumerDependencyMayHaveChanged() {
509
- if (this.stale) {
510
- // We've already notified consumers that this value has potentially changed.
511
- return;
512
- }
513
- // Record that the currently cached value may be stale.
514
- this.stale = true;
515
- // Notify any consumers about the potential change.
516
- this.producerMayHaveChanged();
517
- }
518
- onProducerUpdateValueVersion() {
519
- if (!this.stale) {
520
- // The current value and its version are already up to date.
521
- return;
522
- }
523
- // The current value is stale. Check whether we need to produce a new one.
524
- if (this.value !== UNSET && this.value !== COMPUTING &&
525
- !this.consumerPollProducersForChange()) {
526
- // Even though we were previously notified of a potential dependency update, all of
527
- // our dependencies report that they have not actually changed in value, so we can
528
- // resolve the stale state without needing to recompute the current value.
529
- this.stale = false;
530
- return;
531
- }
532
- // The current value is stale, and needs to be recomputed. It still may not change -
533
- // that depends on whether the newly computed value is equal to the old.
534
- this.recomputeValue();
535
- }
536
- recomputeValue() {
537
- if (this.value === COMPUTING) {
528
+ const COMPUTED_NODE = {
529
+ ...REACTIVE_NODE,
530
+ value: UNSET,
531
+ dirty: true,
532
+ error: null,
533
+ equal: defaultEquals,
534
+ producerMustRecompute(node) {
535
+ // Force a recomputation if there's no current value, or if the current value is in the process
536
+ // of being calculated (which should throw an error).
537
+ return node.value === UNSET || node.value === COMPUTING;
538
+ },
539
+ producerRecomputeValue(node) {
540
+ if (node.value === COMPUTING) {
538
541
  // Our computation somehow led to a cyclic read of itself.
539
542
  throw new Error('Detected cycle in computations.');
540
543
  }
541
- const oldValue = this.value;
542
- this.value = COMPUTING;
543
- // As we're re-running the computation, update our dependent tracking version number.
544
- this.trackingVersion++;
545
- const prevConsumer = setActiveConsumer(this);
544
+ const oldValue = node.value;
545
+ node.value = COMPUTING;
546
+ const prevConsumer = consumerBeforeComputation(node);
546
547
  let newValue;
547
548
  try {
548
- newValue = this.computation();
549
+ newValue = node.computation();
549
550
  }
550
551
  catch (err) {
551
552
  newValue = ERRORED;
552
- this.error = err;
553
+ node.error = err;
553
554
  }
554
555
  finally {
555
- setActiveConsumer(prevConsumer);
556
+ consumerAfterComputation(node, prevConsumer);
556
557
  }
557
- this.stale = false;
558
558
  if (oldValue !== UNSET && oldValue !== ERRORED && newValue !== ERRORED &&
559
- this.equal(oldValue, newValue)) {
559
+ node.equal(oldValue, newValue)) {
560
560
  // No change to `valueVersion` - old and new values are
561
561
  // semantically equivalent.
562
- this.value = oldValue;
562
+ node.value = oldValue;
563
563
  return;
564
564
  }
565
- this.value = newValue;
566
- this.valueVersion++;
567
- }
568
- signal() {
569
- // Check if the value needs updating before returning it.
570
- this.onProducerUpdateValueVersion();
571
- // Record that someone looked at this signal.
572
- this.producerAccessed();
573
- if (this.value === ERRORED) {
574
- throw this.error;
575
- }
576
- return this.value;
577
- }
578
- }
565
+ node.value = newValue;
566
+ node.version++;
567
+ },
568
+ };
579
569
 
580
570
  function defaultThrowError() {
581
571
  throw new Error();
@@ -595,88 +585,24 @@ function setThrowInvalidWriteToSignalError(fn) {
595
585
  * of setting a signal.
596
586
  */
597
587
  let postSignalSetFn = null;
598
- class WritableSignalImpl extends ReactiveNode {
599
- constructor(value, equal) {
600
- super();
601
- this.value = value;
602
- this.equal = equal;
603
- this.consumerAllowSignalWrites = false;
604
- }
605
- onConsumerDependencyMayHaveChanged() {
606
- // This never happens for writable signals as they're not consumers.
607
- }
608
- onProducerUpdateValueVersion() {
609
- // Writable signal value versions are always up to date.
610
- }
611
- /**
612
- * Directly update the value of the signal to a new value, which may or may not be
613
- * equal to the previous.
614
- *
615
- * In the event that `newValue` is semantically equal to the current value, `set` is
616
- * a no-op.
617
- */
618
- set(newValue) {
619
- if (!this.producerUpdatesAllowed) {
620
- throwInvalidWriteToSignalError();
621
- }
622
- if (!this.equal(this.value, newValue)) {
623
- this.value = newValue;
624
- this.valueVersion++;
625
- this.producerMayHaveChanged();
626
- postSignalSetFn?.();
627
- }
628
- }
629
- /**
630
- * Derive a new value for the signal from its current value using the `updater` function.
631
- *
632
- * This is equivalent to calling `set` on the result of running `updater` on the current
633
- * value.
634
- */
635
- update(updater) {
636
- if (!this.producerUpdatesAllowed) {
637
- throwInvalidWriteToSignalError();
638
- }
639
- this.set(updater(this.value));
640
- }
641
- /**
642
- * Calls `mutator` on the current value and assumes that it has been mutated.
643
- */
644
- mutate(mutator) {
645
- if (!this.producerUpdatesAllowed) {
646
- throwInvalidWriteToSignalError();
647
- }
648
- // Mutate bypasses equality checks as it's by definition changing the value.
649
- mutator(this.value);
650
- this.valueVersion++;
651
- this.producerMayHaveChanged();
652
- postSignalSetFn?.();
653
- }
654
- asReadonly() {
655
- if (this.readonlySignal === undefined) {
656
- this.readonlySignal = createSignalFromFunction(this, () => this.signal());
657
- }
658
- return this.readonlySignal;
659
- }
660
- signal() {
661
- this.producerAccessed();
662
- return this.value;
663
- }
664
- }
665
588
  /**
666
589
  * Create a `Signal` that can be set or updated directly.
667
590
  *
668
591
  * @developerPreview
669
592
  */
670
593
  function signal(initialValue, options) {
671
- const signalNode = new WritableSignalImpl(initialValue, options?.equal ?? defaultEquals);
672
- // Casting here is required for g3, as TS inference behavior is slightly different between our
673
- // version/options and g3's.
674
- const signalFn = createSignalFromFunction(signalNode, signalNode.signal.bind(signalNode), {
675
- set: signalNode.set.bind(signalNode),
676
- update: signalNode.update.bind(signalNode),
677
- mutate: signalNode.mutate.bind(signalNode),
678
- asReadonly: signalNode.asReadonly.bind(signalNode)
679
- });
594
+ const node = Object.create(SIGNAL_NODE);
595
+ node.value = initialValue;
596
+ options?.equal && (node.equal = options.equal);
597
+ function signalFn() {
598
+ producerAccessed(node);
599
+ return node.value;
600
+ }
601
+ signalFn.set = signalSetFn;
602
+ signalFn.update = signalUpdateFn;
603
+ signalFn.mutate = signalMutateFn;
604
+ signalFn.asReadonly = signalAsReadonlyFn;
605
+ signalFn[SIGNAL] = node;
680
606
  return signalFn;
681
607
  }
682
608
  function setPostSignalSetFn(fn) {
@@ -684,6 +610,50 @@ function setPostSignalSetFn(fn) {
684
610
  postSignalSetFn = fn;
685
611
  return prev;
686
612
  }
613
+ const SIGNAL_NODE = {
614
+ ...REACTIVE_NODE,
615
+ equal: defaultEquals,
616
+ readonlyFn: undefined,
617
+ };
618
+ function signalValueChanged(node) {
619
+ node.version++;
620
+ producerNotifyConsumers(node);
621
+ postSignalSetFn?.();
622
+ }
623
+ function signalSetFn(newValue) {
624
+ const node = this[SIGNAL];
625
+ if (!producerUpdatesAllowed()) {
626
+ throwInvalidWriteToSignalError();
627
+ }
628
+ if (!node.equal(node.value, newValue)) {
629
+ node.value = newValue;
630
+ signalValueChanged(node);
631
+ }
632
+ }
633
+ function signalUpdateFn(updater) {
634
+ if (!producerUpdatesAllowed()) {
635
+ throwInvalidWriteToSignalError();
636
+ }
637
+ signalSetFn.call(this, updater(this[SIGNAL].value));
638
+ }
639
+ function signalMutateFn(mutator) {
640
+ const node = this[SIGNAL];
641
+ if (!producerUpdatesAllowed()) {
642
+ throwInvalidWriteToSignalError();
643
+ }
644
+ // Mutate bypasses equality checks as it's by definition changing the value.
645
+ mutator(node.value);
646
+ signalValueChanged(node);
647
+ }
648
+ function signalAsReadonlyFn() {
649
+ const node = this[SIGNAL];
650
+ if (node.readonlyFn === undefined) {
651
+ const readonlyFn = () => this();
652
+ readonlyFn[SIGNAL] = node;
653
+ node.readonlyFn = readonlyFn;
654
+ }
655
+ return node.readonlyFn;
656
+ }
687
657
 
688
658
  /**
689
659
  * Execute an arbitrary function in a non-reactive (non-tracking) context. The executed function
@@ -703,63 +673,53 @@ function untracked(nonReactiveReadsFn) {
703
673
  }
704
674
  }
705
675
 
706
- const NOOP_CLEANUP_FN = () => { };
707
- /**
708
- * Watches a reactive expression and allows it to be scheduled to re-run
709
- * when any dependencies notify of a change.
710
- *
711
- * `Watch` doesn't run reactive expressions itself, but relies on a consumer-
712
- * provided scheduling operation to coordinate calling `Watch.run()`.
713
- */
714
- class Watch extends ReactiveNode {
715
- constructor(watch, schedule, allowSignalWrites) {
716
- super();
717
- this.watch = watch;
718
- this.schedule = schedule;
719
- this.dirty = false;
720
- this.cleanupFn = NOOP_CLEANUP_FN;
721
- this.registerOnCleanup = (cleanupFn) => {
722
- this.cleanupFn = cleanupFn;
723
- };
724
- this.consumerAllowSignalWrites = allowSignalWrites;
725
- }
726
- notify() {
727
- if (!this.dirty) {
728
- this.schedule(this);
729
- }
730
- this.dirty = true;
731
- }
732
- onConsumerDependencyMayHaveChanged() {
733
- this.notify();
734
- }
735
- onProducerUpdateValueVersion() {
736
- // Watches are not producers.
737
- }
738
- /**
739
- * Execute the reactive expression in the context of this `Watch` consumer.
740
- *
741
- * Should be called by the user scheduling algorithm when the provided
742
- * `schedule` hook is called by `Watch`.
743
- */
744
- run() {
745
- this.dirty = false;
746
- if (this.trackingVersion !== 0 && !this.consumerPollProducersForChange()) {
676
+ function watch(fn, schedule, allowSignalWrites) {
677
+ const node = Object.create(WATCH_NODE);
678
+ if (allowSignalWrites) {
679
+ node.consumerAllowSignalWrites = true;
680
+ }
681
+ node.fn = fn;
682
+ node.schedule = schedule;
683
+ const registerOnCleanup = (cleanupFn) => {
684
+ node.cleanupFn = cleanupFn;
685
+ };
686
+ const run = () => {
687
+ node.dirty = false;
688
+ if (node.hasRun && !consumerPollProducersForChange(node)) {
747
689
  return;
748
690
  }
749
- const prevConsumer = setActiveConsumer(this);
750
- this.trackingVersion++;
691
+ node.hasRun = true;
692
+ const prevConsumer = consumerBeforeComputation(node);
751
693
  try {
752
- this.cleanupFn();
753
- this.cleanupFn = NOOP_CLEANUP_FN;
754
- this.watch(this.registerOnCleanup);
694
+ node.cleanupFn();
695
+ node.cleanupFn = NOOP_CLEANUP_FN;
696
+ node.fn(registerOnCleanup);
755
697
  }
756
698
  finally {
757
- setActiveConsumer(prevConsumer);
699
+ consumerAfterComputation(node, prevConsumer);
758
700
  }
759
- }
760
- cleanup() {
761
- this.cleanupFn();
762
- }
701
+ };
702
+ node.ref = {
703
+ notify: () => consumerMarkDirty(node),
704
+ run,
705
+ cleanup: () => node.cleanupFn(),
706
+ };
707
+ return node.ref;
708
+ }
709
+ const NOOP_CLEANUP_FN = () => { };
710
+ const WATCH_NODE = {
711
+ ...REACTIVE_NODE,
712
+ consumerIsAlwaysLive: true,
713
+ consumerAllowSignalWrites: false,
714
+ consumerMarkedDirty: (node) => {
715
+ node.schedule(node.ref);
716
+ },
717
+ hasRun: false,
718
+ cleanupFn: NOOP_CLEANUP_FN,
719
+ };
720
+
721
+ function setAlternateWeakRefImpl(impl) {
722
+ // TODO: remove this function
763
723
  }
764
724
 
765
725
  function toSignal(source, options) {