@markless/runtime 0.3.1 → 0.3.2

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.
@@ -0,0 +1,214 @@
1
+ import { ProtocolStatePayload } from "@markless/serializer";
2
+
3
+ //#region src/graph-reconcile.d.ts
4
+ /**
5
+ * One array inside a derived value that reconciles by key instead of by
6
+ * element identity. `path` names the array (`[]` is the derived value itself)
7
+ * and `keyPath` is read on each element to get its identity.
8
+ */
9
+ type RuntimeGraphReconcileKey = {
10
+ readonly path: ReadonlyArray<string>;
11
+ readonly keyPath: ReadonlyArray<string>;
12
+ };
13
+ /** Reconcile options carried by a computed or async computed graph node. */
14
+ type RuntimeGraphReconcileOptions = {
15
+ readonly keyed?: ReadonlyArray<RuntimeGraphReconcileKey>;
16
+ };
17
+ /**
18
+ * Objects a state write touched during the current flush, mapped to the paths
19
+ * written beneath them. State writes mutate objects in place, so a derived
20
+ * value that still holds the same object reference is not unchanged when that
21
+ * object was written through. See `diffDerivedValue`.
22
+ */
23
+ type WriteTouchedRecord = ReadonlyMap<object, ReadonlyArray<ReadonlyArray<string>>>;
24
+ type DiffDerivedValueInput = {
25
+ readonly previous: unknown;
26
+ readonly next: unknown;
27
+ readonly keyed?: ReadonlyArray<RuntimeGraphReconcileKey>;
28
+ readonly touched?: WriteTouchedRecord;
29
+ readonly baselineStale?: boolean;
30
+ /**
31
+ * What an `Object.is`-identical plain object or array below the root means.
32
+ * `'unchanged'` (the default) trusts identity, which is sound for a caller
33
+ * that also supplies the write-touched record of the same flush.
34
+ * `'unknown'` is for a caller with no such record — the async commit — and
35
+ * reports every identical container at its own path instead.
36
+ */
37
+ readonly identicalContainers?: 'unchanged' | 'unknown';
38
+ };
39
+ /**
40
+ * Compares a derived value with the previous value of the same graph node and
41
+ * reports the graph paths that changed. It never mutates either value: the
42
+ * caller replaces the node's value pointer and invalidates the reported paths.
43
+ *
44
+ * Rules (specs/framework/03-state-graph.md "Derived reconciliation"):
45
+ * - identical references are unchanged, unless a write touched that object in
46
+ * this flush, in which case the touched remainders are reported; the same
47
+ * holds when only the previous value held the written object, because an
48
+ * in-place write left that baseline already carrying the new field;
49
+ * - primitives, functions, `Map`, `Set`, `Date` and class instances are leaves;
50
+ * - plain objects reconcile field by field;
51
+ * - arrays reconcile by declared key, otherwise by element identity, and a
52
+ * length or structural mismatch reports the array's own path;
53
+ * - a declared key that is missing or duplicated on either side falls back to
54
+ * structural reconciliation for that array, which reports the array's own
55
+ * path: keys are never matched partially and an index is never identity;
56
+ * - a missing baseline (first value, or a value whose write-touched record was
57
+ * already cleared) reports the whole node.
58
+ *
59
+ * `identicalContainers: 'unknown'` is for a caller that has no write-touched
60
+ * record for the flush that produced the new value, so identity below the root
61
+ * proves nothing: every `Object.is`-identical plain object or array met below
62
+ * the root is reported at its own path and is not walked into. An identical
63
+ * root still reports nothing — nothing narrower is known about it, and the
64
+ * caller decides what a wholly identical value means — and identical
65
+ * primitives and other leaves stay unchanged. The default, `'unchanged'`,
66
+ * trusts identity as above.
67
+ */
68
+ declare function diffDerivedValue(input: DiffDerivedValueInput): ReadonlyArray<ReadonlyArray<string>>;
69
+ //#endregion
70
+ //#region src/graph.d.ts
71
+ type RuntimeGraphCell = {
72
+ readonly graphNodeId: string;
73
+ readonly value: unknown;
74
+ readonly readInitializer?: () => unknown;
75
+ };
76
+ type RuntimeGraphRead = (graphNodeId: string, path?: ReadonlyArray<string>) => unknown;
77
+ type RuntimeGraphComputedDependency = {
78
+ readonly graphNodeId: string;
79
+ readonly path?: ReadonlyArray<string>;
80
+ };
81
+ type RuntimeGraphComputed = {
82
+ readonly graphNodeId: string;
83
+ readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
84
+ readonly compute: (read: RuntimeGraphRead) => unknown; /** How re-derived values reconcile against the node's previous value. */
85
+ readonly reconcile?: RuntimeGraphReconcileOptions;
86
+ };
87
+ type RuntimeGraphComputedDependencyNode = Omit<RuntimeGraphComputed, 'compute'> & {
88
+ readonly compute?: never;
89
+ };
90
+ type RuntimeGraphAsyncSnapshot = {
91
+ readonly status: 'idle';
92
+ readonly version: 0;
93
+ } | {
94
+ readonly status: 'pending';
95
+ readonly version: number;
96
+ readonly key: unknown;
97
+ readonly value?: unknown;
98
+ } | {
99
+ readonly status: 'fulfilled';
100
+ readonly version: number;
101
+ readonly key: unknown;
102
+ readonly value: unknown;
103
+ } | {
104
+ readonly status: 'rejected';
105
+ readonly version: number;
106
+ readonly key: unknown;
107
+ readonly error: unknown;
108
+ };
109
+ type RuntimeGraphAsyncComputed = {
110
+ readonly graphNodeId: string;
111
+ readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
112
+ readonly initialSnapshot?: RuntimeGraphAsyncSnapshot;
113
+ readonly key: (read: RuntimeGraphRead) => unknown; /** How a fulfilled value reconciles against the previously published one. */
114
+ readonly reconcile?: RuntimeGraphReconcileOptions;
115
+ readonly run: (input: {
116
+ readonly key: unknown;
117
+ readonly signal: AbortSignal;
118
+ readonly read: RuntimeGraphRead;
119
+ }) => unknown | Promise<unknown>;
120
+ };
121
+ type DomJournalEntry = {
122
+ readonly type: 'setText';
123
+ readonly locator: string;
124
+ readonly value: unknown;
125
+ } | {
126
+ readonly type: 'setAttr' | 'setProp';
127
+ readonly locator: string;
128
+ readonly name: string;
129
+ readonly value: unknown;
130
+ } | {
131
+ readonly type: 'insertRange';
132
+ readonly locator: string;
133
+ readonly fragment: unknown;
134
+ } | {
135
+ readonly type: 'removeRange';
136
+ readonly locator: string;
137
+ } | {
138
+ readonly type: 'moveRange';
139
+ readonly locator: string;
140
+ readonly before: string;
141
+ } | {
142
+ readonly type: 'runCleanup';
143
+ readonly locator: string;
144
+ };
145
+ type DomJournalResult = DomJournalEntry | ReadonlyArray<DomJournalEntry>;
146
+ type DomJournalListener = (entries: ReadonlyArray<DomJournalEntry>) => void | Promise<void>;
147
+ type RuntimeGraphInput = {
148
+ readonly cells: ReadonlyArray<RuntimeGraphCell>;
149
+ readonly computed?: ReadonlyArray<RuntimeGraphComputed | RuntimeGraphComputedDependencyNode>;
150
+ readonly asyncComputed?: ReadonlyArray<RuntimeGraphAsyncComputed>;
151
+ readonly sharedDefinitions?: ProtocolStatePayload['sharedDefinitions'];
152
+ };
153
+ type RuntimeGraphWrite = {
154
+ readonly graphNodeId: string;
155
+ readonly path?: ReadonlyArray<string>;
156
+ readonly value: unknown;
157
+ };
158
+ type RuntimeGraphSharedWrite = {
159
+ readonly definitionId: string;
160
+ readonly propertyName: string;
161
+ readonly path?: ReadonlyArray<string>;
162
+ readonly value: unknown;
163
+ };
164
+ type RuntimeGraphSharedPatchOperation = readonly [operation: 'set', path: ReadonlyArray<string>, value: unknown];
165
+ type RuntimeGraphSharedPatch = {
166
+ readonly id: string;
167
+ readonly scope?: RuntimeSharedDefinition['scope'];
168
+ readonly version: number;
169
+ readonly patch: ReadonlyArray<RuntimeGraphSharedPatchOperation>;
170
+ };
171
+ type RuntimeGraphUpdate = {
172
+ readonly graphNodeId: string;
173
+ readonly path?: ReadonlyArray<string>;
174
+ readonly update: (value: unknown) => unknown;
175
+ readonly returnValue?: 'previous' | 'next';
176
+ };
177
+ type RuntimeGraphCall = {
178
+ readonly graphNodeId: string;
179
+ readonly path?: ReadonlyArray<string>;
180
+ readonly method: string;
181
+ readonly args?: ReadonlyArray<unknown>;
182
+ };
183
+ type RuntimeGraphDelete = {
184
+ readonly graphNodeId: string;
185
+ readonly path: ReadonlyArray<string>;
186
+ };
187
+ type RuntimeGraphSubscription = {
188
+ readonly id: string;
189
+ readonly graphNodeId: string;
190
+ readonly path?: ReadonlyArray<string>;
191
+ readonly run: (value: unknown) => DomJournalResult | void | Promise<DomJournalResult | void>;
192
+ };
193
+ type RuntimeGraph = {
194
+ readonly read: (graphNodeId: string, path?: ReadonlyArray<string>) => unknown; /** Available only in debug-enabled builds (`__MARKLESS_DEBUG_ENABLED__`). */
195
+ readonly peekAsyncSnapshot?: (graphNodeId: string) => RuntimeGraphAsyncSnapshot | undefined;
196
+ readonly readShared: (definitionId: string, propertyName: string, path?: ReadonlyArray<string>) => unknown;
197
+ readonly writeShared: (write: RuntimeGraphSharedWrite) => boolean;
198
+ readonly getSharedDefinition: (definitionId: string) => NonNullable<ProtocolStatePayload['sharedDefinitions']>[number] | undefined;
199
+ readonly listSharedDefinitions: () => NonNullable<ProtocolStatePayload['sharedDefinitions']>;
200
+ readonly takeSharedPatches: () => RuntimeGraphSharedPatch[];
201
+ readonly applySharedPatch: (patch: RuntimeGraphSharedPatch) => boolean;
202
+ readonly write: (write: RuntimeGraphWrite) => void;
203
+ readonly update: (update: RuntimeGraphUpdate) => unknown;
204
+ readonly call: (call: RuntimeGraphCall) => unknown;
205
+ readonly delete: (deletion: RuntimeGraphDelete) => boolean;
206
+ readonly subscribe: (subscription: RuntimeGraphSubscription) => () => void;
207
+ readonly subscribeJournal: (listener: DomJournalListener) => () => void;
208
+ readonly flush: () => Promise<void>;
209
+ readonly takeJournal: () => DomJournalEntry[];
210
+ };
211
+ type RuntimeSharedDefinition = NonNullable<ProtocolStatePayload['sharedDefinitions']>[number];
212
+ declare function createRuntimeGraph(input: RuntimeGraphInput): RuntimeGraph;
213
+ //#endregion
214
+ export { RuntimeGraphReconcileKey as C, diffDerivedValue as E, DiffDerivedValueInput as S, WriteTouchedRecord as T, RuntimeGraphSharedWrite as _, RuntimeGraphAsyncComputed as a, RuntimeGraphWrite as b, RuntimeGraphCell as c, RuntimeGraphComputedDependencyNode as d, RuntimeGraphDelete as f, RuntimeGraphSharedPatchOperation as g, RuntimeGraphSharedPatch as h, RuntimeGraph as i, RuntimeGraphComputed as l, RuntimeGraphRead as m, DomJournalListener as n, RuntimeGraphAsyncSnapshot as o, RuntimeGraphInput as p, DomJournalResult as r, RuntimeGraphCall as s, DomJournalEntry as t, RuntimeGraphComputedDependency as u, RuntimeGraphSubscription as v, RuntimeGraphReconcileOptions as w, createRuntimeGraph as x, RuntimeGraphUpdate as y };
@@ -144,6 +144,201 @@ function arraySlotsEqual(before, after) {
144
144
  });
145
145
  }
146
146
  //#endregion
147
+ //#region src/graph-reconcile.ts
148
+ const WHOLE_NODE = Object.freeze([Object.freeze([])]);
149
+ /**
150
+ * Compares a derived value with the previous value of the same graph node and
151
+ * reports the graph paths that changed. It never mutates either value: the
152
+ * caller replaces the node's value pointer and invalidates the reported paths.
153
+ *
154
+ * Rules (specs/framework/03-state-graph.md "Derived reconciliation"):
155
+ * - identical references are unchanged, unless a write touched that object in
156
+ * this flush, in which case the touched remainders are reported; the same
157
+ * holds when only the previous value held the written object, because an
158
+ * in-place write left that baseline already carrying the new field;
159
+ * - primitives, functions, `Map`, `Set`, `Date` and class instances are leaves;
160
+ * - plain objects reconcile field by field;
161
+ * - arrays reconcile by declared key, otherwise by element identity, and a
162
+ * length or structural mismatch reports the array's own path;
163
+ * - a declared key that is missing or duplicated on either side falls back to
164
+ * structural reconciliation for that array, which reports the array's own
165
+ * path: keys are never matched partially and an index is never identity;
166
+ * - a missing baseline (first value, or a value whose write-touched record was
167
+ * already cleared) reports the whole node.
168
+ *
169
+ * `identicalContainers: 'unknown'` is for a caller that has no write-touched
170
+ * record for the flush that produced the new value, so identity below the root
171
+ * proves nothing: every `Object.is`-identical plain object or array met below
172
+ * the root is reported at its own path and is not walked into. An identical
173
+ * root still reports nothing — nothing narrower is known about it, and the
174
+ * caller decides what a wholly identical value means — and identical
175
+ * primitives and other leaves stay unchanged. The default, `'unchanged'`,
176
+ * trusts identity as above.
177
+ */
178
+ function diffDerivedValue(input) {
179
+ if (input.baselineStale) return WHOLE_NODE;
180
+ if (input.previous === void 0 && input.next !== void 0) return WHOLE_NODE;
181
+ const changed = [];
182
+ const active = /* @__PURE__ */ new Map();
183
+ function emitTouched(value, path) {
184
+ if (!input.touched || typeof value !== "object" || value === null) return;
185
+ const remainders = input.touched.get(value);
186
+ if (!remainders) return;
187
+ for (const remaining of remainders) {
188
+ const touchedPath = [...path, ...remaining];
189
+ if (changed.some((entry) => samePath(entry, touchedPath))) continue;
190
+ changed.push(touchedPath);
191
+ }
192
+ }
193
+ /**
194
+ * What an identical reference met at `path` reports. With a write-touched
195
+ * record identity means unchanged apart from the recorded remainders; in
196
+ * `'unknown'` mode there is no such record, so an identical container below
197
+ * the root is reported whole rather than trusted or walked into.
198
+ */
199
+ function reportIdentical(value, path) {
200
+ if (input.identicalContainers === "unknown" && path.length > 0 && isDiffableContainer(value)) {
201
+ changed.push(path);
202
+ return;
203
+ }
204
+ emitTouched(value, path);
205
+ }
206
+ function keyedFor(path) {
207
+ return input.keyed?.find((entry) => samePath(entry.path, path));
208
+ }
209
+ function walkArray(previous, next, path) {
210
+ const mark = changed.length;
211
+ const fallBackToStructural = () => {
212
+ changed.length = mark;
213
+ changed.push(path);
214
+ };
215
+ if (previous.length !== next.length) {
216
+ fallBackToStructural();
217
+ return;
218
+ }
219
+ const keyed = keyedFor(path);
220
+ if (keyed) {
221
+ const fault = keyFault(previous, keyed) ?? keyFault(next, keyed);
222
+ if (fault) {
223
+ reportKeyFault(path, keyed, fault);
224
+ fallBackToStructural();
225
+ return;
226
+ }
227
+ }
228
+ for (let index = 0; index < next.length; index++) {
229
+ const previousItem = previous[index];
230
+ const nextItem = next[index];
231
+ const itemPath = [...path, String(index)];
232
+ if (Object.is(previousItem, nextItem)) {
233
+ reportIdentical(nextItem, itemPath);
234
+ continue;
235
+ }
236
+ if (keyed && isDiffableContainer(previousItem) && isDiffableContainer(nextItem)) {
237
+ const previousKey = readPath(previousItem, keyed.keyPath);
238
+ const nextKey = readPath(nextItem, keyed.keyPath);
239
+ if (!Object.is(previousKey, nextKey)) {
240
+ fallBackToStructural();
241
+ return;
242
+ }
243
+ walk(previousItem, nextItem, itemPath);
244
+ continue;
245
+ }
246
+ changed.push(itemPath);
247
+ }
248
+ }
249
+ function walkObject(previous, next, path) {
250
+ for (const key of new Set([...Object.keys(previous), ...Object.keys(next)])) {
251
+ const keyPath = [...path, key];
252
+ const inPrevious = Object.prototype.hasOwnProperty.call(previous, key);
253
+ const inNext = Object.prototype.hasOwnProperty.call(next, key);
254
+ if (!inPrevious || !inNext) {
255
+ changed.push(keyPath);
256
+ continue;
257
+ }
258
+ walk(previous[key], next[key], keyPath);
259
+ }
260
+ }
261
+ function walk(previous, next, path) {
262
+ if (Object.is(previous, next)) {
263
+ reportIdentical(next, path);
264
+ return;
265
+ }
266
+ if (!isDiffableContainer(previous) || !isDiffableContainer(next)) {
267
+ changed.push(path);
268
+ return;
269
+ }
270
+ const previousIsArray = Array.isArray(previous);
271
+ if (previousIsArray !== Array.isArray(next)) {
272
+ changed.push(path);
273
+ return;
274
+ }
275
+ let pairedWithPrevious = active.get(previous);
276
+ if (pairedWithPrevious?.has(next)) return;
277
+ if (!pairedWithPrevious) {
278
+ pairedWithPrevious = /* @__PURE__ */ new Set();
279
+ active.set(previous, pairedWithPrevious);
280
+ }
281
+ pairedWithPrevious.add(next);
282
+ try {
283
+ if (previousIsArray) walkArray(previous, next, path);
284
+ else walkObject(previous, next, path);
285
+ } finally {
286
+ pairedWithPrevious.delete(next);
287
+ if (pairedWithPrevious.size === 0) active.delete(previous);
288
+ }
289
+ emitTouched(previous, path);
290
+ }
291
+ walk(input.previous, input.next, []);
292
+ return changed;
293
+ }
294
+ /**
295
+ * Reads the declared key of every element and reports the first element that
296
+ * has no key or repeats one already used in the same array. Reads only; it
297
+ * never touches the elements it inspects.
298
+ */
299
+ function keyFault(items, keyed) {
300
+ const seen = /* @__PURE__ */ new Set();
301
+ for (let index = 0; index < items.length; index++) {
302
+ const key = readPath(items[index], keyed.keyPath);
303
+ if (key === void 0) return {
304
+ reason: "missing",
305
+ index,
306
+ key
307
+ };
308
+ if (seen.has(key)) return {
309
+ reason: "duplicate",
310
+ index,
311
+ key
312
+ };
313
+ seen.add(key);
314
+ }
315
+ }
316
+ /**
317
+ * Names the array and the offending key in debug-enabled builds. Keyed
318
+ * reconciliation degrading to structural reconciliation is silent in
319
+ * production — the result is still correct, only coarser.
320
+ */
321
+ function reportKeyFault(path, keyed, fault) {
322
+ if (typeof __MARKLESS_DEBUG_ENABLED__ === "undefined" || !__MARKLESS_DEBUG_ENABLED__) return;
323
+ const array = path.length === 0 ? "the derived value" : `derived path ${path.join(".")}`;
324
+ const keyPath = keyed.keyPath.length === 0 ? "the element itself" : keyed.keyPath.join(".");
325
+ const cause = fault.reason === "missing" ? `element ${fault.index} has no key at ${keyPath}` : `key ${describeKey(fault.key)} at ${keyPath} is used by more than one element (element ${fault.index})`;
326
+ console.warn(`markless: reconciling ${array} by key is not possible because ${cause}; falling back to structural reconciliation for that array.`);
327
+ }
328
+ function describeKey(key) {
329
+ return typeof key === "string" ? JSON.stringify(key) : String(key);
330
+ }
331
+ /** Plain objects and arrays are the only values reconciliation walks into. */
332
+ function isDiffableContainer(value) {
333
+ if (typeof value !== "object" || value === null) return false;
334
+ if (Array.isArray(value)) return true;
335
+ const prototype = Object.getPrototypeOf(value);
336
+ return prototype === Object.prototype || prototype === null;
337
+ }
338
+ function samePath(a, b) {
339
+ return a.length === b.length && a.every((segment, index) => b[index] === segment);
340
+ }
341
+ //#endregion
147
342
  //#region src/graph-async.ts
148
343
  const ASYNC_SNAPSHOT_META_KEYS = new Set([
149
344
  "status",
@@ -267,10 +462,59 @@ function startAsyncComputed(input) {
267
462
  }
268
463
  }
269
464
  function publish(input, snapshot) {
465
+ const previous = input.node.snapshot;
270
466
  input.node.snapshot = snapshot;
271
- input.markDirtyPath(input.node.graphNodeId, []);
467
+ for (const path of asyncCommitPaths(input.node, previous, snapshot)) input.markDirtyPath(input.node.graphNodeId, path);
272
468
  input.scheduleFlush();
273
469
  }
470
+ /**
471
+ * The graph paths a snapshot commit changed. Snapshot metadata is addressed as
472
+ * `status`/`version`/`key`/`error`; the fulfilled value is addressed both as
473
+ * `value.<path>` and, for compiled reads, as the bare `<path>` (see
474
+ * `readAsyncComputedNode`), so a changed value path is reported in both
475
+ * coordinate systems.
476
+ *
477
+ * A value commit compares structurally only. Unlike a sync computed, an async
478
+ * snapshot can land long after the flush that wrote the state it embeds, so the
479
+ * per-flush write-touched record is already cleared by the time it publishes.
480
+ * Identity below the root therefore proves nothing here: a runner that returns
481
+ * live state objects publishes the very rows a later write mutated in place,
482
+ * and treating them as unchanged would silently withhold that write from the
483
+ * subscribers of those paths. The diff runs in `identicalContainers: 'unknown'`
484
+ * mode, which reports each identical nested container at its path; a value
485
+ * rebuilt from fresh objects still reconciles field by field, and an identical
486
+ * root is handled by the whole-node rule below.
487
+ */
488
+ function asyncCommitPaths(node, previous, next) {
489
+ const paths = [];
490
+ if (previous.status !== next.status) paths.push(["status"]);
491
+ if (previous.version !== next.version) paths.push(["version"]);
492
+ if (!Object.is(snapshotField(previous, "key"), snapshotField(next, "key"))) paths.push(["key"]);
493
+ if (!Object.is(snapshotField(previous, "error"), snapshotField(next, "error"))) paths.push(["error"]);
494
+ if (next.status === "pending") return paths;
495
+ const previousValue = snapshotField(previous, "value");
496
+ const nextValue = snapshotField(next, "value");
497
+ if (!Object.is(previousValue, nextValue) && isDiffableContainer(previousValue) && isDiffableContainer(nextValue)) {
498
+ const changed = diffDerivedValue({
499
+ previous: previousValue,
500
+ next: nextValue,
501
+ keyed: node.reconcile?.keyed,
502
+ identicalContainers: "unknown"
503
+ });
504
+ if (changed.every((path) => path.length > 0)) {
505
+ for (const path of changed) {
506
+ paths.push(["value", ...path]);
507
+ if (!ASYNC_SNAPSHOT_META_KEYS.has(path[0])) paths.push([...path]);
508
+ }
509
+ return paths;
510
+ }
511
+ }
512
+ paths.push([]);
513
+ return paths;
514
+ }
515
+ function snapshotField(snapshot, field) {
516
+ return snapshot[field];
517
+ }
274
518
  //#endregion
275
519
  //#region src/graph-computed.ts
276
520
  function createRuntimeComputedNodes(computedInput) {
@@ -278,15 +522,35 @@ function createRuntimeComputedNodes(computedInput) {
278
522
  for (const computed of computedInput ?? []) computedNodes.set(computed.graphNodeId, {
279
523
  ...computed,
280
524
  dirty: true,
525
+ invalidated: false,
526
+ baselineStale: false,
281
527
  value: void 0
282
528
  });
283
529
  return computedNodes;
284
530
  }
285
- function readComputedNode(computed, readGraph, path) {
531
+ function readComputedNode(computed, readGraph, path, reconcile) {
286
532
  const { compute } = computed;
287
533
  if (computed.dirty && compute) {
288
- computed.value = compute(readGraph);
534
+ const previous = computed.value;
535
+ const next = compute(readGraph);
536
+ const changed = computed.invalidated && reconcile ? diffDerivedValue({
537
+ previous,
538
+ next,
539
+ keyed: computed.reconcile?.keyed,
540
+ touched: reconcile.touched,
541
+ baselineStale: computed.baselineStale
542
+ }) : [];
543
+ computed.value = next;
289
544
  computed.dirty = false;
545
+ computed.invalidated = false;
546
+ computed.baselineStale = false;
547
+ if (reconcile && changed.length > 0) {
548
+ for (const changedPath of changed) reconcile.dirtyPaths.push({
549
+ graphNodeId: computed.graphNodeId,
550
+ path: changedPath
551
+ });
552
+ reconcile.scheduleFlush();
553
+ }
290
554
  }
291
555
  return readPath(computed.value, path);
292
556
  }
@@ -296,10 +560,8 @@ function markComputedDirty(input) {
296
560
  const computed = input.computedNodes.get(input.graphNodeId);
297
561
  if (!computed?.compute) return;
298
562
  computed.dirty = true;
299
- input.dirtyPaths.push({
300
- graphNodeId: input.graphNodeId,
301
- path: []
302
- });
563
+ computed.invalidated = true;
564
+ input.dirtyComputedIds.add(input.graphNodeId);
303
565
  for (const dependent of input.computedNodes.values()) if (dependent.dependencies.some((dependency) => dependency.graphNodeId === input.graphNodeId) && dependent.compute) markComputedDirty({
304
566
  ...input,
305
567
  graphNodeId: dependent.graphNodeId
@@ -437,6 +699,8 @@ function createRuntimeGraph(input) {
437
699
  const subscriptions = [];
438
700
  const journalListeners = [];
439
701
  const dirtyPaths = [];
702
+ const dirtyComputedIds = /* @__PURE__ */ new Set();
703
+ const writeTouched = /* @__PURE__ */ new Map();
440
704
  const journal = [];
441
705
  let flushScheduled = false;
442
706
  let flushing = false;
@@ -445,9 +709,16 @@ function createRuntimeGraph(input) {
445
709
  cells.set(cell.graphNodeId, cell.value);
446
710
  if (cell.readInitializer) readInitializers.set(cell.graphNodeId, cell.readInitializer);
447
711
  }
712
+ const reconcileContext = {
713
+ dirtyPaths,
714
+ touched: writeTouched,
715
+ scheduleFlush: () => {
716
+ scheduleFlush();
717
+ }
718
+ };
448
719
  const readGraph = (graphNodeId, path = []) => {
449
720
  const computed = computedNodes.get(graphNodeId);
450
- if (computed?.compute) return readComputedNode(computed, readGraph, path);
721
+ if (computed?.compute) return readComputedNode(computed, readGraph, path, reconcileContext);
451
722
  const asyncComputed = asyncComputedNodes.get(graphNodeId);
452
723
  if (asyncComputed) return readAsyncComputedNode(asyncComputed, path, () => demandAsyncComputed$1(asyncComputed));
453
724
  const initialize = readInitializers.get(graphNodeId);
@@ -465,17 +736,34 @@ function createRuntimeGraph(input) {
465
736
  }
466
737
  return readPath(cells.get(graphNodeId), path);
467
738
  };
739
+ const recordWriteTouched = (graphNodeId, path) => {
740
+ if (computedNodes.has(graphNodeId)) return;
741
+ let container = cells.get(graphNodeId);
742
+ for (let depth = 0; depth <= path.length; depth++) {
743
+ if (typeof container === "object" && container !== null) {
744
+ const remainders = writeTouched.get(container) ?? [];
745
+ const remaining = path.slice(depth);
746
+ if (!remainders.some((entry) => samePath(entry, remaining))) {
747
+ remainders.push(remaining);
748
+ writeTouched.set(container, remainders);
749
+ }
750
+ }
751
+ if (depth === path.length) break;
752
+ container = readPath(container, [path[depth]]);
753
+ }
754
+ };
468
755
  const markDirtyPath = (graphNodeId, path) => {
469
756
  dirtyPaths.push({
470
757
  graphNodeId,
471
758
  path
472
759
  });
760
+ recordWriteTouched(graphNodeId, path);
473
761
  markDirtyComputedDependencies({
474
762
  graphNodeId,
475
763
  path,
476
764
  computedNodes,
477
765
  asyncComputedNodes,
478
- dirtyPaths,
766
+ dirtyComputedIds,
479
767
  invalidateAsyncComputed: invalidateAsyncComputed$1
480
768
  });
481
769
  };
@@ -514,12 +802,32 @@ function createRuntimeGraph(input) {
514
802
  activeFlush = runFlush();
515
803
  return activeFlush;
516
804
  };
805
+ const recomputeSubscribedComputeds = () => {
806
+ for (const graphNodeId of Array.from(dirtyComputedIds)) {
807
+ if (!computedNodes.get(graphNodeId)?.dirty) {
808
+ dirtyComputedIds.delete(graphNodeId);
809
+ continue;
810
+ }
811
+ if (!subscriptions.some((subscription) => subscription.graphNodeId === graphNodeId)) continue;
812
+ dirtyComputedIds.delete(graphNodeId);
813
+ readGraph(graphNodeId);
814
+ }
815
+ };
816
+ const settleReconcileBaselines = () => {
817
+ for (const graphNodeId of dirtyComputedIds) {
818
+ const computed = computedNodes.get(graphNodeId);
819
+ if (computed?.dirty) computed.baselineStale = true;
820
+ }
821
+ dirtyComputedIds.clear();
822
+ writeTouched.clear();
823
+ };
517
824
  const runFlush = async () => {
518
825
  flushScheduled = false;
519
826
  flushing = true;
520
827
  try {
521
828
  try {
522
829
  while (dirtyPaths.length > 0) {
830
+ recomputeSubscribedComputeds();
523
831
  const pending = dirtyPaths.splice(0);
524
832
  const ranSubscriptions = /* @__PURE__ */ new Set();
525
833
  for (const subscription of subscriptions) {
@@ -530,6 +838,7 @@ function createRuntimeGraph(input) {
530
838
  }
531
839
  }
532
840
  } finally {
841
+ settleReconcileBaselines();
533
842
  flushing = false;
534
843
  }
535
844
  await notifyJournalListeners();
@@ -555,6 +864,22 @@ function createRuntimeGraph(input) {
555
864
  write(write) {
556
865
  const path = write.path ?? [];
557
866
  const current = cells.get(write.graphNodeId);
867
+ const computed = computedNodes.get(write.graphNodeId);
868
+ if (computed && !computed.compute && path.length === 0) {
869
+ const changed = diffDerivedValue({
870
+ previous: current,
871
+ next: write.value,
872
+ keyed: computed.reconcile?.keyed,
873
+ touched: writeTouched,
874
+ baselineStale: computed.baselineStale
875
+ });
876
+ cells.set(write.graphNodeId, write.value);
877
+ computed.baselineStale = false;
878
+ if (changed.length === 0) return;
879
+ for (const changedPath of changed) markDirtyPath(write.graphNodeId, changedPath);
880
+ scheduleFlush();
881
+ return;
882
+ }
558
883
  if (Object.is(readPath(current, path), write.value)) return;
559
884
  cells.set(write.graphNodeId, writePath(current, path, write.value));
560
885
  markDirtyPath(write.graphNodeId, dirtyPathForGraphWrite(current, path));
@@ -613,4 +938,4 @@ function createRuntimeGraph(input) {
613
938
  };
614
939
  }
615
940
  //#endregion
616
- export { createRuntimeGraph as t };
941
+ export { diffDerivedValue as n, createRuntimeGraph as t };
package/dist/graph.d.ts CHANGED
@@ -1,145 +1,2 @@
1
- import { ProtocolStatePayload } from "@markless/serializer";
2
-
3
- //#region src/graph.d.ts
4
- type RuntimeGraphCell = {
5
- readonly graphNodeId: string;
6
- readonly value: unknown;
7
- readonly readInitializer?: () => unknown;
8
- };
9
- type RuntimeGraphRead = (graphNodeId: string, path?: ReadonlyArray<string>) => unknown;
10
- type RuntimeGraphComputedDependency = {
11
- readonly graphNodeId: string;
12
- readonly path?: ReadonlyArray<string>;
13
- };
14
- type RuntimeGraphComputed = {
15
- readonly graphNodeId: string;
16
- readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
17
- readonly compute: (read: RuntimeGraphRead) => unknown;
18
- };
19
- type RuntimeGraphComputedDependencyNode = Omit<RuntimeGraphComputed, 'compute'> & {
20
- readonly compute?: never;
21
- };
22
- type RuntimeGraphAsyncSnapshot = {
23
- readonly status: 'idle';
24
- readonly version: 0;
25
- } | {
26
- readonly status: 'pending';
27
- readonly version: number;
28
- readonly key: unknown;
29
- readonly value?: unknown;
30
- } | {
31
- readonly status: 'fulfilled';
32
- readonly version: number;
33
- readonly key: unknown;
34
- readonly value: unknown;
35
- } | {
36
- readonly status: 'rejected';
37
- readonly version: number;
38
- readonly key: unknown;
39
- readonly error: unknown;
40
- };
41
- type RuntimeGraphAsyncComputed = {
42
- readonly graphNodeId: string;
43
- readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
44
- readonly initialSnapshot?: RuntimeGraphAsyncSnapshot;
45
- readonly key: (read: RuntimeGraphRead) => unknown;
46
- readonly run: (input: {
47
- readonly key: unknown;
48
- readonly signal: AbortSignal;
49
- readonly read: RuntimeGraphRead;
50
- }) => unknown | Promise<unknown>;
51
- };
52
- type DomJournalEntry = {
53
- readonly type: 'setText';
54
- readonly locator: string;
55
- readonly value: unknown;
56
- } | {
57
- readonly type: 'setAttr' | 'setProp';
58
- readonly locator: string;
59
- readonly name: string;
60
- readonly value: unknown;
61
- } | {
62
- readonly type: 'insertRange';
63
- readonly locator: string;
64
- readonly fragment: unknown;
65
- } | {
66
- readonly type: 'removeRange';
67
- readonly locator: string;
68
- } | {
69
- readonly type: 'moveRange';
70
- readonly locator: string;
71
- readonly before: string;
72
- } | {
73
- readonly type: 'runCleanup';
74
- readonly locator: string;
75
- };
76
- type DomJournalResult = DomJournalEntry | ReadonlyArray<DomJournalEntry>;
77
- type DomJournalListener = (entries: ReadonlyArray<DomJournalEntry>) => void | Promise<void>;
78
- type RuntimeGraphInput = {
79
- readonly cells: ReadonlyArray<RuntimeGraphCell>;
80
- readonly computed?: ReadonlyArray<RuntimeGraphComputed | RuntimeGraphComputedDependencyNode>;
81
- readonly asyncComputed?: ReadonlyArray<RuntimeGraphAsyncComputed>;
82
- readonly sharedDefinitions?: ProtocolStatePayload['sharedDefinitions'];
83
- };
84
- type RuntimeGraphWrite = {
85
- readonly graphNodeId: string;
86
- readonly path?: ReadonlyArray<string>;
87
- readonly value: unknown;
88
- };
89
- type RuntimeGraphSharedWrite = {
90
- readonly definitionId: string;
91
- readonly propertyName: string;
92
- readonly path?: ReadonlyArray<string>;
93
- readonly value: unknown;
94
- };
95
- type RuntimeGraphSharedPatchOperation = readonly [operation: 'set', path: ReadonlyArray<string>, value: unknown];
96
- type RuntimeGraphSharedPatch = {
97
- readonly id: string;
98
- readonly scope?: RuntimeSharedDefinition['scope'];
99
- readonly version: number;
100
- readonly patch: ReadonlyArray<RuntimeGraphSharedPatchOperation>;
101
- };
102
- type RuntimeGraphUpdate = {
103
- readonly graphNodeId: string;
104
- readonly path?: ReadonlyArray<string>;
105
- readonly update: (value: unknown) => unknown;
106
- readonly returnValue?: 'previous' | 'next';
107
- };
108
- type RuntimeGraphCall = {
109
- readonly graphNodeId: string;
110
- readonly path?: ReadonlyArray<string>;
111
- readonly method: string;
112
- readonly args?: ReadonlyArray<unknown>;
113
- };
114
- type RuntimeGraphDelete = {
115
- readonly graphNodeId: string;
116
- readonly path: ReadonlyArray<string>;
117
- };
118
- type RuntimeGraphSubscription = {
119
- readonly id: string;
120
- readonly graphNodeId: string;
121
- readonly path?: ReadonlyArray<string>;
122
- readonly run: (value: unknown) => DomJournalResult | void | Promise<DomJournalResult | void>;
123
- };
124
- type RuntimeGraph = {
125
- readonly read: (graphNodeId: string, path?: ReadonlyArray<string>) => unknown; /** Available only in debug-enabled builds (`__MARKLESS_DEBUG_ENABLED__`). */
126
- readonly peekAsyncSnapshot?: (graphNodeId: string) => RuntimeGraphAsyncSnapshot | undefined;
127
- readonly readShared: (definitionId: string, propertyName: string, path?: ReadonlyArray<string>) => unknown;
128
- readonly writeShared: (write: RuntimeGraphSharedWrite) => boolean;
129
- readonly getSharedDefinition: (definitionId: string) => NonNullable<ProtocolStatePayload['sharedDefinitions']>[number] | undefined;
130
- readonly listSharedDefinitions: () => NonNullable<ProtocolStatePayload['sharedDefinitions']>;
131
- readonly takeSharedPatches: () => RuntimeGraphSharedPatch[];
132
- readonly applySharedPatch: (patch: RuntimeGraphSharedPatch) => boolean;
133
- readonly write: (write: RuntimeGraphWrite) => void;
134
- readonly update: (update: RuntimeGraphUpdate) => unknown;
135
- readonly call: (call: RuntimeGraphCall) => unknown;
136
- readonly delete: (deletion: RuntimeGraphDelete) => boolean;
137
- readonly subscribe: (subscription: RuntimeGraphSubscription) => () => void;
138
- readonly subscribeJournal: (listener: DomJournalListener) => () => void;
139
- readonly flush: () => Promise<void>;
140
- readonly takeJournal: () => DomJournalEntry[];
141
- };
142
- type RuntimeSharedDefinition = NonNullable<ProtocolStatePayload['sharedDefinitions']>[number];
143
- declare function createRuntimeGraph(input: RuntimeGraphInput): RuntimeGraph;
144
- //#endregion
145
- export { DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphComputedDependencyNode, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, createRuntimeGraph };
1
+ import { C as RuntimeGraphReconcileKey, E as diffDerivedValue, S as DiffDerivedValueInput, T as WriteTouchedRecord, _ as RuntimeGraphSharedWrite, a as RuntimeGraphAsyncComputed, b as RuntimeGraphWrite, c as RuntimeGraphCell, d as RuntimeGraphComputedDependencyNode, f as RuntimeGraphDelete, g as RuntimeGraphSharedPatchOperation, h as RuntimeGraphSharedPatch, i as RuntimeGraph, l as RuntimeGraphComputed, m as RuntimeGraphRead, n as DomJournalListener, o as RuntimeGraphAsyncSnapshot, p as RuntimeGraphInput, r as DomJournalResult, s as RuntimeGraphCall, t as DomJournalEntry, u as RuntimeGraphComputedDependency, v as RuntimeGraphSubscription, w as RuntimeGraphReconcileOptions, x as createRuntimeGraph, y as RuntimeGraphUpdate } from "./graph-C1ACA8O1.js";
2
+ export { DiffDerivedValueInput, DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphComputedDependencyNode, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphReconcileKey, RuntimeGraphReconcileOptions, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, WriteTouchedRecord, createRuntimeGraph, diffDerivedValue };
package/dist/graph.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createRuntimeGraph } from "./graph-DoSjSC5T.js";
2
- export { createRuntimeGraph };
1
+ import { n as diffDerivedValue, t as createRuntimeGraph } from "./graph-DAwgoMQ1.js";
2
+ export { createRuntimeGraph, diffDerivedValue };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphComputedDependencyNode, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, createRuntimeGraph } from "./graph.js";
2
- export { DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphComputedDependencyNode, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, createRuntimeGraph };
1
+ import { C as RuntimeGraphReconcileKey, E as diffDerivedValue, S as DiffDerivedValueInput, T as WriteTouchedRecord, _ as RuntimeGraphSharedWrite, a as RuntimeGraphAsyncComputed, b as RuntimeGraphWrite, c as RuntimeGraphCell, d as RuntimeGraphComputedDependencyNode, f as RuntimeGraphDelete, g as RuntimeGraphSharedPatchOperation, h as RuntimeGraphSharedPatch, i as RuntimeGraph, l as RuntimeGraphComputed, m as RuntimeGraphRead, n as DomJournalListener, o as RuntimeGraphAsyncSnapshot, p as RuntimeGraphInput, r as DomJournalResult, s as RuntimeGraphCall, t as DomJournalEntry, u as RuntimeGraphComputedDependency, v as RuntimeGraphSubscription, w as RuntimeGraphReconcileOptions, x as createRuntimeGraph, y as RuntimeGraphUpdate } from "./graph-C1ACA8O1.js";
2
+ export { DiffDerivedValueInput, DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphComputedDependencyNode, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphReconcileKey, RuntimeGraphReconcileOptions, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, WriteTouchedRecord, createRuntimeGraph, diffDerivedValue };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { t as createRuntimeGraph } from "./graph-DoSjSC5T.js";
2
- export { createRuntimeGraph };
1
+ import { n as diffDerivedValue, t as createRuntimeGraph } from "./graph-DAwgoMQ1.js";
2
+ export { createRuntimeGraph, diffDerivedValue };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markless/runtime",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Markless state graph runtime: reads, writes, computed invalidation, flush journal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,7 +25,7 @@
25
25
  }
26
26
  },
27
27
  "dependencies": {
28
- "@markless/serializer": "0.3.1"
28
+ "@markless/serializer": "0.3.2"
29
29
  },
30
30
  "files": [
31
31
  "dist"