@graphrefly/graphrefly 0.22.0 → 0.23.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.
- package/dist/{chunk-RHI3GHZW.js → chunk-263BEJJO.js} +3 -3
- package/dist/{chunk-44HD4BTA.js → chunk-2GQLMQVJ.js} +3 -3
- package/dist/chunk-32N5A454.js +36 -0
- package/dist/chunk-32N5A454.js.map +1 -0
- package/dist/{chunk-IR3KMOLX.js → chunk-CWYPA63G.js} +3 -383
- package/dist/chunk-CWYPA63G.js.map +1 -0
- package/dist/{chunk-TH6COGOP.js → chunk-HVBX5KIW.js} +2 -2
- package/dist/chunk-JFONSPNF.js +391 -0
- package/dist/chunk-JFONSPNF.js.map +1 -0
- package/dist/{chunk-QA3RP5NH.js → chunk-NZMBRXQV.js} +101 -5
- package/dist/chunk-NZMBRXQV.js.map +1 -0
- package/dist/{chunk-MQBQOFDS.js → chunk-PNUZM7PC.js} +12 -31
- package/dist/chunk-PNUZM7PC.js.map +1 -0
- package/dist/{chunk-EQUZ5NLD.js → chunk-PX6PDUJ5.js} +11 -16
- package/dist/chunk-PX6PDUJ5.js.map +1 -0
- package/dist/{chunk-NXC35KC5.js → chunk-XRFJJ2IU.js} +3 -3
- package/dist/{chunk-BLD3IFYF.js → chunk-XTLYW4FR.js} +9 -7
- package/dist/{chunk-BLD3IFYF.js.map → chunk-XTLYW4FR.js.map} +1 -1
- package/dist/compat/nestjs/index.cjs +100 -4
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +6 -6
- package/dist/compat/nestjs/index.d.ts +6 -6
- package/dist/compat/nestjs/index.js +9 -7
- package/dist/core/index.cjs +100 -4
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +3 -3
- package/dist/extra/index.cjs +100 -4
- package/dist/extra/index.cjs.map +1 -1
- package/dist/extra/index.d.cts +4 -4
- package/dist/extra/index.d.ts +4 -4
- package/dist/extra/index.js +9 -7
- package/dist/graph/index.cjs +100 -4
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +5 -5
- package/dist/graph/index.d.ts +5 -5
- package/dist/graph/index.js +4 -4
- package/dist/{graph-ab1yPwIB.d.cts → graph-BtdSRHUc.d.cts} +3 -3
- package/dist/{graph-DFr0diXB.d.ts → graph-CEO2FkLY.d.ts} +3 -3
- package/dist/{index-BvWfZCTt.d.cts → index-B0tfuXwV.d.cts} +3 -3
- package/dist/{index-Dy04P4W3.d.cts → index-BFGjXbiP.d.cts} +2 -2
- package/dist/{index-DrJq9B1T.d.cts → index-BPlWVAKY.d.cts} +3 -3
- package/dist/{index-C9z6rU9P.d.cts → index-BUj3ASVe.d.cts} +25 -7
- package/dist/{index-DLE1Sp-L.d.cts → index-C59uSJAH.d.cts} +2 -2
- package/dist/{index-DsGxLfwL.d.ts → index-CkElcUY6.d.ts} +2 -2
- package/dist/{index-HdJx_BjO.d.ts → index-DSPc5rkv.d.ts} +25 -7
- package/dist/{index-D36MAQ3f.d.ts → index-DgscL7v0.d.ts} +3 -3
- package/dist/{index-BbYZma8G.d.ts → index-RXN94sHK.d.ts} +3 -3
- package/dist/{index-BHm3Ba5q.d.ts → index-jEtF4N7L.d.ts} +2 -2
- package/dist/index.cjs +109 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +15 -15
- package/dist/index.d.ts +15 -15
- package/dist/index.js +26 -22
- package/dist/index.js.map +1 -1
- package/dist/{meta-n3FoVWML.d.ts → meta-3QjzotRv.d.ts} +1 -1
- package/dist/{meta--fr9sxRM.d.cts → meta-B-Lbs4-O.d.cts} +1 -1
- package/dist/{node-C5UD5MGq.d.cts → node-C7PD3sn9.d.cts} +42 -0
- package/dist/{node-C5UD5MGq.d.ts → node-C7PD3sn9.d.ts} +42 -0
- package/dist/{observable-CQRBtEbq.d.ts → observable-EyO-moQY.d.ts} +1 -1
- package/dist/{observable-DWydVy5b.d.cts → observable-axpzv1K2.d.cts} +1 -1
- package/dist/patterns/reactive-layout/index.cjs +214 -117
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +5 -5
- package/dist/patterns/reactive-layout/index.d.ts +5 -5
- package/dist/patterns/reactive-layout/index.js +6 -4
- package/dist/{storage-C9fZfMfM.d.ts → storage-CHT5WE9m.d.ts} +1 -1
- package/dist/{storage-Bew05Xy6.d.cts → storage-DIgAr7M_.d.cts} +1 -1
- package/package.json +2 -1
- package/dist/chunk-EQUZ5NLD.js.map +0 -1
- package/dist/chunk-IR3KMOLX.js.map +0 -1
- package/dist/chunk-MQBQOFDS.js.map +0 -1
- package/dist/chunk-QA3RP5NH.js.map +0 -1
- /package/dist/{chunk-RHI3GHZW.js.map → chunk-263BEJJO.js.map} +0 -0
- /package/dist/{chunk-44HD4BTA.js.map → chunk-2GQLMQVJ.js.map} +0 -0
- /package/dist/{chunk-TH6COGOP.js.map → chunk-HVBX5KIW.js.map} +0 -0
- /package/dist/{chunk-NXC35KC5.js.map → chunk-XRFJJ2IU.js.map} +0 -0
|
@@ -843,6 +843,21 @@ declare class NodeImpl<T = unknown> implements Node<T> {
|
|
|
843
843
|
* treats `0` as "wave settled" — O(1) check for full dep settlement.
|
|
844
844
|
*/
|
|
845
845
|
_dirtyDepCount: number;
|
|
846
|
+
/**
|
|
847
|
+
* Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
|
|
848
|
+
* its already-framed messages here instead of dispatching synchronously.
|
|
849
|
+
* At batch end, `_flushBatchPending` runs (registered via
|
|
850
|
+
* `registerBatchFlushHook`) and delivers the whole accumulated batch as
|
|
851
|
+
* one `downWithBatch` call — collapsing what would otherwise be K
|
|
852
|
+
* separate sink invocations into one. This is the fix for the diamond
|
|
853
|
+
* fan-in K+1 over-fire.
|
|
854
|
+
*
|
|
855
|
+
* `null` outside batch (or after flush). Only ever appended to within
|
|
856
|
+
* a single explicit batch lifetime; reset to `null` on flush. State
|
|
857
|
+
* updates (cache, version, status) still happen per-emit via
|
|
858
|
+
* `_updateState` — only the downstream delivery is coalesced.
|
|
859
|
+
*/
|
|
860
|
+
_batchPendingMessages: Message[] | null;
|
|
846
861
|
/**
|
|
847
862
|
* Set of active pause locks held against this node. Every `[PAUSE, lockId]`
|
|
848
863
|
* adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
|
|
@@ -1127,6 +1142,33 @@ declare class NodeImpl<T = unknown> implements Node<T> {
|
|
|
1127
1142
|
*/
|
|
1128
1143
|
private _updateState;
|
|
1129
1144
|
private _deliverToSinks;
|
|
1145
|
+
/**
|
|
1146
|
+
* @internal Dispatch entry point that respects the per-batch emit
|
|
1147
|
+
* accumulator (Bug 2). Inside an explicit `batch()` scope, append to
|
|
1148
|
+
* `_batchPendingMessages` and register a flush hook on first append.
|
|
1149
|
+
* Outside batch — or during a drain (where `flushInProgress` is true
|
|
1150
|
+
* but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
|
|
1151
|
+
*
|
|
1152
|
+
* Per-emit state updates (`_frameBatch`, `_updateState`) have already
|
|
1153
|
+
* happened by the time we reach here; only the **downstream delivery**
|
|
1154
|
+
* is coalesced. Cache, version, and status are visible mid-batch on
|
|
1155
|
+
* the emitting node itself.
|
|
1156
|
+
*/
|
|
1157
|
+
private _dispatchOrAccumulate;
|
|
1158
|
+
/**
|
|
1159
|
+
* @internal Flushes the accumulated batch through `downWithBatch` and
|
|
1160
|
+
* clears the pending state. Idempotent — safe to call when pending is
|
|
1161
|
+
* already null or empty (e.g. on a `batch()` throw, where the hook
|
|
1162
|
+
* fires for cleanup but the drainPhase queues are wiped after).
|
|
1163
|
+
*
|
|
1164
|
+
* Critical: the accumulated batch is interleaved per-emit framings like
|
|
1165
|
+
* `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
|
|
1166
|
+
* re-frame to sort by tier before handing to `downWithBatch`, which
|
|
1167
|
+
* assumes pre-sorted input. `_frameBatch` also handles the synthetic
|
|
1168
|
+
* DIRTY prepend rule (no-op here — `hasDirty` is true since each
|
|
1169
|
+
* accumulated emit already carries its own DIRTY prefix).
|
|
1170
|
+
*/
|
|
1171
|
+
private _flushBatchPending;
|
|
1130
1172
|
}
|
|
1131
1173
|
/**
|
|
1132
1174
|
* Creates a reactive {@link Node} — the single GraphReFly primitive (§2).
|
|
@@ -843,6 +843,21 @@ declare class NodeImpl<T = unknown> implements Node<T> {
|
|
|
843
843
|
* treats `0` as "wave settled" — O(1) check for full dep settlement.
|
|
844
844
|
*/
|
|
845
845
|
_dirtyDepCount: number;
|
|
846
|
+
/**
|
|
847
|
+
* Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
|
|
848
|
+
* its already-framed messages here instead of dispatching synchronously.
|
|
849
|
+
* At batch end, `_flushBatchPending` runs (registered via
|
|
850
|
+
* `registerBatchFlushHook`) and delivers the whole accumulated batch as
|
|
851
|
+
* one `downWithBatch` call — collapsing what would otherwise be K
|
|
852
|
+
* separate sink invocations into one. This is the fix for the diamond
|
|
853
|
+
* fan-in K+1 over-fire.
|
|
854
|
+
*
|
|
855
|
+
* `null` outside batch (or after flush). Only ever appended to within
|
|
856
|
+
* a single explicit batch lifetime; reset to `null` on flush. State
|
|
857
|
+
* updates (cache, version, status) still happen per-emit via
|
|
858
|
+
* `_updateState` — only the downstream delivery is coalesced.
|
|
859
|
+
*/
|
|
860
|
+
_batchPendingMessages: Message[] | null;
|
|
846
861
|
/**
|
|
847
862
|
* Set of active pause locks held against this node. Every `[PAUSE, lockId]`
|
|
848
863
|
* adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
|
|
@@ -1127,6 +1142,33 @@ declare class NodeImpl<T = unknown> implements Node<T> {
|
|
|
1127
1142
|
*/
|
|
1128
1143
|
private _updateState;
|
|
1129
1144
|
private _deliverToSinks;
|
|
1145
|
+
/**
|
|
1146
|
+
* @internal Dispatch entry point that respects the per-batch emit
|
|
1147
|
+
* accumulator (Bug 2). Inside an explicit `batch()` scope, append to
|
|
1148
|
+
* `_batchPendingMessages` and register a flush hook on first append.
|
|
1149
|
+
* Outside batch — or during a drain (where `flushInProgress` is true
|
|
1150
|
+
* but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
|
|
1151
|
+
*
|
|
1152
|
+
* Per-emit state updates (`_frameBatch`, `_updateState`) have already
|
|
1153
|
+
* happened by the time we reach here; only the **downstream delivery**
|
|
1154
|
+
* is coalesced. Cache, version, and status are visible mid-batch on
|
|
1155
|
+
* the emitting node itself.
|
|
1156
|
+
*/
|
|
1157
|
+
private _dispatchOrAccumulate;
|
|
1158
|
+
/**
|
|
1159
|
+
* @internal Flushes the accumulated batch through `downWithBatch` and
|
|
1160
|
+
* clears the pending state. Idempotent — safe to call when pending is
|
|
1161
|
+
* already null or empty (e.g. on a `batch()` throw, where the hook
|
|
1162
|
+
* fires for cleanup but the drainPhase queues are wiped after).
|
|
1163
|
+
*
|
|
1164
|
+
* Critical: the accumulated batch is interleaved per-emit framings like
|
|
1165
|
+
* `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
|
|
1166
|
+
* re-frame to sort by tier before handing to `downWithBatch`, which
|
|
1167
|
+
* assumes pre-sorted input. `_frameBatch` also handles the synthetic
|
|
1168
|
+
* DIRTY prepend rule (no-op here — `hasDirty` is true since each
|
|
1169
|
+
* accumulated emit already carries its own DIRTY prefix).
|
|
1170
|
+
*/
|
|
1171
|
+
private _flushBatchPending;
|
|
1130
1172
|
}
|
|
1131
1173
|
/**
|
|
1132
1174
|
* Creates a reactive {@link Node} — the single GraphReFly primitive (§2).
|
|
@@ -316,6 +316,109 @@ var ImageSizeAdapter = class {
|
|
|
316
316
|
}
|
|
317
317
|
};
|
|
318
318
|
|
|
319
|
+
// src/core/clock.ts
|
|
320
|
+
function monotonicNs() {
|
|
321
|
+
return Math.trunc(performance.now() * 1e6);
|
|
322
|
+
}
|
|
323
|
+
function wallClockNs() {
|
|
324
|
+
return Date.now() * 1e6;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/graph/codec.ts
|
|
328
|
+
var JsonCodec = {
|
|
329
|
+
name: "json",
|
|
330
|
+
version: 1,
|
|
331
|
+
contentType: "application/json",
|
|
332
|
+
encode(snapshot) {
|
|
333
|
+
const json = JSON.stringify(snapshot);
|
|
334
|
+
return new TextEncoder().encode(json);
|
|
335
|
+
},
|
|
336
|
+
decode(buffer, _codecVersion) {
|
|
337
|
+
const json = new TextDecoder().decode(buffer);
|
|
338
|
+
return JSON.parse(json);
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
var ENVELOPE_VERSION = 1;
|
|
342
|
+
var ENVELOPE_MIN_LEN = 4;
|
|
343
|
+
function encodeEnvelope(codec, payload) {
|
|
344
|
+
const nameBytes = new TextEncoder().encode(codec.name);
|
|
345
|
+
if (nameBytes.length === 0 || nameBytes.length > 255) {
|
|
346
|
+
throw new Error(
|
|
347
|
+
`encodeEnvelope: codec name "${codec.name}" encodes to ${nameBytes.length} bytes (must be 1\u2013255)`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const cv = codec.version;
|
|
351
|
+
if (!Number.isInteger(cv) || cv < 0 || cv > 65535) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`encodeEnvelope: codec.version ${cv} out of u16 range (expected integer 0\u201365535)`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
const totalLen = 1 + 1 + nameBytes.length + 2 + payload.length;
|
|
357
|
+
if (totalLen > 4294967295) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`encodeEnvelope: total envelope size ${totalLen} exceeds 2^32-1 bytes (payload ${payload.length} bytes)`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
const out = new Uint8Array(totalLen);
|
|
363
|
+
let i = 0;
|
|
364
|
+
out[i++] = ENVELOPE_VERSION;
|
|
365
|
+
out[i++] = nameBytes.length;
|
|
366
|
+
out.set(nameBytes, i);
|
|
367
|
+
i += nameBytes.length;
|
|
368
|
+
out[i++] = cv >>> 8 & 255;
|
|
369
|
+
out[i++] = cv & 255;
|
|
370
|
+
out.set(payload, i);
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function decodeEnvelope(bytes, config) {
|
|
374
|
+
if (bytes.length < ENVELOPE_MIN_LEN) {
|
|
375
|
+
throw new Error(`decodeEnvelope: bytes too short (${bytes.length} < ${ENVELOPE_MIN_LEN})`);
|
|
376
|
+
}
|
|
377
|
+
let i = 0;
|
|
378
|
+
const envVersion = bytes[i++];
|
|
379
|
+
if (envVersion !== ENVELOPE_VERSION) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`decodeEnvelope: unsupported envelope version ${envVersion} (expected ${ENVELOPE_VERSION})`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
const nameLen = bytes[i++];
|
|
385
|
+
if (nameLen === 0) {
|
|
386
|
+
throw new Error("decodeEnvelope: name_len must be >= 1");
|
|
387
|
+
}
|
|
388
|
+
if (i + nameLen + 2 > bytes.length) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
`decodeEnvelope: envelope truncated (need ${i + nameLen + 2} bytes, have ${bytes.length})`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
const name = new TextDecoder().decode(bytes.subarray(i, i + nameLen));
|
|
394
|
+
i += nameLen;
|
|
395
|
+
const codecVersion = (bytes[i] << 8 | bytes[i + 1]) >>> 0;
|
|
396
|
+
i += 2;
|
|
397
|
+
const payload = bytes.subarray(i);
|
|
398
|
+
const codec = config.lookupCodec(name);
|
|
399
|
+
if (codec == null) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`decodeEnvelope: codec "${name}" not registered (envelope codec_v=${codecVersion})`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
return { codec, codecVersion, payload };
|
|
405
|
+
}
|
|
406
|
+
function registerBuiltinCodecs(config) {
|
|
407
|
+
config.registerCodec(JsonCodec);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/core/actor.ts
|
|
411
|
+
var DEFAULT_ACTOR = { type: "system", id: "" };
|
|
412
|
+
function normalizeActor(actor) {
|
|
413
|
+
if (actor == null) return DEFAULT_ACTOR;
|
|
414
|
+
const { type, id, ...rest } = actor;
|
|
415
|
+
return {
|
|
416
|
+
type: type ?? "system",
|
|
417
|
+
id: id ?? "",
|
|
418
|
+
...rest
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
319
422
|
// src/core/batch.ts
|
|
320
423
|
var MAX_DRAIN_ITERATIONS = 1e3;
|
|
321
424
|
var batchDepth = 0;
|
|
@@ -323,9 +426,20 @@ var flushInProgress = false;
|
|
|
323
426
|
var drainPhase2 = [];
|
|
324
427
|
var drainPhase3 = [];
|
|
325
428
|
var drainPhase4 = [];
|
|
429
|
+
var flushHooks = [];
|
|
326
430
|
function isBatching() {
|
|
327
431
|
return batchDepth > 0 || flushInProgress;
|
|
328
432
|
}
|
|
433
|
+
function isExplicitlyBatching() {
|
|
434
|
+
return batchDepth > 0;
|
|
435
|
+
}
|
|
436
|
+
function registerBatchFlushHook(hook) {
|
|
437
|
+
if (batchDepth > 0) {
|
|
438
|
+
flushHooks.push(hook);
|
|
439
|
+
} else {
|
|
440
|
+
hook();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
329
443
|
function batch(fn) {
|
|
330
444
|
batchDepth += 1;
|
|
331
445
|
let threw = false;
|
|
@@ -339,6 +453,13 @@ function batch(fn) {
|
|
|
339
453
|
if (batchDepth === 0) {
|
|
340
454
|
if (threw) {
|
|
341
455
|
if (!flushInProgress) {
|
|
456
|
+
const hooks = flushHooks.splice(0);
|
|
457
|
+
for (const h of hooks) {
|
|
458
|
+
try {
|
|
459
|
+
h();
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
342
463
|
drainPhase2.length = 0;
|
|
343
464
|
drainPhase3.length = 0;
|
|
344
465
|
drainPhase4.length = 0;
|
|
@@ -355,7 +476,18 @@ function drainPending() {
|
|
|
355
476
|
const errors = [];
|
|
356
477
|
let iterations = 0;
|
|
357
478
|
try {
|
|
358
|
-
while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0) {
|
|
479
|
+
while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0 || ownsFlush && flushHooks.length > 0) {
|
|
480
|
+
if (ownsFlush && flushHooks.length > 0) {
|
|
481
|
+
const hooks = flushHooks.splice(0);
|
|
482
|
+
for (const h of hooks) {
|
|
483
|
+
try {
|
|
484
|
+
h();
|
|
485
|
+
} catch (e) {
|
|
486
|
+
errors.push(e);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
359
491
|
iterations += 1;
|
|
360
492
|
if (iterations > MAX_DRAIN_ITERATIONS) {
|
|
361
493
|
drainPhase2.length = 0;
|
|
@@ -428,14 +560,6 @@ function downWithBatch(sink, messages, tierOf) {
|
|
|
428
560
|
}
|
|
429
561
|
}
|
|
430
562
|
|
|
431
|
-
// src/core/clock.ts
|
|
432
|
-
function monotonicNs() {
|
|
433
|
-
return Math.trunc(performance.now() * 1e6);
|
|
434
|
-
}
|
|
435
|
-
function wallClockNs() {
|
|
436
|
-
return Date.now() * 1e6;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
563
|
// src/core/messages.ts
|
|
440
564
|
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
441
565
|
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
@@ -459,101 +583,6 @@ var INVALIDATE_ONLY_BATCH = Object.freeze([INVALIDATE_MSG]);
|
|
|
459
583
|
var COMPLETE_ONLY_BATCH = Object.freeze([COMPLETE_MSG]);
|
|
460
584
|
var TEARDOWN_ONLY_BATCH = Object.freeze([TEARDOWN_MSG]);
|
|
461
585
|
|
|
462
|
-
// src/graph/codec.ts
|
|
463
|
-
var JsonCodec = {
|
|
464
|
-
name: "json",
|
|
465
|
-
version: 1,
|
|
466
|
-
contentType: "application/json",
|
|
467
|
-
encode(snapshot) {
|
|
468
|
-
const json = JSON.stringify(snapshot);
|
|
469
|
-
return new TextEncoder().encode(json);
|
|
470
|
-
},
|
|
471
|
-
decode(buffer, _codecVersion) {
|
|
472
|
-
const json = new TextDecoder().decode(buffer);
|
|
473
|
-
return JSON.parse(json);
|
|
474
|
-
}
|
|
475
|
-
};
|
|
476
|
-
var ENVELOPE_VERSION = 1;
|
|
477
|
-
var ENVELOPE_MIN_LEN = 4;
|
|
478
|
-
function encodeEnvelope(codec, payload) {
|
|
479
|
-
const nameBytes = new TextEncoder().encode(codec.name);
|
|
480
|
-
if (nameBytes.length === 0 || nameBytes.length > 255) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
`encodeEnvelope: codec name "${codec.name}" encodes to ${nameBytes.length} bytes (must be 1\u2013255)`
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
const cv = codec.version;
|
|
486
|
-
if (!Number.isInteger(cv) || cv < 0 || cv > 65535) {
|
|
487
|
-
throw new Error(
|
|
488
|
-
`encodeEnvelope: codec.version ${cv} out of u16 range (expected integer 0\u201365535)`
|
|
489
|
-
);
|
|
490
|
-
}
|
|
491
|
-
const totalLen = 1 + 1 + nameBytes.length + 2 + payload.length;
|
|
492
|
-
if (totalLen > 4294967295) {
|
|
493
|
-
throw new Error(
|
|
494
|
-
`encodeEnvelope: total envelope size ${totalLen} exceeds 2^32-1 bytes (payload ${payload.length} bytes)`
|
|
495
|
-
);
|
|
496
|
-
}
|
|
497
|
-
const out = new Uint8Array(totalLen);
|
|
498
|
-
let i = 0;
|
|
499
|
-
out[i++] = ENVELOPE_VERSION;
|
|
500
|
-
out[i++] = nameBytes.length;
|
|
501
|
-
out.set(nameBytes, i);
|
|
502
|
-
i += nameBytes.length;
|
|
503
|
-
out[i++] = cv >>> 8 & 255;
|
|
504
|
-
out[i++] = cv & 255;
|
|
505
|
-
out.set(payload, i);
|
|
506
|
-
return out;
|
|
507
|
-
}
|
|
508
|
-
function decodeEnvelope(bytes, config) {
|
|
509
|
-
if (bytes.length < ENVELOPE_MIN_LEN) {
|
|
510
|
-
throw new Error(`decodeEnvelope: bytes too short (${bytes.length} < ${ENVELOPE_MIN_LEN})`);
|
|
511
|
-
}
|
|
512
|
-
let i = 0;
|
|
513
|
-
const envVersion = bytes[i++];
|
|
514
|
-
if (envVersion !== ENVELOPE_VERSION) {
|
|
515
|
-
throw new Error(
|
|
516
|
-
`decodeEnvelope: unsupported envelope version ${envVersion} (expected ${ENVELOPE_VERSION})`
|
|
517
|
-
);
|
|
518
|
-
}
|
|
519
|
-
const nameLen = bytes[i++];
|
|
520
|
-
if (nameLen === 0) {
|
|
521
|
-
throw new Error("decodeEnvelope: name_len must be >= 1");
|
|
522
|
-
}
|
|
523
|
-
if (i + nameLen + 2 > bytes.length) {
|
|
524
|
-
throw new Error(
|
|
525
|
-
`decodeEnvelope: envelope truncated (need ${i + nameLen + 2} bytes, have ${bytes.length})`
|
|
526
|
-
);
|
|
527
|
-
}
|
|
528
|
-
const name = new TextDecoder().decode(bytes.subarray(i, i + nameLen));
|
|
529
|
-
i += nameLen;
|
|
530
|
-
const codecVersion = (bytes[i] << 8 | bytes[i + 1]) >>> 0;
|
|
531
|
-
i += 2;
|
|
532
|
-
const payload = bytes.subarray(i);
|
|
533
|
-
const codec = config.lookupCodec(name);
|
|
534
|
-
if (codec == null) {
|
|
535
|
-
throw new Error(
|
|
536
|
-
`decodeEnvelope: codec "${name}" not registered (envelope codec_v=${codecVersion})`
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
return { codec, codecVersion, payload };
|
|
540
|
-
}
|
|
541
|
-
function registerBuiltinCodecs(config) {
|
|
542
|
-
config.registerCodec(JsonCodec);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
// src/core/actor.ts
|
|
546
|
-
var DEFAULT_ACTOR = { type: "system", id: "" };
|
|
547
|
-
function normalizeActor(actor) {
|
|
548
|
-
if (actor == null) return DEFAULT_ACTOR;
|
|
549
|
-
const { type, id, ...rest } = actor;
|
|
550
|
-
return {
|
|
551
|
-
type: type ?? "system",
|
|
552
|
-
id: id ?? "",
|
|
553
|
-
...rest
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
|
|
557
586
|
// src/core/config.ts
|
|
558
587
|
var GraphReFlyConfig = class {
|
|
559
588
|
_messageTypes = /* @__PURE__ */ new Map();
|
|
@@ -1088,6 +1117,22 @@ var NodeImpl = class _NodeImpl {
|
|
|
1088
1117
|
* treats `0` as "wave settled" — O(1) check for full dep settlement.
|
|
1089
1118
|
*/
|
|
1090
1119
|
_dirtyDepCount = 0;
|
|
1120
|
+
// --- Per-batch emit accumulator (Bug 2: K+1 fan-in fix) ---
|
|
1121
|
+
/**
|
|
1122
|
+
* Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
|
|
1123
|
+
* its already-framed messages here instead of dispatching synchronously.
|
|
1124
|
+
* At batch end, `_flushBatchPending` runs (registered via
|
|
1125
|
+
* `registerBatchFlushHook`) and delivers the whole accumulated batch as
|
|
1126
|
+
* one `downWithBatch` call — collapsing what would otherwise be K
|
|
1127
|
+
* separate sink invocations into one. This is the fix for the diamond
|
|
1128
|
+
* fan-in K+1 over-fire.
|
|
1129
|
+
*
|
|
1130
|
+
* `null` outside batch (or after flush). Only ever appended to within
|
|
1131
|
+
* a single explicit batch lifetime; reset to `null` on flush. State
|
|
1132
|
+
* updates (cache, version, status) still happen per-emit via
|
|
1133
|
+
* `_updateState` — only the downstream delivery is coalesced.
|
|
1134
|
+
*/
|
|
1135
|
+
_batchPendingMessages = null;
|
|
1091
1136
|
// --- PAUSE/RESUME lock tracking (C0) ---
|
|
1092
1137
|
/**
|
|
1093
1138
|
* Set of active pause locks held against this node. Every `[PAUSE, lockId]`
|
|
@@ -1465,7 +1510,10 @@ var NodeImpl = class _NodeImpl {
|
|
|
1465
1510
|
dep.unsub = noopUnsub;
|
|
1466
1511
|
dep.unsub = dep.node.subscribe((msgs) => {
|
|
1467
1512
|
if (dep.unsub === null) return;
|
|
1513
|
+
const tierOf = this._config.tierOf;
|
|
1514
|
+
let sawSettlement = false;
|
|
1468
1515
|
for (const m of msgs) {
|
|
1516
|
+
if (tierOf(m[0]) >= 3) sawSettlement = true;
|
|
1469
1517
|
this._config.onMessage(
|
|
1470
1518
|
this,
|
|
1471
1519
|
m,
|
|
@@ -1473,6 +1521,7 @@ var NodeImpl = class _NodeImpl {
|
|
|
1473
1521
|
this._actions
|
|
1474
1522
|
);
|
|
1475
1523
|
}
|
|
1524
|
+
if (sawSettlement) this._maybeRunFnOnSettlement();
|
|
1476
1525
|
});
|
|
1477
1526
|
subscribedCount++;
|
|
1478
1527
|
}
|
|
@@ -1527,7 +1576,10 @@ var NodeImpl = class _NodeImpl {
|
|
|
1527
1576
|
try {
|
|
1528
1577
|
record.unsub = depNode.subscribe((msgs) => {
|
|
1529
1578
|
if (record.unsub === null) return;
|
|
1579
|
+
const tierOf = this._config.tierOf;
|
|
1580
|
+
let sawSettlement = false;
|
|
1530
1581
|
for (const m of msgs) {
|
|
1582
|
+
if (tierOf(m[0]) >= 3) sawSettlement = true;
|
|
1531
1583
|
this._config.onMessage(
|
|
1532
1584
|
this,
|
|
1533
1585
|
m,
|
|
@@ -1535,6 +1587,7 @@ var NodeImpl = class _NodeImpl {
|
|
|
1535
1587
|
this._actions
|
|
1536
1588
|
);
|
|
1537
1589
|
}
|
|
1590
|
+
if (sawSettlement) this._maybeRunFnOnSettlement();
|
|
1538
1591
|
});
|
|
1539
1592
|
} catch (err) {
|
|
1540
1593
|
record.unsub = null;
|
|
@@ -1656,7 +1709,6 @@ var NodeImpl = class _NodeImpl {
|
|
|
1656
1709
|
}
|
|
1657
1710
|
return;
|
|
1658
1711
|
}
|
|
1659
|
-
this._maybeRunFnOnSettlement();
|
|
1660
1712
|
}
|
|
1661
1713
|
// --- Centralized dep-state transitions (A3 settlement counters) ---
|
|
1662
1714
|
//
|
|
@@ -2028,10 +2080,10 @@ var NodeImpl = class _NodeImpl {
|
|
|
2028
2080
|
}
|
|
2029
2081
|
}
|
|
2030
2082
|
if (immediate.length > 0) {
|
|
2031
|
-
|
|
2083
|
+
this._dispatchOrAccumulate(immediate);
|
|
2032
2084
|
}
|
|
2033
2085
|
} else {
|
|
2034
|
-
|
|
2086
|
+
this._dispatchOrAccumulate(finalMessages);
|
|
2035
2087
|
}
|
|
2036
2088
|
}
|
|
2037
2089
|
if (equalsError != null) {
|
|
@@ -2154,6 +2206,50 @@ var NodeImpl = class _NodeImpl {
|
|
|
2154
2206
|
const snapshot = [...this._sinks];
|
|
2155
2207
|
for (const sink of snapshot) sink(messages);
|
|
2156
2208
|
};
|
|
2209
|
+
/**
|
|
2210
|
+
* @internal Dispatch entry point that respects the per-batch emit
|
|
2211
|
+
* accumulator (Bug 2). Inside an explicit `batch()` scope, append to
|
|
2212
|
+
* `_batchPendingMessages` and register a flush hook on first append.
|
|
2213
|
+
* Outside batch — or during a drain (where `flushInProgress` is true
|
|
2214
|
+
* but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
|
|
2215
|
+
*
|
|
2216
|
+
* Per-emit state updates (`_frameBatch`, `_updateState`) have already
|
|
2217
|
+
* happened by the time we reach here; only the **downstream delivery**
|
|
2218
|
+
* is coalesced. Cache, version, and status are visible mid-batch on
|
|
2219
|
+
* the emitting node itself.
|
|
2220
|
+
*/
|
|
2221
|
+
_dispatchOrAccumulate(messages) {
|
|
2222
|
+
if (isExplicitlyBatching()) {
|
|
2223
|
+
if (this._batchPendingMessages === null) {
|
|
2224
|
+
this._batchPendingMessages = [];
|
|
2225
|
+
registerBatchFlushHook(() => this._flushBatchPending());
|
|
2226
|
+
}
|
|
2227
|
+
for (const m of messages) this._batchPendingMessages.push(m);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
downWithBatch(this._deliverToSinks, messages, this._config.tierOf);
|
|
2231
|
+
}
|
|
2232
|
+
/**
|
|
2233
|
+
* @internal Flushes the accumulated batch through `downWithBatch` and
|
|
2234
|
+
* clears the pending state. Idempotent — safe to call when pending is
|
|
2235
|
+
* already null or empty (e.g. on a `batch()` throw, where the hook
|
|
2236
|
+
* fires for cleanup but the drainPhase queues are wiped after).
|
|
2237
|
+
*
|
|
2238
|
+
* Critical: the accumulated batch is interleaved per-emit framings like
|
|
2239
|
+
* `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
|
|
2240
|
+
* re-frame to sort by tier before handing to `downWithBatch`, which
|
|
2241
|
+
* assumes pre-sorted input. `_frameBatch` also handles the synthetic
|
|
2242
|
+
* DIRTY prepend rule (no-op here — `hasDirty` is true since each
|
|
2243
|
+
* accumulated emit already carries its own DIRTY prefix).
|
|
2244
|
+
*/
|
|
2245
|
+
_flushBatchPending() {
|
|
2246
|
+
const pending = this._batchPendingMessages;
|
|
2247
|
+
if (pending === null) return;
|
|
2248
|
+
this._batchPendingMessages = null;
|
|
2249
|
+
if (pending.length === 0) return;
|
|
2250
|
+
const framed = this._frameBatch(pending);
|
|
2251
|
+
downWithBatch(this._deliverToSinks, framed, this._config.tierOf);
|
|
2252
|
+
}
|
|
2157
2253
|
};
|
|
2158
2254
|
var isNodeArray = (value) => Array.isArray(value);
|
|
2159
2255
|
var isNodeOptionsObject = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
@@ -4760,6 +4856,12 @@ function reachable(described, from, direction, options = {}) {
|
|
|
4760
4856
|
return paths;
|
|
4761
4857
|
}
|
|
4762
4858
|
|
|
4859
|
+
// src/patterns/_internal.ts
|
|
4860
|
+
function emitToMeta(metaNode, value) {
|
|
4861
|
+
if (metaNode == null) return;
|
|
4862
|
+
downWithBatch((msgs) => metaNode.down(msgs), [[DATA, value]], defaultConfig.tierOf);
|
|
4863
|
+
}
|
|
4864
|
+
|
|
4763
4865
|
// src/patterns/reactive-layout/reactive-layout.ts
|
|
4764
4866
|
function isCJK(s) {
|
|
4765
4867
|
for (const ch of s) {
|
|
@@ -5216,13 +5318,9 @@ function reactiveLayout(opts) {
|
|
|
5216
5318
|
const hitRate = lookups === 0 ? 1 : measureStats.hits / lookups;
|
|
5217
5319
|
const meta = segmentsNode.meta;
|
|
5218
5320
|
if (meta) {
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
const tierOf = defaultConfig.tierOf;
|
|
5223
|
-
downWithBatch((msgs) => meta["cache-hit-rate"]?.down(msgs), [[DATA, hr]], tierOf);
|
|
5224
|
-
downWithBatch((msgs) => meta["segment-count"]?.down(msgs), [[DATA, len]], tierOf);
|
|
5225
|
-
downWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, el]], tierOf);
|
|
5321
|
+
emitToMeta(meta["cache-hit-rate"], hitRate);
|
|
5322
|
+
emitToMeta(meta["segment-count"], result.length);
|
|
5323
|
+
emitToMeta(meta["layout-time-ns"], elapsed);
|
|
5226
5324
|
}
|
|
5227
5325
|
actions.emit(result);
|
|
5228
5326
|
return () => {
|
|
@@ -5442,9 +5540,8 @@ function reactiveBlockLayout(opts) {
|
|
|
5442
5540
|
const elapsed = monotonicNs() - t0;
|
|
5443
5541
|
const meta = measuredBlocksNode.meta;
|
|
5444
5542
|
if (meta) {
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
downWithBatch((msgs) => meta["layout-time-ns"]?.down(msgs), [[DATA, elapsed]], tierOf);
|
|
5543
|
+
emitToMeta(meta["block-count"], result.length);
|
|
5544
|
+
emitToMeta(meta["layout-time-ns"], elapsed);
|
|
5448
5545
|
}
|
|
5449
5546
|
actions.emit(result);
|
|
5450
5547
|
return () => {
|