UNPKG

@angular/core

Version:

Angular - the core framework

561 lines (554 loc) • 20.6 kB
/** * @license Angular v20.1.4 * (c) 2010-2025 Google LLC. https://angular.io/ * License: MIT */ /** * The default equality function used for `signal` and `computed`, which uses referential equality. */ function defaultEquals(a, b) { return Object.is(a, b); } /** * The currently active consumer `ReactiveNode`, if running code in a reactive context. * * Change this via `setActiveConsumer`. */ let activeConsumer = null; let inNotificationPhase = false; /** * Global epoch counter. Incremented whenever a source signal is set. */ let epoch = 1; /** * If set, called after a producer `ReactiveNode` is created. */ let postProducerCreatedFn = null; /** * Symbol used to tell `Signal`s apart from other functions. * * This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values. */ const SIGNAL = /* @__PURE__ */ Symbol('SIGNAL'); function setActiveConsumer(consumer) { const prev = activeConsumer; activeConsumer = consumer; return prev; } function getActiveConsumer() { return activeConsumer; } function isInNotificationPhase() { return inNotificationPhase; } function isReactive(value) { return value[SIGNAL] !== undefined; } const REACTIVE_NODE = { version: 0, lastCleanEpoch: 0, dirty: false, producers: undefined, producersTail: undefined, consumers: undefined, consumersTail: undefined, recomputing: false, consumerAllowSignalWrites: false, consumerIsAlwaysLive: false, kind: 'unknown', producerMustRecompute: () => false, producerRecomputeValue: () => { }, consumerMarkedDirty: () => { }, consumerOnSignalRead: () => { }, }; /** * Called by implementations when a producer's signal is read. */ function producerAccessed(node) { if (inNotificationPhase) { throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode ? `Assertion error: signal read during notification phase` : ''); } if (activeConsumer === null) { // Accessed outside of a reactive context, so nothing to record. return; } activeConsumer.consumerOnSignalRead(node); const prevProducerLink = activeConsumer.producersTail; // If the last producer we accessed is the same as the current one, we can skip adding a new // link if (prevProducerLink !== undefined && prevProducerLink.producer === node) { return; } let nextProducerLink = undefined; const isRecomputing = activeConsumer.recomputing; if (isRecomputing) { // If we're incrementally rebuilding the producers list, we want to check if the next producer // in the list is the same as the one we're trying to add. // If the previous producer is defined, then the next producer is just the one that follows it. // Otherwise, we should check the head of the producers list (the first node that we accessed the last time this consumer was run). nextProducerLink = prevProducerLink !== undefined ? prevProducerLink.nextProducer : activeConsumer.producers; if (nextProducerLink !== undefined && nextProducerLink.producer === node) { // If the next producer is the same as the one we're trying to add, we can just update the // last read version, update the tail of the producers list of this rerun, and return. activeConsumer.producersTail = nextProducerLink; nextProducerLink.lastReadVersion = node.version; return; } } const prevConsumerLink = node.consumersTail; // If the producer we're accessing already has a link to this consumer, we can skip adding a new // link. This can short circuit the creation of a new link in the case where the consumer reads alternating ReeactiveNodes if (prevConsumerLink !== undefined && prevConsumerLink.consumer === activeConsumer && // However, we have to make sure that the link we've discovered isn't from a node that is incrementally rebuilding its producer list (!isRecomputing || isValidLink(prevConsumerLink, activeConsumer))) { // If we found an existing link to the consumer we can just return. return; } // If we got here, it means that we need to create a new link between the producer and the consumer. const isLive = consumerIsLive(activeConsumer); const newLink = { producer: node, consumer: activeConsumer, // instead of eagerly destroying the previous link, we delay until we've finished recomputing // the producers list, so that we can destroy all of the old links at once. nextProducer: nextProducerLink, prevConsumer: prevConsumerLink, lastReadVersion: node.version, nextConsumer: undefined, }; activeConsumer.producersTail = newLink; if (prevProducerLink !== undefined) { prevProducerLink.nextProducer = newLink; } else { activeConsumer.producers = newLink; } if (isLive) { producerAddLiveConsumer(node, newLink); } } /** * Increment the global epoch counter. * * Called by source producers (that is, not computeds) whenever their values change. */ function producerIncrementEpoch() { epoch++; } /** * Ensure this producer's `version` is up-to-date. */ function producerUpdateValueVersion(node) { if (consumerIsLive(node) && !node.dirty) { // A live consumer will be marked dirty by producers, so a clean state means that its version // is guaranteed to be up-to-date. return; } if (!node.dirty && node.lastCleanEpoch === epoch) { // Even non-live consumers can skip polling if they previously found themselves to be clean at // the current epoch, since their dependencies could not possibly have changed (such a change // would've increased the epoch). return; } if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) { // None of our producers report a change since the last time they were read, so no // recomputation of our value is necessary, and we can consider ourselves clean. producerMarkClean(node); return; } node.producerRecomputeValue(node); // After recomputing the value, we're no longer dirty. producerMarkClean(node); } /** * Propagate a dirty notification to live consumers of this producer. */ function producerNotifyConsumers(node) { if (node.consumers === undefined) { return; } // Prevent signal reads when we're updating the graph const prev = inNotificationPhase; inNotificationPhase = true; try { for (let link = node.consumers; link !== undefined; link = link.nextConsumer) { const consumer = link.consumer; if (!consumer.dirty) { consumerMarkDirty(consumer); } } } finally { inNotificationPhase = prev; } } /** * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates, * based on the current consumer context. */ function producerUpdatesAllowed() { return activeConsumer?.consumerAllowSignalWrites !== false; } function consumerMarkDirty(node) { node.dirty = true; producerNotifyConsumers(node); node.consumerMarkedDirty?.(node); } function producerMarkClean(node) { node.dirty = false; node.lastCleanEpoch = epoch; } /** * Prepare this consumer to run a computation in its reactive context. * * Must be called by subclasses which represent reactive computations, before those computations * begin. */ function consumerBeforeComputation(node) { if (node) { node.producersTail = undefined; node.recomputing = true; } return setActiveConsumer(node); } /** * Finalize this consumer's state after a reactive computation has run. * * Must be called by subclasses which represent reactive computations, after those computations * have finished. */ function consumerAfterComputation(node, prevConsumer) { setActiveConsumer(prevConsumer); if (!node) { return; } node.recomputing = false; // We've finished incrementally rebuilding the producers list, now if there are any producers // that are after producersTail, they are stale and should be removed. const producersTail = node.producersTail; let toRemove = producersTail !== undefined ? producersTail.nextProducer : node.producers; if (toRemove !== undefined) { if (consumerIsLive(node)) { // For each stale link, we first unlink it from the producers list of consumers do { toRemove = producerRemoveLiveConsumerLink(toRemove); } while (toRemove !== undefined); } // Now, we can truncate the producers list to remove all stale links. if (producersTail !== undefined) { producersTail.nextProducer = undefined; } else { node.producers = undefined; } } } /** * Determine whether this consumer has any dependencies which have changed since the last time * they were read. */ function consumerPollProducersForChange(node) { // Poll producers for change. for (let link = node.producers; link !== undefined; link = link.nextProducer) { const producer = link.producer; const seenVersion = link.lastReadVersion; // First check the versions. A mismatch means that the producer's value is known to have // changed since the last time we read it. if (seenVersion !== producer.version) { return true; } // The producer's version is the same as the last time we read it, but it might itself be // stale. Force the producer to recompute its version (calculating a new value if necessary). producerUpdateValueVersion(producer); // Now when we do this check, `producer.version` is guaranteed to be up to date, so if the // versions still match then it has not changed since the last time we read it. if (seenVersion !== producer.version) { return true; } } return false; } /** * Disconnect this consumer from the graph. */ function consumerDestroy(node) { if (consumerIsLive(node)) { // Drop all connections from the graph to this node. let link = node.producers; while (link !== undefined) { link = producerRemoveLiveConsumerLink(link); } } // Truncate all the linked lists to drop all connection from this node to the graph. node.producers = undefined; node.producersTail = undefined; node.consumers = undefined; node.consumersTail = undefined; } /** * Add `consumer` as a live consumer of this node. * * Note that this operation is potentially transitive. If this node becomes live, then it becomes * a live consumer of all of its current producers. */ function producerAddLiveConsumer(node, link) { const consumersTail = node.consumersTail; const wasLive = consumerIsLive(node); if (consumersTail !== undefined) { link.nextConsumer = consumersTail.nextConsumer; consumersTail.nextConsumer = link; } else { link.nextConsumer = undefined; node.consumers = link; } link.prevConsumer = consumersTail; node.consumersTail = link; if (!wasLive) { for (let link = node.producers; link !== undefined; link = link.nextProducer) { producerAddLiveConsumer(link.producer, link); } } } function producerRemoveLiveConsumerLink(link) { const producer = link.producer; const nextProducer = link.nextProducer; const nextConsumer = link.nextConsumer; const prevConsumer = link.prevConsumer; link.nextConsumer = undefined; link.prevConsumer = undefined; if (nextConsumer !== undefined) { nextConsumer.prevConsumer = prevConsumer; } else { producer.consumersTail = prevConsumer; } if (prevConsumer !== undefined) { prevConsumer.nextConsumer = nextConsumer; } else { producer.consumers = nextConsumer; if (!consumerIsLive(producer)) { let producerLink = producer.producers; while (producerLink !== undefined) { producerLink = producerRemoveLiveConsumerLink(producerLink); } } } return nextProducer; } function consumerIsLive(node) { return node.consumerIsAlwaysLive || node.consumers !== undefined; } function runPostProducerCreatedFn(node) { postProducerCreatedFn?.(node); } function setPostProducerCreatedFn(fn) { const prev = postProducerCreatedFn; postProducerCreatedFn = fn; return prev; } // While a ReactiveNode is recomputing, it may not have destroyed previous links // This allows us to check if a given link will be destroyed by a reactivenode if it were to finish running immediately without accesing any more producers function isValidLink(checkLink, consumer) { const producersTail = consumer.producersTail; if (producersTail !== undefined) { let link = consumer.producers; do { if (link === checkLink) { return true; } if (link === producersTail) { break; } link = link.nextProducer; } while (link !== undefined); } return false; } /** * Create a computed signal which derives a reactive value from an expression. */ function createComputed(computation, equal) { const node = Object.create(COMPUTED_NODE); node.computation = computation; if (equal !== undefined) { node.equal = equal; } const computed = () => { // Check if the value needs updating before returning it. producerUpdateValueVersion(node); // Record that someone looked at this signal. producerAccessed(node); if (node.value === ERRORED) { throw node.error; } return node.value; }; computed[SIGNAL] = node; if (typeof ngDevMode !== 'undefined' && ngDevMode) { const debugName = node.debugName ? ' (' + node.debugName + ')' : ''; computed.toString = () => `[Computed${debugName}: ${node.value}]`; } runPostProducerCreatedFn(node); return computed; } /** * A dedicated symbol used before a computed value has been calculated for the first time. * Explicitly typed as `any` so we can use it as signal's value. */ const UNSET = /* @__PURE__ */ Symbol('UNSET'); /** * A dedicated symbol used in place of a computed signal value to indicate that a given computation * is in progress. Used to detect cycles in computation chains. * Explicitly typed as `any` so we can use it as signal's value. */ const COMPUTING = /* @__PURE__ */ Symbol('COMPUTING'); /** * A dedicated symbol used in place of a computed signal value to indicate that a given computation * failed. The thrown error is cached until the computation gets dirty again. * Explicitly typed as `any` so we can use it as signal's value. */ const ERRORED = /* @__PURE__ */ Symbol('ERRORED'); // Note: Using an IIFE here to ensure that the spread assignment is not considered // a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`. // TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved. const COMPUTED_NODE = /* @__PURE__ */ (() => { return { ...REACTIVE_NODE, value: UNSET, dirty: true, error: null, equal: defaultEquals, kind: 'computed', producerMustRecompute(node) { // Force a recomputation if there's no current value, or if the current value is in the // process of being calculated (which should throw an error). return node.value === UNSET || node.value === COMPUTING; }, producerRecomputeValue(node) { if (node.value === COMPUTING) { // Our computation somehow led to a cyclic read of itself. throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode ? 'Detected cycle in computations.' : ''); } const oldValue = node.value; node.value = COMPUTING; const prevConsumer = consumerBeforeComputation(node); let newValue; let wasEqual = false; try { newValue = node.computation(); // We want to mark this node as errored if calling `equal` throws; however, we don't want // to track any reactive reads inside `equal`. setActiveConsumer(null); wasEqual = oldValue !== UNSET && oldValue !== ERRORED && newValue !== ERRORED && node.equal(oldValue, newValue); } catch (err) { newValue = ERRORED; node.error = err; } finally { consumerAfterComputation(node, prevConsumer); } if (wasEqual) { // No change to `valueVersion` - old and new values are // semantically equivalent. node.value = oldValue; return; } node.value = newValue; node.version++; }, }; })(); function defaultThrowError() { throw new Error(); } let throwInvalidWriteToSignalErrorFn = defaultThrowError; function throwInvalidWriteToSignalError(node) { throwInvalidWriteToSignalErrorFn(node); } function setThrowInvalidWriteToSignalError(fn) { throwInvalidWriteToSignalErrorFn = fn; } /** * If set, called after `WritableSignal`s are updated. * * This hook can be used to achieve various effects, such as running effects synchronously as part * of setting a signal. */ let postSignalSetFn = null; /** * Creates a `Signal` getter, setter, and updater function. */ function createSignal(initialValue, equal) { const node = Object.create(SIGNAL_NODE); node.value = initialValue; if (equal !== undefined) { node.equal = equal; } const getter = (() => signalGetFn(node)); getter[SIGNAL] = node; if (typeof ngDevMode !== 'undefined' && ngDevMode) { const debugName = node.debugName ? ' (' + node.debugName + ')' : ''; getter.toString = () => `[Signal${debugName}: ${node.value}]`; } runPostProducerCreatedFn(node); const set = (newValue) => signalSetFn(node, newValue); const update = (updateFn) => signalUpdateFn(node, updateFn); return [getter, set, update]; } function setPostSignalSetFn(fn) { const prev = postSignalSetFn; postSignalSetFn = fn; return prev; } function signalGetFn(node) { producerAccessed(node); return node.value; } function signalSetFn(node, newValue) { if (!producerUpdatesAllowed()) { throwInvalidWriteToSignalError(node); } if (!node.equal(node.value, newValue)) { node.value = newValue; signalValueChanged(node); } } function signalUpdateFn(node, updater) { if (!producerUpdatesAllowed()) { throwInvalidWriteToSignalError(node); } signalSetFn(node, updater(node.value)); } function runPostSignalSetFn(node) { postSignalSetFn?.(node); } // Note: Using an IIFE here to ensure that the spread assignment is not considered // a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`. // TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved. const SIGNAL_NODE = /* @__PURE__ */ (() => { return { ...REACTIVE_NODE, equal: defaultEquals, value: undefined, kind: 'signal', }; })(); function signalValueChanged(node) { node.version++; producerIncrementEpoch(); producerNotifyConsumers(node); postSignalSetFn?.(node); } export { COMPUTING, ERRORED, REACTIVE_NODE, SIGNAL, SIGNAL_NODE, UNSET, consumerAfterComputation, consumerBeforeComputation, consumerDestroy, consumerMarkDirty, consumerPollProducersForChange, createComputed, createSignal, defaultEquals, getActiveConsumer, isInNotificationPhase, isReactive, producerAccessed, producerIncrementEpoch, producerMarkClean, producerNotifyConsumers, producerUpdateValueVersion, producerUpdatesAllowed, runPostProducerCreatedFn, runPostSignalSetFn, setActiveConsumer, setPostProducerCreatedFn, setPostSignalSetFn, setThrowInvalidWriteToSignalError, signalGetFn, signalSetFn, signalUpdateFn }; //# sourceMappingURL=signal.mjs.map