@graphrefly/graphrefly 0.21.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/README.md +7 -5
- package/dist/chunk-263BEJJO.js +115 -0
- package/dist/chunk-263BEJJO.js.map +1 -0
- package/dist/chunk-2GQLMQVJ.js +47 -0
- package/dist/chunk-2GQLMQVJ.js.map +1 -0
- package/dist/chunk-32N5A454.js +36 -0
- package/dist/chunk-32N5A454.js.map +1 -0
- package/dist/chunk-7TAQJHQV.js +103 -0
- package/dist/chunk-7TAQJHQV.js.map +1 -0
- package/dist/{chunk-VOQFK7YN.js → chunk-CWYPA63G.js} +109 -259
- package/dist/chunk-CWYPA63G.js.map +1 -0
- package/dist/{chunk-7IGHIFTT.js → chunk-HVBX5KIW.js} +15 -26
- package/dist/chunk-HVBX5KIW.js.map +1 -0
- package/dist/chunk-JFONSPNF.js +391 -0
- package/dist/chunk-JFONSPNF.js.map +1 -0
- package/dist/chunk-NZMBRXQV.js +2330 -0
- package/dist/chunk-NZMBRXQV.js.map +1 -0
- package/dist/{chunk-XWBVAO2R.js → chunk-PNUZM7PC.js} +20 -30
- package/dist/chunk-PNUZM7PC.js.map +1 -0
- package/dist/{chunk-ZTCDY5NQ.js → chunk-PX6PDUJ5.js} +34 -50
- package/dist/chunk-PX6PDUJ5.js.map +1 -0
- package/dist/chunk-XRFJJ2IU.js +2417 -0
- package/dist/chunk-XRFJJ2IU.js.map +1 -0
- package/dist/chunk-XTLYW4FR.js +6829 -0
- package/dist/chunk-XTLYW4FR.js.map +1 -0
- package/dist/compat/nestjs/index.cjs +3489 -2286
- package/dist/compat/nestjs/index.cjs.map +1 -1
- package/dist/compat/nestjs/index.d.cts +6 -4
- package/dist/compat/nestjs/index.d.ts +6 -4
- package/dist/compat/nestjs/index.js +10 -8
- package/dist/core/index.cjs +1706 -1217
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +3 -2
- package/dist/core/index.d.ts +3 -2
- package/dist/core/index.js +37 -34
- package/dist/extra/index.cjs +7519 -6125
- 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 +63 -34
- package/dist/graph/index.cjs +3199 -2212
- package/dist/graph/index.cjs.map +1 -1
- package/dist/graph/index.d.cts +5 -3
- package/dist/graph/index.d.ts +5 -3
- package/dist/graph/index.js +24 -11
- package/dist/graph-BtdSRHUc.d.cts +1128 -0
- package/dist/graph-CEO2FkLY.d.ts +1128 -0
- package/dist/{index-DuN3bhtm.d.ts → index-B0tfuXwV.d.cts} +1697 -586
- package/dist/index-BFGjXbiP.d.cts +315 -0
- package/dist/{index-CgSiUouz.d.ts → index-BPlWVAKY.d.cts} +4 -4
- package/dist/index-BUj3ASVe.d.cts +406 -0
- package/dist/{index-VHA43cGP.d.cts → index-C59uSJAH.d.cts} +2 -2
- package/dist/index-CkElcUY6.d.ts +315 -0
- package/dist/index-DSPc5rkv.d.ts +406 -0
- package/dist/{index-BjtlNirP.d.cts → index-DgscL7v0.d.ts} +4 -4
- package/dist/{index-SFzE_KTa.d.cts → index-RXN94sHK.d.ts} +1697 -586
- package/dist/{index-8a605sg9.d.ts → index-jEtF4N7L.d.ts} +2 -2
- package/dist/index.cjs +9947 -7949
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +214 -37
- package/dist/index.d.ts +214 -37
- package/dist/index.js +919 -648
- package/dist/index.js.map +1 -1
- package/dist/meta-3QjzotRv.d.ts +41 -0
- package/dist/meta-B-Lbs4-O.d.cts +41 -0
- package/dist/node-C7PD3sn9.d.cts +1188 -0
- package/dist/node-C7PD3sn9.d.ts +1188 -0
- package/dist/{observable-DcBwQY7t.d.ts → observable-EyO-moQY.d.ts} +1 -1
- package/dist/{observable-C8Kx_O6k.d.cts → observable-axpzv1K2.d.cts} +1 -1
- package/dist/patterns/reactive-layout/index.cjs +3205 -2138
- package/dist/patterns/reactive-layout/index.cjs.map +1 -1
- package/dist/patterns/reactive-layout/index.d.cts +5 -3
- package/dist/patterns/reactive-layout/index.d.ts +5 -3
- package/dist/patterns/reactive-layout/index.js +7 -4
- package/dist/storage-CHT5WE9m.d.ts +182 -0
- package/dist/storage-DIgAr7M_.d.cts +182 -0
- package/package.json +2 -1
- package/dist/chunk-2UDLYZHT.js +0 -2117
- package/dist/chunk-2UDLYZHT.js.map +0 -1
- package/dist/chunk-4MQ2J6IG.js +0 -1631
- package/dist/chunk-4MQ2J6IG.js.map +0 -1
- package/dist/chunk-7IGHIFTT.js.map +0 -1
- package/dist/chunk-DOSLSFKL.js +0 -162
- package/dist/chunk-DOSLSFKL.js.map +0 -1
- package/dist/chunk-ECN37NVS.js +0 -6227
- package/dist/chunk-ECN37NVS.js.map +0 -1
- package/dist/chunk-G66H6ZRK.js +0 -111
- package/dist/chunk-G66H6ZRK.js.map +0 -1
- package/dist/chunk-VOQFK7YN.js.map +0 -1
- package/dist/chunk-WZ2Z2CRV.js +0 -32
- package/dist/chunk-WZ2Z2CRV.js.map +0 -1
- package/dist/chunk-XWBVAO2R.js.map +0 -1
- package/dist/chunk-ZTCDY5NQ.js.map +0 -1
- package/dist/graph-KsTe57nI.d.cts +0 -750
- package/dist/graph-mILUUqW8.d.ts +0 -750
- package/dist/index-B2SvPEbc.d.ts +0 -257
- package/dist/index-BHfg_Ez3.d.ts +0 -629
- package/dist/index-Bc_diYYJ.d.cts +0 -629
- package/dist/index-UudxGnzc.d.cts +0 -257
- package/dist/meta-BnG7XAaE.d.cts +0 -778
- package/dist/meta-BnG7XAaE.d.ts +0 -778
package/dist/core/index.cjs
CHANGED
|
@@ -20,50 +20,53 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/core/index.ts
|
|
21
21
|
var core_exports = {};
|
|
22
22
|
__export(core_exports, {
|
|
23
|
-
CLEANUP_RESULT: () => CLEANUP_RESULT,
|
|
24
23
|
COMPLETE: () => COMPLETE,
|
|
24
|
+
COMPLETE_MSG: () => COMPLETE_MSG,
|
|
25
|
+
COMPLETE_ONLY_BATCH: () => COMPLETE_ONLY_BATCH,
|
|
25
26
|
DATA: () => DATA,
|
|
26
27
|
DEFAULT_ACTOR: () => DEFAULT_ACTOR,
|
|
27
|
-
DEFAULT_DOWN: () => DEFAULT_DOWN,
|
|
28
28
|
DIRTY: () => DIRTY,
|
|
29
|
-
|
|
29
|
+
DIRTY_MSG: () => DIRTY_MSG,
|
|
30
|
+
DIRTY_ONLY_BATCH: () => DIRTY_ONLY_BATCH,
|
|
30
31
|
ERROR: () => ERROR,
|
|
32
|
+
GraphReFlyConfig: () => GraphReFlyConfig,
|
|
31
33
|
GuardDenied: () => GuardDenied,
|
|
32
34
|
INVALIDATE: () => INVALIDATE,
|
|
35
|
+
INVALIDATE_MSG: () => INVALIDATE_MSG,
|
|
36
|
+
INVALIDATE_ONLY_BATCH: () => INVALIDATE_ONLY_BATCH,
|
|
37
|
+
NodeImpl: () => NodeImpl,
|
|
33
38
|
PAUSE: () => PAUSE,
|
|
34
39
|
RESOLVED: () => RESOLVED,
|
|
40
|
+
RESOLVED_MSG: () => RESOLVED_MSG,
|
|
41
|
+
RESOLVED_ONLY_BATCH: () => RESOLVED_ONLY_BATCH,
|
|
35
42
|
RESUME: () => RESUME,
|
|
36
|
-
ResettableTimer: () => ResettableTimer,
|
|
37
43
|
START: () => START,
|
|
44
|
+
START_MSG: () => START_MSG,
|
|
38
45
|
TEARDOWN: () => TEARDOWN,
|
|
46
|
+
TEARDOWN_MSG: () => TEARDOWN_MSG,
|
|
47
|
+
TEARDOWN_ONLY_BATCH: () => TEARDOWN_ONLY_BATCH,
|
|
39
48
|
accessHintForGuard: () => accessHintForGuard,
|
|
40
49
|
advanceVersion: () => advanceVersion,
|
|
50
|
+
autoTrackNode: () => autoTrackNode,
|
|
41
51
|
batch: () => batch,
|
|
42
|
-
|
|
43
|
-
cleanupResult: () => cleanupResult,
|
|
52
|
+
configure: () => configure,
|
|
44
53
|
createVersioning: () => createVersioning,
|
|
54
|
+
defaultConfig: () => defaultConfig,
|
|
45
55
|
defaultHash: () => defaultHash,
|
|
46
56
|
derived: () => derived,
|
|
47
57
|
downWithBatch: () => downWithBatch,
|
|
48
58
|
dynamicNode: () => dynamicNode,
|
|
49
59
|
effect: () => effect,
|
|
50
60
|
isBatching: () => isBatching,
|
|
51
|
-
isKnownMessageType: () => isKnownMessageType,
|
|
52
|
-
isLocalOnly: () => isLocalOnly,
|
|
53
|
-
isPhase2Message: () => isPhase2Message,
|
|
54
|
-
isTerminalMessage: () => isTerminalMessage,
|
|
55
61
|
isV1: () => isV1,
|
|
56
|
-
knownMessageTypes: () => knownMessageTypes,
|
|
57
|
-
messageTier: () => messageTier,
|
|
58
62
|
monotonicNs: () => monotonicNs,
|
|
59
63
|
node: () => node,
|
|
60
64
|
normalizeActor: () => normalizeActor,
|
|
61
|
-
partitionForBatch: () => partitionForBatch,
|
|
62
65
|
pipe: () => pipe,
|
|
63
66
|
policy: () => policy,
|
|
64
67
|
policyFromRules: () => policyFromRules,
|
|
65
68
|
producer: () => producer,
|
|
66
|
-
|
|
69
|
+
registerBuiltins: () => registerBuiltins,
|
|
67
70
|
resolveDescribeFields: () => resolveDescribeFields,
|
|
68
71
|
state: () => state,
|
|
69
72
|
wallClockNs: () => wallClockNs
|
|
@@ -82,66 +85,27 @@ function normalizeActor(actor) {
|
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
// src/core/messages.ts
|
|
86
|
-
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
87
|
-
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
88
|
-
var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
|
|
89
|
-
var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
|
|
90
|
-
var INVALIDATE = /* @__PURE__ */ Symbol.for("graphrefly/INVALIDATE");
|
|
91
|
-
var PAUSE = /* @__PURE__ */ Symbol.for("graphrefly/PAUSE");
|
|
92
|
-
var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
|
|
93
|
-
var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
|
|
94
|
-
var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
|
|
95
|
-
var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
|
|
96
|
-
var knownMessageTypes = [
|
|
97
|
-
START,
|
|
98
|
-
DATA,
|
|
99
|
-
DIRTY,
|
|
100
|
-
RESOLVED,
|
|
101
|
-
INVALIDATE,
|
|
102
|
-
PAUSE,
|
|
103
|
-
RESUME,
|
|
104
|
-
TEARDOWN,
|
|
105
|
-
COMPLETE,
|
|
106
|
-
ERROR
|
|
107
|
-
];
|
|
108
|
-
var knownMessageSet = new Set(knownMessageTypes);
|
|
109
|
-
function isKnownMessageType(t) {
|
|
110
|
-
return knownMessageSet.has(t);
|
|
111
|
-
}
|
|
112
|
-
function messageTier(t) {
|
|
113
|
-
if (t === START) return 0;
|
|
114
|
-
if (t === DIRTY || t === INVALIDATE) return 1;
|
|
115
|
-
if (t === PAUSE || t === RESUME) return 2;
|
|
116
|
-
if (t === DATA || t === RESOLVED) return 3;
|
|
117
|
-
if (t === COMPLETE || t === ERROR) return 4;
|
|
118
|
-
if (t === TEARDOWN) return 5;
|
|
119
|
-
return 1;
|
|
120
|
-
}
|
|
121
|
-
function isPhase2Message(msg) {
|
|
122
|
-
const t = msg[0];
|
|
123
|
-
return t === DATA || t === RESOLVED;
|
|
124
|
-
}
|
|
125
|
-
function isTerminalMessage(t) {
|
|
126
|
-
return t === COMPLETE || t === ERROR;
|
|
127
|
-
}
|
|
128
|
-
function isLocalOnly(t) {
|
|
129
|
-
if (!knownMessageSet.has(t)) return false;
|
|
130
|
-
return messageTier(t) < 3;
|
|
131
|
-
}
|
|
132
|
-
function propagatesToMeta(t) {
|
|
133
|
-
return t === TEARDOWN;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
88
|
// src/core/batch.ts
|
|
137
89
|
var MAX_DRAIN_ITERATIONS = 1e3;
|
|
138
90
|
var batchDepth = 0;
|
|
139
91
|
var flushInProgress = false;
|
|
140
|
-
var
|
|
141
|
-
var
|
|
92
|
+
var drainPhase2 = [];
|
|
93
|
+
var drainPhase3 = [];
|
|
94
|
+
var drainPhase4 = [];
|
|
95
|
+
var flushHooks = [];
|
|
142
96
|
function isBatching() {
|
|
143
97
|
return batchDepth > 0 || flushInProgress;
|
|
144
98
|
}
|
|
99
|
+
function isExplicitlyBatching() {
|
|
100
|
+
return batchDepth > 0;
|
|
101
|
+
}
|
|
102
|
+
function registerBatchFlushHook(hook) {
|
|
103
|
+
if (batchDepth > 0) {
|
|
104
|
+
flushHooks.push(hook);
|
|
105
|
+
} else {
|
|
106
|
+
hook();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
145
109
|
function batch(fn) {
|
|
146
110
|
batchDepth += 1;
|
|
147
111
|
let threw = false;
|
|
@@ -155,8 +119,16 @@ function batch(fn) {
|
|
|
155
119
|
if (batchDepth === 0) {
|
|
156
120
|
if (threw) {
|
|
157
121
|
if (!flushInProgress) {
|
|
158
|
-
|
|
159
|
-
|
|
122
|
+
const hooks = flushHooks.splice(0);
|
|
123
|
+
for (const h of hooks) {
|
|
124
|
+
try {
|
|
125
|
+
h();
|
|
126
|
+
} catch {
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
drainPhase2.length = 0;
|
|
130
|
+
drainPhase3.length = 0;
|
|
131
|
+
drainPhase4.length = 0;
|
|
160
132
|
}
|
|
161
133
|
} else {
|
|
162
134
|
drainPending();
|
|
@@ -166,147 +138,348 @@ function batch(fn) {
|
|
|
166
138
|
}
|
|
167
139
|
function drainPending() {
|
|
168
140
|
const ownsFlush = !flushInProgress;
|
|
169
|
-
if (ownsFlush)
|
|
170
|
-
flushInProgress = true;
|
|
171
|
-
}
|
|
141
|
+
if (ownsFlush) flushInProgress = true;
|
|
172
142
|
const errors = [];
|
|
143
|
+
let iterations = 0;
|
|
173
144
|
try {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (iterations > MAX_DRAIN_ITERATIONS) {
|
|
179
|
-
pendingPhase2.length = 0;
|
|
180
|
-
pendingPhase3.length = 0;
|
|
181
|
-
throw new Error(
|
|
182
|
-
`batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
const ops = pendingPhase2.splice(0);
|
|
186
|
-
for (const run of ops) {
|
|
145
|
+
while (drainPhase2.length > 0 || drainPhase3.length > 0 || drainPhase4.length > 0 || ownsFlush && flushHooks.length > 0) {
|
|
146
|
+
if (ownsFlush && flushHooks.length > 0) {
|
|
147
|
+
const hooks = flushHooks.splice(0);
|
|
148
|
+
for (const h of hooks) {
|
|
187
149
|
try {
|
|
188
|
-
|
|
150
|
+
h();
|
|
189
151
|
} catch (e) {
|
|
190
152
|
errors.push(e);
|
|
191
153
|
}
|
|
192
154
|
}
|
|
155
|
+
continue;
|
|
193
156
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
157
|
+
iterations += 1;
|
|
158
|
+
if (iterations > MAX_DRAIN_ITERATIONS) {
|
|
159
|
+
drainPhase2.length = 0;
|
|
160
|
+
drainPhase3.length = 0;
|
|
161
|
+
drainPhase4.length = 0;
|
|
162
|
+
throw new Error(
|
|
163
|
+
`batch drain exceeded ${MAX_DRAIN_ITERATIONS} iterations \u2014 likely a reactive cycle`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
const queue = drainPhase2.length > 0 ? drainPhase2 : drainPhase3.length > 0 ? drainPhase3 : drainPhase4;
|
|
167
|
+
const ops = queue.splice(0);
|
|
168
|
+
for (const run of ops) {
|
|
169
|
+
try {
|
|
170
|
+
run();
|
|
171
|
+
} catch (e) {
|
|
172
|
+
errors.push(e);
|
|
210
173
|
}
|
|
211
174
|
}
|
|
212
175
|
}
|
|
213
176
|
} finally {
|
|
214
|
-
if (ownsFlush)
|
|
215
|
-
flushInProgress = false;
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (errors.length === 1) {
|
|
219
|
-
throw errors[0];
|
|
177
|
+
if (ownsFlush) flushInProgress = false;
|
|
220
178
|
}
|
|
179
|
+
if (errors.length === 1) throw errors[0];
|
|
221
180
|
if (errors.length > 1) {
|
|
222
181
|
throw new AggregateError(errors, "batch drain: multiple callbacks threw");
|
|
223
182
|
}
|
|
224
183
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
227
|
-
const deferred = [];
|
|
228
|
-
const terminal = [];
|
|
229
|
-
for (const m of messages) {
|
|
230
|
-
if (isPhase2Message(m)) {
|
|
231
|
-
deferred.push(m);
|
|
232
|
-
} else if (isTerminalMessage(m[0])) {
|
|
233
|
-
terminal.push(m);
|
|
234
|
-
} else {
|
|
235
|
-
immediate.push(m);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return { immediate, deferred, terminal };
|
|
239
|
-
}
|
|
240
|
-
function downWithBatch(sink, messages, phase = 2, options) {
|
|
241
|
-
if (messages.length === 0) {
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
if (options?.strategy === "sequential") {
|
|
245
|
-
_downSequential(sink, messages, phase);
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
const queue = phase === 3 ? pendingPhase3 : pendingPhase2;
|
|
184
|
+
function downWithBatch(sink, messages, tierOf) {
|
|
185
|
+
if (messages.length === 0) return;
|
|
249
186
|
if (messages.length === 1) {
|
|
250
|
-
const
|
|
251
|
-
if (
|
|
252
|
-
if (isBatching()) {
|
|
253
|
-
queue.push(() => sink(messages));
|
|
254
|
-
} else {
|
|
255
|
-
sink(messages);
|
|
256
|
-
}
|
|
257
|
-
} else if (isTerminalMessage(t)) {
|
|
258
|
-
if (isBatching()) {
|
|
259
|
-
queue.push(() => sink(messages));
|
|
260
|
-
} else {
|
|
261
|
-
sink(messages);
|
|
262
|
-
}
|
|
263
|
-
} else {
|
|
187
|
+
const tier = tierOf(messages[0][0]);
|
|
188
|
+
if (tier < 3 || !isBatching()) {
|
|
264
189
|
sink(messages);
|
|
190
|
+
return;
|
|
265
191
|
}
|
|
192
|
+
const queue = tier >= 5 ? drainPhase4 : tier === 4 ? drainPhase3 : drainPhase2;
|
|
193
|
+
queue.push(() => sink(messages));
|
|
266
194
|
return;
|
|
267
195
|
}
|
|
268
|
-
const
|
|
269
|
-
|
|
196
|
+
const n = messages.length;
|
|
197
|
+
let phase2Start = n;
|
|
198
|
+
let phase3Start = n;
|
|
199
|
+
let phase4Start = n;
|
|
200
|
+
let i = 0;
|
|
201
|
+
while (i < n && tierOf(messages[i][0]) < 3) i++;
|
|
202
|
+
phase2Start = i;
|
|
203
|
+
while (i < n && tierOf(messages[i][0]) === 3) i++;
|
|
204
|
+
phase3Start = i;
|
|
205
|
+
while (i < n && tierOf(messages[i][0]) === 4) i++;
|
|
206
|
+
phase4Start = i;
|
|
207
|
+
const batching = isBatching();
|
|
208
|
+
if (phase2Start > 0) {
|
|
209
|
+
const immediate = messages.slice(0, phase2Start);
|
|
270
210
|
sink(immediate);
|
|
271
211
|
}
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
212
|
+
if (phase3Start > phase2Start) {
|
|
213
|
+
const phase2 = messages.slice(phase2Start, phase3Start);
|
|
214
|
+
if (batching) drainPhase2.push(() => sink(phase2));
|
|
215
|
+
else sink(phase2);
|
|
216
|
+
}
|
|
217
|
+
if (phase4Start > phase3Start) {
|
|
218
|
+
const phase3 = messages.slice(phase3Start, phase4Start);
|
|
219
|
+
if (batching) drainPhase3.push(() => sink(phase3));
|
|
220
|
+
else sink(phase3);
|
|
221
|
+
}
|
|
222
|
+
if (n > phase4Start) {
|
|
223
|
+
const phase4 = messages.slice(phase4Start, n);
|
|
224
|
+
if (batching) drainPhase4.push(() => sink(phase4));
|
|
225
|
+
else sink(phase4);
|
|
286
226
|
}
|
|
287
227
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
228
|
+
|
|
229
|
+
// src/core/clock.ts
|
|
230
|
+
function monotonicNs() {
|
|
231
|
+
return Math.trunc(performance.now() * 1e6);
|
|
232
|
+
}
|
|
233
|
+
function wallClockNs() {
|
|
234
|
+
return Date.now() * 1e6;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/core/messages.ts
|
|
238
|
+
var START = /* @__PURE__ */ Symbol.for("graphrefly/START");
|
|
239
|
+
var DATA = /* @__PURE__ */ Symbol.for("graphrefly/DATA");
|
|
240
|
+
var DIRTY = /* @__PURE__ */ Symbol.for("graphrefly/DIRTY");
|
|
241
|
+
var RESOLVED = /* @__PURE__ */ Symbol.for("graphrefly/RESOLVED");
|
|
242
|
+
var INVALIDATE = /* @__PURE__ */ Symbol.for("graphrefly/INVALIDATE");
|
|
243
|
+
var PAUSE = /* @__PURE__ */ Symbol.for("graphrefly/PAUSE");
|
|
244
|
+
var RESUME = /* @__PURE__ */ Symbol.for("graphrefly/RESUME");
|
|
245
|
+
var TEARDOWN = /* @__PURE__ */ Symbol.for("graphrefly/TEARDOWN");
|
|
246
|
+
var COMPLETE = /* @__PURE__ */ Symbol.for("graphrefly/COMPLETE");
|
|
247
|
+
var ERROR = /* @__PURE__ */ Symbol.for("graphrefly/ERROR");
|
|
248
|
+
var DIRTY_MSG = Object.freeze([DIRTY]);
|
|
249
|
+
var RESOLVED_MSG = Object.freeze([RESOLVED]);
|
|
250
|
+
var INVALIDATE_MSG = Object.freeze([INVALIDATE]);
|
|
251
|
+
var START_MSG = Object.freeze([START]);
|
|
252
|
+
var COMPLETE_MSG = Object.freeze([COMPLETE]);
|
|
253
|
+
var TEARDOWN_MSG = Object.freeze([TEARDOWN]);
|
|
254
|
+
var DIRTY_ONLY_BATCH = Object.freeze([DIRTY_MSG]);
|
|
255
|
+
var RESOLVED_ONLY_BATCH = Object.freeze([RESOLVED_MSG]);
|
|
256
|
+
var INVALIDATE_ONLY_BATCH = Object.freeze([INVALIDATE_MSG]);
|
|
257
|
+
var COMPLETE_ONLY_BATCH = Object.freeze([COMPLETE_MSG]);
|
|
258
|
+
var TEARDOWN_ONLY_BATCH = Object.freeze([TEARDOWN_MSG]);
|
|
259
|
+
|
|
260
|
+
// src/core/config.ts
|
|
261
|
+
var GraphReFlyConfig = class {
|
|
262
|
+
_messageTypes = /* @__PURE__ */ new Map();
|
|
263
|
+
_codecs = /* @__PURE__ */ new Map();
|
|
264
|
+
_onMessage;
|
|
265
|
+
_onSubscribe;
|
|
266
|
+
_defaultVersioning;
|
|
267
|
+
_defaultHashFn;
|
|
268
|
+
_inspectorEnabled = !(typeof process !== "undefined" && process.env?.NODE_ENV === "production");
|
|
269
|
+
_globalInspector;
|
|
270
|
+
_frozen = false;
|
|
271
|
+
/**
|
|
272
|
+
* Pre-bound tier lookup — shared by every node bound to this config. Since
|
|
273
|
+
* the registry is frozen on first hook access, this closure can be built
|
|
274
|
+
* once in the constructor and handed directly to `downWithBatch` /
|
|
275
|
+
* `_frameBatch` paths without per-node or per-emission `.bind(config)`
|
|
276
|
+
* allocation.
|
|
277
|
+
*/
|
|
278
|
+
tierOf;
|
|
279
|
+
constructor(init) {
|
|
280
|
+
this._onMessage = init.onMessage;
|
|
281
|
+
this._onSubscribe = init.onSubscribe;
|
|
282
|
+
this._defaultVersioning = init.defaultVersioning;
|
|
283
|
+
this._defaultHashFn = init.defaultHashFn;
|
|
284
|
+
this.tierOf = (t) => {
|
|
285
|
+
const reg = this._messageTypes.get(t);
|
|
286
|
+
return reg != null ? reg.tier : 1;
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
// --- Hook getters (freeze on read) ---
|
|
290
|
+
get onMessage() {
|
|
291
|
+
this._frozen = true;
|
|
292
|
+
return this._onMessage;
|
|
293
|
+
}
|
|
294
|
+
get onSubscribe() {
|
|
295
|
+
this._frozen = true;
|
|
296
|
+
return this._onSubscribe;
|
|
297
|
+
}
|
|
298
|
+
// --- Hook setters (throw when frozen) ---
|
|
299
|
+
set onMessage(v) {
|
|
300
|
+
this._assertUnfrozen();
|
|
301
|
+
this._onMessage = v;
|
|
302
|
+
}
|
|
303
|
+
set onSubscribe(v) {
|
|
304
|
+
this._assertUnfrozen();
|
|
305
|
+
this._onSubscribe = v;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Default versioning level applied to every node bound to this config,
|
|
309
|
+
* unless the node's own `opts.versioning` provides an explicit override.
|
|
310
|
+
* Setting this is only allowed before the config freezes (i.e., before
|
|
311
|
+
* the first node is created) so every node in the graph sees a
|
|
312
|
+
* consistent starting level. Individual nodes can still opt into a
|
|
313
|
+
* higher level via `opts.versioning`, or post-hoc via
|
|
314
|
+
* `NodeImpl._applyVersioning(level)` when the node is quiescent.
|
|
315
|
+
*
|
|
316
|
+
* v0 is the minimum opt-in — unversioned nodes (`undefined`) skip
|
|
317
|
+
* the version counter entirely. v1 adds content-addressed cid.
|
|
318
|
+
* Future levels (v2, v3) are reserved for linked-history and
|
|
319
|
+
* cryptographic attestation extensions.
|
|
320
|
+
*/
|
|
321
|
+
get defaultVersioning() {
|
|
322
|
+
return this._defaultVersioning;
|
|
323
|
+
}
|
|
324
|
+
set defaultVersioning(v) {
|
|
325
|
+
this._assertUnfrozen();
|
|
326
|
+
this._defaultVersioning = v;
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Default content-hash function applied to every versioned node bound
|
|
330
|
+
* to this config, unless the node's own `opts.versioningHash` provides
|
|
331
|
+
* an explicit override. Use this when a graph needs a non-default hash
|
|
332
|
+
* — e.g., swap the vendored sync SHA-256 for a faster non-crypto hash
|
|
333
|
+
* (xxHash, FNV-1a) in hot-path workloads, or a stronger hash when
|
|
334
|
+
* versioning v1 cids are used as audit anchors.
|
|
335
|
+
*
|
|
336
|
+
* Only settable before the config freezes. Individual nodes can still
|
|
337
|
+
* override via `opts.versioningHash`.
|
|
338
|
+
*/
|
|
339
|
+
get defaultHashFn() {
|
|
340
|
+
return this._defaultHashFn;
|
|
341
|
+
}
|
|
342
|
+
set defaultHashFn(v) {
|
|
343
|
+
this._assertUnfrozen();
|
|
344
|
+
this._defaultHashFn = v;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* When `false`, structured observation options (`causal`, `timeline`)
|
|
348
|
+
* and `Graph.trace()` writes are no-ops. Raw `Graph.observe()` always
|
|
349
|
+
* works. Default: `true` outside production (`NODE_ENV !== "production"`).
|
|
350
|
+
*
|
|
351
|
+
* Settable at any time — inspector gating is an operational concern, not
|
|
352
|
+
* a protocol invariant, so it does NOT require freeze before node creation.
|
|
353
|
+
*/
|
|
354
|
+
get inspectorEnabled() {
|
|
355
|
+
return this._inspectorEnabled;
|
|
356
|
+
}
|
|
357
|
+
set inspectorEnabled(v) {
|
|
358
|
+
this._inspectorEnabled = v;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Process-global observability hook (Redux-DevTools-style full-graph
|
|
362
|
+
* tracer). Fires once per outgoing batch from every node bound to this
|
|
363
|
+
* config, gated by {@link inspectorEnabled}. See {@link GlobalInspectorHook}.
|
|
364
|
+
*
|
|
365
|
+
* Settable at any time — like {@link inspectorEnabled} this is operational,
|
|
366
|
+
* not protocol-shaping, so it does NOT trigger config freeze.
|
|
367
|
+
*/
|
|
368
|
+
get globalInspector() {
|
|
369
|
+
return this._globalInspector;
|
|
370
|
+
}
|
|
371
|
+
set globalInspector(v) {
|
|
372
|
+
this._globalInspector = v;
|
|
373
|
+
}
|
|
374
|
+
// --- Registry (writes require unfrozen; reads are free lookups) ---
|
|
375
|
+
/**
|
|
376
|
+
* Register a custom message type. Must be called before any node that
|
|
377
|
+
* uses this config has been created — otherwise throws. Default
|
|
378
|
+
* `wireCrossing` is `tier >= 3`.
|
|
379
|
+
*/
|
|
380
|
+
registerMessageType(t, input) {
|
|
381
|
+
this._assertUnfrozen();
|
|
382
|
+
this._messageTypes.set(t, {
|
|
383
|
+
tier: input.tier,
|
|
384
|
+
wireCrossing: input.wireCrossing ?? input.tier >= 3,
|
|
385
|
+
metaPassthrough: input.metaPassthrough ?? true
|
|
386
|
+
});
|
|
387
|
+
return this;
|
|
388
|
+
}
|
|
389
|
+
/** Tier for `t`. Unknown types default to tier 1 (immediate, after START). */
|
|
390
|
+
messageTier(t) {
|
|
391
|
+
const reg = this._messageTypes.get(t);
|
|
392
|
+
return reg != null ? reg.tier : 1;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Whether `t` is registered as wire-crossing. Unknown types default to
|
|
396
|
+
* `true` (spec §1.3.6 forward-compat — unknowns cross the wire).
|
|
397
|
+
*/
|
|
398
|
+
isWireCrossing(t) {
|
|
399
|
+
const reg = this._messageTypes.get(t);
|
|
400
|
+
return reg != null ? reg.wireCrossing : true;
|
|
401
|
+
}
|
|
402
|
+
/** Convenience inverse of {@link isWireCrossing}. */
|
|
403
|
+
isLocalOnly(t) {
|
|
404
|
+
return !this.isWireCrossing(t);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Whether `t` is forwarded to meta companions by `Graph.signal`. Defaults
|
|
408
|
+
* to `true` for unknowns (forward-compat — new types pass through meta by
|
|
409
|
+
* default; opt-in filter via `registerMessageType({metaPassthrough: false})`).
|
|
410
|
+
*/
|
|
411
|
+
isMetaPassthrough(t) {
|
|
412
|
+
const reg = this._messageTypes.get(t);
|
|
413
|
+
return reg != null ? reg.metaPassthrough : true;
|
|
414
|
+
}
|
|
415
|
+
/** Whether `t` is a registered (built-in or custom) type. */
|
|
416
|
+
isKnownMessageType(t) {
|
|
417
|
+
return this._messageTypes.has(t);
|
|
418
|
+
}
|
|
419
|
+
// --- Codec registry (writes require unfrozen; reads are free lookups) ---
|
|
420
|
+
/**
|
|
421
|
+
* Register a graph codec by `codec.name`. Used by the envelope-based
|
|
422
|
+
* `graph.snapshot({format: "bytes", codec: name})` path and
|
|
423
|
+
* `Graph.decode(bytes)` auto-dispatch. Must be called before any node
|
|
424
|
+
* bound to this config is created — otherwise throws.
|
|
425
|
+
*
|
|
426
|
+
* Re-registering the same name overwrites, so user codecs can shadow
|
|
427
|
+
* built-in ones before freeze (e.g., to swap a zstd-wrapped dag-cbor in
|
|
428
|
+
* for `"dag-cbor"`).
|
|
429
|
+
*/
|
|
430
|
+
registerCodec(codec) {
|
|
431
|
+
this._assertUnfrozen();
|
|
432
|
+
this._codecs.set(codec.name, codec);
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Resolve a registered codec by name. Returns `undefined` for unknown
|
|
437
|
+
* names. Typed callers cast to their concrete codec interface (e.g.,
|
|
438
|
+
* `config.lookupCodec<GraphCodec>("json")`) — this method stays
|
|
439
|
+
* layer-pure (no import of graph-layer types into `core/`).
|
|
440
|
+
*/
|
|
441
|
+
lookupCodec(name) {
|
|
442
|
+
return this._codecs.get(name);
|
|
443
|
+
}
|
|
444
|
+
/** @internal Used by tests and dev tooling — check freeze state without triggering it. */
|
|
445
|
+
_isFrozen() {
|
|
446
|
+
return this._frozen;
|
|
447
|
+
}
|
|
448
|
+
_assertUnfrozen() {
|
|
449
|
+
if (this._frozen) {
|
|
450
|
+
throw new Error(
|
|
451
|
+
"GraphReFlyConfig is frozen: a node has already captured this config. Register custom types and set hooks before creating any node."
|
|
452
|
+
);
|
|
308
453
|
}
|
|
309
454
|
}
|
|
455
|
+
};
|
|
456
|
+
function registerBuiltins(cfg) {
|
|
457
|
+
cfg.registerMessageType(START, { tier: 0, wireCrossing: false });
|
|
458
|
+
cfg.registerMessageType(DIRTY, { tier: 1, wireCrossing: false });
|
|
459
|
+
cfg.registerMessageType(INVALIDATE, {
|
|
460
|
+
tier: 1,
|
|
461
|
+
wireCrossing: false,
|
|
462
|
+
metaPassthrough: false
|
|
463
|
+
});
|
|
464
|
+
cfg.registerMessageType(PAUSE, { tier: 2, wireCrossing: false });
|
|
465
|
+
cfg.registerMessageType(RESUME, { tier: 2, wireCrossing: false });
|
|
466
|
+
cfg.registerMessageType(DATA, { tier: 3, wireCrossing: true });
|
|
467
|
+
cfg.registerMessageType(RESOLVED, { tier: 3, wireCrossing: true });
|
|
468
|
+
cfg.registerMessageType(COMPLETE, {
|
|
469
|
+
tier: 4,
|
|
470
|
+
wireCrossing: true,
|
|
471
|
+
metaPassthrough: false
|
|
472
|
+
});
|
|
473
|
+
cfg.registerMessageType(ERROR, {
|
|
474
|
+
tier: 4,
|
|
475
|
+
wireCrossing: true,
|
|
476
|
+
metaPassthrough: false
|
|
477
|
+
});
|
|
478
|
+
cfg.registerMessageType(TEARDOWN, {
|
|
479
|
+
tier: 5,
|
|
480
|
+
wireCrossing: true,
|
|
481
|
+
metaPassthrough: false
|
|
482
|
+
});
|
|
310
483
|
}
|
|
311
484
|
|
|
312
485
|
// src/core/guard.ts
|
|
@@ -407,16 +580,25 @@ function accessHintForGuard(guard) {
|
|
|
407
580
|
return allowed.join("+");
|
|
408
581
|
}
|
|
409
582
|
|
|
410
|
-
// src/
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
583
|
+
// src/graph/codec.ts
|
|
584
|
+
var JsonCodec = {
|
|
585
|
+
name: "json",
|
|
586
|
+
version: 1,
|
|
587
|
+
contentType: "application/json",
|
|
588
|
+
encode(snapshot) {
|
|
589
|
+
const json = JSON.stringify(snapshot);
|
|
590
|
+
return new TextEncoder().encode(json);
|
|
591
|
+
},
|
|
592
|
+
decode(buffer, _codecVersion) {
|
|
593
|
+
const json = new TextDecoder().decode(buffer);
|
|
594
|
+
return JSON.parse(json);
|
|
595
|
+
}
|
|
596
|
+
};
|
|
597
|
+
function registerBuiltinCodecs(config) {
|
|
598
|
+
config.registerCodec(JsonCodec);
|
|
416
599
|
}
|
|
417
600
|
|
|
418
601
|
// src/core/versioning.ts
|
|
419
|
-
var import_node_crypto = require("crypto");
|
|
420
602
|
function canonicalizeForHash(value) {
|
|
421
603
|
if (value === void 0) return null;
|
|
422
604
|
if (typeof value === "number") {
|
|
@@ -445,13 +627,153 @@ function canonicalizeForHash(value) {
|
|
|
445
627
|
}
|
|
446
628
|
return null;
|
|
447
629
|
}
|
|
630
|
+
var SHA256_K = /* @__PURE__ */ new Uint32Array([
|
|
631
|
+
1116352408,
|
|
632
|
+
1899447441,
|
|
633
|
+
3049323471,
|
|
634
|
+
3921009573,
|
|
635
|
+
961987163,
|
|
636
|
+
1508970993,
|
|
637
|
+
2453635748,
|
|
638
|
+
2870763221,
|
|
639
|
+
3624381080,
|
|
640
|
+
310598401,
|
|
641
|
+
607225278,
|
|
642
|
+
1426881987,
|
|
643
|
+
1925078388,
|
|
644
|
+
2162078206,
|
|
645
|
+
2614888103,
|
|
646
|
+
3248222580,
|
|
647
|
+
3835390401,
|
|
648
|
+
4022224774,
|
|
649
|
+
264347078,
|
|
650
|
+
604807628,
|
|
651
|
+
770255983,
|
|
652
|
+
1249150122,
|
|
653
|
+
1555081692,
|
|
654
|
+
1996064986,
|
|
655
|
+
2554220882,
|
|
656
|
+
2821834349,
|
|
657
|
+
2952996808,
|
|
658
|
+
3210313671,
|
|
659
|
+
3336571891,
|
|
660
|
+
3584528711,
|
|
661
|
+
113926993,
|
|
662
|
+
338241895,
|
|
663
|
+
666307205,
|
|
664
|
+
773529912,
|
|
665
|
+
1294757372,
|
|
666
|
+
1396182291,
|
|
667
|
+
1695183700,
|
|
668
|
+
1986661051,
|
|
669
|
+
2177026350,
|
|
670
|
+
2456956037,
|
|
671
|
+
2730485921,
|
|
672
|
+
2820302411,
|
|
673
|
+
3259730800,
|
|
674
|
+
3345764771,
|
|
675
|
+
3516065817,
|
|
676
|
+
3600352804,
|
|
677
|
+
4094571909,
|
|
678
|
+
275423344,
|
|
679
|
+
430227734,
|
|
680
|
+
506948616,
|
|
681
|
+
659060556,
|
|
682
|
+
883997877,
|
|
683
|
+
958139571,
|
|
684
|
+
1322822218,
|
|
685
|
+
1537002063,
|
|
686
|
+
1747873779,
|
|
687
|
+
1955562222,
|
|
688
|
+
2024104815,
|
|
689
|
+
2227730452,
|
|
690
|
+
2361852424,
|
|
691
|
+
2428436474,
|
|
692
|
+
2756734187,
|
|
693
|
+
3204031479,
|
|
694
|
+
3329325298
|
|
695
|
+
]);
|
|
696
|
+
var UTF8_ENCODER = /* @__PURE__ */ new TextEncoder();
|
|
697
|
+
function sha256Hex(msg) {
|
|
698
|
+
const bytes = UTF8_ENCODER.encode(msg);
|
|
699
|
+
const msgLen = bytes.length;
|
|
700
|
+
const bitLen = msgLen * 8;
|
|
701
|
+
const totalLen = msgLen + 9 + 63 & ~63;
|
|
702
|
+
const padded = new Uint8Array(totalLen);
|
|
703
|
+
padded.set(bytes);
|
|
704
|
+
padded[msgLen] = 128;
|
|
705
|
+
const dv = new DataView(padded.buffer);
|
|
706
|
+
dv.setUint32(totalLen - 4, bitLen >>> 0, false);
|
|
707
|
+
dv.setUint32(totalLen - 8, Math.floor(bitLen / 4294967296) >>> 0, false);
|
|
708
|
+
let h0 = 1779033703;
|
|
709
|
+
let h1 = 3144134277;
|
|
710
|
+
let h2 = 1013904242;
|
|
711
|
+
let h3 = 2773480762;
|
|
712
|
+
let h4 = 1359893119;
|
|
713
|
+
let h5 = 2600822924;
|
|
714
|
+
let h6 = 528734635;
|
|
715
|
+
let h7 = 1541459225;
|
|
716
|
+
const W = new Uint32Array(64);
|
|
717
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
718
|
+
for (let off = 0; off < totalLen; off += 64) {
|
|
719
|
+
for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4, false);
|
|
720
|
+
for (let i = 16; i < 64; i++) {
|
|
721
|
+
const w15 = W[i - 15];
|
|
722
|
+
const w2 = W[i - 2];
|
|
723
|
+
const s0 = rotr(w15, 7) ^ rotr(w15, 18) ^ w15 >>> 3;
|
|
724
|
+
const s1 = rotr(w2, 17) ^ rotr(w2, 19) ^ w2 >>> 10;
|
|
725
|
+
W[i] = W[i - 16] + s0 + W[i - 7] + s1 >>> 0;
|
|
726
|
+
}
|
|
727
|
+
let a = h0;
|
|
728
|
+
let b = h1;
|
|
729
|
+
let c = h2;
|
|
730
|
+
let d = h3;
|
|
731
|
+
let e = h4;
|
|
732
|
+
let f = h5;
|
|
733
|
+
let g = h6;
|
|
734
|
+
let h = h7;
|
|
735
|
+
for (let i = 0; i < 64; i++) {
|
|
736
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
737
|
+
const ch = e & f ^ ~e & g;
|
|
738
|
+
const t1 = h + S1 + ch + SHA256_K[i] + W[i] >>> 0;
|
|
739
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
740
|
+
const mj = a & b ^ a & c ^ b & c;
|
|
741
|
+
const t2 = S0 + mj >>> 0;
|
|
742
|
+
h = g;
|
|
743
|
+
g = f;
|
|
744
|
+
f = e;
|
|
745
|
+
e = d + t1 >>> 0;
|
|
746
|
+
d = c;
|
|
747
|
+
c = b;
|
|
748
|
+
b = a;
|
|
749
|
+
a = t1 + t2 >>> 0;
|
|
750
|
+
}
|
|
751
|
+
h0 = h0 + a >>> 0;
|
|
752
|
+
h1 = h1 + b >>> 0;
|
|
753
|
+
h2 = h2 + c >>> 0;
|
|
754
|
+
h3 = h3 + d >>> 0;
|
|
755
|
+
h4 = h4 + e >>> 0;
|
|
756
|
+
h5 = h5 + f >>> 0;
|
|
757
|
+
h6 = h6 + g >>> 0;
|
|
758
|
+
h7 = h7 + h >>> 0;
|
|
759
|
+
}
|
|
760
|
+
const toHex = (x) => x.toString(16).padStart(8, "0");
|
|
761
|
+
return toHex(h0) + toHex(h1) + toHex(h2) + toHex(h3) + toHex(h4) + toHex(h5) + toHex(h6) + toHex(h7);
|
|
762
|
+
}
|
|
448
763
|
function defaultHash(value) {
|
|
449
764
|
const canonical = canonicalizeForHash(value ?? null);
|
|
450
765
|
const json = JSON.stringify(canonical);
|
|
451
|
-
return (
|
|
766
|
+
return sha256Hex(json).slice(0, 16);
|
|
767
|
+
}
|
|
768
|
+
function randomUuid() {
|
|
769
|
+
const c = globalThis.crypto;
|
|
770
|
+
if (c?.randomUUID) return c.randomUUID();
|
|
771
|
+
const r = () => Math.floor(Math.random() * 4294967296).toString(16).padStart(8, "0");
|
|
772
|
+
const hex = r() + r() + r() + r();
|
|
773
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${(parseInt(hex.slice(16, 17), 16) & 3 | 8).toString(16)}${hex.slice(17, 20)}-${hex.slice(20, 32)}`;
|
|
452
774
|
}
|
|
453
775
|
function createVersioning(level, initialValue, opts) {
|
|
454
|
-
const id = opts?.id ?? (
|
|
776
|
+
const id = opts?.id ?? randomUuid();
|
|
455
777
|
if (level === 0) {
|
|
456
778
|
return { id, version: 0 };
|
|
457
779
|
}
|
|
@@ -470,288 +792,418 @@ function isV1(info) {
|
|
|
470
792
|
return "cid" in info;
|
|
471
793
|
}
|
|
472
794
|
|
|
473
|
-
// src/core/node
|
|
474
|
-
var
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (args.length > 0) r.value = args[0];
|
|
479
|
-
return r;
|
|
480
|
-
}
|
|
481
|
-
var isCleanupResult = (value) => typeof value === "object" && value !== null && CLEANUP_RESULT in value;
|
|
482
|
-
var isCleanupFn = (value) => typeof value === "function";
|
|
483
|
-
function statusAfterMessage(status, msg) {
|
|
484
|
-
const t = msg[0];
|
|
485
|
-
if (t === DIRTY) return "dirty";
|
|
486
|
-
if (t === DATA) return "settled";
|
|
487
|
-
if (t === RESOLVED) return "resolved";
|
|
488
|
-
if (t === COMPLETE) return "completed";
|
|
489
|
-
if (t === ERROR) return "errored";
|
|
490
|
-
if (t === INVALIDATE) return "dirty";
|
|
491
|
-
if (t === TEARDOWN) return "disconnected";
|
|
492
|
-
return status;
|
|
493
|
-
}
|
|
494
|
-
function createIntBitSet(size) {
|
|
495
|
-
const fullMask = size >= 32 ? -1 : ~(-1 << size);
|
|
496
|
-
let bits = 0;
|
|
795
|
+
// src/core/node.ts
|
|
796
|
+
var noopUnsub = () => {
|
|
797
|
+
};
|
|
798
|
+
var MAX_RERUN_DEPTH = 100;
|
|
799
|
+
function createDepRecord(n) {
|
|
497
800
|
return {
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return (bits & 1 << i) !== 0;
|
|
506
|
-
},
|
|
507
|
-
covers(other) {
|
|
508
|
-
const otherBits = other._bits();
|
|
509
|
-
return (bits & otherBits) === otherBits;
|
|
510
|
-
},
|
|
511
|
-
any() {
|
|
512
|
-
return bits !== 0;
|
|
513
|
-
},
|
|
514
|
-
reset() {
|
|
515
|
-
bits = 0;
|
|
516
|
-
},
|
|
517
|
-
setAll() {
|
|
518
|
-
bits = fullMask;
|
|
519
|
-
},
|
|
520
|
-
_bits() {
|
|
521
|
-
return bits;
|
|
522
|
-
}
|
|
801
|
+
node: n,
|
|
802
|
+
unsub: null,
|
|
803
|
+
prevData: void 0,
|
|
804
|
+
dirty: false,
|
|
805
|
+
involvedThisWave: false,
|
|
806
|
+
dataBatch: [],
|
|
807
|
+
terminal: void 0
|
|
523
808
|
};
|
|
524
809
|
}
|
|
525
|
-
function
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
},
|
|
536
|
-
has(i) {
|
|
537
|
-
return (words[i >>> 5] & 1 << (i & 31)) !== 0;
|
|
538
|
-
},
|
|
539
|
-
covers(other) {
|
|
540
|
-
const ow = other._words;
|
|
541
|
-
for (let w = 0; w < words.length; w++) {
|
|
542
|
-
if ((words[w] & ow[w]) >>> 0 !== ow[w]) return false;
|
|
543
|
-
}
|
|
544
|
-
return true;
|
|
545
|
-
},
|
|
546
|
-
any() {
|
|
547
|
-
for (let w = 0; w < words.length; w++) {
|
|
548
|
-
if (words[w] !== 0) return true;
|
|
549
|
-
}
|
|
550
|
-
return false;
|
|
551
|
-
},
|
|
552
|
-
reset() {
|
|
553
|
-
words.fill(0);
|
|
554
|
-
},
|
|
555
|
-
setAll() {
|
|
556
|
-
for (let w = 0; w < words.length - 1; w++) words[w] = 4294967295;
|
|
557
|
-
if (words.length > 0) words[words.length - 1] = lastWordMask;
|
|
558
|
-
},
|
|
559
|
-
_words: words
|
|
560
|
-
};
|
|
810
|
+
function resetDepRecord(d) {
|
|
811
|
+
d.prevData = void 0;
|
|
812
|
+
d.dirty = false;
|
|
813
|
+
d.involvedThisWave = false;
|
|
814
|
+
d.dataBatch.length = 0;
|
|
815
|
+
d.terminal = void 0;
|
|
816
|
+
}
|
|
817
|
+
function normalizeMessages(input) {
|
|
818
|
+
if (input.length === 0) return input;
|
|
819
|
+
return typeof input[0] === "symbol" ? [input] : input;
|
|
561
820
|
}
|
|
562
|
-
|
|
563
|
-
|
|
821
|
+
var defaultOnMessage = (node2, msg, ctx, _actions) => {
|
|
822
|
+
if (ctx.direction === "down-in") {
|
|
823
|
+
node2._onDepMessage(ctx.depIndex, msg);
|
|
824
|
+
}
|
|
825
|
+
return void 0;
|
|
826
|
+
};
|
|
827
|
+
var defaultOnSubscribe = (node2, sink, _ctx, _actions) => {
|
|
828
|
+
const impl = node2;
|
|
829
|
+
if (impl._status === "completed" || impl._status === "errored") return;
|
|
830
|
+
const cached = impl._cached;
|
|
831
|
+
const initial = cached === void 0 ? [START_MSG] : [START_MSG, [DATA, cached]];
|
|
832
|
+
if (impl._status === "dirty") initial.push(DIRTY_MSG);
|
|
833
|
+
downWithBatch(sink, initial, impl._config.tierOf);
|
|
834
|
+
};
|
|
835
|
+
var defaultConfig = new GraphReFlyConfig({
|
|
836
|
+
onMessage: defaultOnMessage,
|
|
837
|
+
onSubscribe: defaultOnSubscribe
|
|
838
|
+
});
|
|
839
|
+
registerBuiltins(defaultConfig);
|
|
840
|
+
registerBuiltinCodecs(defaultConfig);
|
|
841
|
+
function configure(fn) {
|
|
842
|
+
if (defaultConfig._isFrozen()) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
"configure() called after a node was created \u2014 the default GraphReFlyConfig is frozen. Call configure(...) at application startup, before any node factories run."
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
fn(defaultConfig);
|
|
564
848
|
}
|
|
565
|
-
var
|
|
566
|
-
// --- Identity
|
|
849
|
+
var NodeImpl = class _NodeImpl {
|
|
850
|
+
// --- Identity ---
|
|
567
851
|
_optsName;
|
|
568
|
-
_registryName;
|
|
569
|
-
/** @internal Read by `describeNode` before inference. */
|
|
570
852
|
_describeKind;
|
|
571
853
|
meta;
|
|
572
|
-
|
|
854
|
+
/**
|
|
855
|
+
* Cached `Object.keys(meta).length > 0` check. `meta` is frozen at
|
|
856
|
+
* construction so this boolean never flips. Used by `_emit` to skip
|
|
857
|
+
* the meta TEARDOWN fan-out block allocation on the common "no meta"
|
|
858
|
+
* hot path.
|
|
859
|
+
*/
|
|
860
|
+
_hasMeta;
|
|
861
|
+
// --- Config ---
|
|
862
|
+
_config;
|
|
863
|
+
// --- Topology ---
|
|
864
|
+
/** Mutable for autoTrackNode / Graph.connect() post-construction dep addition. */
|
|
865
|
+
_deps;
|
|
866
|
+
_sinks = null;
|
|
867
|
+
_sinkCount = 0;
|
|
868
|
+
// --- State ---
|
|
869
|
+
_cached;
|
|
870
|
+
_status;
|
|
871
|
+
_cleanup;
|
|
872
|
+
_store = {};
|
|
873
|
+
_waveHasNewData = false;
|
|
874
|
+
_hasNewTerminal = false;
|
|
875
|
+
_hasCalledFnOnce = false;
|
|
876
|
+
_paused = false;
|
|
877
|
+
_pendingWave = false;
|
|
878
|
+
_isExecutingFn = false;
|
|
879
|
+
_pendingRerun = false;
|
|
880
|
+
_rerunDepth = 0;
|
|
881
|
+
// --- Settlement counter (A3) ---
|
|
882
|
+
/**
|
|
883
|
+
* Count of deps currently in `dirty === true`. `_maybeRunFnOnSettlement`
|
|
884
|
+
* treats `0` as "wave settled" — O(1) check for full dep settlement.
|
|
885
|
+
*/
|
|
886
|
+
_dirtyDepCount = 0;
|
|
887
|
+
// --- Per-batch emit accumulator (Bug 2: K+1 fan-in fix) ---
|
|
888
|
+
/**
|
|
889
|
+
* Inside an explicit `batch(() => ...)` scope, every `_emit` accumulates
|
|
890
|
+
* its already-framed messages here instead of dispatching synchronously.
|
|
891
|
+
* At batch end, `_flushBatchPending` runs (registered via
|
|
892
|
+
* `registerBatchFlushHook`) and delivers the whole accumulated batch as
|
|
893
|
+
* one `downWithBatch` call — collapsing what would otherwise be K
|
|
894
|
+
* separate sink invocations into one. This is the fix for the diamond
|
|
895
|
+
* fan-in K+1 over-fire.
|
|
896
|
+
*
|
|
897
|
+
* `null` outside batch (or after flush). Only ever appended to within
|
|
898
|
+
* a single explicit batch lifetime; reset to `null` on flush. State
|
|
899
|
+
* updates (cache, version, status) still happen per-emit via
|
|
900
|
+
* `_updateState` — only the downstream delivery is coalesced.
|
|
901
|
+
*/
|
|
902
|
+
_batchPendingMessages = null;
|
|
903
|
+
// --- PAUSE/RESUME lock tracking (C0) ---
|
|
904
|
+
/**
|
|
905
|
+
* Set of active pause locks held against this node. Every `[PAUSE, lockId]`
|
|
906
|
+
* adds its `lockId` to the set; every `[RESUME, lockId]` removes it.
|
|
907
|
+
* `_paused` is a derived quantity: `_pauseLocks.size > 0`. Multi-pauser
|
|
908
|
+
* correctness — one controller releasing its lock does NOT resume the
|
|
909
|
+
* node while another controller still holds its lock.
|
|
910
|
+
*/
|
|
911
|
+
_pauseLocks = null;
|
|
912
|
+
/**
|
|
913
|
+
* Buffered DATA messages held while paused. Only populated when
|
|
914
|
+
* `_pausable === "resumeAll"` (bufferAll mode). On final lock release
|
|
915
|
+
* the buffer is replayed through the node's outgoing pipeline in the
|
|
916
|
+
* order received. Non-bufferAll pause mode drops DATA on the floor
|
|
917
|
+
* (upstream is expected to honor PAUSE by suppressing production).
|
|
918
|
+
*/
|
|
919
|
+
_pauseBuffer = null;
|
|
920
|
+
// --- Options (frozen at construction) ---
|
|
921
|
+
_fn;
|
|
573
922
|
_equals;
|
|
574
923
|
_resubscribable;
|
|
575
924
|
_resetOnTeardown;
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
925
|
+
_autoComplete;
|
|
926
|
+
_autoError;
|
|
927
|
+
_pausable;
|
|
579
928
|
_guard;
|
|
580
|
-
/** @internal Subclasses update this through {@link _recordMutation}. */
|
|
581
|
-
_lastMutation;
|
|
582
|
-
// --- Versioning ---
|
|
583
929
|
_hashFn;
|
|
584
930
|
_versioning;
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
931
|
+
/**
|
|
932
|
+
* Explicit versioning level, tracked separately from `_versioning` so
|
|
933
|
+
* monotonicity checks and future v2/v3 extensions don't rely on the
|
|
934
|
+
* fragile `"cid" in _versioning` shape discriminator. `undefined` means
|
|
935
|
+
* the node has no versioning attached; `0` / `1` / future levels name
|
|
936
|
+
* the tier. Mutated in lockstep with `_versioning` by the constructor
|
|
937
|
+
* and by `_applyVersioning`.
|
|
938
|
+
*/
|
|
939
|
+
_versioningLevel;
|
|
940
|
+
// --- ABAC ---
|
|
941
|
+
_lastMutation;
|
|
942
|
+
/**
|
|
943
|
+
* @internal Per-node inspector hooks for `Graph.observe(path,
|
|
944
|
+
* { causal, derived })`. Fires in `_onDepMessage` and `_execFn`.
|
|
945
|
+
* Attached via `_setInspectorHook` (returns a disposer). Multiple
|
|
946
|
+
* observers can attach simultaneously — all registered hooks fire for
|
|
947
|
+
* every event.
|
|
948
|
+
*/
|
|
949
|
+
_inspectorHooks;
|
|
950
|
+
// --- Actions (built once in the constructor) ---
|
|
599
951
|
_actions;
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
constructor(opts) {
|
|
952
|
+
constructor(deps, fn, opts) {
|
|
953
|
+
this._config = opts.config ?? defaultConfig;
|
|
954
|
+
void this._config.onMessage;
|
|
604
955
|
this._optsName = opts.name;
|
|
605
956
|
this._describeKind = opts.describeKind;
|
|
606
957
|
this._equals = opts.equals ?? Object.is;
|
|
607
958
|
this._resubscribable = opts.resubscribable ?? false;
|
|
608
959
|
this._resetOnTeardown = opts.resetOnTeardown ?? false;
|
|
609
|
-
this.
|
|
610
|
-
this.
|
|
960
|
+
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
961
|
+
this._autoError = opts.errorWhenDepsError ?? true;
|
|
962
|
+
this._pausable = opts.pausable ?? true;
|
|
611
963
|
this._guard = opts.guard;
|
|
612
|
-
this.
|
|
613
|
-
this.
|
|
614
|
-
this.
|
|
615
|
-
this.
|
|
964
|
+
this._fn = fn;
|
|
965
|
+
this._cached = opts.initial !== void 0 ? opts.initial : void 0;
|
|
966
|
+
this._status = deps.length === 0 && fn == null && this._cached !== void 0 ? "settled" : "sentinel";
|
|
967
|
+
this._hashFn = opts.versioningHash ?? this._config.defaultHashFn ?? defaultHash;
|
|
968
|
+
const versioningLevel = opts.versioning ?? this._config.defaultVersioning;
|
|
969
|
+
this._versioningLevel = versioningLevel;
|
|
970
|
+
this._versioning = versioningLevel != null ? createVersioning(versioningLevel, this._cached === void 0 ? void 0 : this._cached, {
|
|
616
971
|
id: opts.versioningId,
|
|
617
972
|
hash: this._hashFn
|
|
618
973
|
}) : void 0;
|
|
974
|
+
this._deps = deps.map(createDepRecord);
|
|
619
975
|
const meta = {};
|
|
620
976
|
for (const [k, v] of Object.entries(opts.meta ?? {})) {
|
|
621
|
-
|
|
977
|
+
const metaOpts = {
|
|
978
|
+
initial: v,
|
|
979
|
+
name: `${opts.name ?? "node"}:meta:${k}`,
|
|
980
|
+
describeKind: "state",
|
|
981
|
+
config: this._config
|
|
982
|
+
};
|
|
983
|
+
if (opts.guard != null) metaOpts.guard = opts.guard;
|
|
984
|
+
meta[k] = new _NodeImpl([], void 0, metaOpts);
|
|
622
985
|
}
|
|
623
986
|
Object.freeze(meta);
|
|
624
987
|
this.meta = meta;
|
|
988
|
+
this._hasMeta = Object.keys(meta).length > 0;
|
|
625
989
|
const self = this;
|
|
626
990
|
this._actions = {
|
|
627
|
-
down(messages) {
|
|
628
|
-
self._onManualEmit();
|
|
629
|
-
self._downInternal(messages);
|
|
630
|
-
},
|
|
631
991
|
emit(value) {
|
|
632
|
-
self.
|
|
633
|
-
self._downAutoValue(value);
|
|
992
|
+
self._emit([[DATA, value]]);
|
|
634
993
|
},
|
|
635
|
-
|
|
636
|
-
self.
|
|
994
|
+
down(messageOrMessages) {
|
|
995
|
+
self._emit(normalizeMessages(messageOrMessages));
|
|
996
|
+
},
|
|
997
|
+
up(messageOrMessages) {
|
|
998
|
+
self._emitUp(normalizeMessages(messageOrMessages));
|
|
637
999
|
}
|
|
638
1000
|
};
|
|
639
|
-
this.
|
|
1001
|
+
this.down = this.down.bind(this);
|
|
1002
|
+
this.up = this.up.bind(this);
|
|
640
1003
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
*/
|
|
645
|
-
_onManualEmit() {
|
|
1004
|
+
// --- Derived state ---
|
|
1005
|
+
get _isTerminal() {
|
|
1006
|
+
return this._status === "completed" || this._status === "errored";
|
|
646
1007
|
}
|
|
647
|
-
// ---
|
|
1008
|
+
// --- Public getters ---
|
|
648
1009
|
get name() {
|
|
649
|
-
return this.
|
|
650
|
-
}
|
|
651
|
-
/** @internal Assigned by `Graph.add` when registered without an options `name`. */
|
|
652
|
-
_assignRegistryName(localName) {
|
|
653
|
-
if (this._optsName !== void 0 || this._registryName !== void 0) return;
|
|
654
|
-
this._registryName = localName;
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* @internal Attach/remove inspector hook for graph-level observability.
|
|
658
|
-
* Returns a disposer that restores the previous hook.
|
|
659
|
-
*/
|
|
660
|
-
_setInspectorHook(hook) {
|
|
661
|
-
const prev = this._inspectorHook;
|
|
662
|
-
this._inspectorHook = hook;
|
|
663
|
-
return () => {
|
|
664
|
-
if (this._inspectorHook === hook) {
|
|
665
|
-
this._inspectorHook = prev;
|
|
666
|
-
}
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
/** @internal Used by subclasses to surface inspector events. */
|
|
670
|
-
_emitInspectorHook(event) {
|
|
671
|
-
this._inspectorHook?.(event);
|
|
1010
|
+
return this._optsName;
|
|
672
1011
|
}
|
|
673
1012
|
get status() {
|
|
674
1013
|
return this._status;
|
|
675
1014
|
}
|
|
1015
|
+
get cache() {
|
|
1016
|
+
return this._cached === void 0 ? void 0 : this._cached;
|
|
1017
|
+
}
|
|
676
1018
|
get lastMutation() {
|
|
677
1019
|
return this._lastMutation;
|
|
678
1020
|
}
|
|
679
1021
|
get v() {
|
|
680
1022
|
return this._versioning;
|
|
681
1023
|
}
|
|
682
|
-
/** @internal Used by `Graph.setVersioning` to retroactively apply versioning. */
|
|
683
|
-
_applyVersioning(level, opts) {
|
|
684
|
-
if (this._versioning != null) return;
|
|
685
|
-
this._hashFn = opts?.hash ?? this._hashFn;
|
|
686
|
-
this._versioning = createVersioning(
|
|
687
|
-
level,
|
|
688
|
-
this._cached === NO_VALUE ? void 0 : this._cached,
|
|
689
|
-
{
|
|
690
|
-
id: opts?.id,
|
|
691
|
-
hash: this._hashFn
|
|
692
|
-
}
|
|
693
|
-
);
|
|
694
|
-
}
|
|
695
1024
|
hasGuard() {
|
|
696
1025
|
return this._guard != null;
|
|
697
1026
|
}
|
|
698
|
-
allowsObserve(actor) {
|
|
699
|
-
if (this._guard == null) return true;
|
|
700
|
-
return this._guard(normalizeActor(actor), "observe");
|
|
701
|
-
}
|
|
702
|
-
// --- Public transport ---
|
|
703
|
-
get() {
|
|
704
|
-
return this._cached === NO_VALUE ? void 0 : this._cached;
|
|
705
|
-
}
|
|
706
|
-
down(messages, options) {
|
|
707
|
-
if (messages.length === 0) return;
|
|
708
|
-
if (!options?.internal && this._guard != null) {
|
|
709
|
-
const actor = normalizeActor(options?.actor);
|
|
710
|
-
const delivery = options?.delivery ?? "write";
|
|
711
|
-
const action = delivery === "signal" ? "signal" : "write";
|
|
712
|
-
if (!this._guard(actor, action)) {
|
|
713
|
-
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
714
|
-
}
|
|
715
|
-
this._recordMutation(actor);
|
|
716
|
-
}
|
|
717
|
-
this._downInternal(messages);
|
|
718
|
-
}
|
|
719
|
-
/** @internal Record a successful guarded mutation (called by `down` and subclass `up`). */
|
|
720
|
-
_recordMutation(actor) {
|
|
721
|
-
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
722
|
-
}
|
|
723
1027
|
/**
|
|
724
|
-
*
|
|
725
|
-
*
|
|
726
|
-
*
|
|
1028
|
+
* @internal Retroactively attach (or upgrade) versioning state on this
|
|
1029
|
+
* node. Intended for `Graph.setVersioning(level)` bulk application and
|
|
1030
|
+
* for rare cases where a specific node needs to be bumped to a higher
|
|
1031
|
+
* level (e.g., `v0 → v1`) after construction.
|
|
1032
|
+
*
|
|
1033
|
+
* **Safety:** the mutation is rejected mid-wave. Specifically,
|
|
1034
|
+
* throws if the node is currently executing its fn (`_isExecutingFn`).
|
|
1035
|
+
* Callers at quiescent points — before the first sink subscribes, or
|
|
1036
|
+
* after all sinks unsubscribe, or between external `down()` / `emit()`
|
|
1037
|
+
* invocations — are safe. The re-entrance window that motivated §10.6.4
|
|
1038
|
+
* removal was the "transition `_versioning` from `undefined` to a fresh
|
|
1039
|
+
* object mid-`_updateState`" case; that path is now guarded.
|
|
1040
|
+
*
|
|
1041
|
+
* **Monotonicity:** levels can only go up. Downgrade (e.g., `v1 → v0`)
|
|
1042
|
+
* is a no-op — once a node carries higher-level metadata, dropping it
|
|
1043
|
+
* mid-graph would tear the linked-history invariant for v1 and above.
|
|
1044
|
+
*
|
|
1045
|
+
* **Linked-history boundary (D1, 2026-04-13):** upgrading v0 → v1
|
|
1046
|
+
* produces a **fresh history root**. The new v1 state has `cid =
|
|
1047
|
+
* hash(currentCachedValue)` and `prev = null`, not a synthetic `prev`
|
|
1048
|
+
* anchored to any previous v0 value. The v0 monotonic `version` counter
|
|
1049
|
+
* is preserved across the upgrade, but the linked-cid chain (spec §7)
|
|
1050
|
+
* starts fresh at the upgrade point. Downstream audit tools that walk
|
|
1051
|
+
* `v.cid.prev` backwards through time will see a `null` boundary at
|
|
1052
|
+
* the upgrade — **this is intentional**: v0 had no cid to link to, and
|
|
1053
|
+
* fabricating one would lie about the hash. Callers that require an
|
|
1054
|
+
* unbroken cid chain from birth must attach versioning at construction
|
|
1055
|
+
* via `opts.versioning` or `config.defaultVersioning`, not retroactively.
|
|
1056
|
+
*
|
|
1057
|
+
* @param level - New minimum versioning level.
|
|
1058
|
+
* @param opts - Optional id / hash overrides; applied only if the
|
|
1059
|
+
* node currently has no versioning state.
|
|
727
1060
|
*/
|
|
728
|
-
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
// --- Subscribe (uniform across node shapes) ---
|
|
734
|
-
subscribe(sink, hints) {
|
|
735
|
-
if (hints?.actor != null && this._guard != null) {
|
|
736
|
-
const actor = normalizeActor(hints.actor);
|
|
737
|
-
if (!this._guard(actor, "observe")) {
|
|
738
|
-
throw new GuardDenied({ actor, action: "observe", nodeName: this.name });
|
|
739
|
-
}
|
|
1061
|
+
_applyVersioning(level, opts) {
|
|
1062
|
+
if (this._isExecutingFn) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`Node "${this.name}": _applyVersioning cannot run mid-fn \u2014 call it outside of \`_execFn\` (typically at graph setup time before the first subscribe).`
|
|
1065
|
+
);
|
|
740
1066
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
this._status = "disconnected";
|
|
745
|
-
this._onResubscribe?.();
|
|
1067
|
+
const currentLevel = this._versioningLevel;
|
|
1068
|
+
if (currentLevel != null && level <= currentLevel) {
|
|
1069
|
+
return;
|
|
746
1070
|
}
|
|
747
|
-
this.
|
|
748
|
-
if (
|
|
749
|
-
|
|
750
|
-
|
|
1071
|
+
const hash = opts?.hash ?? this._hashFn;
|
|
1072
|
+
if (hash !== this._hashFn) this._hashFn = hash;
|
|
1073
|
+
const initialValue = this._cached === void 0 ? void 0 : this._cached;
|
|
1074
|
+
const current = this._versioning;
|
|
1075
|
+
const preservedId = current?.id ?? opts?.id;
|
|
1076
|
+
const preservedVersion = current?.version ?? 0;
|
|
1077
|
+
const fresh = createVersioning(level, initialValue, {
|
|
1078
|
+
id: preservedId,
|
|
1079
|
+
hash
|
|
1080
|
+
});
|
|
1081
|
+
fresh.version = preservedVersion;
|
|
1082
|
+
this._versioning = fresh;
|
|
1083
|
+
this._versioningLevel = level;
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* @internal Attach an inspector hook. Returns a disposer that removes
|
|
1087
|
+
* the hook. Used by `Graph.observe(path, { causal, derived })` to build
|
|
1088
|
+
* causal traces. Multiple hooks may be attached concurrently — all fire
|
|
1089
|
+
* for every event in registration order. Passing `undefined` is a no-op
|
|
1090
|
+
* and returns a no-op disposer.
|
|
1091
|
+
*/
|
|
1092
|
+
_setInspectorHook(hook) {
|
|
1093
|
+
if (hook == null) return () => {
|
|
1094
|
+
};
|
|
1095
|
+
if (this._inspectorHooks == null) this._inspectorHooks = /* @__PURE__ */ new Set();
|
|
1096
|
+
this._inspectorHooks.add(hook);
|
|
1097
|
+
return () => {
|
|
1098
|
+
this._inspectorHooks?.delete(hook);
|
|
1099
|
+
if (this._inspectorHooks?.size === 0) this._inspectorHooks = void 0;
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
allowsObserve(actor) {
|
|
1103
|
+
if (this._guard == null) return true;
|
|
1104
|
+
return this._guard(normalizeActor(actor), "observe");
|
|
1105
|
+
}
|
|
1106
|
+
// --- Guard helper ---
|
|
1107
|
+
_checkGuard(options) {
|
|
1108
|
+
if (options?.internal || this._guard == null) return;
|
|
1109
|
+
const actor = normalizeActor(options?.actor);
|
|
1110
|
+
const action = options?.delivery === "signal" ? "signal" : "write";
|
|
1111
|
+
if (!this._guard(actor, action)) {
|
|
1112
|
+
throw new GuardDenied({ actor, action, nodeName: this.name });
|
|
1113
|
+
}
|
|
1114
|
+
this._lastMutation = { actor, timestamp_ns: wallClockNs() };
|
|
1115
|
+
}
|
|
1116
|
+
// --- Public transport ---
|
|
1117
|
+
down(messageOrMessages, options) {
|
|
1118
|
+
const messages = normalizeMessages(messageOrMessages);
|
|
1119
|
+
if (messages.length === 0) return;
|
|
1120
|
+
this._checkGuard(options);
|
|
1121
|
+
this._emit(messages);
|
|
1122
|
+
}
|
|
1123
|
+
emit(value, options) {
|
|
1124
|
+
this._checkGuard(options);
|
|
1125
|
+
this._emit([[DATA, value]]);
|
|
1126
|
+
}
|
|
1127
|
+
up(messageOrMessages, options) {
|
|
1128
|
+
if (this._deps.length === 0) return;
|
|
1129
|
+
const messages = normalizeMessages(messageOrMessages);
|
|
1130
|
+
if (messages.length === 0) return;
|
|
1131
|
+
this._checkGuard(options);
|
|
1132
|
+
const forwardOpts = options ?? { internal: true };
|
|
1133
|
+
this._validateUpTiers(messages);
|
|
1134
|
+
for (const d of this._deps) {
|
|
1135
|
+
d.node.up?.(messages, forwardOpts);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* @internal Internal up-path used by `actions.up(...)` from inside fn.
|
|
1140
|
+
* Same tier validation as public `up`, but bypasses the guard check
|
|
1141
|
+
* since the fn context is already inside an authorized operation.
|
|
1142
|
+
*/
|
|
1143
|
+
_emitUp(messages) {
|
|
1144
|
+
if (this._deps.length === 0) return;
|
|
1145
|
+
if (messages.length === 0) return;
|
|
1146
|
+
this._validateUpTiers(messages);
|
|
1147
|
+
for (const d of this._deps) {
|
|
1148
|
+
d.node.up?.(messages, { internal: true });
|
|
751
1149
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
1150
|
+
}
|
|
1151
|
+
/**
|
|
1152
|
+
* @internal Enforce spec §1.2 — up-direction messages are restricted to
|
|
1153
|
+
* tier 0–2 and tier 5 (START, DIRTY, INVALIDATE, PAUSE, RESUME,
|
|
1154
|
+
* TEARDOWN). Tier 3 (DATA/RESOLVED) and tier 4 (COMPLETE/ERROR) are
|
|
1155
|
+
* downstream-only. Emitting tier-3/4 via `up` would bypass equals
|
|
1156
|
+
* substitution and cache advance entirely and is a protocol bug.
|
|
1157
|
+
*/
|
|
1158
|
+
_validateUpTiers(messages) {
|
|
1159
|
+
const tierOf = this._config.tierOf;
|
|
1160
|
+
for (const m of messages) {
|
|
1161
|
+
const tier = tierOf(m[0]);
|
|
1162
|
+
if (tier === 3 || tier === 4) {
|
|
1163
|
+
throw new Error(
|
|
1164
|
+
`Node "${this.name}": tier-${tier} messages cannot flow up \u2014 DATA/RESOLVED/COMPLETE/ERROR are downstream-only. Use \`down(...)\` for value delivery; \`up(...)\` is for control signals (DIRTY, INVALIDATE, PAUSE, RESUME, TEARDOWN).`
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
subscribe(sink, actor) {
|
|
1170
|
+
if (actor != null && this._guard != null) {
|
|
1171
|
+
const a = normalizeActor(actor);
|
|
1172
|
+
if (!this._guard(a, "observe")) {
|
|
1173
|
+
throw new GuardDenied({ actor: a, action: "observe", nodeName: this.name });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
const wasTerminal = this._isTerminal;
|
|
1177
|
+
const afterTerminalReset = wasTerminal && this._resubscribable;
|
|
1178
|
+
if (afterTerminalReset) {
|
|
1179
|
+
this._cached = void 0;
|
|
1180
|
+
this._status = "sentinel";
|
|
1181
|
+
this._store = {};
|
|
1182
|
+
this._hasCalledFnOnce = false;
|
|
1183
|
+
this._waveHasNewData = false;
|
|
1184
|
+
this._hasNewTerminal = false;
|
|
1185
|
+
this._paused = false;
|
|
1186
|
+
this._pendingWave = false;
|
|
1187
|
+
this._pendingRerun = false;
|
|
1188
|
+
this._isExecutingFn = false;
|
|
1189
|
+
this._rerunDepth = 0;
|
|
1190
|
+
this._dirtyDepCount = 0;
|
|
1191
|
+
this._pauseLocks = null;
|
|
1192
|
+
this._pauseBuffer = null;
|
|
1193
|
+
for (const d of this._deps) resetDepRecord(d);
|
|
1194
|
+
}
|
|
1195
|
+
this._sinkCount += 1;
|
|
1196
|
+
let subCleanup;
|
|
1197
|
+
try {
|
|
1198
|
+
subCleanup = this._config.onSubscribe(
|
|
1199
|
+
this,
|
|
1200
|
+
sink,
|
|
1201
|
+
{ sinkCount: this._sinkCount, afterTerminalReset },
|
|
1202
|
+
this._actions
|
|
1203
|
+
);
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
this._sinkCount -= 1;
|
|
1206
|
+
throw err;
|
|
755
1207
|
}
|
|
756
1208
|
if (this._sinks == null) {
|
|
757
1209
|
this._sinks = sink;
|
|
@@ -760,11 +1212,24 @@ var NodeBase = class {
|
|
|
760
1212
|
} else {
|
|
761
1213
|
this._sinks.add(sink);
|
|
762
1214
|
}
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1215
|
+
const isTerminalNow = this._isTerminal;
|
|
1216
|
+
if (this._sinkCount === 1 && !isTerminalNow) {
|
|
1217
|
+
try {
|
|
1218
|
+
this._activate();
|
|
1219
|
+
} catch (err) {
|
|
1220
|
+
this._sinkCount -= 1;
|
|
1221
|
+
this._removeSink(sink);
|
|
1222
|
+
if (this._sinkCount === 0) this._status = "sentinel";
|
|
1223
|
+
if (typeof subCleanup === "function") {
|
|
1224
|
+
try {
|
|
1225
|
+
subCleanup();
|
|
1226
|
+
} catch {
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
throw err;
|
|
1230
|
+
}
|
|
766
1231
|
}
|
|
767
|
-
if (
|
|
1232
|
+
if (this._status === "sentinel" && this._cached === void 0) {
|
|
768
1233
|
this._status = "pending";
|
|
769
1234
|
}
|
|
770
1235
|
let removed = false;
|
|
@@ -772,853 +1237,802 @@ var NodeBase = class {
|
|
|
772
1237
|
if (removed) return;
|
|
773
1238
|
removed = true;
|
|
774
1239
|
this._sinkCount -= 1;
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
}
|
|
779
|
-
if (this._sinks == null) return;
|
|
780
|
-
if (typeof this._sinks === "function") {
|
|
781
|
-
if (this._sinks === sink) this._sinks = null;
|
|
782
|
-
} else {
|
|
783
|
-
this._sinks.delete(sink);
|
|
784
|
-
if (this._sinks.size === 1) {
|
|
785
|
-
const [only] = this._sinks;
|
|
786
|
-
this._sinks = only;
|
|
787
|
-
} else if (this._sinks.size === 0) {
|
|
788
|
-
this._sinks = null;
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
if (this._sinks == null) {
|
|
792
|
-
this._onDeactivate();
|
|
793
|
-
}
|
|
1240
|
+
this._removeSink(sink);
|
|
1241
|
+
if (typeof subCleanup === "function") subCleanup();
|
|
1242
|
+
if (this._sinks == null) this._deactivate();
|
|
794
1243
|
};
|
|
795
1244
|
}
|
|
796
|
-
|
|
1245
|
+
_removeSink(sink) {
|
|
1246
|
+
if (this._sinks === sink) {
|
|
1247
|
+
this._sinks = null;
|
|
1248
|
+
} else if (this._sinks != null && typeof this._sinks !== "function") {
|
|
1249
|
+
this._sinks.delete(sink);
|
|
1250
|
+
if (this._sinks.size === 1) {
|
|
1251
|
+
const [only] = this._sinks;
|
|
1252
|
+
this._sinks = only;
|
|
1253
|
+
} else if (this._sinks.size === 0) {
|
|
1254
|
+
this._sinks = null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
// --- Lifecycle ---
|
|
797
1259
|
/**
|
|
798
|
-
*
|
|
799
|
-
*
|
|
1260
|
+
* @internal First-sink activation. For a producer (no deps + fn),
|
|
1261
|
+
* invokes fn once. For a compute node (has deps), subscribes to every
|
|
1262
|
+
* dep with the pre-set-dirty trick so the first-run gate waits for
|
|
1263
|
+
* every dep to settle at least once.
|
|
800
1264
|
*/
|
|
801
|
-
|
|
802
|
-
if (
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1265
|
+
_activate() {
|
|
1266
|
+
if (this._deps.length === 0) {
|
|
1267
|
+
if (this._fn) this._execFn();
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
this._dirtyDepCount = 0;
|
|
1271
|
+
const initialLen = this._deps.length;
|
|
1272
|
+
let subscribedCount = 0;
|
|
1273
|
+
try {
|
|
1274
|
+
for (let i = 0; i < initialLen; i++) {
|
|
1275
|
+
const depIdx = i;
|
|
1276
|
+
const dep = this._deps[i];
|
|
1277
|
+
dep.unsub = noopUnsub;
|
|
1278
|
+
dep.unsub = dep.node.subscribe((msgs) => {
|
|
1279
|
+
if (dep.unsub === null) return;
|
|
1280
|
+
const tierOf = this._config.tierOf;
|
|
1281
|
+
let sawSettlement = false;
|
|
1282
|
+
for (const m of msgs) {
|
|
1283
|
+
if (tierOf(m[0]) >= 3) sawSettlement = true;
|
|
1284
|
+
this._config.onMessage(
|
|
1285
|
+
this,
|
|
1286
|
+
m,
|
|
1287
|
+
{ direction: "down-in", depIndex: depIdx },
|
|
1288
|
+
this._actions
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
if (sawSettlement) this._maybeRunFnOnSettlement();
|
|
1292
|
+
});
|
|
1293
|
+
subscribedCount++;
|
|
818
1294
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
1295
|
+
} catch (err) {
|
|
1296
|
+
this._deps[subscribedCount].unsub = null;
|
|
1297
|
+
for (let j = 0; j < subscribedCount; j++) {
|
|
1298
|
+
const d = this._deps[j];
|
|
1299
|
+
if (d.unsub != null) {
|
|
1300
|
+
const u = d.unsub;
|
|
1301
|
+
d.unsub = null;
|
|
1302
|
+
try {
|
|
1303
|
+
u();
|
|
1304
|
+
} catch {
|
|
1305
|
+
}
|
|
1306
|
+
resetDepRecord(d);
|
|
826
1307
|
}
|
|
827
|
-
return;
|
|
828
1308
|
}
|
|
1309
|
+
this._dirtyDepCount = 0;
|
|
1310
|
+
throw err;
|
|
829
1311
|
}
|
|
830
|
-
downWithBatch(this._boundDownToSinks, sinkMessages);
|
|
831
|
-
}
|
|
832
|
-
_canSkipDirty() {
|
|
833
|
-
return this._sinkCount === 1 && this._singleDepSinkCount === 1;
|
|
834
1312
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
1313
|
+
/**
|
|
1314
|
+
* @internal Append a dep post-construction. Used by `autoTrackNode`
|
|
1315
|
+
* (runtime dep discovery) and `Graph.connect()` (post-construction
|
|
1316
|
+
* wiring). Subscribes immediately — if DATA arrives synchronously
|
|
1317
|
+
* during subscribe and fn is currently executing, the re-run is
|
|
1318
|
+
* deferred via `_pendingRerun` flag (see `_execFn` guard).
|
|
1319
|
+
*
|
|
1320
|
+
* **Dedup:** idempotent on duplicate `depNode` — if `depNode` is
|
|
1321
|
+
* already in `_deps`, returns the existing index without mutating
|
|
1322
|
+
* state. Callers can safely invoke `_addDep` without their own
|
|
1323
|
+
* "already added" check. `autoTrackNode` still keeps a `depIndexMap`
|
|
1324
|
+
* as a fast-path lookup for known deps (returning cached `data[idx]`
|
|
1325
|
+
* without calling `_addDep` at all); this internal dedup is the
|
|
1326
|
+
* backstop for any caller that doesn't track its own dep set.
|
|
1327
|
+
*
|
|
1328
|
+
* @returns The index of the new dep in `_deps`, or the existing index
|
|
1329
|
+
* if the dep was already present.
|
|
1330
|
+
*/
|
|
1331
|
+
_addDep(depNode) {
|
|
1332
|
+
for (let i = 0; i < this._deps.length; i++) {
|
|
1333
|
+
if (this._deps[i].node === depNode) return i;
|
|
1334
|
+
}
|
|
1335
|
+
const depIdx = this._deps.length;
|
|
1336
|
+
const record = createDepRecord(depNode);
|
|
1337
|
+
this._deps.push(record);
|
|
1338
|
+
if (this._sinks == null) return depIdx;
|
|
1339
|
+
record.dirty = true;
|
|
1340
|
+
this._dirtyDepCount++;
|
|
1341
|
+
if (this._status !== "dirty") this._emit(DIRTY_ONLY_BATCH);
|
|
1342
|
+
record.unsub = noopUnsub;
|
|
1343
|
+
try {
|
|
1344
|
+
record.unsub = depNode.subscribe((msgs) => {
|
|
1345
|
+
if (record.unsub === null) return;
|
|
1346
|
+
const tierOf = this._config.tierOf;
|
|
1347
|
+
let sawSettlement = false;
|
|
1348
|
+
for (const m of msgs) {
|
|
1349
|
+
if (tierOf(m[0]) >= 3) sawSettlement = true;
|
|
1350
|
+
this._config.onMessage(
|
|
1351
|
+
this,
|
|
1352
|
+
m,
|
|
1353
|
+
{ direction: "down-in", depIndex: depIdx },
|
|
1354
|
+
this._actions
|
|
1355
|
+
);
|
|
1356
|
+
}
|
|
1357
|
+
if (sawSettlement) this._maybeRunFnOnSettlement();
|
|
1358
|
+
});
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
record.unsub = null;
|
|
1361
|
+
this._deps.pop();
|
|
1362
|
+
this._dirtyDepCount--;
|
|
1363
|
+
throw err;
|
|
844
1364
|
}
|
|
1365
|
+
return depIdx;
|
|
845
1366
|
}
|
|
846
1367
|
/**
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
1368
|
+
* @internal Unsubscribes from deps, fires fn cleanup (both shapes),
|
|
1369
|
+
* clears wave/store state, and (for compute nodes) drops `_cached` per
|
|
1370
|
+
* the ROM/RAM rule. Idempotent: second call is a no-op.
|
|
1371
|
+
*
|
|
1372
|
+
* @param skipStatusUpdate — When `true`, the caller takes responsibility
|
|
1373
|
+
* for setting `_status` after deactivation (e.g. TEARDOWN always sets
|
|
1374
|
+
* `"sentinel"` unconditionally). When `false` (default), deactivation
|
|
1375
|
+
* applies the ROM rule: compute nodes → `"sentinel"`, state nodes
|
|
1376
|
+
* preserve their current status.
|
|
850
1377
|
*/
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
this.
|
|
859
|
-
if (this._versioning != null) {
|
|
860
|
-
advanceVersion(this._versioning, m[1], this._hashFn);
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
if (t === INVALIDATE) {
|
|
864
|
-
this._onInvalidate();
|
|
865
|
-
this._cached = NO_VALUE;
|
|
1378
|
+
_deactivate(skipStatusUpdate = false) {
|
|
1379
|
+
const cleanup = this._cleanup;
|
|
1380
|
+
this._cleanup = void 0;
|
|
1381
|
+
if (typeof cleanup === "function") {
|
|
1382
|
+
try {
|
|
1383
|
+
cleanup();
|
|
1384
|
+
} catch (err) {
|
|
1385
|
+
this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
|
|
866
1386
|
}
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1387
|
+
} else if (cleanup != null && typeof cleanup.deactivation === "function") {
|
|
1388
|
+
try {
|
|
1389
|
+
cleanup.deactivation();
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
this._emit([[ERROR, this._wrapFnError("cleanup.deactivation threw", err)]]);
|
|
870
1392
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1393
|
+
}
|
|
1394
|
+
for (const d of this._deps) {
|
|
1395
|
+
if (d.unsub != null) {
|
|
1396
|
+
const u = d.unsub;
|
|
1397
|
+
d.unsub = null;
|
|
876
1398
|
try {
|
|
877
|
-
|
|
878
|
-
}
|
|
879
|
-
this._onDeactivate();
|
|
1399
|
+
u();
|
|
1400
|
+
} catch {
|
|
880
1401
|
}
|
|
881
1402
|
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
1403
|
+
resetDepRecord(d);
|
|
1404
|
+
}
|
|
1405
|
+
this._waveHasNewData = false;
|
|
1406
|
+
this._hasNewTerminal = false;
|
|
1407
|
+
this._hasCalledFnOnce = false;
|
|
1408
|
+
this._paused = false;
|
|
1409
|
+
this._pendingWave = false;
|
|
1410
|
+
this._pendingRerun = false;
|
|
1411
|
+
this._rerunDepth = 0;
|
|
1412
|
+
this._store = {};
|
|
1413
|
+
this._dirtyDepCount = 0;
|
|
1414
|
+
this._pauseLocks = null;
|
|
1415
|
+
this._pauseBuffer = null;
|
|
1416
|
+
if (this._fn != null) {
|
|
1417
|
+
this._cached = void 0;
|
|
885
1418
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
*/
|
|
892
|
-
_onInvalidate() {
|
|
893
|
-
}
|
|
894
|
-
/**
|
|
895
|
-
* Subclass hook: invoked when TEARDOWN arrives, before `_onDeactivate`.
|
|
896
|
-
* {@link NodeImpl} uses this to run any pending cleanup fn.
|
|
897
|
-
*/
|
|
898
|
-
_onTeardown() {
|
|
899
|
-
}
|
|
900
|
-
/** Forward a signal to all companion meta nodes (best-effort). */
|
|
901
|
-
_propagateToMeta(t) {
|
|
902
|
-
for (const metaNode of Object.values(this.meta)) {
|
|
903
|
-
try {
|
|
904
|
-
metaNode._downInternal([[t]]);
|
|
905
|
-
} catch {
|
|
1419
|
+
if (!skipStatusUpdate) {
|
|
1420
|
+
if (this._fn != null || this._deps.length > 0) {
|
|
1421
|
+
if (!this._isTerminal || this._resubscribable) {
|
|
1422
|
+
this._status = "sentinel";
|
|
1423
|
+
}
|
|
906
1424
|
}
|
|
907
1425
|
}
|
|
908
1426
|
}
|
|
1427
|
+
// --- Dep message dispatch (§3.5 singleton default) ---
|
|
909
1428
|
/**
|
|
910
|
-
*
|
|
911
|
-
*
|
|
1429
|
+
* @internal Default per-tier dispatch for incoming dep messages. Called
|
|
1430
|
+
* by `defaultOnMessage`. Updates the DepRecord, triggers wave
|
|
1431
|
+
* completion, and forwards passthrough traffic.
|
|
912
1432
|
*/
|
|
913
|
-
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
const eqMsg = eqErr instanceof Error ? eqErr.message : String(eqErr);
|
|
920
|
-
const wrapped = new Error(`Node "${this.name}": equals threw: ${eqMsg}`, {
|
|
921
|
-
cause: eqErr
|
|
922
|
-
});
|
|
923
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
924
|
-
return;
|
|
1433
|
+
_onDepMessage(depIndex, msg) {
|
|
1434
|
+
const dep = this._deps[depIndex];
|
|
1435
|
+
const t = msg[0];
|
|
1436
|
+
if (this._inspectorHooks != null) {
|
|
1437
|
+
const ev = { kind: "dep_message", depIndex, message: msg };
|
|
1438
|
+
for (const hook of this._inspectorHooks) hook(ev);
|
|
925
1439
|
}
|
|
926
|
-
if (
|
|
927
|
-
|
|
1440
|
+
if (t === START) return;
|
|
1441
|
+
if (t === DIRTY) {
|
|
1442
|
+
this._depDirtied(dep);
|
|
928
1443
|
return;
|
|
929
1444
|
}
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
// src/core/node.ts
|
|
935
|
-
var NodeImpl = class extends NodeBase {
|
|
936
|
-
// --- Dep configuration (set once) ---
|
|
937
|
-
_deps;
|
|
938
|
-
_fn;
|
|
939
|
-
_opts;
|
|
940
|
-
_hasDeps;
|
|
941
|
-
_isSingleDep;
|
|
942
|
-
_autoComplete;
|
|
943
|
-
// --- Wave tracking masks ---
|
|
944
|
-
_depDirtyMask;
|
|
945
|
-
_depSettledMask;
|
|
946
|
-
_depCompleteMask;
|
|
947
|
-
_allDepsCompleteMask;
|
|
948
|
-
// --- Identity-skip optimization ---
|
|
949
|
-
_lastDepValues;
|
|
950
|
-
_cleanup;
|
|
951
|
-
// --- Upstream bookkeeping ---
|
|
952
|
-
_upstreamUnsubs = [];
|
|
953
|
-
// --- Fn behavior flag ---
|
|
954
|
-
/** @internal Read by `describeNode` to infer `"operator"` label. */
|
|
955
|
-
_manualEmitUsed = false;
|
|
956
|
-
constructor(deps, fn, opts) {
|
|
957
|
-
super(opts);
|
|
958
|
-
this._deps = deps;
|
|
959
|
-
this._fn = fn;
|
|
960
|
-
this._opts = opts;
|
|
961
|
-
this._hasDeps = deps.length > 0;
|
|
962
|
-
this._isSingleDep = deps.length === 1 && fn != null;
|
|
963
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
964
|
-
if (!this._hasDeps && fn == null && this._cached !== NO_VALUE) {
|
|
965
|
-
this._status = "settled";
|
|
966
|
-
}
|
|
967
|
-
this._depDirtyMask = createBitSet(deps.length);
|
|
968
|
-
this._depSettledMask = createBitSet(deps.length);
|
|
969
|
-
this._depCompleteMask = createBitSet(deps.length);
|
|
970
|
-
this._allDepsCompleteMask = createBitSet(deps.length);
|
|
971
|
-
for (let i = 0; i < deps.length; i++) this._allDepsCompleteMask.set(i);
|
|
972
|
-
this.down = this.down.bind(this);
|
|
973
|
-
this.up = this.up.bind(this);
|
|
974
|
-
}
|
|
975
|
-
// --- Meta node factory (called from base constructor) ---
|
|
976
|
-
_createMetaNode(key, initialValue, opts) {
|
|
977
|
-
return node({
|
|
978
|
-
initial: initialValue,
|
|
979
|
-
name: `${opts.name ?? "node"}:meta:${key}`,
|
|
980
|
-
describeKind: "state",
|
|
981
|
-
...opts.guard != null ? { guard: opts.guard } : {}
|
|
982
|
-
});
|
|
983
|
-
}
|
|
984
|
-
// --- Manual emit tracker (set by actions.down / actions.emit) ---
|
|
985
|
-
_onManualEmit() {
|
|
986
|
-
this._manualEmitUsed = true;
|
|
987
|
-
}
|
|
988
|
-
// --- Up / unsubscribe ---
|
|
989
|
-
up(messages, options) {
|
|
990
|
-
if (!this._hasDeps) return;
|
|
991
|
-
if (!options?.internal && this._guard != null) {
|
|
992
|
-
const actor = normalizeActor(options?.actor);
|
|
993
|
-
if (!this._guard(actor, "write")) {
|
|
994
|
-
throw new GuardDenied({ actor, action: "write", nodeName: this.name });
|
|
995
|
-
}
|
|
996
|
-
this._recordMutation(actor);
|
|
1445
|
+
if (t === INVALIDATE) {
|
|
1446
|
+
this._depInvalidated(dep);
|
|
1447
|
+
this._emit(INVALIDATE_ONLY_BATCH);
|
|
1448
|
+
return;
|
|
997
1449
|
}
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
} else {
|
|
1002
|
-
dep.up?.(messages, options);
|
|
1003
|
-
}
|
|
1450
|
+
if (t === PAUSE || t === RESUME) {
|
|
1451
|
+
this._emit([msg]);
|
|
1452
|
+
return;
|
|
1004
1453
|
}
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
for (const dep of this._deps) {
|
|
1009
|
-
dep.up?.(messages, { internal: true });
|
|
1454
|
+
if (t === TEARDOWN) {
|
|
1455
|
+
this._emit(TEARDOWN_ONLY_BATCH);
|
|
1456
|
+
return;
|
|
1010
1457
|
}
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1458
|
+
if (t === DATA) {
|
|
1459
|
+
this._depSettledAsData(dep, msg[1]);
|
|
1460
|
+
} else if (t === RESOLVED) {
|
|
1461
|
+
this._depSettledAsResolved(dep);
|
|
1462
|
+
} else if (t === COMPLETE) {
|
|
1463
|
+
this._depSettledAsTerminal(dep, true);
|
|
1464
|
+
} else if (t === ERROR) {
|
|
1465
|
+
this._depSettledAsTerminal(dep, msg[1]);
|
|
1466
|
+
} else {
|
|
1467
|
+
this._emit([msg]);
|
|
1020
1468
|
return;
|
|
1021
1469
|
}
|
|
1022
|
-
if (this._fn) {
|
|
1023
|
-
|
|
1470
|
+
if (!this._fn) {
|
|
1471
|
+
if (t === DATA || t === RESOLVED) {
|
|
1472
|
+
this._emit([msg]);
|
|
1473
|
+
}
|
|
1474
|
+
if (t === COMPLETE || t === ERROR) {
|
|
1475
|
+
this._maybeAutoTerminalAfterWave();
|
|
1476
|
+
}
|
|
1024
1477
|
return;
|
|
1025
1478
|
}
|
|
1026
1479
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1480
|
+
// --- Centralized dep-state transitions (A3 settlement counters) ---
|
|
1481
|
+
//
|
|
1482
|
+
// Every mutation to `DepRecord.dirty` / `DepRecord.prevData` /
|
|
1483
|
+
// `DepRecord.terminal` must go through one of these helpers so the
|
|
1484
|
+
// `_dirtyDepCount` and `_sentinelDepCount` counters stay in sync with
|
|
1485
|
+
// the per-record flags. `_maybeRunFnOnSettlement` reads the counters
|
|
1486
|
+
// and never re-scans the `_deps` array.
|
|
1487
|
+
/**
|
|
1488
|
+
* Called when a dep transitions `dirty: false → true` (either from an
|
|
1489
|
+
* incoming DIRTY, or pre-set during `_activate` / `_addDep` /
|
|
1490
|
+
* `_depInvalidated`). No-op if the dep is already dirty. Fires the
|
|
1491
|
+
* downstream DIRTY emit if we're the first to dirty this wave.
|
|
1492
|
+
*/
|
|
1493
|
+
_depDirtied(dep) {
|
|
1494
|
+
if (dep.dirty) return;
|
|
1495
|
+
dep.dirty = true;
|
|
1496
|
+
dep.involvedThisWave = true;
|
|
1497
|
+
this._dirtyDepCount++;
|
|
1498
|
+
if (this._status !== "dirty") {
|
|
1499
|
+
this._emit(DIRTY_ONLY_BATCH);
|
|
1038
1500
|
}
|
|
1039
1501
|
}
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
this._cleanup = void 0;
|
|
1050
|
-
cleanup?.();
|
|
1051
|
-
}
|
|
1052
|
-
// --- Upstream connect / disconnect ---
|
|
1053
|
-
_connectUpstream() {
|
|
1054
|
-
if (!this._hasDeps) return;
|
|
1055
|
-
if (this._upstreamUnsubs.length > 0) return;
|
|
1056
|
-
this._depDirtyMask.setAll();
|
|
1057
|
-
this._depSettledMask.reset();
|
|
1058
|
-
this._depCompleteMask.reset();
|
|
1059
|
-
const depValuesBefore = this._lastDepValues;
|
|
1060
|
-
const subHints = this._isSingleDep ? { singleDep: true } : void 0;
|
|
1061
|
-
for (let i = 0; i < this._deps.length; i += 1) {
|
|
1062
|
-
const dep = this._deps[i];
|
|
1063
|
-
this._upstreamUnsubs.push(
|
|
1064
|
-
dep.subscribe((msgs) => this._handleDepMessages(i, msgs), subHints)
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
if (this._fn && this._onMessage && !this._terminal && this._lastDepValues === depValuesBefore) {
|
|
1068
|
-
this._runFn();
|
|
1502
|
+
/**
|
|
1503
|
+
* Called when a dep delivers new DATA: clears dirty, stores the payload,
|
|
1504
|
+
* marks wave-has-data, and — if this is the dep's first DATA — clears
|
|
1505
|
+
* its sentinel slot so the first-run gate can open.
|
|
1506
|
+
*/
|
|
1507
|
+
_depSettledAsData(dep, value) {
|
|
1508
|
+
if (dep.dirty) {
|
|
1509
|
+
dep.dirty = false;
|
|
1510
|
+
this._dirtyDepCount--;
|
|
1069
1511
|
}
|
|
1512
|
+
dep.involvedThisWave = true;
|
|
1513
|
+
dep.dataBatch.push(value);
|
|
1514
|
+
this._waveHasNewData = true;
|
|
1070
1515
|
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1516
|
+
/**
|
|
1517
|
+
* Called when a dep emits RESOLVED (wave settled, value unchanged).
|
|
1518
|
+
* Clears dirty; does NOT touch `prevData` / `terminal` / sentinel
|
|
1519
|
+
* count — sentinel only exits on first DATA or terminal, not RESOLVED.
|
|
1520
|
+
*/
|
|
1521
|
+
_depSettledAsResolved(dep) {
|
|
1522
|
+
if (dep.dirty) {
|
|
1523
|
+
dep.dirty = false;
|
|
1524
|
+
this._dirtyDepCount--;
|
|
1075
1525
|
}
|
|
1076
|
-
this._depDirtyMask.reset();
|
|
1077
|
-
this._depSettledMask.reset();
|
|
1078
|
-
this._depCompleteMask.reset();
|
|
1079
1526
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
this._depDirtyMask.clear(index);
|
|
1091
|
-
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1092
|
-
this._depDirtyMask.reset();
|
|
1093
|
-
this._depSettledMask.reset();
|
|
1094
|
-
this._runFn();
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
continue;
|
|
1098
|
-
}
|
|
1099
|
-
} catch (err) {
|
|
1100
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1101
|
-
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
1102
|
-
cause: err
|
|
1103
|
-
});
|
|
1104
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1105
|
-
return;
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
if (messageTier(t) < 1) continue;
|
|
1109
|
-
if (!this._fn) {
|
|
1110
|
-
if (t === COMPLETE && this._deps.length > 1) {
|
|
1111
|
-
this._depCompleteMask.set(index);
|
|
1112
|
-
this._maybeCompleteFromDeps();
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
this._downInternal([msg]);
|
|
1116
|
-
continue;
|
|
1117
|
-
}
|
|
1118
|
-
if (t === DIRTY) {
|
|
1119
|
-
this._onDepDirty(index);
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
if (t === DATA || t === RESOLVED) {
|
|
1123
|
-
this._onDepSettled(index);
|
|
1124
|
-
continue;
|
|
1125
|
-
}
|
|
1126
|
-
if (t === COMPLETE) {
|
|
1127
|
-
this._depCompleteMask.set(index);
|
|
1128
|
-
this._depDirtyMask.clear(index);
|
|
1129
|
-
this._depSettledMask.clear(index);
|
|
1130
|
-
if (this._depDirtyMask.any() && this._depSettledMask.covers(this._depDirtyMask)) {
|
|
1131
|
-
this._depDirtyMask.reset();
|
|
1132
|
-
this._depSettledMask.reset();
|
|
1133
|
-
this._runFn();
|
|
1134
|
-
} else if (!this._depDirtyMask.any() && this._status === "dirty") {
|
|
1135
|
-
this._depSettledMask.reset();
|
|
1136
|
-
this._runFn();
|
|
1137
|
-
}
|
|
1138
|
-
this._maybeCompleteFromDeps();
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
if (t === ERROR) {
|
|
1142
|
-
this._downInternal([msg]);
|
|
1143
|
-
continue;
|
|
1144
|
-
}
|
|
1145
|
-
if (t === INVALIDATE || t === TEARDOWN || t === PAUSE || t === RESUME) {
|
|
1146
|
-
this._downInternal([msg]);
|
|
1147
|
-
continue;
|
|
1148
|
-
}
|
|
1149
|
-
this._downInternal([msg]);
|
|
1527
|
+
/**
|
|
1528
|
+
* Called when a dep delivers COMPLETE (`terminal = true`) or ERROR
|
|
1529
|
+
* (`terminal = errorPayload`). Clears dirty, stores the terminal, and
|
|
1530
|
+
* — if the dep had never contributed a DATA yet — leaves sentinel
|
|
1531
|
+
* since the gate treats "terminated without data" as gate-open too.
|
|
1532
|
+
*/
|
|
1533
|
+
_depSettledAsTerminal(dep, terminal) {
|
|
1534
|
+
if (dep.dirty) {
|
|
1535
|
+
dep.dirty = false;
|
|
1536
|
+
this._dirtyDepCount--;
|
|
1150
1537
|
}
|
|
1538
|
+
dep.terminal = terminal;
|
|
1539
|
+
dep.involvedThisWave = true;
|
|
1540
|
+
this._hasNewTerminal = true;
|
|
1151
1541
|
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1542
|
+
/**
|
|
1543
|
+
* Called when a dep emits INVALIDATE: clears prevData, terminal, and
|
|
1544
|
+
* dataBatch. The dep is now back in the "never delivered a real value"
|
|
1545
|
+
* state — `prevData === undefined` so the sentinel check in fn will fire.
|
|
1546
|
+
*/
|
|
1547
|
+
_depInvalidated(dep) {
|
|
1548
|
+
dep.prevData = void 0;
|
|
1549
|
+
dep.terminal = void 0;
|
|
1550
|
+
dep.dataBatch.length = 0;
|
|
1551
|
+
if (!dep.dirty) {
|
|
1552
|
+
dep.dirty = true;
|
|
1553
|
+
dep.involvedThisWave = true;
|
|
1554
|
+
this._dirtyDepCount++;
|
|
1555
|
+
} else {
|
|
1556
|
+
dep.involvedThisWave = false;
|
|
1158
1557
|
}
|
|
1159
1558
|
}
|
|
1160
|
-
|
|
1161
|
-
if (!this.
|
|
1162
|
-
|
|
1559
|
+
_maybeRunFnOnSettlement() {
|
|
1560
|
+
if (this._isTerminal && !this._resubscribable) return;
|
|
1561
|
+
if (this._dirtyDepCount > 0) return;
|
|
1562
|
+
if (this._paused) {
|
|
1563
|
+
this._pendingWave = true;
|
|
1564
|
+
return;
|
|
1163
1565
|
}
|
|
1164
|
-
this.
|
|
1165
|
-
|
|
1166
|
-
this.
|
|
1167
|
-
this.
|
|
1168
|
-
|
|
1566
|
+
if (!this._waveHasNewData && !this._hasNewTerminal && this._hasCalledFnOnce) {
|
|
1567
|
+
this._clearWaveFlags();
|
|
1568
|
+
this._emit(RESOLVED_ONLY_BATCH);
|
|
1569
|
+
this._maybeAutoTerminalAfterWave();
|
|
1570
|
+
return;
|
|
1169
1571
|
}
|
|
1572
|
+
if (this._fn) this._execFn();
|
|
1573
|
+
this._maybeAutoTerminalAfterWave();
|
|
1170
1574
|
}
|
|
1171
|
-
|
|
1172
|
-
if (this.
|
|
1173
|
-
|
|
1575
|
+
_maybeAutoTerminalAfterWave() {
|
|
1576
|
+
if (this._deps.length === 0) return;
|
|
1577
|
+
if (this._isTerminal) return;
|
|
1578
|
+
const erroredDep = this._deps.find((d) => d.terminal !== void 0 && d.terminal !== true);
|
|
1579
|
+
if (erroredDep != null) {
|
|
1580
|
+
if (this._autoError) {
|
|
1581
|
+
this._emit([[ERROR, erroredDep.terminal]]);
|
|
1582
|
+
}
|
|
1583
|
+
return;
|
|
1584
|
+
}
|
|
1585
|
+
if (this._autoComplete && this._deps.every((d) => d.terminal !== void 0)) {
|
|
1586
|
+
this._emit(COMPLETE_ONLY_BATCH);
|
|
1174
1587
|
}
|
|
1175
1588
|
}
|
|
1176
1589
|
// --- Fn execution ---
|
|
1177
|
-
|
|
1590
|
+
/**
|
|
1591
|
+
* @internal Runs the node fn once. Default cleanup (function form) fires
|
|
1592
|
+
* before the new run; `{ deactivation }` cleanup survives.
|
|
1593
|
+
*/
|
|
1594
|
+
_execFn() {
|
|
1178
1595
|
if (!this._fn) return;
|
|
1179
|
-
if (this.
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
let allSame = true;
|
|
1187
|
-
for (let i = 0; i < n; i++) {
|
|
1188
|
-
if (!Object.is(depValues[i], prev[i])) {
|
|
1189
|
-
allSame = false;
|
|
1190
|
-
break;
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
if (allSame) {
|
|
1194
|
-
if (this._status === "dirty") {
|
|
1195
|
-
this._downInternal([[RESOLVED]]);
|
|
1196
|
-
}
|
|
1197
|
-
return;
|
|
1198
|
-
}
|
|
1199
|
-
}
|
|
1200
|
-
const prevCleanup = this._cleanup;
|
|
1596
|
+
if (this._isTerminal && !this._resubscribable) return;
|
|
1597
|
+
if (this._isExecutingFn) {
|
|
1598
|
+
this._pendingRerun = true;
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const prevCleanup = this._cleanup;
|
|
1602
|
+
if (typeof prevCleanup === "function") {
|
|
1201
1603
|
this._cleanup = void 0;
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
const out = this._fn(depValues, this._actions);
|
|
1207
|
-
if (isCleanupResult(out)) {
|
|
1208
|
-
this._cleanup = out.cleanup;
|
|
1209
|
-
if (this._manualEmitUsed) return;
|
|
1210
|
-
if ("value" in out) {
|
|
1211
|
-
this._downAutoValue(out.value);
|
|
1212
|
-
}
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
if (isCleanupFn(out)) {
|
|
1216
|
-
this._cleanup = out;
|
|
1604
|
+
try {
|
|
1605
|
+
prevCleanup();
|
|
1606
|
+
} catch (err) {
|
|
1607
|
+
this._emit([[ERROR, this._wrapFnError("cleanup threw", err)]]);
|
|
1217
1608
|
return;
|
|
1218
1609
|
}
|
|
1219
|
-
if (this._manualEmitUsed) return;
|
|
1220
|
-
if (out === void 0) return;
|
|
1221
|
-
this._downAutoValue(out);
|
|
1222
|
-
} catch (err) {
|
|
1223
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1224
|
-
const wrapped = new Error(`Node "${this.name}": fn threw: ${errMsg}`, { cause: err });
|
|
1225
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1226
1610
|
}
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
if (isNodeArray(depsOrFn)) {
|
|
1236
|
-
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1237
|
-
} else if (isNodeOptions(depsOrFn)) {
|
|
1238
|
-
opts = depsOrFn;
|
|
1239
|
-
} else {
|
|
1240
|
-
opts = (isNodeOptions(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
1241
|
-
}
|
|
1242
|
-
return new NodeImpl(deps, fn, opts);
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
// src/core/bridge.ts
|
|
1246
|
-
var DEFAULT_DOWN = [
|
|
1247
|
-
DATA,
|
|
1248
|
-
DIRTY,
|
|
1249
|
-
RESOLVED,
|
|
1250
|
-
COMPLETE,
|
|
1251
|
-
ERROR,
|
|
1252
|
-
TEARDOWN,
|
|
1253
|
-
PAUSE,
|
|
1254
|
-
RESUME,
|
|
1255
|
-
INVALIDATE
|
|
1256
|
-
];
|
|
1257
|
-
var STANDARD_TYPES = /* @__PURE__ */ new Set([
|
|
1258
|
-
DATA,
|
|
1259
|
-
DIRTY,
|
|
1260
|
-
RESOLVED,
|
|
1261
|
-
COMPLETE,
|
|
1262
|
-
ERROR,
|
|
1263
|
-
TEARDOWN,
|
|
1264
|
-
PAUSE,
|
|
1265
|
-
RESUME,
|
|
1266
|
-
INVALIDATE
|
|
1267
|
-
]);
|
|
1268
|
-
function bridge(from, to, opts) {
|
|
1269
|
-
const allowedDown = new Set(opts?.down ?? DEFAULT_DOWN);
|
|
1270
|
-
const onMessage = (msg, _depIndex, _actions) => {
|
|
1271
|
-
const type = msg[0];
|
|
1272
|
-
if (!STANDARD_TYPES.has(type)) {
|
|
1273
|
-
to.down([msg]);
|
|
1274
|
-
return true;
|
|
1275
|
-
}
|
|
1276
|
-
if (type === COMPLETE || type === ERROR) {
|
|
1277
|
-
if (allowedDown.has(type)) {
|
|
1278
|
-
to.down([msg]);
|
|
1611
|
+
const batchData = this._deps.map(
|
|
1612
|
+
(d) => !d.involvedThisWave ? void 0 : d.dataBatch.length > 0 ? [...d.dataBatch] : []
|
|
1613
|
+
);
|
|
1614
|
+
const prevData = this._deps.map((d) => d.prevData);
|
|
1615
|
+
for (let i = 0; i < this._deps.length; i++) {
|
|
1616
|
+
const batch2 = batchData[i];
|
|
1617
|
+
if (batch2 != null && batch2.length > 0) {
|
|
1618
|
+
this._deps[i].prevData = batch2[batch2.length - 1];
|
|
1279
1619
|
}
|
|
1280
|
-
return false;
|
|
1281
1620
|
}
|
|
1282
|
-
|
|
1283
|
-
|
|
1621
|
+
const terminalDeps = this._deps.map((d) => d.terminal);
|
|
1622
|
+
const ctx = { prevData, terminalDeps, store: this._store };
|
|
1623
|
+
this._hasCalledFnOnce = true;
|
|
1624
|
+
this._clearWaveFlags();
|
|
1625
|
+
if (this._inspectorHooks != null) {
|
|
1626
|
+
const ev = { kind: "run", batchData, prevData };
|
|
1627
|
+
for (const hook of this._inspectorHooks) hook(ev);
|
|
1628
|
+
}
|
|
1629
|
+
this._isExecutingFn = true;
|
|
1630
|
+
try {
|
|
1631
|
+
const result = this._fn(batchData, this._actions, ctx);
|
|
1632
|
+
if (typeof result === "function") {
|
|
1633
|
+
this._cleanup = result;
|
|
1634
|
+
} else if (result != null && typeof result === "object" && typeof result.deactivation === "function") {
|
|
1635
|
+
this._cleanup = result;
|
|
1636
|
+
}
|
|
1637
|
+
} catch (err) {
|
|
1638
|
+
this._emit([[ERROR, this._wrapFnError("fn threw", err)]]);
|
|
1639
|
+
} finally {
|
|
1640
|
+
this._isExecutingFn = false;
|
|
1641
|
+
if (this._pendingRerun) {
|
|
1642
|
+
this._pendingRerun = false;
|
|
1643
|
+
this._rerunDepth += 1;
|
|
1644
|
+
if (this._rerunDepth > MAX_RERUN_DEPTH) {
|
|
1645
|
+
this._rerunDepth = 0;
|
|
1646
|
+
this._emit([
|
|
1647
|
+
[
|
|
1648
|
+
ERROR,
|
|
1649
|
+
new Error(
|
|
1650
|
+
`Node "${this.name}": _pendingRerun depth exceeded ${MAX_RERUN_DEPTH} \u2014 likely a reactive cycle`
|
|
1651
|
+
)
|
|
1652
|
+
]
|
|
1653
|
+
]);
|
|
1654
|
+
} else {
|
|
1655
|
+
this._maybeRunFnOnSettlement();
|
|
1656
|
+
}
|
|
1657
|
+
} else {
|
|
1658
|
+
this._rerunDepth = 0;
|
|
1659
|
+
}
|
|
1660
|
+
this._clearWaveFlags();
|
|
1284
1661
|
}
|
|
1285
|
-
to.down([msg]);
|
|
1286
|
-
return true;
|
|
1287
|
-
};
|
|
1288
|
-
return node([from], void 0, {
|
|
1289
|
-
name: opts?.name,
|
|
1290
|
-
describeKind: "effect",
|
|
1291
|
-
onMessage,
|
|
1292
|
-
meta: { _internal: true }
|
|
1293
|
-
});
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// src/core/dynamic-node.ts
|
|
1297
|
-
var MAX_RERUN = 16;
|
|
1298
|
-
function dynamicNode(fn, opts) {
|
|
1299
|
-
return new DynamicNodeImpl(fn, opts ?? {});
|
|
1300
|
-
}
|
|
1301
|
-
var DynamicNodeImpl = class extends NodeBase {
|
|
1302
|
-
_fn;
|
|
1303
|
-
_autoComplete;
|
|
1304
|
-
// Dynamic deps tracking
|
|
1305
|
-
/** @internal Read by `describeNode`. */
|
|
1306
|
-
_deps = [];
|
|
1307
|
-
_depUnsubs = [];
|
|
1308
|
-
_depIndexMap = /* @__PURE__ */ new Map();
|
|
1309
|
-
_depDirtyBits = /* @__PURE__ */ new Set();
|
|
1310
|
-
_depSettledBits = /* @__PURE__ */ new Set();
|
|
1311
|
-
_depCompleteBits = /* @__PURE__ */ new Set();
|
|
1312
|
-
// Execution state
|
|
1313
|
-
_running = false;
|
|
1314
|
-
_rewiring = false;
|
|
1315
|
-
_bufferedDepMessages = [];
|
|
1316
|
-
_trackedValues = /* @__PURE__ */ new Map();
|
|
1317
|
-
_rerunCount = 0;
|
|
1318
|
-
constructor(fn, opts) {
|
|
1319
|
-
super(opts);
|
|
1320
|
-
this._fn = fn;
|
|
1321
|
-
this._autoComplete = opts.completeWhenDepsComplete ?? true;
|
|
1322
|
-
this.down = this.down.bind(this);
|
|
1323
|
-
this.up = this.up.bind(this);
|
|
1324
1662
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
}
|
|
1663
|
+
_clearWaveFlags() {
|
|
1664
|
+
this._waveHasNewData = false;
|
|
1665
|
+
this._hasNewTerminal = false;
|
|
1666
|
+
for (const d of this._deps) {
|
|
1667
|
+
d.involvedThisWave = false;
|
|
1668
|
+
d.dataBatch.length = 0;
|
|
1669
|
+
}
|
|
1332
1670
|
}
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
return
|
|
1671
|
+
_wrapFnError(label, err) {
|
|
1672
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1673
|
+
return new Error(`Node "${this.name}": ${label}: ${msg}`, { cause: err });
|
|
1336
1674
|
}
|
|
1337
|
-
// ---
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1675
|
+
// --- Framing (tier sort + synthetic DIRTY prefix) ---
|
|
1676
|
+
/**
|
|
1677
|
+
* @internal Stable tier sort + synthetic DIRTY prefix for an outgoing
|
|
1678
|
+
* batch. Fast path: already-monotone single-tier batches (the common
|
|
1679
|
+
* case from interned singletons like `DIRTY_ONLY_BATCH`) return the
|
|
1680
|
+
* input unchanged. General path: decorate-sort-undecorate into a new
|
|
1681
|
+
* array, then prepend `[DIRTY]` after any tier-0 START entries when
|
|
1682
|
+
* a tier-3 payload is present and the node isn't already dirty.
|
|
1683
|
+
*
|
|
1684
|
+
* Single source of truth for the spec §1.3.1 framing invariant. Every
|
|
1685
|
+
* outgoing path hits `_frameBatch` exactly once via `_emit`.
|
|
1686
|
+
*/
|
|
1687
|
+
_frameBatch(messages) {
|
|
1688
|
+
const tierOf = this._config.tierOf;
|
|
1689
|
+
if (messages.length === 1) {
|
|
1690
|
+
const t = tierOf(messages[0][0]);
|
|
1691
|
+
if (t === 3 && this._status !== "dirty") {
|
|
1692
|
+
return [DIRTY_MSG, messages[0]];
|
|
1344
1693
|
}
|
|
1345
|
-
|
|
1694
|
+
return messages;
|
|
1346
1695
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1696
|
+
let monotone = true;
|
|
1697
|
+
let hasTier3 = false;
|
|
1698
|
+
let hasDirty = false;
|
|
1699
|
+
let prevTier = -1;
|
|
1700
|
+
for (const m of messages) {
|
|
1701
|
+
const tier = tierOf(m[0]);
|
|
1702
|
+
if (tier < prevTier) monotone = false;
|
|
1703
|
+
if (tier === 3) hasTier3 = true;
|
|
1704
|
+
if (m[0] === DIRTY) hasDirty = true;
|
|
1705
|
+
prevTier = tier;
|
|
1706
|
+
}
|
|
1707
|
+
let sorted = messages;
|
|
1708
|
+
if (!monotone) {
|
|
1709
|
+
const indexed = messages.map((m, i) => ({ m, i, tier: tierOf(m[0]) }));
|
|
1710
|
+
indexed.sort((a, b) => a.tier - b.tier || a.i - b.i);
|
|
1711
|
+
sorted = indexed.map((x) => x.m);
|
|
1712
|
+
}
|
|
1713
|
+
if (hasTier3 && !hasDirty && this._status !== "dirty") {
|
|
1714
|
+
let insertAt = 0;
|
|
1715
|
+
while (insertAt < sorted.length && tierOf(sorted[insertAt][0]) === 0) insertAt++;
|
|
1716
|
+
if (insertAt === 0) return [DIRTY_MSG, ...sorted];
|
|
1717
|
+
return [...sorted.slice(0, insertAt), DIRTY_MSG, ...sorted.slice(insertAt)];
|
|
1349
1718
|
}
|
|
1719
|
+
return sorted;
|
|
1350
1720
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1721
|
+
// --- Emit pipeline ---
|
|
1722
|
+
/**
|
|
1723
|
+
* @internal The unified dispatch waist — one call = one wave.
|
|
1724
|
+
*
|
|
1725
|
+
* Pipeline stages, in order:
|
|
1726
|
+
*
|
|
1727
|
+
* 1. Early-return on empty batch.
|
|
1728
|
+
* 2. Terminal filter — post-COMPLETE/ERROR only TEARDOWN/INVALIDATE
|
|
1729
|
+
* still propagate so graph teardown and cache-clear still work.
|
|
1730
|
+
* 3. Tier sort (stable) — the batch can be in any order when it
|
|
1731
|
+
* arrives; the walker downstream (`downWithBatch`) assumes
|
|
1732
|
+
* ascending tier monotone, and so does `_updateState`'s tier-3
|
|
1733
|
+
* slice walk. This is the single source of truth for ordering.
|
|
1734
|
+
* 4. Synthetic DIRTY prefix — if a tier-3 payload is present, no
|
|
1735
|
+
* DIRTY is already in the batch, and the node isn't already in
|
|
1736
|
+
* `"dirty"` status, prepend `[DIRTY]` after any tier-0 START
|
|
1737
|
+
* entries. Guarantees spec §1.3.1 (DIRTY precedes DATA within
|
|
1738
|
+
* the same batch) uniformly across every entry point.
|
|
1739
|
+
* 5. PAUSE/RESUME lock bookkeeping (C0) — update `_pauseLocks`,
|
|
1740
|
+
* derive `_paused`, filter unknown-lockId RESUME, replay
|
|
1741
|
+
* bufferAll buffer on final lock release.
|
|
1742
|
+
* 6. Meta TEARDOWN fan-out — notify meta children before
|
|
1743
|
+
* `_updateState`'s TEARDOWN branch calls `_deactivate`. Hoisted
|
|
1744
|
+
* out of the walk to keep `_updateState` re-entrance-free.
|
|
1745
|
+
* 7. `_updateState` — walk the batch in tier order, advancing
|
|
1746
|
+
* `_cached` / `_status` / `_versioning` and running equals
|
|
1747
|
+
* substitution on tier-3 DATA (§3.5.1). Returns
|
|
1748
|
+
* `{finalMessages, equalsError?}`.
|
|
1749
|
+
* 8. `downWithBatch` dispatch (or bufferAll capture if paused with
|
|
1750
|
+
* `pausable: "resumeAll"`).
|
|
1751
|
+
* 9. Recursive ERROR emission if equals threw mid-walk.
|
|
1752
|
+
*
|
|
1753
|
+
* `node.down` / `node.emit` / `actions.down` / `actions.emit` all
|
|
1754
|
+
* converge here — the unified `_emit` waist (spec §1.3.1).
|
|
1755
|
+
*/
|
|
1756
|
+
_emit(messages) {
|
|
1757
|
+
if (messages.length === 0) return;
|
|
1758
|
+
let deliverable = messages;
|
|
1759
|
+
const terminal = this._isTerminal;
|
|
1760
|
+
if (terminal && !this._resubscribable) {
|
|
1761
|
+
const pass = messages.filter((m) => m[0] === TEARDOWN || m[0] === INVALIDATE);
|
|
1762
|
+
if (pass.length === 0) return;
|
|
1763
|
+
deliverable = pass;
|
|
1354
1764
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
_runFn() {
|
|
1379
|
-
if (this._terminal && !this._resubscribable) return;
|
|
1380
|
-
if (this._running) return;
|
|
1381
|
-
this._running = true;
|
|
1382
|
-
this._rerunCount = 0;
|
|
1383
|
-
let result;
|
|
1384
|
-
try {
|
|
1385
|
-
for (; ; ) {
|
|
1386
|
-
const trackedDeps = [];
|
|
1387
|
-
const trackedValuesMap = /* @__PURE__ */ new Map();
|
|
1388
|
-
const trackedSet = /* @__PURE__ */ new Set();
|
|
1389
|
-
const get = (dep) => {
|
|
1390
|
-
if (!trackedSet.has(dep)) {
|
|
1391
|
-
trackedSet.add(dep);
|
|
1392
|
-
trackedDeps.push(dep);
|
|
1393
|
-
trackedValuesMap.set(dep, dep.get());
|
|
1765
|
+
deliverable = this._frameBatch(deliverable);
|
|
1766
|
+
let filtered = null;
|
|
1767
|
+
for (let i = 0; i < deliverable.length; i++) {
|
|
1768
|
+
const m = deliverable[i];
|
|
1769
|
+
const t = m[0];
|
|
1770
|
+
if (t !== PAUSE && t !== RESUME) {
|
|
1771
|
+
if (filtered != null) filtered.push(m);
|
|
1772
|
+
continue;
|
|
1773
|
+
}
|
|
1774
|
+
if (m.length < 2) {
|
|
1775
|
+
throw new Error(
|
|
1776
|
+
`Node "${this.name}": [[${t === PAUSE ? "PAUSE" : "RESUME"}]] must carry a lockId payload \u2014 bare PAUSE/RESUME is a protocol violation (C0 rule). Use \`[[PAUSE, lockId]]\` / \`[[RESUME, lockId]]\`.`
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
let forward = true;
|
|
1780
|
+
if (this._pausable !== false) {
|
|
1781
|
+
const lockId = m[1];
|
|
1782
|
+
if (t === PAUSE) {
|
|
1783
|
+
if (this._pauseLocks == null) this._pauseLocks = /* @__PURE__ */ new Set();
|
|
1784
|
+
this._pauseLocks.add(lockId);
|
|
1785
|
+
this._paused = true;
|
|
1786
|
+
if (this._pausable === "resumeAll" && this._pauseBuffer == null) {
|
|
1787
|
+
this._pauseBuffer = [];
|
|
1394
1788
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
}
|
|
1411
|
-
this._rewiring = true;
|
|
1412
|
-
this._bufferedDepMessages = [];
|
|
1413
|
-
try {
|
|
1414
|
-
this._rewire(trackedDeps);
|
|
1415
|
-
} finally {
|
|
1416
|
-
this._rewiring = false;
|
|
1417
|
-
}
|
|
1418
|
-
let needsRerun = false;
|
|
1419
|
-
for (const entry of this._bufferedDepMessages) {
|
|
1420
|
-
for (const msg of entry.msgs) {
|
|
1421
|
-
if (msg[0] === DATA) {
|
|
1422
|
-
const dep = this._deps[entry.index];
|
|
1423
|
-
const trackedValue = dep != null ? this._trackedValues.get(dep) : void 0;
|
|
1424
|
-
const actualValue = msg[1];
|
|
1425
|
-
if (!this._equals(trackedValue, actualValue)) {
|
|
1426
|
-
needsRerun = true;
|
|
1427
|
-
break;
|
|
1789
|
+
} else {
|
|
1790
|
+
if (this._pauseLocks == null || !this._pauseLocks.has(lockId)) {
|
|
1791
|
+
forward = false;
|
|
1792
|
+
} else {
|
|
1793
|
+
this._pauseLocks.delete(lockId);
|
|
1794
|
+
if (this._pauseLocks.size === 0) {
|
|
1795
|
+
this._paused = false;
|
|
1796
|
+
if (this._pauseBuffer != null && this._pauseBuffer.length > 0) {
|
|
1797
|
+
const drain = this._pauseBuffer;
|
|
1798
|
+
this._pauseBuffer = [];
|
|
1799
|
+
this._emit(drain);
|
|
1800
|
+
}
|
|
1801
|
+
if (this._pendingWave) {
|
|
1802
|
+
this._pendingWave = false;
|
|
1803
|
+
this._maybeRunFnOnSettlement();
|
|
1428
1804
|
}
|
|
1429
1805
|
}
|
|
1430
1806
|
}
|
|
1431
|
-
if (needsRerun) break;
|
|
1432
|
-
}
|
|
1433
|
-
if (needsRerun) {
|
|
1434
|
-
this._rerunCount += 1;
|
|
1435
|
-
if (this._rerunCount > MAX_RERUN) {
|
|
1436
|
-
this._bufferedDepMessages = [];
|
|
1437
|
-
this._downInternal([
|
|
1438
|
-
[
|
|
1439
|
-
ERROR,
|
|
1440
|
-
new Error(
|
|
1441
|
-
`dynamicNode "${this.name ?? "anonymous"}": rewire did not stabilize within ${MAX_RERUN} iterations`
|
|
1442
|
-
)
|
|
1443
|
-
]
|
|
1444
|
-
]);
|
|
1445
|
-
return;
|
|
1446
|
-
}
|
|
1447
|
-
this._bufferedDepMessages = [];
|
|
1448
|
-
continue;
|
|
1449
1807
|
}
|
|
1450
|
-
const drain = this._bufferedDepMessages;
|
|
1451
|
-
this._bufferedDepMessages = [];
|
|
1452
|
-
for (const entry of drain) {
|
|
1453
|
-
for (const msg of entry.msgs) {
|
|
1454
|
-
this._updateMasksForMessage(entry.index, msg);
|
|
1455
|
-
}
|
|
1456
|
-
}
|
|
1457
|
-
this._depDirtyBits.clear();
|
|
1458
|
-
this._depSettledBits.clear();
|
|
1459
|
-
break;
|
|
1460
1808
|
}
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
}
|
|
1466
|
-
_rewire(newDeps) {
|
|
1467
|
-
const oldMap = this._depIndexMap;
|
|
1468
|
-
const newMap = /* @__PURE__ */ new Map();
|
|
1469
|
-
const newUnsubs = [];
|
|
1470
|
-
for (let i = 0; i < newDeps.length; i++) {
|
|
1471
|
-
const dep = newDeps[i];
|
|
1472
|
-
newMap.set(dep, i);
|
|
1473
|
-
const oldIdx = oldMap.get(dep);
|
|
1474
|
-
if (oldIdx !== void 0) {
|
|
1475
|
-
newUnsubs.push(this._depUnsubs[oldIdx]);
|
|
1476
|
-
this._depUnsubs[oldIdx] = () => {
|
|
1477
|
-
};
|
|
1478
|
-
} else {
|
|
1479
|
-
const idx = i;
|
|
1480
|
-
const unsub = dep.subscribe((msgs) => this._handleDepMessages(idx, msgs));
|
|
1481
|
-
newUnsubs.push(unsub);
|
|
1809
|
+
if (!forward) {
|
|
1810
|
+
if (filtered == null) filtered = deliverable.slice(0, i);
|
|
1811
|
+
} else if (filtered != null) {
|
|
1812
|
+
filtered.push(m);
|
|
1482
1813
|
}
|
|
1483
1814
|
}
|
|
1484
|
-
|
|
1485
|
-
if (
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1815
|
+
if (filtered != null) {
|
|
1816
|
+
if (filtered.length === 0) return;
|
|
1817
|
+
deliverable = filtered;
|
|
1488
1818
|
}
|
|
1489
|
-
this.
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
const newCompleteBits = /* @__PURE__ */ new Set();
|
|
1495
|
-
for (const oldIdx of this._depCompleteBits) {
|
|
1496
|
-
for (const [dep, idx] of oldMap) {
|
|
1497
|
-
if (idx === oldIdx && newMap.has(dep)) {
|
|
1498
|
-
newCompleteBits.add(newMap.get(dep));
|
|
1499
|
-
break;
|
|
1819
|
+
if (this._hasMeta && deliverable.some((m) => m[0] === TEARDOWN)) {
|
|
1820
|
+
for (const k of Object.keys(this.meta)) {
|
|
1821
|
+
try {
|
|
1822
|
+
this.meta[k]._emit(TEARDOWN_ONLY_BATCH);
|
|
1823
|
+
} catch {
|
|
1500
1824
|
}
|
|
1501
1825
|
}
|
|
1502
1826
|
}
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
if (this._rewiring) {
|
|
1508
|
-
this._bufferedDepMessages.push({ index, msgs: messages });
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
for (const msg of messages) {
|
|
1512
|
-
this._emitInspectorHook({ kind: "dep_message", depIndex: index, message: msg });
|
|
1513
|
-
const t = msg[0];
|
|
1514
|
-
if (this._onMessage) {
|
|
1827
|
+
const { finalMessages, equalsError } = this._updateState(deliverable);
|
|
1828
|
+
if (finalMessages.length > 0 && this._config.inspectorEnabled) {
|
|
1829
|
+
const inspector = this._config.globalInspector;
|
|
1830
|
+
if (inspector != null) {
|
|
1515
1831
|
try {
|
|
1516
|
-
|
|
1517
|
-
} catch
|
|
1518
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1519
|
-
const wrapped = new Error(`Node "${this.name}": onMessage threw: ${errMsg}`, {
|
|
1520
|
-
cause: err
|
|
1521
|
-
});
|
|
1522
|
-
this._downInternal([[ERROR, wrapped]]);
|
|
1523
|
-
return;
|
|
1832
|
+
inspector({ kind: "emit", node: this, messages: finalMessages });
|
|
1833
|
+
} catch {
|
|
1524
1834
|
}
|
|
1525
1835
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
this.
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1836
|
+
}
|
|
1837
|
+
if (finalMessages.length > 0) {
|
|
1838
|
+
if (this._paused && this._pausable === "resumeAll" && this._pauseBuffer != null) {
|
|
1839
|
+
const tierOf = this._config.tierOf;
|
|
1840
|
+
const immediate = [];
|
|
1841
|
+
for (const m of finalMessages) {
|
|
1842
|
+
const tier = tierOf(m[0]);
|
|
1843
|
+
if (tier < 3 || tier === 5) {
|
|
1844
|
+
immediate.push(m);
|
|
1845
|
+
} else {
|
|
1846
|
+
this._pauseBuffer.push(m);
|
|
1847
|
+
}
|
|
1533
1848
|
}
|
|
1534
|
-
|
|
1849
|
+
if (immediate.length > 0) {
|
|
1850
|
+
this._dispatchOrAccumulate(immediate);
|
|
1851
|
+
}
|
|
1852
|
+
} else {
|
|
1853
|
+
this._dispatchOrAccumulate(finalMessages);
|
|
1535
1854
|
}
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1855
|
+
}
|
|
1856
|
+
if (equalsError != null) {
|
|
1857
|
+
this._emit([[ERROR, equalsError]]);
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1861
|
+
* @internal Walk an outgoing (already-framed) batch, updating own
|
|
1862
|
+
* cache / status / versioning and running equals substitution on
|
|
1863
|
+
* every tier-3 DATA (§3.5.1). Framing — tier sort and synthetic
|
|
1864
|
+
* DIRTY prefix — has already happened upstream in `_frameBatch`.
|
|
1865
|
+
* This walk trusts the input is in monotone tier order and that the
|
|
1866
|
+
* spec §1.3.1 DIRTY/RESOLVED precedence invariant is already
|
|
1867
|
+
* satisfied by the frame.
|
|
1868
|
+
*
|
|
1869
|
+
* Equals substitution: every DATA payload is compared against the
|
|
1870
|
+
* live `_cached`; when equal, the tuple is rewritten to `[RESOLVED]`
|
|
1871
|
+
* in a per-call copy and cache is not re-advanced. `.cache` remains
|
|
1872
|
+
* coherent with "the last DATA payload this node actually sent
|
|
1873
|
+
* downstream".
|
|
1874
|
+
*
|
|
1875
|
+
* Returns `{ finalMessages, equalsError? }`:
|
|
1876
|
+
* - `finalMessages` — the array to deliver to sinks (may be
|
|
1877
|
+
* `messages` unchanged, a rewritten copy with DATA→RESOLVED
|
|
1878
|
+
* substitutions, or a truncated prefix when equals throws mid-walk).
|
|
1879
|
+
* - `equalsError` — present only when the configured `equals` function
|
|
1880
|
+
* threw on some DATA message. `_emit` delivers the prefix first,
|
|
1881
|
+
* then emits a fresh ERROR batch via a recursive `_emit` call so
|
|
1882
|
+
* subscribers observe `[...walked_prefix, ERROR]` in order.
|
|
1883
|
+
*/
|
|
1884
|
+
_updateState(messages) {
|
|
1885
|
+
const tierOf = this._config.tierOf;
|
|
1886
|
+
let rewritten;
|
|
1887
|
+
let equalsError;
|
|
1888
|
+
let abortedAt = -1;
|
|
1889
|
+
let dataCount = 0;
|
|
1890
|
+
for (const m of messages) {
|
|
1891
|
+
if (tierOf(m[0]) === 3) dataCount++;
|
|
1892
|
+
}
|
|
1893
|
+
const checkEquals = dataCount <= 1;
|
|
1894
|
+
let lastDataIdx = -1;
|
|
1895
|
+
if (this._versioning != null && dataCount > 1) {
|
|
1896
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1897
|
+
if (messages[i][0] === DATA) {
|
|
1898
|
+
lastDataIdx = i;
|
|
1899
|
+
break;
|
|
1543
1900
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1904
|
+
const m = messages[i];
|
|
1905
|
+
const t = m[0];
|
|
1906
|
+
if (t === DATA) {
|
|
1907
|
+
if (m.length >= 2) {
|
|
1908
|
+
let unchanged = false;
|
|
1909
|
+
if (checkEquals && this._cached !== void 0) {
|
|
1910
|
+
try {
|
|
1911
|
+
unchanged = this._equals(this._cached, m[1]);
|
|
1912
|
+
} catch (err) {
|
|
1913
|
+
equalsError = this._wrapFnError("equals threw", err);
|
|
1914
|
+
abortedAt = i;
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
if (unchanged) {
|
|
1919
|
+
if (rewritten == null) rewritten = messages.slice(0, i);
|
|
1920
|
+
rewritten.push(RESOLVED_MSG);
|
|
1921
|
+
this._status = "resolved";
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
this._cached = m[1];
|
|
1925
|
+
if (this._versioning != null) {
|
|
1926
|
+
if (lastDataIdx < 0 || i === lastDataIdx) {
|
|
1927
|
+
advanceVersion(this._versioning, m[1], this._hashFn);
|
|
1551
1928
|
}
|
|
1552
1929
|
}
|
|
1553
1930
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
if (
|
|
1561
|
-
this.
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1931
|
+
this._status = "settled";
|
|
1932
|
+
if (rewritten != null) rewritten.push(m);
|
|
1933
|
+
} else {
|
|
1934
|
+
if (rewritten != null) rewritten.push(m);
|
|
1935
|
+
if (t === DIRTY) {
|
|
1936
|
+
this._status = "dirty";
|
|
1937
|
+
} else if (t === RESOLVED) {
|
|
1938
|
+
this._status = "resolved";
|
|
1939
|
+
} else if (t === COMPLETE) {
|
|
1940
|
+
this._status = "completed";
|
|
1941
|
+
} else if (t === ERROR) {
|
|
1942
|
+
this._status = "errored";
|
|
1943
|
+
} else if (t === INVALIDATE) {
|
|
1944
|
+
this._cached = void 0;
|
|
1945
|
+
this._status = "dirty";
|
|
1946
|
+
const c = this._cleanup;
|
|
1947
|
+
if (typeof c === "function") {
|
|
1948
|
+
this._cleanup = void 0;
|
|
1949
|
+
try {
|
|
1950
|
+
c();
|
|
1951
|
+
} catch {
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
} else if (t === TEARDOWN) {
|
|
1955
|
+
if (this._resetOnTeardown) this._cached = void 0;
|
|
1956
|
+
this._deactivate(
|
|
1957
|
+
/* skipStatusUpdate */
|
|
1958
|
+
true
|
|
1959
|
+
);
|
|
1960
|
+
this._status = "sentinel";
|
|
1567
1961
|
}
|
|
1568
|
-
continue;
|
|
1569
1962
|
}
|
|
1570
|
-
if (t === ERROR) {
|
|
1571
|
-
this._downInternal([msg]);
|
|
1572
|
-
continue;
|
|
1573
|
-
}
|
|
1574
|
-
if (t === INVALIDATE || t === TEARDOWN || t === PAUSE || t === RESUME) {
|
|
1575
|
-
this._downInternal([msg]);
|
|
1576
|
-
continue;
|
|
1577
|
-
}
|
|
1578
|
-
this._downInternal([msg]);
|
|
1579
1963
|
}
|
|
1964
|
+
const base = abortedAt >= 0 ? rewritten ?? messages.slice(0, abortedAt) : rewritten ?? messages;
|
|
1965
|
+
return equalsError != null ? { finalMessages: base, equalsError } : { finalMessages: base };
|
|
1580
1966
|
}
|
|
1967
|
+
_deliverToSinks = (messages) => {
|
|
1968
|
+
if (this._sinks == null) return;
|
|
1969
|
+
if (typeof this._sinks === "function") {
|
|
1970
|
+
this._sinks(messages);
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
const snapshot = [...this._sinks];
|
|
1974
|
+
for (const sink of snapshot) sink(messages);
|
|
1975
|
+
};
|
|
1581
1976
|
/**
|
|
1582
|
-
*
|
|
1583
|
-
*
|
|
1584
|
-
*
|
|
1977
|
+
* @internal Dispatch entry point that respects the per-batch emit
|
|
1978
|
+
* accumulator (Bug 2). Inside an explicit `batch()` scope, append to
|
|
1979
|
+
* `_batchPendingMessages` and register a flush hook on first append.
|
|
1980
|
+
* Outside batch — or during a drain (where `flushInProgress` is true
|
|
1981
|
+
* but `batchDepth` is 0) — dispatch synchronously through `downWithBatch`.
|
|
1982
|
+
*
|
|
1983
|
+
* Per-emit state updates (`_frameBatch`, `_updateState`) have already
|
|
1984
|
+
* happened by the time we reach here; only the **downstream delivery**
|
|
1985
|
+
* is coalesced. Cache, version, and status are visible mid-batch on
|
|
1986
|
+
* the emitting node itself.
|
|
1585
1987
|
*/
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
this.
|
|
1593
|
-
|
|
1594
|
-
} else if (t === COMPLETE) {
|
|
1595
|
-
this._depCompleteBits.add(index);
|
|
1596
|
-
this._depDirtyBits.delete(index);
|
|
1597
|
-
this._depSettledBits.delete(index);
|
|
1598
|
-
}
|
|
1599
|
-
}
|
|
1600
|
-
_allDirtySettled() {
|
|
1601
|
-
if (this._depDirtyBits.size === 0) return false;
|
|
1602
|
-
for (const idx of this._depDirtyBits) {
|
|
1603
|
-
if (!this._depSettledBits.has(idx)) return false;
|
|
1988
|
+
_dispatchOrAccumulate(messages) {
|
|
1989
|
+
if (isExplicitlyBatching()) {
|
|
1990
|
+
if (this._batchPendingMessages === null) {
|
|
1991
|
+
this._batchPendingMessages = [];
|
|
1992
|
+
registerBatchFlushHook(() => this._flushBatchPending());
|
|
1993
|
+
}
|
|
1994
|
+
for (const m of messages) this._batchPendingMessages.push(m);
|
|
1995
|
+
return;
|
|
1604
1996
|
}
|
|
1605
|
-
|
|
1997
|
+
downWithBatch(this._deliverToSinks, messages, this._config.tierOf);
|
|
1606
1998
|
}
|
|
1607
1999
|
/**
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1610
|
-
*
|
|
1611
|
-
*
|
|
2000
|
+
* @internal Flushes the accumulated batch through `downWithBatch` and
|
|
2001
|
+
* clears the pending state. Idempotent — safe to call when pending is
|
|
2002
|
+
* already null or empty (e.g. on a `batch()` throw, where the hook
|
|
2003
|
+
* fires for cleanup but the drainPhase queues are wiped after).
|
|
2004
|
+
*
|
|
2005
|
+
* Critical: the accumulated batch is interleaved per-emit framings like
|
|
2006
|
+
* `[DIRTY, DATA(1), DIRTY, DATA(2)]` — non-monotone tier order. We must
|
|
2007
|
+
* re-frame to sort by tier before handing to `downWithBatch`, which
|
|
2008
|
+
* assumes pre-sorted input. `_frameBatch` also handles the synthetic
|
|
2009
|
+
* DIRTY prepend rule (no-op here — `hasDirty` is true since each
|
|
2010
|
+
* accumulated emit already carries its own DIRTY prefix).
|
|
1612
2011
|
*/
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
2012
|
+
_flushBatchPending() {
|
|
2013
|
+
const pending = this._batchPendingMessages;
|
|
2014
|
+
if (pending === null) return;
|
|
2015
|
+
this._batchPendingMessages = null;
|
|
2016
|
+
if (pending.length === 0) return;
|
|
2017
|
+
const framed = this._frameBatch(pending);
|
|
2018
|
+
downWithBatch(this._deliverToSinks, framed, this._config.tierOf);
|
|
1620
2019
|
}
|
|
1621
2020
|
};
|
|
2021
|
+
var isNodeArray = (value) => Array.isArray(value);
|
|
2022
|
+
var isNodeOptionsObject = (value) => typeof value === "object" && value != null && !Array.isArray(value);
|
|
2023
|
+
function node(depsOrFn, fnOrOpts, optsArg) {
|
|
2024
|
+
const deps = isNodeArray(depsOrFn) ? depsOrFn : [];
|
|
2025
|
+
const fn = typeof depsOrFn === "function" ? depsOrFn : typeof fnOrOpts === "function" ? fnOrOpts : void 0;
|
|
2026
|
+
let opts = {};
|
|
2027
|
+
if (isNodeArray(depsOrFn)) {
|
|
2028
|
+
opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
2029
|
+
} else if (isNodeOptionsObject(depsOrFn)) {
|
|
2030
|
+
opts = depsOrFn;
|
|
2031
|
+
} else {
|
|
2032
|
+
opts = (isNodeOptionsObject(fnOrOpts) ? fnOrOpts : optsArg) ?? {};
|
|
2033
|
+
}
|
|
2034
|
+
return new NodeImpl(deps, fn, opts);
|
|
2035
|
+
}
|
|
1622
2036
|
|
|
1623
2037
|
// src/core/meta.ts
|
|
1624
2038
|
function resolveDescribeFields(detail, fields) {
|
|
@@ -1628,106 +2042,181 @@ function resolveDescribeFields(detail, fields) {
|
|
|
1628
2042
|
return /* @__PURE__ */ new Set(["type", "status", "value", "deps", "meta", "v"]);
|
|
1629
2043
|
case "full":
|
|
1630
2044
|
return null;
|
|
1631
|
-
// null = include everything
|
|
1632
2045
|
default:
|
|
1633
2046
|
return /* @__PURE__ */ new Set(["type", "deps"]);
|
|
1634
2047
|
}
|
|
1635
2048
|
}
|
|
1636
2049
|
|
|
1637
2050
|
// src/core/sugar.ts
|
|
2051
|
+
function sentinelGuard(batchData, ctx, allowPartial) {
|
|
2052
|
+
if (allowPartial) return false;
|
|
2053
|
+
return batchData.some(
|
|
2054
|
+
(batch2, i) => !(batch2 != null && batch2.length > 0) && ctx.prevData[i] === void 0
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
1638
2057
|
function state(initial, opts) {
|
|
1639
2058
|
return node([], { ...opts, initial });
|
|
1640
2059
|
}
|
|
1641
2060
|
function producer(fn, opts) {
|
|
1642
|
-
|
|
2061
|
+
const wrapped = (_data, actions, ctx) => fn(actions, ctx) ?? void 0;
|
|
2062
|
+
return node(wrapped, { describeKind: "producer", ...opts });
|
|
1643
2063
|
}
|
|
1644
2064
|
function derived(deps, fn, opts) {
|
|
1645
|
-
|
|
2065
|
+
const allowPartial = opts?.partial ?? false;
|
|
2066
|
+
const wrapped = (batchData, actions, ctx) => {
|
|
2067
|
+
if (sentinelGuard(batchData, ctx, allowPartial)) {
|
|
2068
|
+
actions.down([[RESOLVED]]);
|
|
2069
|
+
return void 0;
|
|
2070
|
+
}
|
|
2071
|
+
const data = batchData.map(
|
|
2072
|
+
(batch2, i) => batch2 != null && batch2.length > 0 ? batch2.at(-1) : ctx.prevData[i]
|
|
2073
|
+
);
|
|
2074
|
+
actions.emit(fn(data, ctx));
|
|
2075
|
+
return void 0;
|
|
2076
|
+
};
|
|
2077
|
+
return node(deps, wrapped, { describeKind: "derived", ...opts });
|
|
1646
2078
|
}
|
|
1647
2079
|
function effect(deps, fn, opts) {
|
|
1648
|
-
|
|
2080
|
+
const allowPartial = opts?.partial ?? false;
|
|
2081
|
+
const wrapped = (batchData, actions, ctx) => {
|
|
2082
|
+
if (sentinelGuard(batchData, ctx, allowPartial)) {
|
|
2083
|
+
actions.down([[RESOLVED]]);
|
|
2084
|
+
return void 0;
|
|
2085
|
+
}
|
|
2086
|
+
const data = batchData.map(
|
|
2087
|
+
(batch2, i) => batch2 != null && batch2.length > 0 ? batch2.at(-1) : ctx.prevData[i]
|
|
2088
|
+
);
|
|
2089
|
+
return fn(data, actions, ctx) ?? void 0;
|
|
2090
|
+
};
|
|
2091
|
+
return node(deps, wrapped, { describeKind: "effect", ...opts });
|
|
2092
|
+
}
|
|
2093
|
+
function dynamicNode(allDeps, fn, opts) {
|
|
2094
|
+
const depIndex = /* @__PURE__ */ new Map();
|
|
2095
|
+
allDeps.forEach((d, i) => {
|
|
2096
|
+
depIndex.set(d, i);
|
|
2097
|
+
});
|
|
2098
|
+
return derived(
|
|
2099
|
+
allDeps,
|
|
2100
|
+
// data[i] is already sugar-unwrapped to a scalar by derived()'s wrapper.
|
|
2101
|
+
(data, ctx) => {
|
|
2102
|
+
const track = (dep) => {
|
|
2103
|
+
const i = depIndex.get(dep);
|
|
2104
|
+
if (i == null) {
|
|
2105
|
+
throw new Error(`dynamicNode: untracked dep "${dep.name ?? "<unnamed>"}"`);
|
|
2106
|
+
}
|
|
2107
|
+
return data[i];
|
|
2108
|
+
};
|
|
2109
|
+
return fn(track, ctx);
|
|
2110
|
+
},
|
|
2111
|
+
opts
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
function autoTrackNode(fn, opts) {
|
|
2115
|
+
let implRef;
|
|
2116
|
+
const depIndexMap = /* @__PURE__ */ new Map();
|
|
2117
|
+
const allowPartial = opts?.partial ?? false;
|
|
2118
|
+
const wrappedFn = (batchData, actions, ctx) => {
|
|
2119
|
+
let foundNew = false;
|
|
2120
|
+
const track = (dep) => {
|
|
2121
|
+
const idx = depIndexMap.get(dep);
|
|
2122
|
+
if (idx !== void 0) {
|
|
2123
|
+
if (idx < batchData.length) {
|
|
2124
|
+
const batch2 = batchData[idx];
|
|
2125
|
+
if (batch2 != null && batch2.length > 0) return batch2.at(-1);
|
|
2126
|
+
return ctx.prevData[idx];
|
|
2127
|
+
}
|
|
2128
|
+
return dep.cache;
|
|
2129
|
+
}
|
|
2130
|
+
foundNew = true;
|
|
2131
|
+
const newIdx = implRef._addDep(dep);
|
|
2132
|
+
depIndexMap.set(dep, newIdx);
|
|
2133
|
+
return dep.cache;
|
|
2134
|
+
};
|
|
2135
|
+
if (!allowPartial && depIndexMap.size > 0) {
|
|
2136
|
+
for (const [, idx] of depIndexMap) {
|
|
2137
|
+
if (idx < batchData.length) {
|
|
2138
|
+
const batch2 = batchData[idx];
|
|
2139
|
+
if (!(batch2 != null && batch2.length > 0) && ctx.prevData[idx] === void 0) {
|
|
2140
|
+
actions.down([[RESOLVED]]);
|
|
2141
|
+
return void 0;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
try {
|
|
2147
|
+
const result = fn(track, ctx);
|
|
2148
|
+
if (!foundNew) {
|
|
2149
|
+
actions.emit(result);
|
|
2150
|
+
if (ctx.store.__autoTrackLastDiscoveryError != null) {
|
|
2151
|
+
delete ctx.store.__autoTrackLastDiscoveryError;
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
if (!foundNew) throw err;
|
|
2156
|
+
ctx.store.__autoTrackLastDiscoveryError = err;
|
|
2157
|
+
}
|
|
2158
|
+
return void 0;
|
|
2159
|
+
};
|
|
2160
|
+
implRef = new NodeImpl([], wrappedFn, {
|
|
2161
|
+
describeKind: "derived",
|
|
2162
|
+
...opts
|
|
2163
|
+
});
|
|
2164
|
+
return implRef;
|
|
1649
2165
|
}
|
|
1650
2166
|
function pipe(source, ...ops) {
|
|
1651
2167
|
let current = source;
|
|
1652
|
-
for (const op of ops)
|
|
1653
|
-
current = op(current);
|
|
1654
|
-
}
|
|
2168
|
+
for (const op of ops) current = op(current);
|
|
1655
2169
|
return current;
|
|
1656
2170
|
}
|
|
1657
|
-
|
|
1658
|
-
// src/core/timer.ts
|
|
1659
|
-
var ResettableTimer = class {
|
|
1660
|
-
_timer;
|
|
1661
|
-
_gen = 0;
|
|
1662
|
-
/** Schedule callback after delayMs. Cancels any pending timer. */
|
|
1663
|
-
start(delayMs, callback) {
|
|
1664
|
-
this.cancel();
|
|
1665
|
-
this._gen += 1;
|
|
1666
|
-
const gen = this._gen;
|
|
1667
|
-
this._timer = setTimeout(() => {
|
|
1668
|
-
this._timer = void 0;
|
|
1669
|
-
if (gen !== this._gen) return;
|
|
1670
|
-
callback();
|
|
1671
|
-
}, delayMs);
|
|
1672
|
-
}
|
|
1673
|
-
/** Cancel the pending timer (if any). */
|
|
1674
|
-
cancel() {
|
|
1675
|
-
if (this._timer !== void 0) {
|
|
1676
|
-
clearTimeout(this._timer);
|
|
1677
|
-
this._timer = void 0;
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
/** Whether a timer is currently pending. */
|
|
1681
|
-
get pending() {
|
|
1682
|
-
return this._timer !== void 0;
|
|
1683
|
-
}
|
|
1684
|
-
};
|
|
1685
2171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1686
2172
|
0 && (module.exports = {
|
|
1687
|
-
CLEANUP_RESULT,
|
|
1688
2173
|
COMPLETE,
|
|
2174
|
+
COMPLETE_MSG,
|
|
2175
|
+
COMPLETE_ONLY_BATCH,
|
|
1689
2176
|
DATA,
|
|
1690
2177
|
DEFAULT_ACTOR,
|
|
1691
|
-
DEFAULT_DOWN,
|
|
1692
2178
|
DIRTY,
|
|
1693
|
-
|
|
2179
|
+
DIRTY_MSG,
|
|
2180
|
+
DIRTY_ONLY_BATCH,
|
|
1694
2181
|
ERROR,
|
|
2182
|
+
GraphReFlyConfig,
|
|
1695
2183
|
GuardDenied,
|
|
1696
2184
|
INVALIDATE,
|
|
2185
|
+
INVALIDATE_MSG,
|
|
2186
|
+
INVALIDATE_ONLY_BATCH,
|
|
2187
|
+
NodeImpl,
|
|
1697
2188
|
PAUSE,
|
|
1698
2189
|
RESOLVED,
|
|
2190
|
+
RESOLVED_MSG,
|
|
2191
|
+
RESOLVED_ONLY_BATCH,
|
|
1699
2192
|
RESUME,
|
|
1700
|
-
ResettableTimer,
|
|
1701
2193
|
START,
|
|
2194
|
+
START_MSG,
|
|
1702
2195
|
TEARDOWN,
|
|
2196
|
+
TEARDOWN_MSG,
|
|
2197
|
+
TEARDOWN_ONLY_BATCH,
|
|
1703
2198
|
accessHintForGuard,
|
|
1704
2199
|
advanceVersion,
|
|
2200
|
+
autoTrackNode,
|
|
1705
2201
|
batch,
|
|
1706
|
-
|
|
1707
|
-
cleanupResult,
|
|
2202
|
+
configure,
|
|
1708
2203
|
createVersioning,
|
|
2204
|
+
defaultConfig,
|
|
1709
2205
|
defaultHash,
|
|
1710
2206
|
derived,
|
|
1711
2207
|
downWithBatch,
|
|
1712
2208
|
dynamicNode,
|
|
1713
2209
|
effect,
|
|
1714
2210
|
isBatching,
|
|
1715
|
-
isKnownMessageType,
|
|
1716
|
-
isLocalOnly,
|
|
1717
|
-
isPhase2Message,
|
|
1718
|
-
isTerminalMessage,
|
|
1719
2211
|
isV1,
|
|
1720
|
-
knownMessageTypes,
|
|
1721
|
-
messageTier,
|
|
1722
2212
|
monotonicNs,
|
|
1723
2213
|
node,
|
|
1724
2214
|
normalizeActor,
|
|
1725
|
-
partitionForBatch,
|
|
1726
2215
|
pipe,
|
|
1727
2216
|
policy,
|
|
1728
2217
|
policyFromRules,
|
|
1729
2218
|
producer,
|
|
1730
|
-
|
|
2219
|
+
registerBuiltins,
|
|
1731
2220
|
resolveDescribeFields,
|
|
1732
2221
|
state,
|
|
1733
2222
|
wallClockNs
|