@markless/runtime 0.3.2 → 0.4.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.
@@ -1,72 +1,5 @@
1
1
  import { ProtocolStatePayload } from "@markless/serializer";
2
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
3
  //#region src/graph.d.ts
71
4
  type RuntimeGraphCell = {
72
5
  readonly graphNodeId: string;
@@ -149,6 +82,12 @@ type RuntimeGraphInput = {
149
82
  readonly computed?: ReadonlyArray<RuntimeGraphComputed | RuntimeGraphComputedDependencyNode>;
150
83
  readonly asyncComputed?: ReadonlyArray<RuntimeGraphAsyncComputed>;
151
84
  readonly sharedDefinitions?: ProtocolStatePayload['sharedDefinitions'];
85
+ /**
86
+ * Installs derived reconciliation, from
87
+ * `@markless/runtime/graph-reconcile`. Without it a computed invalidation
88
+ * dirties the whole node and an async commit dirties the whole snapshot.
89
+ */
90
+ readonly reconcile?: DerivedReconcilePlaneFactory;
152
91
  };
153
92
  type RuntimeGraphWrite = {
154
93
  readonly graphNodeId: string;
@@ -211,4 +150,125 @@ type RuntimeGraph = {
211
150
  type RuntimeSharedDefinition = NonNullable<ProtocolStatePayload['sharedDefinitions']>[number];
212
151
  declare function createRuntimeGraph(input: RuntimeGraphInput): RuntimeGraph;
213
152
  //#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 };
153
+ //#region src/graph-scheduler.d.ts
154
+ type DirtyPath = {
155
+ readonly graphNodeId: string;
156
+ readonly path: ReadonlyArray<string>;
157
+ };
158
+ //#endregion
159
+ //#region src/graph-computed.d.ts
160
+ type RuntimeComputedNode = Omit<RuntimeGraphComputed, 'compute'> & {
161
+ readonly compute?: RuntimeGraphComputed['compute'];
162
+ dirty: boolean;
163
+ value: unknown;
164
+ };
165
+ //#endregion
166
+ //#region src/graph-async.d.ts
167
+ type RuntimeAsyncComputedNode = RuntimeGraphAsyncComputed & {
168
+ controller?: AbortController;
169
+ gate: number;
170
+ snapshot: RuntimeGraphAsyncSnapshot;
171
+ version: number;
172
+ };
173
+ //#endregion
174
+ //#region src/graph-reconcile.d.ts
175
+ /**
176
+ * One array inside a derived value that reconciles by key instead of by
177
+ * element identity. `path` names the array (`[]` is the derived value itself)
178
+ * and `keyPath` is read on each element to get its identity.
179
+ */
180
+ type RuntimeGraphReconcileKey = {
181
+ readonly path: ReadonlyArray<string>;
182
+ readonly keyPath: ReadonlyArray<string>;
183
+ };
184
+ /** Reconcile options carried by a computed or async computed graph node. */
185
+ type RuntimeGraphReconcileOptions = {
186
+ readonly keyed?: ReadonlyArray<RuntimeGraphReconcileKey>;
187
+ };
188
+ /**
189
+ * Objects a state write touched during the current flush, mapped to the paths
190
+ * written beneath them. State writes mutate objects in place, so a derived
191
+ * value that still holds the same object reference is not unchanged when that
192
+ * object was written through. See `diffDerivedValue`.
193
+ */
194
+ type WriteTouchedRecord = ReadonlyMap<object, ReadonlyArray<ReadonlyArray<string>>>;
195
+ type DiffDerivedValueInput = {
196
+ readonly previous: unknown;
197
+ readonly next: unknown;
198
+ readonly keyed?: ReadonlyArray<RuntimeGraphReconcileKey>;
199
+ readonly touched?: WriteTouchedRecord;
200
+ readonly baselineStale?: boolean;
201
+ /**
202
+ * What an `Object.is`-identical plain object or array below the root means.
203
+ * `'unchanged'` (the default) trusts identity, which is sound for a caller
204
+ * that also supplies the write-touched record of the same flush.
205
+ * `'unknown'` is for a caller with no such record — the async commit — and
206
+ * reports every identical container at its own path instead.
207
+ */
208
+ readonly identicalContainers?: 'unchanged' | 'unknown';
209
+ };
210
+ /**
211
+ * Compares a derived value with the previous value of the same graph node and
212
+ * reports the graph paths that changed. It never mutates either value: the
213
+ * caller replaces the node's value pointer and invalidates the reported paths.
214
+ *
215
+ * Rules (specs/framework/03-state-graph.md "Derived reconciliation"):
216
+ * - identical references are unchanged, unless a write touched that object in
217
+ * this flush, in which case the touched remainders are reported; the same
218
+ * holds when only the previous value held the written object, because an
219
+ * in-place write left that baseline already carrying the new field;
220
+ * - primitives, functions, `Map`, `Set`, `Date` and class instances are leaves;
221
+ * - plain objects reconcile field by field;
222
+ * - arrays reconcile by declared key, otherwise by element identity, and a
223
+ * length or structural mismatch reports the array's own path;
224
+ * - a declared key that is missing or duplicated on either side falls back to
225
+ * structural reconciliation for that array, which reports the array's own
226
+ * path: keys are never matched partially and an index is never identity;
227
+ * - a missing baseline (first value, or a value whose write-touched record was
228
+ * already cleared) reports the whole node.
229
+ *
230
+ * `identicalContainers: 'unknown'` is for a caller that has no write-touched
231
+ * record for the flush that produced the new value, so identity below the root
232
+ * proves nothing: every `Object.is`-identical plain object or array met below
233
+ * the root is reported at its own path and is not walked into. An identical
234
+ * root still reports nothing — nothing narrower is known about it, and the
235
+ * caller decides what a wholly identical value means — and identical
236
+ * primitives and other leaves stay unchanged. The default, `'unchanged'`,
237
+ * trusts identity as above.
238
+ */
239
+ declare function diffDerivedValue(input: DiffDerivedValueInput): ReadonlyArray<ReadonlyArray<string>>;
240
+ /** Plain objects and arrays are the only values reconciliation walks into. */
241
+ declare function isDiffableContainer(value: unknown): value is object;
242
+ declare function samePath(a: ReadonlyArray<string>, b: ReadonlyArray<string>): boolean;
243
+ /**
244
+ * What the plane needs from the graph it is installed in. The graph passes its
245
+ * own internals; nothing here is part of the public runtime surface.
246
+ */
247
+ type DerivedReconcileInternals = {
248
+ readonly cells: Map<string, unknown>;
249
+ readonly computedNodes: ReadonlyMap<string, RuntimeComputedNode>;
250
+ readonly subscriptions: ReadonlyArray<RuntimeGraphSubscription>;
251
+ readonly dirtyPaths: DirtyPath[];
252
+ readonly readGraph: RuntimeGraphRead;
253
+ readonly markDirtyPath: (graphNodeId: string, path: ReadonlyArray<string>) => void;
254
+ readonly scheduleFlush: () => void;
255
+ };
256
+ /**
257
+ * Derived reconciliation as a plug-in plane. The core graph carries the hook
258
+ * points and nothing else, so an app that never installs the plane does not
259
+ * load this module and its computed invalidations dirty the whole node the way
260
+ * they did before reconciliation existed.
261
+ */
262
+ type DerivedReconcilePlane = {
263
+ /** Records the objects a state write mutated in place during this flush. */readonly recordWrite: (graphNodeId: string, path: ReadonlyArray<string>) => void; /** A dependency write invalidated this computed; its paths are not known yet. */
264
+ readonly invalidateComputed: (computed: RuntimeComputedNode) => void; /** Reports the paths a just-recomputed value moved, and wakes their subscribers. */
265
+ readonly reconcileComputed: (computed: RuntimeComputedNode, previous: unknown) => void; /** Commits a whole derived value onto a cell-backed computed node. */
266
+ readonly commitDerived: (write: RuntimeGraphWrite, path: ReadonlyArray<string>) => boolean; /** Dirties the paths an async snapshot commit changed. */
267
+ readonly publishAsync: (node: RuntimeAsyncComputedNode, previous: RuntimeGraphAsyncSnapshot) => void; /** Flush-pass start: recompute the dirty computeds something is subscribed to. */
268
+ readonly recomputeSubscribed: () => void; /** Flush end: drop the per-flush record and mark the baselines it leaves stale. */
269
+ readonly settleBaselines: () => void;
270
+ };
271
+ type DerivedReconcilePlaneFactory = (internals: DerivedReconcileInternals) => DerivedReconcilePlane;
272
+ declare const createDerivedReconcilePlane: DerivedReconcilePlaneFactory;
273
+ //#endregion
274
+ export { RuntimeGraphUpdate as A, RuntimeGraphDelete as C, RuntimeGraphSharedPatchOperation as D, RuntimeGraphSharedPatch as E, createRuntimeGraph as M, RuntimeGraphSharedWrite as O, RuntimeGraphComputedDependencyNode as S, RuntimeGraphRead as T, RuntimeGraphAsyncSnapshot as _, RuntimeGraphReconcileKey as a, RuntimeGraphComputed as b, createDerivedReconcilePlane as c, samePath as d, DomJournalEntry as f, RuntimeGraphAsyncComputed as g, RuntimeGraph as h, DiffDerivedValueInput as i, RuntimeGraphWrite as j, RuntimeGraphSubscription as k, diffDerivedValue as l, DomJournalResult as m, DerivedReconcilePlane as n, RuntimeGraphReconcileOptions as o, DomJournalListener as p, DerivedReconcilePlaneFactory as r, WriteTouchedRecord as s, DerivedReconcileInternals as t, isDiffableContainer as u, RuntimeGraphCall as v, RuntimeGraphInput as w, RuntimeGraphComputedDependency as x, RuntimeGraphCell as y };
@@ -0,0 +1,2 @@
1
+ import { a as RuntimeGraphReconcileKey, c as createDerivedReconcilePlane, d as samePath, i as DiffDerivedValueInput, l as diffDerivedValue, n as DerivedReconcilePlane, o as RuntimeGraphReconcileOptions, r as DerivedReconcilePlaneFactory, s as WriteTouchedRecord, t as DerivedReconcileInternals, u as isDiffableContainer } from "./graph-reconcile-DoBBfYsT.js";
2
+ export { DerivedReconcileInternals, DerivedReconcilePlane, DerivedReconcilePlaneFactory, DiffDerivedValueInput, RuntimeGraphReconcileKey, RuntimeGraphReconcileOptions, WriteTouchedRecord, createDerivedReconcilePlane, diffDerivedValue, isDiffableContainer, samePath };
@@ -0,0 +1,324 @@
1
+ import { t as ASYNC_SNAPSHOT_META_KEYS, u as readPath } from "./graph-async-Db6SqB5x.js";
2
+ //#region src/graph-reconcile.ts
3
+ const WHOLE_NODE = Object.freeze([Object.freeze([])]);
4
+ /**
5
+ * Compares a derived value with the previous value of the same graph node and
6
+ * reports the graph paths that changed. It never mutates either value: the
7
+ * caller replaces the node's value pointer and invalidates the reported paths.
8
+ *
9
+ * Rules (specs/framework/03-state-graph.md "Derived reconciliation"):
10
+ * - identical references are unchanged, unless a write touched that object in
11
+ * this flush, in which case the touched remainders are reported; the same
12
+ * holds when only the previous value held the written object, because an
13
+ * in-place write left that baseline already carrying the new field;
14
+ * - primitives, functions, `Map`, `Set`, `Date` and class instances are leaves;
15
+ * - plain objects reconcile field by field;
16
+ * - arrays reconcile by declared key, otherwise by element identity, and a
17
+ * length or structural mismatch reports the array's own path;
18
+ * - a declared key that is missing or duplicated on either side falls back to
19
+ * structural reconciliation for that array, which reports the array's own
20
+ * path: keys are never matched partially and an index is never identity;
21
+ * - a missing baseline (first value, or a value whose write-touched record was
22
+ * already cleared) reports the whole node.
23
+ *
24
+ * `identicalContainers: 'unknown'` is for a caller that has no write-touched
25
+ * record for the flush that produced the new value, so identity below the root
26
+ * proves nothing: every `Object.is`-identical plain object or array met below
27
+ * the root is reported at its own path and is not walked into. An identical
28
+ * root still reports nothing — nothing narrower is known about it, and the
29
+ * caller decides what a wholly identical value means — and identical
30
+ * primitives and other leaves stay unchanged. The default, `'unchanged'`,
31
+ * trusts identity as above.
32
+ */
33
+ function diffDerivedValue(input) {
34
+ if (input.baselineStale) return WHOLE_NODE;
35
+ if (input.previous === void 0 && input.next !== void 0) return WHOLE_NODE;
36
+ const changed = [];
37
+ const active = /* @__PURE__ */ new Map();
38
+ function emitTouched(value, path) {
39
+ if (!input.touched || typeof value !== "object" || value === null) return;
40
+ const remainders = input.touched.get(value);
41
+ if (!remainders) return;
42
+ for (const remaining of remainders) {
43
+ const touchedPath = [...path, ...remaining];
44
+ if (changed.some((entry) => samePath(entry, touchedPath))) continue;
45
+ changed.push(touchedPath);
46
+ }
47
+ }
48
+ /**
49
+ * What an identical reference met at `path` reports. With a write-touched
50
+ * record identity means unchanged apart from the recorded remainders; in
51
+ * `'unknown'` mode there is no such record, so an identical container below
52
+ * the root is reported whole rather than trusted or walked into.
53
+ */
54
+ function reportIdentical(value, path) {
55
+ if (input.identicalContainers === "unknown" && path.length > 0 && isDiffableContainer(value)) {
56
+ changed.push(path);
57
+ return;
58
+ }
59
+ emitTouched(value, path);
60
+ }
61
+ function keyedFor(path) {
62
+ return input.keyed?.find((entry) => samePath(entry.path, path));
63
+ }
64
+ function walkArray(previous, next, path) {
65
+ const mark = changed.length;
66
+ const fallBackToStructural = () => {
67
+ changed.length = mark;
68
+ changed.push(path);
69
+ };
70
+ if (previous.length !== next.length) {
71
+ fallBackToStructural();
72
+ return;
73
+ }
74
+ const keyed = keyedFor(path);
75
+ if (keyed) {
76
+ const fault = keyFault(previous, keyed) ?? keyFault(next, keyed);
77
+ if (fault) {
78
+ reportKeyFault(path, keyed, fault);
79
+ fallBackToStructural();
80
+ return;
81
+ }
82
+ }
83
+ for (let index = 0; index < next.length; index++) {
84
+ const previousItem = previous[index];
85
+ const nextItem = next[index];
86
+ const itemPath = [...path, String(index)];
87
+ if (Object.is(previousItem, nextItem)) {
88
+ reportIdentical(nextItem, itemPath);
89
+ continue;
90
+ }
91
+ if (keyed && isDiffableContainer(previousItem) && isDiffableContainer(nextItem)) {
92
+ const previousKey = readPath(previousItem, keyed.keyPath);
93
+ const nextKey = readPath(nextItem, keyed.keyPath);
94
+ if (!Object.is(previousKey, nextKey)) {
95
+ fallBackToStructural();
96
+ return;
97
+ }
98
+ walk(previousItem, nextItem, itemPath);
99
+ continue;
100
+ }
101
+ changed.push(itemPath);
102
+ }
103
+ }
104
+ function walkObject(previous, next, path) {
105
+ for (const key of new Set([...Object.keys(previous), ...Object.keys(next)])) {
106
+ const keyPath = [...path, key];
107
+ const inPrevious = Object.prototype.hasOwnProperty.call(previous, key);
108
+ const inNext = Object.prototype.hasOwnProperty.call(next, key);
109
+ if (!inPrevious || !inNext) {
110
+ changed.push(keyPath);
111
+ continue;
112
+ }
113
+ walk(previous[key], next[key], keyPath);
114
+ }
115
+ }
116
+ function walk(previous, next, path) {
117
+ if (Object.is(previous, next)) {
118
+ reportIdentical(next, path);
119
+ return;
120
+ }
121
+ if (!isDiffableContainer(previous) || !isDiffableContainer(next)) {
122
+ changed.push(path);
123
+ return;
124
+ }
125
+ const previousIsArray = Array.isArray(previous);
126
+ if (previousIsArray !== Array.isArray(next)) {
127
+ changed.push(path);
128
+ return;
129
+ }
130
+ let pairedWithPrevious = active.get(previous);
131
+ if (pairedWithPrevious?.has(next)) return;
132
+ if (!pairedWithPrevious) {
133
+ pairedWithPrevious = /* @__PURE__ */ new Set();
134
+ active.set(previous, pairedWithPrevious);
135
+ }
136
+ pairedWithPrevious.add(next);
137
+ try {
138
+ if (previousIsArray) walkArray(previous, next, path);
139
+ else walkObject(previous, next, path);
140
+ } finally {
141
+ pairedWithPrevious.delete(next);
142
+ if (pairedWithPrevious.size === 0) active.delete(previous);
143
+ }
144
+ emitTouched(previous, path);
145
+ }
146
+ walk(input.previous, input.next, []);
147
+ return changed;
148
+ }
149
+ /**
150
+ * Reads the declared key of every element and reports the first element that
151
+ * has no key or repeats one already used in the same array. Reads only; it
152
+ * never touches the elements it inspects.
153
+ */
154
+ function keyFault(items, keyed) {
155
+ const seen = /* @__PURE__ */ new Set();
156
+ for (let index = 0; index < items.length; index++) {
157
+ const key = readPath(items[index], keyed.keyPath);
158
+ if (key === void 0) return {
159
+ reason: "missing",
160
+ index,
161
+ key
162
+ };
163
+ if (seen.has(key)) return {
164
+ reason: "duplicate",
165
+ index,
166
+ key
167
+ };
168
+ seen.add(key);
169
+ }
170
+ }
171
+ /**
172
+ * Names the array and the offending key in debug-enabled builds. Keyed
173
+ * reconciliation degrading to structural reconciliation is silent in
174
+ * production — the result is still correct, only coarser.
175
+ */
176
+ function reportKeyFault(path, keyed, fault) {
177
+ if (typeof __MARKLESS_DEBUG_ENABLED__ === "undefined" || !__MARKLESS_DEBUG_ENABLED__) return;
178
+ const array = path.length === 0 ? "the derived value" : `derived path ${path.join(".")}`;
179
+ const keyPath = keyed.keyPath.length === 0 ? "the element itself" : keyed.keyPath.join(".");
180
+ 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})`;
181
+ console.warn(`markless: reconciling ${array} by key is not possible because ${cause}; falling back to structural reconciliation for that array.`);
182
+ }
183
+ function describeKey(key) {
184
+ return typeof key === "string" ? JSON.stringify(key) : String(key);
185
+ }
186
+ /** Plain objects and arrays are the only values reconciliation walks into. */
187
+ function isDiffableContainer(value) {
188
+ if (typeof value !== "object" || value === null) return false;
189
+ if (Array.isArray(value)) return true;
190
+ const prototype = Object.getPrototypeOf(value);
191
+ return prototype === Object.prototype || prototype === null;
192
+ }
193
+ function samePath(a, b) {
194
+ return a.length === b.length && a.every((segment, index) => b[index] === segment);
195
+ }
196
+ const createDerivedReconcilePlane = (internals) => {
197
+ const { cells, computedNodes, subscriptions, dirtyPaths } = internals;
198
+ const { readGraph, markDirtyPath, scheduleFlush } = internals;
199
+ const writeTouched = /* @__PURE__ */ new Map();
200
+ const dirtyComputed = /* @__PURE__ */ new Set();
201
+ const invalidated = /* @__PURE__ */ new Set();
202
+ const baselineStale = /* @__PURE__ */ new Set();
203
+ const changedPaths = (computed, previous, next) => diffDerivedValue({
204
+ previous,
205
+ next,
206
+ keyed: computed.reconcile?.keyed,
207
+ touched: writeTouched,
208
+ baselineStale: baselineStale.has(computed)
209
+ });
210
+ return {
211
+ recordWrite(graphNodeId, path) {
212
+ if (computedNodes.size === 0 || computedNodes.has(graphNodeId)) return;
213
+ let container = cells.get(graphNodeId);
214
+ for (let depth = 0; depth <= path.length; depth++) {
215
+ if (typeof container === "object" && container !== null) {
216
+ const remainders = writeTouched.get(container) ?? [];
217
+ const remaining = path.slice(depth);
218
+ if (!remainders.some((entry) => samePath(entry, remaining))) {
219
+ remainders.push(remaining);
220
+ writeTouched.set(container, remainders);
221
+ }
222
+ }
223
+ if (depth === path.length) break;
224
+ container = readPath(container, [path[depth]]);
225
+ }
226
+ },
227
+ invalidateComputed(computed) {
228
+ invalidated.add(computed);
229
+ dirtyComputed.add(computed);
230
+ },
231
+ reconcileComputed(computed, previous) {
232
+ const changed = invalidated.has(computed) ? changedPaths(computed, previous, computed.value) : [];
233
+ invalidated.delete(computed);
234
+ baselineStale.delete(computed);
235
+ if (changed.length === 0) return;
236
+ for (const path of changed) dirtyPaths.push({
237
+ graphNodeId: computed.graphNodeId,
238
+ path
239
+ });
240
+ scheduleFlush();
241
+ },
242
+ commitDerived(write, path) {
243
+ const graphNodeId = write.graphNodeId;
244
+ const computed = computedNodes.get(graphNodeId);
245
+ if (!computed || computed.compute || path.length > 0) return false;
246
+ const changed = changedPaths(computed, cells.get(graphNodeId), write.value);
247
+ cells.set(graphNodeId, write.value);
248
+ baselineStale.delete(computed);
249
+ if (changed.length === 0) return true;
250
+ for (const changedPath of changed) markDirtyPath(graphNodeId, changedPath);
251
+ scheduleFlush();
252
+ return true;
253
+ },
254
+ publishAsync(node, previous) {
255
+ for (const path of asyncCommitPaths(node, previous, node.snapshot)) markDirtyPath(node.graphNodeId, path);
256
+ },
257
+ recomputeSubscribed() {
258
+ for (const computed of Array.from(dirtyComputed)) {
259
+ if (!computed.dirty) {
260
+ dirtyComputed.delete(computed);
261
+ continue;
262
+ }
263
+ if (!subscriptions.some((subscription) => subscription.graphNodeId === computed.graphNodeId)) continue;
264
+ dirtyComputed.delete(computed);
265
+ readGraph(computed.graphNodeId);
266
+ }
267
+ },
268
+ settleBaselines() {
269
+ for (const computed of dirtyComputed) if (computed.dirty) baselineStale.add(computed);
270
+ dirtyComputed.clear();
271
+ writeTouched.clear();
272
+ }
273
+ };
274
+ };
275
+ /**
276
+ * The graph paths a snapshot commit changed. Snapshot metadata is addressed as
277
+ * `status`/`version`/`key`/`error`; the fulfilled value is addressed both as
278
+ * `value.<path>` and, for compiled reads, as the bare `<path>` (see
279
+ * `readAsyncComputedNode`), so a changed value path is reported in both
280
+ * coordinate systems.
281
+ *
282
+ * A value commit compares structurally only. Unlike a sync computed, an async
283
+ * snapshot can land long after the flush that wrote the state it embeds, so the
284
+ * per-flush write-touched record is already cleared by the time it publishes.
285
+ * Identity below the root therefore proves nothing here: a runner that returns
286
+ * live state objects publishes the very rows a later write mutated in place,
287
+ * and treating them as unchanged would silently withhold that write from the
288
+ * subscribers of those paths. The diff runs in `identicalContainers: 'unknown'`
289
+ * mode, which reports each identical nested container at its path; a value
290
+ * rebuilt from fresh objects still reconciles field by field, and an identical
291
+ * root is handled by the whole-node rule below.
292
+ */
293
+ function asyncCommitPaths(node, previous, next) {
294
+ const paths = [];
295
+ if (previous.status !== next.status) paths.push(["status"]);
296
+ if (previous.version !== next.version) paths.push(["version"]);
297
+ if (!Object.is(snapshotField(previous, "key"), snapshotField(next, "key"))) paths.push(["key"]);
298
+ if (!Object.is(snapshotField(previous, "error"), snapshotField(next, "error"))) paths.push(["error"]);
299
+ if (next.status === "pending") return paths;
300
+ const previousValue = snapshotField(previous, "value");
301
+ const nextValue = snapshotField(next, "value");
302
+ if (!Object.is(previousValue, nextValue) && isDiffableContainer(previousValue) && isDiffableContainer(nextValue)) {
303
+ const changed = diffDerivedValue({
304
+ previous: previousValue,
305
+ next: nextValue,
306
+ keyed: node.reconcile?.keyed,
307
+ identicalContainers: "unknown"
308
+ });
309
+ if (changed.every((path) => path.length > 0)) {
310
+ for (const path of changed) {
311
+ paths.push(["value", ...path]);
312
+ if (!ASYNC_SNAPSHOT_META_KEYS.has(path[0])) paths.push([...path]);
313
+ }
314
+ return paths;
315
+ }
316
+ }
317
+ paths.push([]);
318
+ return paths;
319
+ }
320
+ function snapshotField(snapshot, field) {
321
+ return snapshot[field];
322
+ }
323
+ //#endregion
324
+ export { createDerivedReconcilePlane, diffDerivedValue, isDiffableContainer, samePath };
package/dist/graph.d.ts CHANGED
@@ -1,2 +1,2 @@
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 };
1
+ import { A as RuntimeGraphUpdate, C as RuntimeGraphDelete, D as RuntimeGraphSharedPatchOperation, E as RuntimeGraphSharedPatch, M as createRuntimeGraph, O as RuntimeGraphSharedWrite, S as RuntimeGraphComputedDependencyNode, T as RuntimeGraphRead, _ as RuntimeGraphAsyncSnapshot, a as RuntimeGraphReconcileKey, b as RuntimeGraphComputed, f as DomJournalEntry, g as RuntimeGraphAsyncComputed, h as RuntimeGraph, i as DiffDerivedValueInput, j as RuntimeGraphWrite, k as RuntimeGraphSubscription, m as DomJournalResult, n as DerivedReconcilePlane, o as RuntimeGraphReconcileOptions, p as DomJournalListener, r as DerivedReconcilePlaneFactory, s as WriteTouchedRecord, t as DerivedReconcileInternals, v as RuntimeGraphCall, w as RuntimeGraphInput, x as RuntimeGraphComputedDependency, y as RuntimeGraphCell } from "./graph-reconcile-DoBBfYsT.js";
2
+ export { DerivedReconcileInternals, DerivedReconcilePlane, DerivedReconcilePlaneFactory, 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 };
package/dist/graph.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as diffDerivedValue, t as createRuntimeGraph } from "./graph-DAwgoMQ1.js";
2
- export { createRuntimeGraph, diffDerivedValue };
1
+ import { t as createRuntimeGraph } from "./graph-Duhosz6u.js";
2
+ export { createRuntimeGraph };
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
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 };
1
+ import { A as RuntimeGraphUpdate, C as RuntimeGraphDelete, D as RuntimeGraphSharedPatchOperation, E as RuntimeGraphSharedPatch, M as createRuntimeGraph, O as RuntimeGraphSharedWrite, S as RuntimeGraphComputedDependencyNode, T as RuntimeGraphRead, _ as RuntimeGraphAsyncSnapshot, a as RuntimeGraphReconcileKey, b as RuntimeGraphComputed, f as DomJournalEntry, g as RuntimeGraphAsyncComputed, h as RuntimeGraph, i as DiffDerivedValueInput, j as RuntimeGraphWrite, k as RuntimeGraphSubscription, m as DomJournalResult, n as DerivedReconcilePlane, o as RuntimeGraphReconcileOptions, p as DomJournalListener, r as DerivedReconcilePlaneFactory, s as WriteTouchedRecord, t as DerivedReconcileInternals, v as RuntimeGraphCall, w as RuntimeGraphInput, x as RuntimeGraphComputedDependency, y as RuntimeGraphCell } from "./graph-reconcile-DoBBfYsT.js";
2
+ export { DerivedReconcileInternals, DerivedReconcilePlane, DerivedReconcilePlaneFactory, 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 };
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { n as diffDerivedValue, t as createRuntimeGraph } from "./graph-DAwgoMQ1.js";
2
- export { createRuntimeGraph, diffDerivedValue };
1
+ import { t as createRuntimeGraph } from "./graph-Duhosz6u.js";
2
+ export { createRuntimeGraph };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@markless/runtime",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "Markless state graph runtime: reads, writes, computed invalidation, flush journal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -22,10 +22,14 @@
22
22
  "./graph": {
23
23
  "types": "./dist/graph.d.ts",
24
24
  "default": "./dist/graph.js"
25
+ },
26
+ "./graph-reconcile": {
27
+ "types": "./dist/graph-reconcile.d.ts",
28
+ "default": "./dist/graph-reconcile.js"
25
29
  }
26
30
  },
27
31
  "dependencies": {
28
- "@markless/serializer": "0.3.2"
32
+ "@markless/serializer": "0.4.0"
29
33
  },
30
34
  "files": [
31
35
  "dist"