@markless/runtime 0.1.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/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/graph-B7awrnzg.js +561 -0
- package/dist/graph.d.ts +140 -0
- package/dist/graph.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jack Shelton
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
//#region src/graph-core.ts
|
|
2
|
+
function findLastSharedReturnProperty(properties, propertyName) {
|
|
3
|
+
if (!properties) return void 0;
|
|
4
|
+
for (let index = properties.length - 1; index >= 0; index--) {
|
|
5
|
+
const property = properties[index];
|
|
6
|
+
if (property?.name === propertyName) return property;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
function readPath(value, path) {
|
|
10
|
+
let current = value;
|
|
11
|
+
for (const segment of path) {
|
|
12
|
+
if (current == null) return void 0;
|
|
13
|
+
current = current[segment];
|
|
14
|
+
}
|
|
15
|
+
return current;
|
|
16
|
+
}
|
|
17
|
+
function writePath(value, path, nextValue) {
|
|
18
|
+
if (path.length === 0) return nextValue;
|
|
19
|
+
const root = isObject(value) ? value : {};
|
|
20
|
+
let current = root;
|
|
21
|
+
for (const segment of path.slice(0, -1)) {
|
|
22
|
+
const child = current[segment];
|
|
23
|
+
if (!isObject(child)) current[segment] = {};
|
|
24
|
+
current = current[segment];
|
|
25
|
+
}
|
|
26
|
+
current[path[path.length - 1]] = nextValue;
|
|
27
|
+
return root;
|
|
28
|
+
}
|
|
29
|
+
function dirtyPathForGraphWrite(value, path) {
|
|
30
|
+
if (path[path.length - 1] !== "length") return path;
|
|
31
|
+
const parentPath = path.slice(0, -1);
|
|
32
|
+
const parent = readPath(value, parentPath);
|
|
33
|
+
if (!Array.isArray(parent)) return path;
|
|
34
|
+
return parentPath;
|
|
35
|
+
}
|
|
36
|
+
function deletePath(value, path) {
|
|
37
|
+
if (path.length === 0) throw new TypeError("Cannot delete a graph node root. Delete a property path instead.");
|
|
38
|
+
let current = value;
|
|
39
|
+
for (const segment of path.slice(0, -1)) {
|
|
40
|
+
if (current == null) throw new TypeError(`Cannot delete graph path "${path.join(".")}".`);
|
|
41
|
+
current = current[segment];
|
|
42
|
+
}
|
|
43
|
+
if (current == null) throw new TypeError(`Cannot delete graph path "${path.join(".")}".`);
|
|
44
|
+
if (!isObject(current)) return {
|
|
45
|
+
result: true,
|
|
46
|
+
mutated: false
|
|
47
|
+
};
|
|
48
|
+
const key = path[path.length - 1];
|
|
49
|
+
const hadProperty = Object.prototype.hasOwnProperty.call(current, key);
|
|
50
|
+
const result = delete current[key];
|
|
51
|
+
return {
|
|
52
|
+
result,
|
|
53
|
+
mutated: hadProperty && result
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function pathsIntersect(a, b) {
|
|
57
|
+
return isPrefix(a, b) || isPrefix(b, a);
|
|
58
|
+
}
|
|
59
|
+
function isPrefix(prefix, value) {
|
|
60
|
+
if (prefix.length > value.length) return false;
|
|
61
|
+
return prefix.every((segment, index) => value[index] === segment);
|
|
62
|
+
}
|
|
63
|
+
function isObject(value) {
|
|
64
|
+
return typeof value === "object" && value !== null;
|
|
65
|
+
}
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/graph-collections.ts
|
|
68
|
+
function applyCollectionCall(target, method, args) {
|
|
69
|
+
if (!isSupportedCollectionTarget(target)) throw new TypeError(`Cannot call collection method "${method}" because the graph path is not an Array, Map, Set, or Date.`);
|
|
70
|
+
if (!isSupportedCollectionMethod(method)) throw new TypeError(`Unsupported graph collection method "${method}".`);
|
|
71
|
+
const callable = target[method];
|
|
72
|
+
if (typeof callable !== "function") throw new TypeError(`Unsupported graph collection method "${method}".`);
|
|
73
|
+
return Reflect.apply(callable, target, [...args]);
|
|
74
|
+
}
|
|
75
|
+
function collectionCallMutated(method, result, beforeMutation) {
|
|
76
|
+
if (method === "delete") return result === true;
|
|
77
|
+
if (method === "clear") return beforeMutation?.type === "size" && beforeMutation.value > 0;
|
|
78
|
+
if (method === "pop") return beforeMutation?.type === "size" && beforeMutation.value > 0;
|
|
79
|
+
if (method === "shift") return beforeMutation?.type === "size" && beforeMutation.value > 0;
|
|
80
|
+
if (method === "push" || method === "unshift") return beforeMutation?.type !== "size" || result !== beforeMutation.value;
|
|
81
|
+
if (arrayContentMutationMethod(method)) return beforeMutation?.type !== "array" || !arraySlotsEqual(beforeMutation.slots, beforeMutation.target);
|
|
82
|
+
if (method === "add") return beforeMutation?.type !== "set-add" || !beforeMutation.hadValue;
|
|
83
|
+
if (method === "set") return beforeMutation?.type !== "map-set" || !beforeMutation.hadKey || beforeMutation.valueChanged;
|
|
84
|
+
if (dateMutationMethod(method)) return beforeMutation?.type !== "date" || !Object.is(beforeMutation.time, beforeMutation.target.getTime());
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
function collectionMutationSnapshot(target, method, args) {
|
|
88
|
+
if (method === "add" && target instanceof Set) return {
|
|
89
|
+
type: "set-add",
|
|
90
|
+
hadValue: target.has(args[0])
|
|
91
|
+
};
|
|
92
|
+
if (method === "set" && target instanceof Map) {
|
|
93
|
+
const hadKey = target.has(args[0]);
|
|
94
|
+
return {
|
|
95
|
+
type: "map-set",
|
|
96
|
+
hadKey,
|
|
97
|
+
valueChanged: !hadKey || !Object.is(target.get(args[0]), args[1])
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (target instanceof Date && dateMutationMethod(method)) return {
|
|
101
|
+
type: "date",
|
|
102
|
+
time: target.getTime(),
|
|
103
|
+
target
|
|
104
|
+
};
|
|
105
|
+
if (arrayContentMutationMethod(method) && Array.isArray(target)) return {
|
|
106
|
+
type: "array",
|
|
107
|
+
slots: arraySlotSnapshot(target),
|
|
108
|
+
target
|
|
109
|
+
};
|
|
110
|
+
if (Array.isArray(target)) return {
|
|
111
|
+
type: "size",
|
|
112
|
+
value: target.length
|
|
113
|
+
};
|
|
114
|
+
if (target instanceof Map || target instanceof Set) return {
|
|
115
|
+
type: "size",
|
|
116
|
+
value: target.size
|
|
117
|
+
};
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
function isSupportedCollectionMethod(name) {
|
|
121
|
+
return name === "add" || name === "clear" || name === "copyWithin" || name === "delete" || name === "fill" || name === "pop" || name === "push" || name === "reverse" || name === "set" || name === "shift" || name === "sort" || name === "splice" || name === "unshift" || dateMutationMethod(name);
|
|
122
|
+
}
|
|
123
|
+
function isSupportedCollectionTarget(target) {
|
|
124
|
+
return Array.isArray(target) || target instanceof Map || target instanceof Set || target instanceof Date;
|
|
125
|
+
}
|
|
126
|
+
function arrayContentMutationMethod(method) {
|
|
127
|
+
return method === "copyWithin" || method === "fill" || method === "reverse" || method === "sort" || method === "splice";
|
|
128
|
+
}
|
|
129
|
+
function dateMutationMethod(method) {
|
|
130
|
+
return method === "setDate" || method === "setFullYear" || method === "setHours" || method === "setMilliseconds" || method === "setMinutes" || method === "setMonth" || method === "setSeconds" || method === "setTime" || method === "setUTCDate" || method === "setUTCFullYear" || method === "setUTCHours" || method === "setUTCMilliseconds" || method === "setUTCMinutes" || method === "setUTCMonth" || method === "setUTCSeconds" || method === "setYear";
|
|
131
|
+
}
|
|
132
|
+
function arraySlotSnapshot(target) {
|
|
133
|
+
return Array.from({ length: target.length }, (_, index) => ({
|
|
134
|
+
exists: Object.prototype.hasOwnProperty.call(target, index),
|
|
135
|
+
value: target[index]
|
|
136
|
+
}));
|
|
137
|
+
}
|
|
138
|
+
function arraySlotsEqual(before, after) {
|
|
139
|
+
if (!Array.isArray(after)) return false;
|
|
140
|
+
if (before.length !== after.length) return false;
|
|
141
|
+
return before.every((slot, index) => {
|
|
142
|
+
const exists = Object.prototype.hasOwnProperty.call(after, index);
|
|
143
|
+
return slot.exists === exists && (!slot.exists || Object.is(slot.value, after[index]));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/graph-async.ts
|
|
148
|
+
const ASYNC_SNAPSHOT_META_KEYS = new Set([
|
|
149
|
+
"status",
|
|
150
|
+
"version",
|
|
151
|
+
"key",
|
|
152
|
+
"error",
|
|
153
|
+
"value"
|
|
154
|
+
]);
|
|
155
|
+
function createRuntimeAsyncComputedNodes(asyncComputedInput) {
|
|
156
|
+
const asyncComputedNodes = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const asyncComputed of asyncComputedInput ?? []) {
|
|
158
|
+
const initialSnapshot = asyncComputed.initialSnapshot ?? {
|
|
159
|
+
status: "idle",
|
|
160
|
+
version: 0
|
|
161
|
+
};
|
|
162
|
+
asyncComputedNodes.set(asyncComputed.graphNodeId, {
|
|
163
|
+
...asyncComputed,
|
|
164
|
+
demanded: initialSnapshot.status !== "idle",
|
|
165
|
+
keyValue: "key" in initialSnapshot ? initialSnapshot.key : void 0,
|
|
166
|
+
pendingSnapshotNeedsRunner: initialSnapshot.status === "pending",
|
|
167
|
+
snapshot: initialSnapshot,
|
|
168
|
+
version: initialSnapshot.version
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return asyncComputedNodes;
|
|
172
|
+
}
|
|
173
|
+
function readAsyncComputedNode(node, path, demand) {
|
|
174
|
+
demand();
|
|
175
|
+
const head = path[0];
|
|
176
|
+
if (head === void 0 || ASYNC_SNAPSHOT_META_KEYS.has(head)) return readPath(node.snapshot, path);
|
|
177
|
+
return readPath(node.snapshot.value, path);
|
|
178
|
+
}
|
|
179
|
+
function demandAsyncComputed(input) {
|
|
180
|
+
if (!input.node.pendingSnapshotNeedsRunner && input.node.snapshot.status !== "idle") return;
|
|
181
|
+
input.node.demanded = true;
|
|
182
|
+
startAsyncComputed({
|
|
183
|
+
...input,
|
|
184
|
+
key: input.node.key(input.readGraph)
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function invalidateAsyncComputed(input) {
|
|
188
|
+
if (!input.node.demanded) return;
|
|
189
|
+
const nextKey = input.node.key(input.readGraph);
|
|
190
|
+
if (input.node.snapshot.status !== "idle" && Object.is(input.node.keyValue, nextKey)) return;
|
|
191
|
+
startAsyncComputed({
|
|
192
|
+
...input,
|
|
193
|
+
key: nextKey
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function startAsyncComputed(input) {
|
|
197
|
+
input.node.controller?.abort();
|
|
198
|
+
input.node.pendingSnapshotNeedsRunner = false;
|
|
199
|
+
const controller = new AbortController();
|
|
200
|
+
const version = input.node.version + 1;
|
|
201
|
+
input.node.controller = controller;
|
|
202
|
+
input.node.keyValue = input.key;
|
|
203
|
+
input.node.version = version;
|
|
204
|
+
input.node.snapshot = {
|
|
205
|
+
status: "pending",
|
|
206
|
+
version,
|
|
207
|
+
key: input.key,
|
|
208
|
+
value: input.node.snapshot.value
|
|
209
|
+
};
|
|
210
|
+
const commit = (snapshot) => {
|
|
211
|
+
if (input.node.version !== version || controller.signal.aborted) return;
|
|
212
|
+
input.node.snapshot = snapshot;
|
|
213
|
+
input.markDirtyPath(input.node.graphNodeId, []);
|
|
214
|
+
input.scheduleFlush();
|
|
215
|
+
};
|
|
216
|
+
const commitRejected = (error) => commit({
|
|
217
|
+
status: "rejected",
|
|
218
|
+
version,
|
|
219
|
+
key: input.key,
|
|
220
|
+
error
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
Promise.resolve(input.node.run({
|
|
224
|
+
key: input.key,
|
|
225
|
+
signal: controller.signal,
|
|
226
|
+
read: input.readGraph
|
|
227
|
+
})).then((value) => commit({
|
|
228
|
+
status: "fulfilled",
|
|
229
|
+
version,
|
|
230
|
+
key: input.key,
|
|
231
|
+
value
|
|
232
|
+
}), commitRejected);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
commitRejected(error);
|
|
235
|
+
}
|
|
236
|
+
input.markDirtyPath(input.node.graphNodeId, []);
|
|
237
|
+
input.scheduleFlush();
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/graph-computed.ts
|
|
241
|
+
function createRuntimeComputedNodes(computedInput) {
|
|
242
|
+
const computedNodes = /* @__PURE__ */ new Map();
|
|
243
|
+
for (const computed of computedInput ?? []) computedNodes.set(computed.graphNodeId, {
|
|
244
|
+
...computed,
|
|
245
|
+
dirty: true,
|
|
246
|
+
value: void 0
|
|
247
|
+
});
|
|
248
|
+
return computedNodes;
|
|
249
|
+
}
|
|
250
|
+
function readComputedNode(computed, readGraph, path) {
|
|
251
|
+
if (computed.dirty) {
|
|
252
|
+
computed.value = computed.compute(readGraph);
|
|
253
|
+
computed.dirty = false;
|
|
254
|
+
}
|
|
255
|
+
return readPath(computed.value, path);
|
|
256
|
+
}
|
|
257
|
+
function markComputedDirty(input) {
|
|
258
|
+
if (input.visited.has(input.graphNodeId)) return;
|
|
259
|
+
input.visited.add(input.graphNodeId);
|
|
260
|
+
const computed = input.computedNodes.get(input.graphNodeId);
|
|
261
|
+
if (computed) computed.dirty = true;
|
|
262
|
+
input.dirtyPaths.push({
|
|
263
|
+
graphNodeId: input.graphNodeId,
|
|
264
|
+
path: []
|
|
265
|
+
});
|
|
266
|
+
for (const dependent of input.computedNodes.values()) if (dependent.dependencies.some((dependency) => dependency.graphNodeId === input.graphNodeId)) markComputedDirty({
|
|
267
|
+
...input,
|
|
268
|
+
graphNodeId: dependent.graphNodeId
|
|
269
|
+
});
|
|
270
|
+
for (const dependent of input.asyncComputedNodes.values()) if (dependent.dependencies.some((dependency) => dependency.graphNodeId === input.graphNodeId)) input.invalidateAsyncComputed(dependent);
|
|
271
|
+
}
|
|
272
|
+
function markDirtyComputedDependencies(input) {
|
|
273
|
+
for (const computed of input.computedNodes.values()) if (computed.dependencies.some((dependency) => dependency.graphNodeId === input.graphNodeId && pathsIntersect(input.path, dependency.path ?? []))) markComputedDirty({
|
|
274
|
+
...input,
|
|
275
|
+
graphNodeId: computed.graphNodeId,
|
|
276
|
+
visited: /* @__PURE__ */ new Set()
|
|
277
|
+
});
|
|
278
|
+
for (const asyncComputed of input.asyncComputedNodes.values()) if (asyncComputed.dependencies.some((dependency) => dependency.graphNodeId === input.graphNodeId && pathsIntersect(input.path, dependency.path ?? []))) input.invalidateAsyncComputed(asyncComputed);
|
|
279
|
+
}
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/graph-scheduler.ts
|
|
282
|
+
function appendJournalResult(journal, result) {
|
|
283
|
+
if (!result) return;
|
|
284
|
+
if (isDomJournalEntryArray(result)) {
|
|
285
|
+
journal.push(...result);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
journal.push(result);
|
|
289
|
+
}
|
|
290
|
+
function scheduleMicrotask(callback) {
|
|
291
|
+
if (typeof queueMicrotask === "function") {
|
|
292
|
+
queueMicrotask(callback);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
Promise.resolve().then(callback);
|
|
296
|
+
}
|
|
297
|
+
function isDomJournalEntryArray(result) {
|
|
298
|
+
return Array.isArray(result);
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/graph-shared.ts
|
|
302
|
+
function createSharedGraphPlane(input) {
|
|
303
|
+
const sharedDefinitions = /* @__PURE__ */ new Map();
|
|
304
|
+
const sharedPatches = [];
|
|
305
|
+
for (const definition of input.sharedDefinitionsInput ?? []) sharedDefinitions.set(definition.id, definition);
|
|
306
|
+
const resolveSharedGraphPath = (definitionId, propertyName, path) => {
|
|
307
|
+
const definition = sharedDefinitions.get(definitionId);
|
|
308
|
+
if (!definition) return void 0;
|
|
309
|
+
const property = findLastSharedReturnProperty(definition.returnProperties, propertyName);
|
|
310
|
+
if (!property || property.kind !== "graph") return void 0;
|
|
311
|
+
return {
|
|
312
|
+
definition,
|
|
313
|
+
graphNodeId: property.graphNodeId,
|
|
314
|
+
graphPath: [...property.path, ...path],
|
|
315
|
+
exposedPath: [property.name, ...path]
|
|
316
|
+
};
|
|
317
|
+
};
|
|
318
|
+
const setSharedDefinitionVersion = (definition, version) => {
|
|
319
|
+
const nextDefinition = {
|
|
320
|
+
...definition,
|
|
321
|
+
version
|
|
322
|
+
};
|
|
323
|
+
sharedDefinitions.set(definition.id, nextDefinition);
|
|
324
|
+
return nextDefinition;
|
|
325
|
+
};
|
|
326
|
+
const nextSharedDefinitionVersion = (definition) => setSharedDefinitionVersion(definition, definition.version + 1);
|
|
327
|
+
const applySharedPatch = (patch) => {
|
|
328
|
+
const definition = sharedDefinitions.get(patch.id);
|
|
329
|
+
if (!definition) return false;
|
|
330
|
+
if (patch.version <= definition.version) return false;
|
|
331
|
+
if (patch.scope && definition.scope && patch.scope !== definition.scope) return false;
|
|
332
|
+
if (patch.patch.length === 0) return false;
|
|
333
|
+
const resolvedPatches = [];
|
|
334
|
+
for (const [operation, exposedPath, value] of patch.patch) {
|
|
335
|
+
if (operation !== "set") return false;
|
|
336
|
+
const [propertyName, ...path] = exposedPath;
|
|
337
|
+
if (!propertyName) return false;
|
|
338
|
+
const resolved = resolveSharedGraphPath(patch.id, propertyName, path);
|
|
339
|
+
if (!resolved) return false;
|
|
340
|
+
resolvedPatches.push({
|
|
341
|
+
graphNodeId: resolved.graphNodeId,
|
|
342
|
+
graphPath: resolved.graphPath,
|
|
343
|
+
value
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
for (const resolved of resolvedPatches) {
|
|
347
|
+
const current = input.cells.get(resolved.graphNodeId);
|
|
348
|
+
input.cells.set(resolved.graphNodeId, writePath(current, resolved.graphPath, resolved.value));
|
|
349
|
+
input.markDirtyPath(resolved.graphNodeId, dirtyPathForGraphWrite(current, resolved.graphPath));
|
|
350
|
+
}
|
|
351
|
+
setSharedDefinitionVersion(definition, patch.version);
|
|
352
|
+
input.scheduleFlush();
|
|
353
|
+
return true;
|
|
354
|
+
};
|
|
355
|
+
return {
|
|
356
|
+
readShared(definitionId, propertyName, path = []) {
|
|
357
|
+
const resolved = resolveSharedGraphPath(definitionId, propertyName, path);
|
|
358
|
+
if (!resolved) return void 0;
|
|
359
|
+
return input.readGraph(resolved.graphNodeId, resolved.graphPath);
|
|
360
|
+
},
|
|
361
|
+
writeShared(write) {
|
|
362
|
+
const target = resolveSharedGraphPath(write.definitionId, write.propertyName, write.path ?? []);
|
|
363
|
+
if (!target) return false;
|
|
364
|
+
const current = input.cells.get(target.graphNodeId);
|
|
365
|
+
input.cells.set(target.graphNodeId, writePath(current, target.graphPath, write.value));
|
|
366
|
+
const nextDefinition = nextSharedDefinitionVersion(target.definition);
|
|
367
|
+
sharedPatches.push({
|
|
368
|
+
id: nextDefinition.id,
|
|
369
|
+
...nextDefinition.scope ? { scope: nextDefinition.scope } : {},
|
|
370
|
+
version: nextDefinition.version,
|
|
371
|
+
patch: [[
|
|
372
|
+
"set",
|
|
373
|
+
target.exposedPath,
|
|
374
|
+
write.value
|
|
375
|
+
]]
|
|
376
|
+
});
|
|
377
|
+
input.markDirtyPath(target.graphNodeId, dirtyPathForGraphWrite(current, target.graphPath));
|
|
378
|
+
input.scheduleFlush();
|
|
379
|
+
return true;
|
|
380
|
+
},
|
|
381
|
+
getSharedDefinition(definitionId) {
|
|
382
|
+
return sharedDefinitions.get(definitionId);
|
|
383
|
+
},
|
|
384
|
+
listSharedDefinitions() {
|
|
385
|
+
return [...sharedDefinitions.values()];
|
|
386
|
+
},
|
|
387
|
+
takeSharedPatches() {
|
|
388
|
+
return sharedPatches.splice(0);
|
|
389
|
+
},
|
|
390
|
+
applySharedPatch
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
//#endregion
|
|
394
|
+
//#region src/graph.ts
|
|
395
|
+
function createRuntimeGraph(input) {
|
|
396
|
+
const cells = /* @__PURE__ */ new Map();
|
|
397
|
+
const computedNodes = createRuntimeComputedNodes(input.computed);
|
|
398
|
+
const asyncComputedNodes = createRuntimeAsyncComputedNodes(input.asyncComputed);
|
|
399
|
+
const subscriptions = [];
|
|
400
|
+
const journalListeners = [];
|
|
401
|
+
const dirtyPaths = [];
|
|
402
|
+
const journal = [];
|
|
403
|
+
let flushScheduled = false;
|
|
404
|
+
let flushing = false;
|
|
405
|
+
let activeFlush;
|
|
406
|
+
for (const cell of input.cells) cells.set(cell.graphNodeId, cell.value);
|
|
407
|
+
const readGraph = (graphNodeId, path = []) => {
|
|
408
|
+
const computed = computedNodes.get(graphNodeId);
|
|
409
|
+
if (computed) return readComputedNode(computed, readGraph, path);
|
|
410
|
+
const asyncComputed = asyncComputedNodes.get(graphNodeId);
|
|
411
|
+
if (asyncComputed) return readAsyncComputedNode(asyncComputed, path, () => demandAsyncComputed$1(asyncComputed));
|
|
412
|
+
return readPath(cells.get(graphNodeId), path);
|
|
413
|
+
};
|
|
414
|
+
const markDirtyPath = (graphNodeId, path) => {
|
|
415
|
+
dirtyPaths.push({
|
|
416
|
+
graphNodeId,
|
|
417
|
+
path
|
|
418
|
+
});
|
|
419
|
+
markDirtyComputedDependencies({
|
|
420
|
+
graphNodeId,
|
|
421
|
+
path,
|
|
422
|
+
computedNodes,
|
|
423
|
+
asyncComputedNodes,
|
|
424
|
+
dirtyPaths,
|
|
425
|
+
invalidateAsyncComputed: invalidateAsyncComputed$1
|
|
426
|
+
});
|
|
427
|
+
};
|
|
428
|
+
const scheduleFlush = () => {
|
|
429
|
+
if (flushScheduled || flushing) return;
|
|
430
|
+
flushScheduled = true;
|
|
431
|
+
scheduleMicrotask(() => {
|
|
432
|
+
flush();
|
|
433
|
+
});
|
|
434
|
+
};
|
|
435
|
+
const demandAsyncComputed$1 = (node) => {
|
|
436
|
+
demandAsyncComputed({
|
|
437
|
+
node,
|
|
438
|
+
readGraph,
|
|
439
|
+
markDirtyPath,
|
|
440
|
+
scheduleFlush
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
const invalidateAsyncComputed$1 = (node) => {
|
|
444
|
+
invalidateAsyncComputed({
|
|
445
|
+
node,
|
|
446
|
+
readGraph,
|
|
447
|
+
markDirtyPath,
|
|
448
|
+
scheduleFlush
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
const sharedGraph = createSharedGraphPlane({
|
|
452
|
+
cells,
|
|
453
|
+
sharedDefinitionsInput: input.sharedDefinitions,
|
|
454
|
+
readGraph,
|
|
455
|
+
markDirtyPath,
|
|
456
|
+
scheduleFlush
|
|
457
|
+
});
|
|
458
|
+
const flush = () => {
|
|
459
|
+
if (activeFlush) return activeFlush;
|
|
460
|
+
activeFlush = runFlush();
|
|
461
|
+
return activeFlush;
|
|
462
|
+
};
|
|
463
|
+
const runFlush = async () => {
|
|
464
|
+
flushScheduled = false;
|
|
465
|
+
flushing = true;
|
|
466
|
+
try {
|
|
467
|
+
try {
|
|
468
|
+
while (dirtyPaths.length > 0) {
|
|
469
|
+
const pending = dirtyPaths.splice(0);
|
|
470
|
+
const ranSubscriptions = /* @__PURE__ */ new Set();
|
|
471
|
+
for (const subscription of subscriptions) {
|
|
472
|
+
const subscriptionPath = subscription.path ?? [];
|
|
473
|
+
if (!pending.some((path) => path.graphNodeId === subscription.graphNodeId && pathsIntersect(path.path, subscriptionPath)) || ranSubscriptions.has(subscription.id)) continue;
|
|
474
|
+
ranSubscriptions.add(subscription.id);
|
|
475
|
+
appendJournalResult(journal, await subscription.run(readGraph(subscription.graphNodeId, subscriptionPath)));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
} finally {
|
|
479
|
+
flushing = false;
|
|
480
|
+
}
|
|
481
|
+
await notifyJournalListeners();
|
|
482
|
+
} finally {
|
|
483
|
+
activeFlush = void 0;
|
|
484
|
+
if (dirtyPaths.length > 0) scheduleFlush();
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
const notifyJournalListeners = async () => {
|
|
488
|
+
if (journalListeners.length === 0 || journal.length === 0) return;
|
|
489
|
+
const entries = journal.splice(0);
|
|
490
|
+
for (const listener of journalListeners) await listener(entries);
|
|
491
|
+
};
|
|
492
|
+
return {
|
|
493
|
+
read: readGraph,
|
|
494
|
+
readShared: sharedGraph.readShared,
|
|
495
|
+
writeShared: sharedGraph.writeShared,
|
|
496
|
+
getSharedDefinition: sharedGraph.getSharedDefinition,
|
|
497
|
+
listSharedDefinitions: sharedGraph.listSharedDefinitions,
|
|
498
|
+
takeSharedPatches: sharedGraph.takeSharedPatches,
|
|
499
|
+
applySharedPatch: sharedGraph.applySharedPatch,
|
|
500
|
+
write(write) {
|
|
501
|
+
const path = write.path ?? [];
|
|
502
|
+
const current = cells.get(write.graphNodeId);
|
|
503
|
+
if (Object.is(readPath(current, path), write.value)) return;
|
|
504
|
+
cells.set(write.graphNodeId, writePath(current, path, write.value));
|
|
505
|
+
markDirtyPath(write.graphNodeId, dirtyPathForGraphWrite(current, path));
|
|
506
|
+
scheduleFlush();
|
|
507
|
+
},
|
|
508
|
+
update(update) {
|
|
509
|
+
const path = update.path ?? [];
|
|
510
|
+
const currentValue = readPath(cells.get(update.graphNodeId), path);
|
|
511
|
+
const nextValue = update.update(currentValue);
|
|
512
|
+
if (!Object.is(currentValue, nextValue)) {
|
|
513
|
+
const current = cells.get(update.graphNodeId);
|
|
514
|
+
cells.set(update.graphNodeId, writePath(current, path, nextValue));
|
|
515
|
+
markDirtyPath(update.graphNodeId, dirtyPathForGraphWrite(current, path));
|
|
516
|
+
scheduleFlush();
|
|
517
|
+
}
|
|
518
|
+
if (update.returnValue === "previous") return currentValue;
|
|
519
|
+
if (update.returnValue === "next") return nextValue;
|
|
520
|
+
},
|
|
521
|
+
call(call) {
|
|
522
|
+
const path = call.path ?? [];
|
|
523
|
+
const target = readPath(cells.get(call.graphNodeId), path);
|
|
524
|
+
const beforeMutation = collectionMutationSnapshot(target, call.method, call.args ?? []);
|
|
525
|
+
const result = applyCollectionCall(target, call.method, call.args ?? []);
|
|
526
|
+
if (collectionCallMutated(call.method, result, beforeMutation)) {
|
|
527
|
+
markDirtyPath(call.graphNodeId, path);
|
|
528
|
+
scheduleFlush();
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
},
|
|
532
|
+
delete(deletion) {
|
|
533
|
+
const outcome = deletePath(cells.get(deletion.graphNodeId), deletion.path);
|
|
534
|
+
if (outcome.mutated) {
|
|
535
|
+
markDirtyPath(deletion.graphNodeId, deletion.path);
|
|
536
|
+
scheduleFlush();
|
|
537
|
+
}
|
|
538
|
+
return outcome.result;
|
|
539
|
+
},
|
|
540
|
+
subscribe(subscription) {
|
|
541
|
+
subscriptions.push(subscription);
|
|
542
|
+
return () => {
|
|
543
|
+
const index = subscriptions.indexOf(subscription);
|
|
544
|
+
if (index >= 0) subscriptions.splice(index, 1);
|
|
545
|
+
};
|
|
546
|
+
},
|
|
547
|
+
subscribeJournal(listener) {
|
|
548
|
+
journalListeners.push(listener);
|
|
549
|
+
return () => {
|
|
550
|
+
const index = journalListeners.indexOf(listener);
|
|
551
|
+
if (index >= 0) journalListeners.splice(index, 1);
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
flush,
|
|
555
|
+
takeJournal() {
|
|
556
|
+
return journal.splice(0);
|
|
557
|
+
}
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
export { createRuntimeGraph as t };
|
package/dist/graph.d.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
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
|
+
};
|
|
8
|
+
type RuntimeGraphRead = (graphNodeId: string, path?: ReadonlyArray<string>) => unknown;
|
|
9
|
+
type RuntimeGraphComputedDependency = {
|
|
10
|
+
readonly graphNodeId: string;
|
|
11
|
+
readonly path?: ReadonlyArray<string>;
|
|
12
|
+
};
|
|
13
|
+
type RuntimeGraphComputed = {
|
|
14
|
+
readonly graphNodeId: string;
|
|
15
|
+
readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
|
|
16
|
+
readonly compute: (read: RuntimeGraphRead) => unknown;
|
|
17
|
+
};
|
|
18
|
+
type RuntimeGraphAsyncSnapshot = {
|
|
19
|
+
readonly status: 'idle';
|
|
20
|
+
readonly version: 0;
|
|
21
|
+
} | {
|
|
22
|
+
readonly status: 'pending';
|
|
23
|
+
readonly version: number;
|
|
24
|
+
readonly key: unknown;
|
|
25
|
+
readonly value?: unknown;
|
|
26
|
+
} | {
|
|
27
|
+
readonly status: 'fulfilled';
|
|
28
|
+
readonly version: number;
|
|
29
|
+
readonly key: unknown;
|
|
30
|
+
readonly value: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
readonly status: 'rejected';
|
|
33
|
+
readonly version: number;
|
|
34
|
+
readonly key: unknown;
|
|
35
|
+
readonly error: unknown;
|
|
36
|
+
};
|
|
37
|
+
type RuntimeGraphAsyncComputed = {
|
|
38
|
+
readonly graphNodeId: string;
|
|
39
|
+
readonly dependencies: ReadonlyArray<RuntimeGraphComputedDependency>;
|
|
40
|
+
readonly initialSnapshot?: RuntimeGraphAsyncSnapshot;
|
|
41
|
+
readonly key: (read: RuntimeGraphRead) => unknown;
|
|
42
|
+
readonly run: (input: {
|
|
43
|
+
readonly key: unknown;
|
|
44
|
+
readonly signal: AbortSignal;
|
|
45
|
+
readonly read: RuntimeGraphRead;
|
|
46
|
+
}) => unknown | Promise<unknown>;
|
|
47
|
+
};
|
|
48
|
+
type DomJournalEntry = {
|
|
49
|
+
readonly type: 'setText';
|
|
50
|
+
readonly locator: string;
|
|
51
|
+
readonly value: unknown;
|
|
52
|
+
} | {
|
|
53
|
+
readonly type: 'setAttr' | 'setProp';
|
|
54
|
+
readonly locator: string;
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly value: unknown;
|
|
57
|
+
} | {
|
|
58
|
+
readonly type: 'insertRange';
|
|
59
|
+
readonly locator: string;
|
|
60
|
+
readonly fragment: unknown;
|
|
61
|
+
} | {
|
|
62
|
+
readonly type: 'removeRange';
|
|
63
|
+
readonly locator: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly type: 'moveRange';
|
|
66
|
+
readonly locator: string;
|
|
67
|
+
readonly before: string;
|
|
68
|
+
} | {
|
|
69
|
+
readonly type: 'runCleanup';
|
|
70
|
+
readonly locator: string;
|
|
71
|
+
};
|
|
72
|
+
type DomJournalResult = DomJournalEntry | ReadonlyArray<DomJournalEntry>;
|
|
73
|
+
type DomJournalListener = (entries: ReadonlyArray<DomJournalEntry>) => void | Promise<void>;
|
|
74
|
+
type RuntimeGraphInput = {
|
|
75
|
+
readonly cells: ReadonlyArray<RuntimeGraphCell>;
|
|
76
|
+
readonly computed?: ReadonlyArray<RuntimeGraphComputed>;
|
|
77
|
+
readonly asyncComputed?: ReadonlyArray<RuntimeGraphAsyncComputed>;
|
|
78
|
+
readonly sharedDefinitions?: ProtocolStatePayload['sharedDefinitions'];
|
|
79
|
+
};
|
|
80
|
+
type RuntimeGraphWrite = {
|
|
81
|
+
readonly graphNodeId: string;
|
|
82
|
+
readonly path?: ReadonlyArray<string>;
|
|
83
|
+
readonly value: unknown;
|
|
84
|
+
};
|
|
85
|
+
type RuntimeGraphSharedWrite = {
|
|
86
|
+
readonly definitionId: string;
|
|
87
|
+
readonly propertyName: string;
|
|
88
|
+
readonly path?: ReadonlyArray<string>;
|
|
89
|
+
readonly value: unknown;
|
|
90
|
+
};
|
|
91
|
+
type RuntimeGraphSharedPatchOperation = readonly [operation: 'set', path: ReadonlyArray<string>, value: unknown];
|
|
92
|
+
type RuntimeGraphSharedPatch = {
|
|
93
|
+
readonly id: string;
|
|
94
|
+
readonly scope?: RuntimeSharedDefinition['scope'];
|
|
95
|
+
readonly version: number;
|
|
96
|
+
readonly patch: ReadonlyArray<RuntimeGraphSharedPatchOperation>;
|
|
97
|
+
};
|
|
98
|
+
type RuntimeGraphUpdate = {
|
|
99
|
+
readonly graphNodeId: string;
|
|
100
|
+
readonly path?: ReadonlyArray<string>;
|
|
101
|
+
readonly update: (value: unknown) => unknown;
|
|
102
|
+
readonly returnValue?: 'previous' | 'next';
|
|
103
|
+
};
|
|
104
|
+
type RuntimeGraphCall = {
|
|
105
|
+
readonly graphNodeId: string;
|
|
106
|
+
readonly path?: ReadonlyArray<string>;
|
|
107
|
+
readonly method: string;
|
|
108
|
+
readonly args?: ReadonlyArray<unknown>;
|
|
109
|
+
};
|
|
110
|
+
type RuntimeGraphDelete = {
|
|
111
|
+
readonly graphNodeId: string;
|
|
112
|
+
readonly path: ReadonlyArray<string>;
|
|
113
|
+
};
|
|
114
|
+
type RuntimeGraphSubscription = {
|
|
115
|
+
readonly id: string;
|
|
116
|
+
readonly graphNodeId: string;
|
|
117
|
+
readonly path?: ReadonlyArray<string>;
|
|
118
|
+
readonly run: (value: unknown) => DomJournalResult | void | Promise<DomJournalResult | void>;
|
|
119
|
+
};
|
|
120
|
+
type RuntimeGraph = {
|
|
121
|
+
readonly read: (graphNodeId: string, path?: ReadonlyArray<string>) => unknown;
|
|
122
|
+
readonly readShared: (definitionId: string, propertyName: string, path?: ReadonlyArray<string>) => unknown;
|
|
123
|
+
readonly writeShared: (write: RuntimeGraphSharedWrite) => boolean;
|
|
124
|
+
readonly getSharedDefinition: (definitionId: string) => NonNullable<ProtocolStatePayload['sharedDefinitions']>[number] | undefined;
|
|
125
|
+
readonly listSharedDefinitions: () => NonNullable<ProtocolStatePayload['sharedDefinitions']>;
|
|
126
|
+
readonly takeSharedPatches: () => RuntimeGraphSharedPatch[];
|
|
127
|
+
readonly applySharedPatch: (patch: RuntimeGraphSharedPatch) => boolean;
|
|
128
|
+
readonly write: (write: RuntimeGraphWrite) => void;
|
|
129
|
+
readonly update: (update: RuntimeGraphUpdate) => unknown;
|
|
130
|
+
readonly call: (call: RuntimeGraphCall) => unknown;
|
|
131
|
+
readonly delete: (deletion: RuntimeGraphDelete) => boolean;
|
|
132
|
+
readonly subscribe: (subscription: RuntimeGraphSubscription) => () => void;
|
|
133
|
+
readonly subscribeJournal: (listener: DomJournalListener) => () => void;
|
|
134
|
+
readonly flush: () => Promise<void>;
|
|
135
|
+
readonly takeJournal: () => DomJournalEntry[];
|
|
136
|
+
};
|
|
137
|
+
type RuntimeSharedDefinition = NonNullable<ProtocolStatePayload['sharedDefinitions']>[number];
|
|
138
|
+
declare function createRuntimeGraph(input: RuntimeGraphInput): RuntimeGraph;
|
|
139
|
+
//#endregion
|
|
140
|
+
export { DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, createRuntimeGraph };
|
package/dist/graph.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { DomJournalEntry, DomJournalListener, DomJournalResult, RuntimeGraph, RuntimeGraphAsyncComputed, RuntimeGraphAsyncSnapshot, RuntimeGraphCall, RuntimeGraphCell, RuntimeGraphComputed, RuntimeGraphComputedDependency, 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, RuntimeGraphDelete, RuntimeGraphInput, RuntimeGraphRead, RuntimeGraphSharedPatch, RuntimeGraphSharedPatchOperation, RuntimeGraphSharedWrite, RuntimeGraphSubscription, RuntimeGraphUpdate, RuntimeGraphWrite, createRuntimeGraph };
|
package/dist/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@markless/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Markless state graph runtime: reads, writes, computed invalidation, flush journal.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/markless-dev/markless.git",
|
|
9
|
+
"directory": "packages/runtime"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./graph": {
|
|
22
|
+
"types": "./dist/graph.d.ts",
|
|
23
|
+
"default": "./dist/graph.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@markless/serializer": "0.1.0"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {}
|
|
33
|
+
}
|