signal.mjs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. /**
  2. * @license Angular v20.1.0
  3. * (c) 2010-2025 Google LLC. https://angular.io/
  4. * License: MIT
  5. */
  6. /**
  7. * The default equality function used for `signal` and `computed`, which uses referential equality.
  8. */
  9. function defaultEquals(a, b) {
  10. return Object.is(a, b);
  11. }
  12. /**
  13. * The currently active consumer `ReactiveNode`, if running code in a reactive context.
  14. *
  15. * Change this via `setActiveConsumer`.
  16. */
  17. let activeConsumer = null;
  18. let inNotificationPhase = false;
  19. /**
  20. * Global epoch counter. Incremented whenever a source signal is set.
  21. */
  22. let epoch = 1;
  23. /**
  24. * If set, called after a producer `ReactiveNode` is created.
  25. */
  26. let postProducerCreatedFn = null;
  27. /**
  28. * Symbol used to tell `Signal`s apart from other functions.
  29. *
  30. * This can be used to auto-unwrap signals in various cases, or to auto-wrap non-signal values.
  31. */
  32. const SIGNAL = /* @__PURE__ */ Symbol('SIGNAL');
  33. function setActiveConsumer(consumer) {
  34. const prev = activeConsumer;
  35. activeConsumer = consumer;
  36. return prev;
  37. }
  38. function getActiveConsumer() {
  39. return activeConsumer;
  40. }
  41. function isInNotificationPhase() {
  42. return inNotificationPhase;
  43. }
  44. function isReactive(value) {
  45. return value[SIGNAL] !== undefined;
  46. }
  47. const REACTIVE_NODE = {
  48. version: 0,
  49. lastCleanEpoch: 0,
  50. dirty: false,
  51. producerNode: undefined,
  52. producerLastReadVersion: undefined,
  53. producerIndexOfThis: undefined,
  54. nextProducerIndex: 0,
  55. liveConsumerNode: undefined,
  56. liveConsumerIndexOfThis: undefined,
  57. consumerAllowSignalWrites: false,
  58. consumerIsAlwaysLive: false,
  59. kind: 'unknown',
  60. producerMustRecompute: () => false,
  61. producerRecomputeValue: () => { },
  62. consumerMarkedDirty: () => { },
  63. consumerOnSignalRead: () => { },
  64. };
  65. /**
  66. * Called by implementations when a producer's signal is read.
  67. */
  68. function producerAccessed(node) {
  69. if (inNotificationPhase) {
  70. throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode
  71. ? `Assertion error: signal read during notification phase`
  72. : '');
  73. }
  74. if (activeConsumer === null) {
  75. // Accessed outside of a reactive context, so nothing to record.
  76. return;
  77. }
  78. activeConsumer.consumerOnSignalRead(node);
  79. // This producer is the `idx`th dependency of `activeConsumer`.
  80. const idx = activeConsumer.nextProducerIndex++;
  81. assertConsumerNode(activeConsumer);
  82. if (idx < activeConsumer.producerNode.length && activeConsumer.producerNode[idx] !== node) {
  83. // There's been a change in producers since the last execution of `activeConsumer`.
  84. // `activeConsumer.producerNode[idx]` holds a stale dependency which will be be removed and
  85. // replaced with `this`.
  86. //
  87. // If `activeConsumer` isn't live, then this is a no-op, since we can replace the producer in
  88. // `activeConsumer.producerNode` directly. However, if `activeConsumer` is live, then we need
  89. // to remove it from the stale producer's `liveConsumer`s.
  90. if (consumerIsLive(activeConsumer)) {
  91. const staleProducer = activeConsumer.producerNode[idx];
  92. producerRemoveLiveConsumerAtIndex(staleProducer, activeConsumer.producerIndexOfThis[idx]);
  93. // At this point, the only record of `staleProducer` is the reference at
  94. // `activeConsumer.producerNode[idx]` which will be overwritten below.
  95. }
  96. }
  97. if (activeConsumer.producerNode[idx] !== node) {
  98. // We're a new dependency of the consumer (at `idx`).
  99. activeConsumer.producerNode[idx] = node;
  100. // If the active consumer is live, then add it as a live consumer. If not, then use 0 as a
  101. // placeholder value.
  102. activeConsumer.producerIndexOfThis[idx] = consumerIsLive(activeConsumer)
  103. ? producerAddLiveConsumer(node, activeConsumer, idx)
  104. : 0;
  105. }
  106. activeConsumer.producerLastReadVersion[idx] = node.version;
  107. }
  108. /**
  109. * Increment the global epoch counter.
  110. *
  111. * Called by source producers (that is, not computeds) whenever their values change.
  112. */
  113. function producerIncrementEpoch() {
  114. epoch++;
  115. }
  116. /**
  117. * Ensure this producer's `version` is up-to-date.
  118. */
  119. function producerUpdateValueVersion(node) {
  120. if (consumerIsLive(node) && !node.dirty) {
  121. // A live consumer will be marked dirty by producers, so a clean state means that its version
  122. // is guaranteed to be up-to-date.
  123. return;
  124. }
  125. if (!node.dirty && node.lastCleanEpoch === epoch) {
  126. // Even non-live consumers can skip polling if they previously found themselves to be clean at
  127. // the current epoch, since their dependencies could not possibly have changed (such a change
  128. // would've increased the epoch).
  129. return;
  130. }
  131. if (!node.producerMustRecompute(node) && !consumerPollProducersForChange(node)) {
  132. // None of our producers report a change since the last time they were read, so no
  133. // recomputation of our value is necessary, and we can consider ourselves clean.
  134. producerMarkClean(node);
  135. return;
  136. }
  137. node.producerRecomputeValue(node);
  138. // After recomputing the value, we're no longer dirty.
  139. producerMarkClean(node);
  140. }
  141. /**
  142. * Propagate a dirty notification to live consumers of this producer.
  143. */
  144. function producerNotifyConsumers(node) {
  145. if (node.liveConsumerNode === undefined) {
  146. return;
  147. }
  148. // Prevent signal reads when we're updating the graph
  149. const prev = inNotificationPhase;
  150. inNotificationPhase = true;
  151. try {
  152. for (const consumer of node.liveConsumerNode) {
  153. if (!consumer.dirty) {
  154. consumerMarkDirty(consumer);
  155. }
  156. }
  157. }
  158. finally {
  159. inNotificationPhase = prev;
  160. }
  161. }
  162. /**
  163. * Whether this `ReactiveNode` in its producer capacity is currently allowed to initiate updates,
  164. * based on the current consumer context.
  165. */
  166. function producerUpdatesAllowed() {
  167. return activeConsumer?.consumerAllowSignalWrites !== false;
  168. }
  169. function consumerMarkDirty(node) {
  170. node.dirty = true;
  171. producerNotifyConsumers(node);
  172. node.consumerMarkedDirty?.(node);
  173. }
  174. function producerMarkClean(node) {
  175. node.dirty = false;
  176. node.lastCleanEpoch = epoch;
  177. }
  178. /**
  179. * Prepare this consumer to run a computation in its reactive context.
  180. *
  181. * Must be called by subclasses which represent reactive computations, before those computations
  182. * begin.
  183. */
  184. function consumerBeforeComputation(node) {
  185. node && (node.nextProducerIndex = 0);
  186. return setActiveConsumer(node);
  187. }
  188. /**
  189. * Finalize this consumer's state after a reactive computation has run.
  190. *
  191. * Must be called by subclasses which represent reactive computations, after those computations
  192. * have finished.
  193. */
  194. function consumerAfterComputation(node, prevConsumer) {
  195. setActiveConsumer(prevConsumer);
  196. if (!node ||
  197. node.producerNode === undefined ||
  198. node.producerIndexOfThis === undefined ||
  199. node.producerLastReadVersion === undefined) {
  200. return;
  201. }
  202. if (consumerIsLive(node)) {
  203. // For live consumers, we need to remove the producer -> consumer edge for any stale producers
  204. // which weren't dependencies after the recomputation.
  205. for (let i = node.nextProducerIndex; i < node.producerNode.length; i++) {
  206. producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
  207. }
  208. }
  209. // Truncate the producer tracking arrays.
  210. // Perf note: this is essentially truncating the length to `node.nextProducerIndex`, but
  211. // benchmarking has shown that individual pop operations are faster.
  212. while (node.producerNode.length > node.nextProducerIndex) {
  213. node.producerNode.pop();
  214. node.producerLastReadVersion.pop();
  215. node.producerIndexOfThis.pop();
  216. }
  217. }
  218. /**
  219. * Determine whether this consumer has any dependencies which have changed since the last time
  220. * they were read.
  221. */
  222. function consumerPollProducersForChange(node) {
  223. assertConsumerNode(node);
  224. // Poll producers for change.
  225. for (let i = 0; i < node.producerNode.length; i++) {
  226. const producer = node.producerNode[i];
  227. const seenVersion = node.producerLastReadVersion[i];
  228. // First check the versions. A mismatch means that the producer's value is known to have
  229. // changed since the last time we read it.
  230. if (seenVersion !== producer.version) {
  231. return true;
  232. }
  233. // The producer's version is the same as the last time we read it, but it might itself be
  234. // stale. Force the producer to recompute its version (calculating a new value if necessary).
  235. producerUpdateValueVersion(producer);
  236. // Now when we do this check, `producer.version` is guaranteed to be up to date, so if the
  237. // versions still match then it has not changed since the last time we read it.
  238. if (seenVersion !== producer.version) {
  239. return true;
  240. }
  241. }
  242. return false;
  243. }
  244. /**
  245. * Disconnect this consumer from the graph.
  246. */
  247. function consumerDestroy(node) {
  248. assertConsumerNode(node);
  249. if (consumerIsLive(node)) {
  250. // Drop all connections from the graph to this node.
  251. for (let i = 0; i < node.producerNode.length; i++) {
  252. producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
  253. }
  254. }
  255. // Truncate all the arrays to drop all connection from this node to the graph.
  256. node.producerNode.length =
  257. node.producerLastReadVersion.length =
  258. node.producerIndexOfThis.length =
  259. 0;
  260. if (node.liveConsumerNode) {
  261. node.liveConsumerNode.length = node.liveConsumerIndexOfThis.length = 0;
  262. }
  263. }
  264. /**
  265. * Add `consumer` as a live consumer of this node.
  266. *
  267. * Note that this operation is potentially transitive. If this node becomes live, then it becomes
  268. * a live consumer of all of its current producers.
  269. */
  270. function producerAddLiveConsumer(node, consumer, indexOfThis) {
  271. assertProducerNode(node);
  272. if (node.liveConsumerNode.length === 0 && isConsumerNode(node)) {
  273. // When going from 0 to 1 live consumers, we become a live consumer to our producers.
  274. for (let i = 0; i < node.producerNode.length; i++) {
  275. node.producerIndexOfThis[i] = producerAddLiveConsumer(node.producerNode[i], node, i);
  276. }
  277. }
  278. node.liveConsumerIndexOfThis.push(indexOfThis);
  279. return node.liveConsumerNode.push(consumer) - 1;
  280. }
  281. /**
  282. * Remove the live consumer at `idx`.
  283. */
  284. function producerRemoveLiveConsumerAtIndex(node, idx) {
  285. assertProducerNode(node);
  286. if (typeof ngDevMode !== 'undefined' && ngDevMode && idx >= node.liveConsumerNode.length) {
  287. throw new Error(`Assertion error: active consumer index ${idx} is out of bounds of ${node.liveConsumerNode.length} consumers)`);
  288. }
  289. if (node.liveConsumerNode.length === 1 && isConsumerNode(node)) {
  290. // When removing the last live consumer, we will no longer be live. We need to remove
  291. // ourselves from our producers' tracking (which may cause consumer-producers to lose
  292. // liveness as well).
  293. for (let i = 0; i < node.producerNode.length; i++) {
  294. producerRemoveLiveConsumerAtIndex(node.producerNode[i], node.producerIndexOfThis[i]);
  295. }
  296. }
  297. // Move the last value of `liveConsumers` into `idx`. Note that if there's only a single
  298. // live consumer, this is a no-op.
  299. const lastIdx = node.liveConsumerNode.length - 1;
  300. node.liveConsumerNode[idx] = node.liveConsumerNode[lastIdx];
  301. node.liveConsumerIndexOfThis[idx] = node.liveConsumerIndexOfThis[lastIdx];
  302. // Truncate the array.
  303. node.liveConsumerNode.length--;
  304. node.liveConsumerIndexOfThis.length--;
  305. // If the index is still valid, then we need to fix the index pointer from the producer to this
  306. // consumer, and update it from `lastIdx` to `idx` (accounting for the move above).
  307. if (idx < node.liveConsumerNode.length) {
  308. const idxProducer = node.liveConsumerIndexOfThis[idx];
  309. const consumer = node.liveConsumerNode[idx];
  310. assertConsumerNode(consumer);
  311. consumer.producerIndexOfThis[idxProducer] = idx;
  312. }
  313. }
  314. function consumerIsLive(node) {
  315. return node.consumerIsAlwaysLive || (node?.liveConsumerNode?.length ?? 0) > 0;
  316. }
  317. function assertConsumerNode(node) {
  318. node.producerNode ??= [];
  319. node.producerIndexOfThis ??= [];
  320. node.producerLastReadVersion ??= [];
  321. }
  322. function assertProducerNode(node) {
  323. node.liveConsumerNode ??= [];
  324. node.liveConsumerIndexOfThis ??= [];
  325. }
  326. function isConsumerNode(node) {
  327. return node.producerNode !== undefined;
  328. }
  329. function runPostProducerCreatedFn(node) {
  330. postProducerCreatedFn?.(node);
  331. }
  332. function setPostProducerCreatedFn(fn) {
  333. const prev = postProducerCreatedFn;
  334. postProducerCreatedFn = fn;
  335. return prev;
  336. }
  337. /**
  338. * Create a computed signal which derives a reactive value from an expression.
  339. */
  340. function createComputed(computation, equal) {
  341. const node = Object.create(COMPUTED_NODE);
  342. node.computation = computation;
  343. if (equal !== undefined) {
  344. node.equal = equal;
  345. }
  346. const computed = () => {
  347. // Check if the value needs updating before returning it.
  348. producerUpdateValueVersion(node);
  349. // Record that someone looked at this signal.
  350. producerAccessed(node);
  351. if (node.value === ERRORED) {
  352. throw node.error;
  353. }
  354. return node.value;
  355. };
  356. computed[SIGNAL] = node;
  357. if (typeof ngDevMode !== 'undefined' && ngDevMode) {
  358. const debugName = node.debugName ? ' (' + node.debugName + ')' : '';
  359. computed.toString = () => `[Computed${debugName}: ${node.value}]`;
  360. }
  361. runPostProducerCreatedFn(node);
  362. return computed;
  363. }
  364. /**
  365. * A dedicated symbol used before a computed value has been calculated for the first time.
  366. * Explicitly typed as `any` so we can use it as signal's value.
  367. */
  368. const UNSET = /* @__PURE__ */ Symbol('UNSET');
  369. /**
  370. * A dedicated symbol used in place of a computed signal value to indicate that a given computation
  371. * is in progress. Used to detect cycles in computation chains.
  372. * Explicitly typed as `any` so we can use it as signal's value.
  373. */
  374. const COMPUTING = /* @__PURE__ */ Symbol('COMPUTING');
  375. /**
  376. * A dedicated symbol used in place of a computed signal value to indicate that a given computation
  377. * failed. The thrown error is cached until the computation gets dirty again.
  378. * Explicitly typed as `any` so we can use it as signal's value.
  379. */
  380. const ERRORED = /* @__PURE__ */ Symbol('ERRORED');
  381. // Note: Using an IIFE here to ensure that the spread assignment is not considered
  382. // a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
  383. // TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
  384. const COMPUTED_NODE = /* @__PURE__ */ (() => {
  385. return {
  386. ...REACTIVE_NODE,
  387. value: UNSET,
  388. dirty: true,
  389. error: null,
  390. equal: defaultEquals,
  391. kind: 'computed',
  392. producerMustRecompute(node) {
  393. // Force a recomputation if there's no current value, or if the current value is in the
  394. // process of being calculated (which should throw an error).
  395. return node.value === UNSET || node.value === COMPUTING;
  396. },
  397. producerRecomputeValue(node) {
  398. if (node.value === COMPUTING) {
  399. // Our computation somehow led to a cyclic read of itself.
  400. throw new Error(typeof ngDevMode !== 'undefined' && ngDevMode ? 'Detected cycle in computations.' : '');
  401. }
  402. const oldValue = node.value;
  403. node.value = COMPUTING;
  404. const prevConsumer = consumerBeforeComputation(node);
  405. let newValue;
  406. let wasEqual = false;
  407. try {
  408. newValue = node.computation();
  409. // We want to mark this node as errored if calling `equal` throws; however, we don't want
  410. // to track any reactive reads inside `equal`.
  411. setActiveConsumer(null);
  412. wasEqual =
  413. oldValue !== UNSET &&
  414. oldValue !== ERRORED &&
  415. newValue !== ERRORED &&
  416. node.equal(oldValue, newValue);
  417. }
  418. catch (err) {
  419. newValue = ERRORED;
  420. node.error = err;
  421. }
  422. finally {
  423. consumerAfterComputation(node, prevConsumer);
  424. }
  425. if (wasEqual) {
  426. // No change to `valueVersion` - old and new values are
  427. // semantically equivalent.
  428. node.value = oldValue;
  429. return;
  430. }
  431. node.value = newValue;
  432. node.version++;
  433. },
  434. };
  435. })();
  436. function defaultThrowError() {
  437. throw new Error();
  438. }
  439. let throwInvalidWriteToSignalErrorFn = defaultThrowError;
  440. function throwInvalidWriteToSignalError(node) {
  441. throwInvalidWriteToSignalErrorFn(node);
  442. }
  443. function setThrowInvalidWriteToSignalError(fn) {
  444. throwInvalidWriteToSignalErrorFn = fn;
  445. }
  446. /**
  447. * If set, called after `WritableSignal`s are updated.
  448. *
  449. * This hook can be used to achieve various effects, such as running effects synchronously as part
  450. * of setting a signal.
  451. */
  452. let postSignalSetFn = null;
  453. /**
  454. * Creates a `Signal` getter, setter, and updater function.
  455. */
  456. function createSignal(initialValue, equal) {
  457. const node = Object.create(SIGNAL_NODE);
  458. node.value = initialValue;
  459. if (equal !== undefined) {
  460. node.equal = equal;
  461. }
  462. const getter = (() => signalGetFn(node));
  463. getter[SIGNAL] = node;
  464. if (typeof ngDevMode !== 'undefined' && ngDevMode) {
  465. const debugName = node.debugName ? ' (' + node.debugName + ')' : '';
  466. getter.toString = () => `[Signal${debugName}: ${node.value}]`;
  467. }
  468. runPostProducerCreatedFn(node);
  469. const set = (newValue) => signalSetFn(node, newValue);
  470. const update = (updateFn) => signalUpdateFn(node, updateFn);
  471. return [getter, set, update];
  472. }
  473. function setPostSignalSetFn(fn) {
  474. const prev = postSignalSetFn;
  475. postSignalSetFn = fn;
  476. return prev;
  477. }
  478. function signalGetFn(node) {
  479. producerAccessed(node);
  480. return node.value;
  481. }
  482. function signalSetFn(node, newValue) {
  483. if (!producerUpdatesAllowed()) {
  484. throwInvalidWriteToSignalError(node);
  485. }
  486. if (!node.equal(node.value, newValue)) {
  487. node.value = newValue;
  488. signalValueChanged(node);
  489. }
  490. }
  491. function signalUpdateFn(node, updater) {
  492. if (!producerUpdatesAllowed()) {
  493. throwInvalidWriteToSignalError(node);
  494. }
  495. signalSetFn(node, updater(node.value));
  496. }
  497. function runPostSignalSetFn(node) {
  498. postSignalSetFn?.(node);
  499. }
  500. // Note: Using an IIFE here to ensure that the spread assignment is not considered
  501. // a side-effect, ending up preserving `COMPUTED_NODE` and `REACTIVE_NODE`.
  502. // TODO: remove when https://github.com/evanw/esbuild/issues/3392 is resolved.
  503. const SIGNAL_NODE = /* @__PURE__ */ (() => {
  504. return {
  505. ...REACTIVE_NODE,
  506. equal: defaultEquals,
  507. value: undefined,
  508. kind: 'signal',
  509. };
  510. })();
  511. function signalValueChanged(node) {
  512. node.version++;
  513. producerIncrementEpoch();
  514. producerNotifyConsumers(node);
  515. postSignalSetFn?.(node);
  516. }
  517. 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 };
  518. //# sourceMappingURL=signal.mjs.map