@graphrefly/graphrefly 0.22.0 → 0.24.0

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 (78) hide show
  1. package/dist/{chunk-QA3RP5NH.js → chunk-5DJTTKX3.js} +111 -17
  2. package/dist/chunk-5DJTTKX3.js.map +1 -0
  3. package/dist/{chunk-BLD3IFYF.js → chunk-5WGT55R4.js} +9 -7
  4. package/dist/{chunk-BLD3IFYF.js.map → chunk-5WGT55R4.js.map} +1 -1
  5. package/dist/{chunk-IR3KMOLX.js → chunk-AOCBDH4T.js} +3 -383
  6. package/dist/chunk-AOCBDH4T.js.map +1 -0
  7. package/dist/{chunk-TH6COGOP.js → chunk-H4RVA4VE.js} +2 -2
  8. package/dist/chunk-HWPIFSW2.js +36 -0
  9. package/dist/chunk-HWPIFSW2.js.map +1 -0
  10. package/dist/{chunk-MQBQOFDS.js → chunk-IPLKX3L2.js} +12 -31
  11. package/dist/chunk-IPLKX3L2.js.map +1 -0
  12. package/dist/{chunk-44HD4BTA.js → chunk-MW4VAKAO.js} +3 -3
  13. package/dist/chunk-PY4XCDLR.js +391 -0
  14. package/dist/chunk-PY4XCDLR.js.map +1 -0
  15. package/dist/{chunk-RHI3GHZW.js → chunk-QOWVNWOC.js} +3 -3
  16. package/dist/{chunk-EQUZ5NLD.js → chunk-TDEXAMGO.js} +11 -16
  17. package/dist/chunk-TDEXAMGO.js.map +1 -0
  18. package/dist/{chunk-NXC35KC5.js → chunk-XOFWRC73.js} +3 -3
  19. package/dist/compat/nestjs/index.cjs +110 -16
  20. package/dist/compat/nestjs/index.cjs.map +1 -1
  21. package/dist/compat/nestjs/index.d.cts +6 -6
  22. package/dist/compat/nestjs/index.d.ts +6 -6
  23. package/dist/compat/nestjs/index.js +9 -7
  24. package/dist/core/index.cjs +110 -16
  25. package/dist/core/index.cjs.map +1 -1
  26. package/dist/core/index.d.cts +3 -3
  27. package/dist/core/index.d.ts +3 -3
  28. package/dist/core/index.js +3 -3
  29. package/dist/extra/index.cjs +110 -16
  30. package/dist/extra/index.cjs.map +1 -1
  31. package/dist/extra/index.d.cts +4 -4
  32. package/dist/extra/index.d.ts +4 -4
  33. package/dist/extra/index.js +9 -7
  34. package/dist/graph/index.cjs +110 -16
  35. package/dist/graph/index.cjs.map +1 -1
  36. package/dist/graph/index.d.cts +5 -5
  37. package/dist/graph/index.d.ts +5 -5
  38. package/dist/graph/index.js +4 -4
  39. package/dist/{graph-DFr0diXB.d.ts → graph-B6NFqv3z.d.ts} +3 -3
  40. package/dist/{graph-ab1yPwIB.d.cts → graph-D-3JIQme.d.cts} +3 -3
  41. package/dist/{index-BvWfZCTt.d.cts → index-1z8vRTCt.d.cts} +3 -3
  42. package/dist/{index-Dy04P4W3.d.cts → index-AMWewNDe.d.cts} +2 -2
  43. package/dist/{index-C9z6rU9P.d.cts → index-BJB7t9gg.d.cts} +19 -15
  44. package/dist/{index-BbYZma8G.d.ts → index-BysCTzJz.d.ts} +3 -3
  45. package/dist/{index-HdJx_BjO.d.ts → index-C-TXEa7C.d.ts} +19 -15
  46. package/dist/{index-DsGxLfwL.d.ts → index-CYkjxu3s.d.ts} +2 -2
  47. package/dist/{index-BHm3Ba5q.d.ts → index-D7XgsUt7.d.ts} +2 -2
  48. package/dist/{index-D36MAQ3f.d.ts → index-DiobMNwE.d.ts} +3 -3
  49. package/dist/{index-DrJq9B1T.d.cts → index-J7Kc0oIQ.d.cts} +3 -3
  50. package/dist/{index-DLE1Sp-L.d.cts → index-b5BYtczN.d.cts} +2 -2
  51. package/dist/index.cjs +129 -46
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.cts +15 -15
  54. package/dist/index.d.ts +15 -15
  55. package/dist/index.js +36 -42
  56. package/dist/index.js.map +1 -1
  57. package/dist/{meta-n3FoVWML.d.ts → meta-CnkLA_43.d.ts} +1 -1
  58. package/dist/{meta--fr9sxRM.d.cts → meta-DWbkoq1s.d.cts} +1 -1
  59. package/dist/{node-C5UD5MGq.d.cts → node-B-f-Lu-k.d.cts} +57 -13
  60. package/dist/{node-C5UD5MGq.d.ts → node-B-f-Lu-k.d.ts} +57 -13
  61. package/dist/{observable-DWydVy5b.d.cts → observable-DBnrwcar.d.cts} +1 -1
  62. package/dist/{observable-CQRBtEbq.d.ts → observable-uP-wy_uK.d.ts} +1 -1
  63. package/dist/patterns/reactive-layout/index.cjs +224 -129
  64. package/dist/patterns/reactive-layout/index.cjs.map +1 -1
  65. package/dist/patterns/reactive-layout/index.d.cts +5 -5
  66. package/dist/patterns/reactive-layout/index.d.ts +5 -5
  67. package/dist/patterns/reactive-layout/index.js +6 -4
  68. package/dist/{storage-Bew05Xy6.d.cts → storage-BuTdpCI1.d.cts} +1 -1
  69. package/dist/{storage-C9fZfMfM.d.ts → storage-F2X1U1x0.d.ts} +1 -1
  70. package/package.json +3 -2
  71. package/dist/chunk-EQUZ5NLD.js.map +0 -1
  72. package/dist/chunk-IR3KMOLX.js.map +0 -1
  73. package/dist/chunk-MQBQOFDS.js.map +0 -1
  74. package/dist/chunk-QA3RP5NH.js.map +0 -1
  75. /package/dist/{chunk-TH6COGOP.js.map → chunk-H4RVA4VE.js.map} +0 -0
  76. /package/dist/{chunk-44HD4BTA.js.map → chunk-MW4VAKAO.js.map} +0 -0
  77. /package/dist/{chunk-RHI3GHZW.js.map → chunk-QOWVNWOC.js.map} +0 -0
  78. /package/dist/{chunk-NXC35KC5.js.map → chunk-XOFWRC73.js.map} +0 -0
@@ -240,9 +240,20 @@ var flushInProgress = false;
240
240
  var drainPhase2 = [];
241
241
  var drainPhase3 = [];
242
242
  var drainPhase4 = [];
243
+ var flushHooks = [];
243
244
  function isBatching() {
244
245
  return batchDepth > 0 || flushInProgress;
245
246
  }
247
+ function isExplicitlyBatching() {
248
+ return batchDepth > 0;
249
+ }
250
+ function registerBatchFlushHook(hook) {
251
+ if (batchDepth > 0) {
252
+ flushHooks.push(hook);
253
+ } else {
254
+ hook();
255
+ }
256
+ }
246
257
  function batch(fn) {
247
258
  batchDepth += 1;
248
259
  let threw = false;
@@ -256,6 +267,13 @@ function batch(fn) {
256
267
  if (batchDepth === 0) {
257
268
  if (threw) {
258
269
  if (!flushInProgress) {
270
+ const hooks = flushHooks.splice(0);
271
+ for (const h of hooks) {
272
+ try {
273
+ h();
274
+ } catch {
275
+ }
276
+ }
259
277
  drainPhase2.length = 0;
260
278
  drainPhase3.length = 0;
261
279
  drainPhase4.length = 0;
@@ -272,7 +290,18 @@ function drainPending() {
272
290
  const errors = [];
273
291
  let iterations = 0;
274
292
  try {
275
- while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0) {
293
+ while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0 || ownsFlush && flushHooks.length > 0) {
294
+ if (ownsFlush && flushHooks.length > 0) {
295
+ const hooks = flushHooks.splice(0);
296
+ for (const h of hooks) {
297
+ try {
298
+ h();
299
+ } catch (e) {
300
+ errors.push(e);
301
+ }
302
+ }
303
+ continue;
304
+ }
276
305
  iterations += 1;
277
306
  if (iterations > MAX_DRAIN_ITERATIONS) {
278
307
  drainPhase2.length = 0;
@@ -962,6 +991,22 @@ var NodeImpl = class _NodeImpl {
962
991
  * treats `0` as "wave settled" — O(1) check for full dep settlement.
963
992
  */
964
993
  _dirtyDepCount = 0;
994
+ // --- Per-batch emit accumulator (Bug 2: K+1 fan-in fix) ---
995
+ /**
996
+ * Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
997
+ * its already-framed messages here instead of dispatching synchronously.
998
+ * At batch end, `_flushBatchPending` runs (registered via
999
+ * `registerBatchFlushHook`) and delivers the whole accumulated batch as
1000
+ * one `downWithBatch` call — collapsing what would otherwise be K
1001
+ * separate sink invocations into one. This is the fix for the diamond
1002
+ * fan-in K+1 over-fire.
1003
+ *
1004
+ * `null` outside batch (or after flush). Only ever appended to within
1005
+ * a single explicit batch lifetime; reset to `null` on flush. State
1006
+ * updates (cache, version, status) still happen per-emit via
1007
+ * `_updateState` — only the downstream delivery is coalesced.
1008
+ */
1009
+ _batchPendingMessages = null;
965
1010
  // --- PAUSE/RESUME lock tracking (C0) ---
966
1011
  /**
967
1012
  * Set of active pause locks held against this node. Every `[PAUSE, lockId]`
@@ -1339,7 +1384,10 @@ var NodeImpl = class _NodeImpl {
1339
1384
  dep.unsub = noopUnsub;
1340
1385
  dep.unsub = dep.node.subscribe((msgs) => {
1341
1386
  if (dep.unsub === null) return;
1387
+ const tierOf = this._config.tierOf;
1388
+ let sawSettlement = false;
1342
1389
  for (const m of msgs) {
1390
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1343
1391
  this._config.onMessage(
1344
1392
  this,
1345
1393
  m,
@@ -1347,6 +1395,7 @@ var NodeImpl = class _NodeImpl {
1347
1395
  this._actions
1348
1396
  );
1349
1397
  }
1398
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1350
1399
  });
1351
1400
  subscribedCount++;
1352
1401
  }
@@ -1401,7 +1450,10 @@ var NodeImpl = class _NodeImpl {
1401
1450
  try {
1402
1451
  record.unsub = depNode.subscribe((msgs) => {
1403
1452
  if (record.unsub === null) return;
1453
+ const tierOf = this._config.tierOf;
1454
+ let sawSettlement = false;
1404
1455
  for (const m of msgs) {
1456
+ if (tierOf(m[0]) >= 3) sawSettlement = true;
1405
1457
  this._config.onMessage(
1406
1458
  this,
1407
1459
  m,
@@ -1409,6 +1461,7 @@ var NodeImpl = class _NodeImpl {
1409
1461
  this._actions
1410
1462
  );
1411
1463
  }
1464
+ if (sawSettlement) this._maybeRunFnOnSettlement();
1412
1465
  });
1413
1466
  } catch (err) {
1414
1467
  record.unsub = null;
@@ -1530,7 +1583,6 @@ var NodeImpl = class _NodeImpl {
1530
1583
  }
1531
1584
  return;
1532
1585
  }
1533
- this._maybeRunFnOnSettlement();
1534
1586
  }
1535
1587
  // --- Centralized dep-state transitions (A3 settlement counters) ---
1536
1588
  //
@@ -1776,37 +1828,35 @@ var NodeImpl = class _NodeImpl {
1776
1828
  // --- Emit pipeline ---
1777
1829
  /**
1778
1830
  * @internal The unified dispatch waist — one call = one wave.
1831
+ * See `GRAPHREFLY-SPEC.md` §1.3.1 for protocol context — the stages
1832
+ * below are the implementation order.
1779
1833
  *
1780
1834
  * Pipeline stages, in order:
1781
1835
  *
1782
- * 1. Early-return on empty batch.
1783
- * 2. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
1836
+ * 1. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
1784
1837
  * still propagate so graph teardown and cache-clear still work.
1785
- * 3. Tier sort (stable) — the batch can be in any order when it
1838
+ * 2. Tier sort (stable) — the batch can be in any order when it
1786
1839
  * arrives; the walker downstream (`downWithBatch`) assumes
1787
1840
  * ascending tier monotone, and so does `_updateState`'s tier-3
1788
1841
  * slice walk. This is the single source of truth for ordering.
1789
- * 4. Synthetic DIRTY prefix — if a tier-3 payload is present, no
1842
+ * 3. Synthetic DIRTY prefix — if a tier-3 payload is present, no
1790
1843
  * DIRTY is already in the batch, and the node isn't already in
1791
1844
  * `"dirty"` status, prepend `[DIRTY]` after any tier-0 START
1792
1845
  * entries. Guarantees spec §1.3.1 (DIRTY precedes DATA within
1793
1846
  * the same batch) uniformly across every entry point.
1794
- * 5. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
1847
+ * 4. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
1795
1848
  * derive `_paused`, filter unknown-lockId RESUME, replay
1796
1849
  * bufferAll buffer on final lock release.
1797
- * 6. Meta TEARDOWN fan-out — notify meta children before
1850
+ * 5. Meta TEARDOWN fan-out — notify meta children before
1798
1851
  * `_updateState`'s TEARDOWN branch calls `_deactivate`. Hoisted
1799
1852
  * out of the walk to keep `_updateState` re-entrance-free.
1800
- * 7. `_updateState` — walk the batch in tier order, advancing
1853
+ * 6. `_updateState` — walk the batch in tier order, advancing
1801
1854
  * `_cached` / `_status` / `_versioning` and running equals
1802
1855
  * substitution on tier-3 DATA (§3.5.1). Returns
1803
1856
  * `{finalMessages, equalsError?}`.
1804
- * 8. `downWithBatch` dispatch (or bufferAll capture if paused with
1857
+ * 7. `downWithBatch` dispatch (or bufferAll capture if paused with
1805
1858
  * `pausable: "resumeAll"`).
1806
- * 9. Recursive ERROR emission if equals threw mid-walk.
1807
- *
1808
- * `node.down` / `node.emit` / `actions.down` / `actions.emit` all
1809
- * converge here — the unified `_emit` waist (spec §1.3.1).
1859
+ * 8. Recursive ERROR emission if equals threw mid-walk.
1810
1860
  */
1811
1861
  _emit(messages) {
1812
1862
  if (messages.length === 0) return;
@@ -1902,10 +1952,10 @@ var NodeImpl = class _NodeImpl {
1902
1952
  }
1903
1953
  }
1904
1954
  if (immediate.length > 0) {
1905
- downWithBatch(this._deliverToSinks, immediate, tierOf);
1955
+ this._dispatchOrAccumulate(immediate);
1906
1956
  }
1907
1957
  } else {
1908
- downWithBatch(this._deliverToSinks, finalMessages, this._config.tierOf);
1958
+ this._dispatchOrAccumulate(finalMessages);
1909
1959
  }
1910
1960
  }
1911
1961
  if (equalsError != null) {
@@ -2028,6 +2078,50 @@ var NodeImpl = class _NodeImpl {
2028
2078
  const snapshot = [...this._sinks];
2029
2079
  for (const sink of snapshot) sink(messages);
2030
2080
  };
2081
+ /**
2082
+ * @internal Dispatch entry point that respects the per-batch emit
2083
+ * accumulator (Bug 2). Inside an explicit `batch()` scope, append to
2084
+ * `_batchPendingMessages` and register a flush hook on first append.
2085
+ * Outside batch — or during a drain (where `flushInProgress` is true
2086
+ * but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
2087
+ *
2088
+ * Per-emit state updates (`_frameBatch`, `_updateState`) have already
2089
+ * happened by the time we reach here; only the **downstream delivery**
2090
+ * is coalesced. Cache, version, and status are visible mid-batch on
2091
+ * the emitting node itself.
2092
+ */
2093
+ _dispatchOrAccumulate(messages) {
2094
+ if (isExplicitlyBatching()) {
2095
+ if (this._batchPendingMessages === null) {
2096
+ this._batchPendingMessages = [];
2097
+ registerBatchFlushHook(() => this._flushBatchPending());
2098
+ }
2099
+ for (const m of messages) this._batchPendingMessages.push(m);
2100
+ return;
2101
+ }
2102
+ downWithBatch(this._deliverToSinks, messages, this._config.tierOf);
2103
+ }
2104
+ /**
2105
+ * @internal Flushes the accumulated batch through `downWithBatch` and
2106
+ * clears the pending state. Idempotent — safe to call when pending is
2107
+ * already null or empty (e.g. on a `batch()` throw, where the hook
2108
+ * fires for cleanup but the drainPhase queues are wiped after).
2109
+ *
2110
+ * Critical: the accumulated batch is interleaved per-emit framings like
2111
+ * `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
2112
+ * re-frame to sort by tier before handing to `downWithBatch`, which
2113
+ * assumes pre-sorted input. `_frameBatch` also handles the synthetic
2114
+ * DIRTY prepend rule (no-op here — `hasDirty` is true since each
2115
+ * accumulated emit already carries its own DIRTY prefix).
2116
+ */
2117
+ _flushBatchPending() {
2118
+ const pending = this._batchPendingMessages;
2119
+ if (pending === null) return;
2120
+ this._batchPendingMessages = null;
2121
+ if (pending.length === 0) return;
2122
+ const framed = this._frameBatch(pending);
2123
+ downWithBatch(this._deliverToSinks, framed, this._config.tierOf);
2124
+ }
2031
2125
  };
2032
2126
  var isNodeArray = (value) => Array.isArray(value);
2033
2127
  var isNodeOptionsObject = (value) => typeof value === "object" && value != null && !Array.isArray(value);
@@ -2231,4 +2325,4 @@ export {
2231
2325
  autoTrackNode,
2232
2326
  pipe
2233
2327
  };
2234
- //# sourceMappingURL=chunk-QA3RP5NH.js.map
2328
+ //# sourceMappingURL=chunk-5DJTTKX3.js.map