doxum 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -27
- package/dist/{contract-DGkZcTiP.d.cts → contract-BsY1XLiG.d.ts} +2 -1
- package/dist/{contract-Dh3az8aN.d.ts → contract-D8kjdfqT.d.cts} +2 -1
- package/dist/{access-5D4KKFm7.js → dependency-C5_R1tvQ.js} +40 -262
- package/dist/dependency-C5_R1tvQ.js.map +1 -0
- package/dist/{access-z56fVqZ2.cjs → dependency-Dfzs3khk.cjs} +62 -344
- package/dist/dependency-Dfzs3khk.cjs.map +1 -0
- package/dist/driver-CNxqMVFH.cjs +69 -0
- package/dist/driver-CNxqMVFH.cjs.map +1 -0
- package/dist/driver-CbzfW5MR.js +46 -0
- package/dist/driver-CbzfW5MR.js.map +1 -0
- package/dist/index.cjs +2636 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2594 -4
- package/dist/index.js.map +1 -1
- package/dist/integration.cjs +2 -3
- package/dist/integration.cjs.map +1 -1
- package/dist/integration.d.cts +1 -1
- package/dist/integration.d.ts +1 -1
- package/dist/integration.js +1 -2
- package/dist/integration.js.map +1 -1
- package/dist/local-sync.cjs +154 -329
- package/dist/local-sync.cjs.map +1 -1
- package/dist/local-sync.d.cts +17 -22
- package/dist/local-sync.d.ts +17 -22
- package/dist/local-sync.js +149 -326
- package/dist/local-sync.js.map +1 -1
- package/dist/ownership-B-VAPcce.js +242 -0
- package/dist/ownership-B-VAPcce.js.map +1 -0
- package/dist/ownership-CU-9DKJQ.cjs +301 -0
- package/dist/ownership-CU-9DKJQ.cjs.map +1 -0
- package/package.json +1 -1
- package/skills/doxum-runtime/references/guide.en.md +52 -1
- package/skills/doxum-runtime/references/guide.zh-CN.md +46 -1
- package/skills/doxum-runtime/references/invariants.en.md +14 -4
- package/skills/doxum-runtime/references/invariants.zh-CN.md +1 -1
- package/dist/access-5D4KKFm7.js.map +0 -1
- package/dist/access-z56fVqZ2.cjs.map +0 -1
- package/dist/dependency-BRn1TRDi.js +0 -20
- package/dist/dependency-BRn1TRDi.js.map +0 -1
- package/dist/dependency-DmQXyj7q.cjs +0 -25
- package/dist/dependency-DmQXyj7q.cjs.map +0 -1
- package/dist/readable-B8E3FMq6.cjs +0 -2614
- package/dist/readable-B8E3FMq6.cjs.map +0 -1
- package/dist/readable-CIj02gIj.js +0 -2579
- package/dist/readable-CIj02gIj.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,29 +1,2619 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const
|
|
3
|
-
const
|
|
4
|
-
const
|
|
2
|
+
const require_dependency = require("./dependency-Dfzs3khk.cjs");
|
|
3
|
+
const require_ownership = require("./ownership-CU-9DKJQ.cjs");
|
|
4
|
+
const require_driver = require("./driver-CNxqMVFH.cjs");
|
|
5
|
+
//#region core/src/impact.ts
|
|
6
|
+
const emptyCollectionImpact = Object.freeze({
|
|
7
|
+
kind: "incremental",
|
|
8
|
+
added: /* @__PURE__ */ new Set(),
|
|
9
|
+
removed: /* @__PURE__ */ new Set(),
|
|
10
|
+
updated: /* @__PURE__ */ new Set(),
|
|
11
|
+
orderChanged: false
|
|
12
|
+
});
|
|
13
|
+
const resetCollectionImpact = Object.freeze({ kind: "reset" });
|
|
14
|
+
const createImpact = (input) => {
|
|
15
|
+
const kind = input.reset ? "reset" : "incremental";
|
|
16
|
+
const paths = input.paths ?? input.operations.map((operation) => operation.at);
|
|
17
|
+
let pathBuckets;
|
|
18
|
+
const somePath = (address, test) => {
|
|
19
|
+
if (address.length === 0 || paths.length <= 8) return paths.some(test);
|
|
20
|
+
if (!pathBuckets) {
|
|
21
|
+
pathBuckets = /* @__PURE__ */ new Map();
|
|
22
|
+
for (const changed of paths) {
|
|
23
|
+
const first = changed[0];
|
|
24
|
+
const bucket = pathBuckets.get(first) ?? [];
|
|
25
|
+
bucket.push(changed);
|
|
26
|
+
pathBuckets.set(first, bucket);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (pathBuckets.get(void 0)?.some(test)) return true;
|
|
30
|
+
return pathBuckets.get(address[0])?.some(test) ?? false;
|
|
31
|
+
};
|
|
32
|
+
const pathsAffect = (address) => address.length === 0 ? paths.length > 0 : somePath(address, (changed) => require_dependency.overlaps(changed, address));
|
|
33
|
+
let collectionCache;
|
|
34
|
+
const findCollection = (address) => input.collections?.find((entry) => entry.address.length === address.length && entry.address.every((segment, index) => segment === address[index]));
|
|
35
|
+
return {
|
|
36
|
+
kind,
|
|
37
|
+
operations: input.operations,
|
|
38
|
+
affects: (value) => {
|
|
39
|
+
require_ownership.profile.impact.affects();
|
|
40
|
+
if (kind === "reset") return true;
|
|
41
|
+
if (!require_dependency.belongs(value, input.schema)) return false;
|
|
42
|
+
if (pathsAffect(require_dependency.address(value))) return true;
|
|
43
|
+
if (value.kind === "collection") {
|
|
44
|
+
const collection = findCollection(require_dependency.address(value));
|
|
45
|
+
if (!collection) return false;
|
|
46
|
+
const id$1 = require_dependency.id(value);
|
|
47
|
+
if (id$1 !== void 0) return collection.added.has(id$1) || collection.removed.has(id$1) || collection.updated.has(id$1);
|
|
48
|
+
return collection.added.size > 0 || collection.removed.size > 0 || collection.updated.size > 0 || collection.orderChanged;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
},
|
|
52
|
+
collection: (selector) => {
|
|
53
|
+
if (selector.schema !== input.schema) throw new Error("Collection selector belongs to another schema.");
|
|
54
|
+
if (kind === "reset") return resetCollectionImpact;
|
|
55
|
+
const cached = collectionCache?.find((entry) => entry.address.length === selector.address.length && entry.address.every((segment, index) => segment === selector.address[index]))?.value;
|
|
56
|
+
if (cached) return cached;
|
|
57
|
+
const exact = findCollection(selector.address);
|
|
58
|
+
if (somePath(selector.address, (changed) => require_dependency.contains(changed, selector.address) && changed.length < selector.address.length)) {
|
|
59
|
+
const reset = { kind: "reset" };
|
|
60
|
+
(collectionCache ??= []).push({
|
|
61
|
+
address: selector.address,
|
|
62
|
+
value: reset
|
|
63
|
+
});
|
|
64
|
+
return reset;
|
|
65
|
+
}
|
|
66
|
+
if (!exact) {
|
|
67
|
+
const empty = emptyCollectionImpact;
|
|
68
|
+
(collectionCache ??= []).push({
|
|
69
|
+
address: selector.address,
|
|
70
|
+
value: empty
|
|
71
|
+
});
|
|
72
|
+
return empty;
|
|
73
|
+
}
|
|
74
|
+
const incremental = {
|
|
75
|
+
kind: "incremental",
|
|
76
|
+
added: exact.added,
|
|
77
|
+
removed: exact.removed,
|
|
78
|
+
updated: exact.updated,
|
|
79
|
+
orderChanged: exact.orderChanged
|
|
80
|
+
};
|
|
81
|
+
(collectionCache ??= []).push({
|
|
82
|
+
address: selector.address,
|
|
83
|
+
value: incremental
|
|
84
|
+
});
|
|
85
|
+
return incremental;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region core/src/history.ts
|
|
91
|
+
const createHistory = (input) => {
|
|
92
|
+
const undos = [];
|
|
93
|
+
const redos = [];
|
|
94
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
95
|
+
let state = Object.freeze({
|
|
96
|
+
undoDepth: 0,
|
|
97
|
+
redoDepth: 0
|
|
98
|
+
});
|
|
99
|
+
const current = () => {
|
|
100
|
+
if (state.undoDepth !== undos.length || state.redoDepth !== redos.length) state = Object.freeze({
|
|
101
|
+
undoDepth: undos.length,
|
|
102
|
+
redoDepth: redos.length
|
|
103
|
+
});
|
|
104
|
+
return state;
|
|
105
|
+
};
|
|
106
|
+
const emit = () => {
|
|
107
|
+
if (listeners.size === 0) return;
|
|
108
|
+
current();
|
|
109
|
+
Array.from(listeners).forEach((listener) => listener());
|
|
110
|
+
};
|
|
111
|
+
const clear = () => {
|
|
112
|
+
if (undos.length || redos.length) {
|
|
113
|
+
undos.length = 0;
|
|
114
|
+
redos.length = 0;
|
|
115
|
+
emit();
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
api: {
|
|
120
|
+
current,
|
|
121
|
+
subscribe: (listener) => {
|
|
122
|
+
listeners.add(listener);
|
|
123
|
+
return () => listeners.delete(listener);
|
|
124
|
+
},
|
|
125
|
+
undo: () => {
|
|
126
|
+
const entry = undos.pop();
|
|
127
|
+
if (!entry) return {
|
|
128
|
+
status: "unchanged",
|
|
129
|
+
revision: input.revision()
|
|
130
|
+
};
|
|
131
|
+
const result = input.apply(entry.inverse);
|
|
132
|
+
if (result.status === "committed") {
|
|
133
|
+
redos.push(entry);
|
|
134
|
+
emit();
|
|
135
|
+
} else undos.push(entry);
|
|
136
|
+
return result;
|
|
137
|
+
},
|
|
138
|
+
redo: () => {
|
|
139
|
+
const entry = redos.pop();
|
|
140
|
+
if (!entry) return {
|
|
141
|
+
status: "unchanged",
|
|
142
|
+
revision: input.revision()
|
|
143
|
+
};
|
|
144
|
+
const result = input.apply(entry.operations);
|
|
145
|
+
if (result.status === "committed") {
|
|
146
|
+
undos.push(entry);
|
|
147
|
+
emit();
|
|
148
|
+
} else redos.push(entry);
|
|
149
|
+
return result;
|
|
150
|
+
},
|
|
151
|
+
clear
|
|
152
|
+
},
|
|
153
|
+
record: (operations, inverse) => {
|
|
154
|
+
if (input.capacity <= 0) return;
|
|
155
|
+
undos.push({
|
|
156
|
+
operations,
|
|
157
|
+
inverse
|
|
158
|
+
});
|
|
159
|
+
if (undos.length > input.capacity) undos.shift();
|
|
160
|
+
redos.length = 0;
|
|
161
|
+
emit();
|
|
162
|
+
},
|
|
163
|
+
invalidate: clear
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region core/src/access/writer.ts
|
|
168
|
+
const schemaRoot = (schema) => require_dependency.object(schema.shape);
|
|
169
|
+
const emit = (sink, operation) => sink(operation);
|
|
170
|
+
const writerFor = (node, inputAddress, sink) => {
|
|
171
|
+
const address = Object.freeze(inputAddress);
|
|
172
|
+
if (node.kind === "field") return {
|
|
173
|
+
set: (value) => emit(sink, {
|
|
174
|
+
type: "field.set",
|
|
175
|
+
at: address,
|
|
176
|
+
value
|
|
177
|
+
}),
|
|
178
|
+
clear: node.optional ? () => emit(sink, {
|
|
179
|
+
type: "field.clear",
|
|
180
|
+
at: address
|
|
181
|
+
}) : void 0
|
|
182
|
+
};
|
|
183
|
+
if (node.kind === "dict" || node.kind === "record") return {
|
|
184
|
+
set: (key, value) => emit(sink, {
|
|
185
|
+
type: "dict.set",
|
|
186
|
+
at: address,
|
|
187
|
+
key,
|
|
188
|
+
value
|
|
189
|
+
}),
|
|
190
|
+
delete: (key) => emit(sink, {
|
|
191
|
+
type: "dict.delete",
|
|
192
|
+
at: address,
|
|
193
|
+
key
|
|
194
|
+
}),
|
|
195
|
+
replace: (value) => emit(sink, {
|
|
196
|
+
type: "dict.replace",
|
|
197
|
+
at: address,
|
|
198
|
+
value
|
|
199
|
+
})
|
|
200
|
+
};
|
|
201
|
+
if (node.kind === "list") return {
|
|
202
|
+
insert: (value, anchor) => emit(sink, {
|
|
203
|
+
type: "list.insert",
|
|
204
|
+
at: address,
|
|
205
|
+
key: node.keyOf(value),
|
|
206
|
+
value,
|
|
207
|
+
anchor
|
|
208
|
+
}),
|
|
209
|
+
move: (key, anchor) => emit(sink, {
|
|
210
|
+
type: "list.move",
|
|
211
|
+
at: address,
|
|
212
|
+
key,
|
|
213
|
+
anchor
|
|
214
|
+
}),
|
|
215
|
+
remove: (key) => emit(sink, {
|
|
216
|
+
type: "list.remove",
|
|
217
|
+
at: address,
|
|
218
|
+
key
|
|
219
|
+
}),
|
|
220
|
+
replace: (value) => emit(sink, {
|
|
221
|
+
type: "list.replace",
|
|
222
|
+
at: address,
|
|
223
|
+
value,
|
|
224
|
+
keys: value.map(node.keyOf)
|
|
225
|
+
})
|
|
226
|
+
};
|
|
227
|
+
if (node.kind === "tree") return {
|
|
228
|
+
insert: (treeNodeId, value, parentId, index) => emit(sink, {
|
|
229
|
+
type: "tree.insert",
|
|
230
|
+
at: address,
|
|
231
|
+
treeNodeId,
|
|
232
|
+
value,
|
|
233
|
+
parentId,
|
|
234
|
+
index
|
|
235
|
+
}),
|
|
236
|
+
move: (treeNodeId, parentId, index) => emit(sink, {
|
|
237
|
+
type: "tree.move",
|
|
238
|
+
at: address,
|
|
239
|
+
treeNodeId,
|
|
240
|
+
parentId,
|
|
241
|
+
index
|
|
242
|
+
}),
|
|
243
|
+
remove: (treeNodeId) => emit(sink, {
|
|
244
|
+
type: "tree.remove",
|
|
245
|
+
at: address,
|
|
246
|
+
treeNodeId
|
|
247
|
+
}),
|
|
248
|
+
set: (treeNodeId, value) => emit(sink, {
|
|
249
|
+
type: "tree.set",
|
|
250
|
+
at: address,
|
|
251
|
+
treeNodeId,
|
|
252
|
+
value
|
|
253
|
+
}),
|
|
254
|
+
replace: (value) => emit(sink, {
|
|
255
|
+
type: "tree.replace",
|
|
256
|
+
at: address,
|
|
257
|
+
value
|
|
258
|
+
})
|
|
259
|
+
};
|
|
260
|
+
if (node.kind === "table") {
|
|
261
|
+
let items;
|
|
262
|
+
return {
|
|
263
|
+
create: (input, anchor) => {
|
|
264
|
+
emit(sink, {
|
|
265
|
+
type: "entity.create",
|
|
266
|
+
at: address,
|
|
267
|
+
entries: (Array.isArray(input) ? input : [input]).map(({ id, value }) => ({
|
|
268
|
+
id,
|
|
269
|
+
value
|
|
270
|
+
})),
|
|
271
|
+
anchor
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
item: (id) => {
|
|
275
|
+
const cached = items?.get(id);
|
|
276
|
+
if (cached) return cached;
|
|
277
|
+
const writer = writerFor(node.value, [...address, id], sink);
|
|
278
|
+
(items ??= /* @__PURE__ */ new Map()).set(id, writer);
|
|
279
|
+
return writer;
|
|
280
|
+
},
|
|
281
|
+
remove: (input) => emit(sink, {
|
|
282
|
+
type: "entity.remove",
|
|
283
|
+
at: address,
|
|
284
|
+
ids: Array.isArray(input) ? [...input] : [input]
|
|
285
|
+
}),
|
|
286
|
+
move: (id, anchor) => emit(sink, {
|
|
287
|
+
type: "entity.move",
|
|
288
|
+
at: address,
|
|
289
|
+
id,
|
|
290
|
+
anchor
|
|
291
|
+
})
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
if (node.kind === "map") {
|
|
295
|
+
let items;
|
|
296
|
+
return {
|
|
297
|
+
create: (input) => {
|
|
298
|
+
emit(sink, {
|
|
299
|
+
type: "entity.create",
|
|
300
|
+
at: address,
|
|
301
|
+
entries: (Array.isArray(input) ? input : [input]).map(({ id, value }) => ({
|
|
302
|
+
id,
|
|
303
|
+
value
|
|
304
|
+
}))
|
|
305
|
+
});
|
|
306
|
+
},
|
|
307
|
+
item: (id) => {
|
|
308
|
+
const cached = items?.get(id);
|
|
309
|
+
if (cached) return cached;
|
|
310
|
+
const writer = writerFor(node.value, [...address, id], sink);
|
|
311
|
+
(items ??= /* @__PURE__ */ new Map()).set(id, writer);
|
|
312
|
+
return writer;
|
|
313
|
+
},
|
|
314
|
+
remove: (input) => emit(sink, {
|
|
315
|
+
type: "entity.remove",
|
|
316
|
+
at: address,
|
|
317
|
+
ids: Array.isArray(input) ? [...input] : [input]
|
|
318
|
+
})
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
if (node.kind === "single") return writerFor(node.value, address, sink);
|
|
322
|
+
if (node.kind === "variant") return { replace: (value) => emit(sink, {
|
|
323
|
+
type: "variant.replace",
|
|
324
|
+
at: address,
|
|
325
|
+
value
|
|
326
|
+
}) };
|
|
327
|
+
let children;
|
|
328
|
+
return new Proxy({}, { get: (_target, property) => {
|
|
329
|
+
if (typeof property !== "string" || !node.shape[property]) return void 0;
|
|
330
|
+
const cached = children?.get(property);
|
|
331
|
+
if (cached) return cached;
|
|
332
|
+
const writer = writerFor(node.shape[property], [...address, property], sink);
|
|
333
|
+
(children ??= /* @__PURE__ */ new Map()).set(property, writer);
|
|
334
|
+
return writer;
|
|
335
|
+
} });
|
|
336
|
+
};
|
|
337
|
+
const documentWriter = (schema, sink) => writerFor(schemaRoot(schema), [], sink);
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region core/src/mutation/issue.ts
|
|
340
|
+
const ownAddress$1 = (address) => Object.freeze(address.slice());
|
|
341
|
+
const at$1 = (address, code, message, operation) => Object.freeze({
|
|
342
|
+
source: "mutation",
|
|
343
|
+
code,
|
|
344
|
+
address: ownAddress$1(address),
|
|
345
|
+
message,
|
|
346
|
+
...operation === void 0 ? {} : { operation }
|
|
347
|
+
});
|
|
348
|
+
const from = (operation, code, message) => at$1(operation.at, code, message, operation.type);
|
|
349
|
+
const input = (code, message) => at$1([], code, message);
|
|
350
|
+
//#endregion
|
|
351
|
+
//#region core/src/mutation/anchor.ts
|
|
352
|
+
const length = (keys) => keys.length;
|
|
353
|
+
const keyAt = (keys, index) => Array.isArray(keys) ? keys[index] : keys.at(index);
|
|
354
|
+
const keyIndex = (keys, key) => Array.isArray(keys) ? keys.indexOf(key) : keys.index(key);
|
|
355
|
+
const keys = (values, keyOf) => ({
|
|
356
|
+
length: values.length,
|
|
357
|
+
at: (index) => index >= 0 && index < values.length ? keyOf(values[index]) : void 0,
|
|
358
|
+
index: (key) => {
|
|
359
|
+
for (let index = 0; index < values.length; index += 1) if (keyOf(values[index]) === key) return index;
|
|
360
|
+
return -1;
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
const index = (keys, anchor) => {
|
|
364
|
+
if (!anchor) return length(keys);
|
|
365
|
+
if ("at" in anchor) return anchor.at === "start" ? 0 : length(keys);
|
|
366
|
+
if ("before" in anchor) return keyIndex(keys, anchor.before);
|
|
367
|
+
return keyIndex(keys, anchor.after) + 1;
|
|
368
|
+
};
|
|
369
|
+
const at = (keys, position) => position <= 0 ? { at: "start" } : { after: keyAt(keys, position - 1) };
|
|
370
|
+
const valid = (keys, anchor) => {
|
|
371
|
+
if (!anchor) return true;
|
|
372
|
+
if (typeof anchor !== "object" || anchor === null) return false;
|
|
373
|
+
if (Number("at" in anchor) + Number("before" in anchor) + Number("after" in anchor) !== 1) return false;
|
|
374
|
+
if ("at" in anchor) return anchor.at === "start" || anchor.at === "end";
|
|
375
|
+
return keyIndex(keys, "before" in anchor ? anchor.before : anchor.after) >= 0;
|
|
376
|
+
};
|
|
377
|
+
const afterRemove = (keys, removedIndex, anchor) => {
|
|
378
|
+
const position = index(keys, anchor);
|
|
379
|
+
return position > removedIndex ? position - 1 : position;
|
|
380
|
+
};
|
|
381
|
+
const validPositions = (length, positions) => {
|
|
382
|
+
let previous = -1;
|
|
383
|
+
for (let index = 0; index < positions.length; index += 1) {
|
|
384
|
+
const position = positions[index];
|
|
385
|
+
if (!Number.isInteger(position) || position < 0 || position <= previous || position > length + index) return false;
|
|
386
|
+
previous = position;
|
|
387
|
+
}
|
|
388
|
+
return true;
|
|
389
|
+
};
|
|
390
|
+
const restore = (keys, inserted, positions) => {
|
|
391
|
+
const currentLength = keys.length;
|
|
392
|
+
if (inserted.length !== positions.length || !validPositions(currentLength, positions)) throw new Error("Ordered restore positions are invalid.");
|
|
393
|
+
let readIndex = currentLength - 1;
|
|
394
|
+
let insertedIndex = inserted.length - 1;
|
|
395
|
+
keys.length = currentLength + inserted.length;
|
|
396
|
+
for (let writeIndex = keys.length - 1; writeIndex >= 0; writeIndex -= 1) if (insertedIndex >= 0 && positions[insertedIndex] === writeIndex) {
|
|
397
|
+
keys[writeIndex] = inserted[insertedIndex];
|
|
398
|
+
insertedIndex -= 1;
|
|
399
|
+
} else {
|
|
400
|
+
keys[writeIndex] = keys[readIndex];
|
|
401
|
+
readIndex -= 1;
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region core/src/mutation/execute-entity.ts
|
|
406
|
+
const resolveEntityCollection = (node, value) => {
|
|
407
|
+
if (node.kind === "table") {
|
|
408
|
+
if (!require_ownership.isRecord(value) || !Array.isArray(value.ids) || !require_ownership.isRecord(value.byId)) return { status: "invalid" };
|
|
409
|
+
return {
|
|
410
|
+
status: "resolved",
|
|
411
|
+
collection: {
|
|
412
|
+
kind: "table",
|
|
413
|
+
ids: value.ids,
|
|
414
|
+
byId: value.byId
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return require_ownership.isRecord(value) ? {
|
|
419
|
+
status: "resolved",
|
|
420
|
+
collection: {
|
|
421
|
+
kind: "map",
|
|
422
|
+
byId: value
|
|
423
|
+
}
|
|
424
|
+
} : { status: "invalid" };
|
|
425
|
+
};
|
|
426
|
+
const rejected$4 = (operation, code, message) => ({
|
|
427
|
+
status: "rejected",
|
|
428
|
+
issue: from(operation, code, message)
|
|
429
|
+
});
|
|
430
|
+
const executeEntity = (target, operation, copyPayload) => {
|
|
431
|
+
if (target.node.kind !== "table" && target.node.kind !== "map") return rejected$4(operation, "invalid-address", "Collection target is invalid.");
|
|
432
|
+
const resolution = resolveEntityCollection(target.node, target.value);
|
|
433
|
+
if (resolution.status === "invalid") return rejected$4(operation, "invalid-address", "Collection target is invalid.");
|
|
434
|
+
const collection = resolution.collection;
|
|
435
|
+
const ids = collection.kind === "table" ? collection.ids : void 0;
|
|
436
|
+
if (operation.type === "entity.move" && collection.kind === "map") return rejected$4(operation, "invalid-operation", "Map entries are unordered.");
|
|
437
|
+
const byId = collection.byId;
|
|
438
|
+
if (operation.type === "entity.create") {
|
|
439
|
+
if (operation.entries.length === 0) return { status: "unchanged" };
|
|
440
|
+
const seen = /* @__PURE__ */ new Set();
|
|
441
|
+
for (const entry of operation.entries) {
|
|
442
|
+
if (seen.has(entry.id)) return rejected$4(operation, "duplicate-entity", "Entity ids must be unique within a batch.");
|
|
443
|
+
seen.add(entry.id);
|
|
444
|
+
if (Object.prototype.hasOwnProperty.call(byId, entry.id)) return rejected$4(operation, "duplicate-entity", `Entity '${entry.id}' already exists.`);
|
|
445
|
+
}
|
|
446
|
+
if (collection.kind === "map" && (operation.anchor || operation.positions)) return rejected$4(operation, "invalid-anchor", "Map entries do not support ordering metadata.");
|
|
447
|
+
if (ids && operation.positions) {
|
|
448
|
+
if (operation.positions.length !== operation.entries.length || !validPositions(ids.length, operation.positions)) return rejected$4(operation, "invalid-operation", "Entity restore positions are invalid.");
|
|
449
|
+
} else if (ids && !valid(ids, operation.anchor)) return rejected$4(operation, "invalid-anchor", "Collection anchor is invalid.");
|
|
450
|
+
for (const entry of operation.entries) byId[entry.id] = copyPayload ? require_ownership.cloneValue(entry.value, "canonical") : require_ownership.transferPayload(entry.value);
|
|
451
|
+
if (ids) if (operation.positions) restore(ids, operation.entries.map((entry) => entry.id), operation.positions);
|
|
452
|
+
else if (!operation.anchor || "at" in operation.anchor && operation.anchor.at === "end") for (const entry of operation.entries) ids.push(entry.id);
|
|
453
|
+
else {
|
|
454
|
+
const insertion = index(ids, operation.anchor);
|
|
455
|
+
const currentLength = ids.length;
|
|
456
|
+
ids.length = currentLength + operation.entries.length;
|
|
457
|
+
for (let index = currentLength - 1; index >= insertion; index -= 1) ids[index + operation.entries.length] = ids[index];
|
|
458
|
+
for (let index = 0; index < operation.entries.length; index += 1) ids[insertion + index] = operation.entries[index].id;
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
status: "changed",
|
|
462
|
+
inverse: [{
|
|
463
|
+
type: "entity.remove",
|
|
464
|
+
at: operation.at,
|
|
465
|
+
ids: operation.entries.map((entry) => entry.id)
|
|
466
|
+
}]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
if (operation.type === "entity.remove") {
|
|
470
|
+
if (operation.ids.length === 0) return { status: "unchanged" };
|
|
471
|
+
const seen = /* @__PURE__ */ new Set();
|
|
472
|
+
for (const id of operation.ids) {
|
|
473
|
+
if (seen.has(id)) return rejected$4(operation, "duplicate-entity", "Entity ids must be unique within a batch.");
|
|
474
|
+
seen.add(id);
|
|
475
|
+
if (!Object.prototype.hasOwnProperty.call(byId, id)) return rejected$4(operation, "missing-entity", `Entity '${id}' does not exist.`);
|
|
476
|
+
}
|
|
477
|
+
const entries = [];
|
|
478
|
+
const positions = [];
|
|
479
|
+
if (ids) {
|
|
480
|
+
const positionsById = /* @__PURE__ */ new Map();
|
|
481
|
+
for (let index = 0; index < ids.length; index += 1) {
|
|
482
|
+
const id = ids[index];
|
|
483
|
+
if (seen.has(id)) positionsById.set(id, index);
|
|
484
|
+
}
|
|
485
|
+
for (const id of operation.ids) if (!positionsById.has(id)) return rejected$4(operation, "invalid-collection", `Collection index is missing '${id}'.`);
|
|
486
|
+
let writeIndex = 0;
|
|
487
|
+
for (let index = 0; index < ids.length; index += 1) {
|
|
488
|
+
const id = ids[index];
|
|
489
|
+
if (seen.has(id)) {
|
|
490
|
+
entries.push({
|
|
491
|
+
id,
|
|
492
|
+
value: require_ownership.cloneValue(byId[id], "inverse")
|
|
493
|
+
});
|
|
494
|
+
positions.push(index);
|
|
495
|
+
delete byId[id];
|
|
496
|
+
} else ids[writeIndex++] = id;
|
|
497
|
+
}
|
|
498
|
+
ids.length = writeIndex;
|
|
499
|
+
} else for (const id of operation.ids) {
|
|
500
|
+
entries.push({
|
|
501
|
+
id,
|
|
502
|
+
value: require_ownership.cloneValue(byId[id], "inverse")
|
|
503
|
+
});
|
|
504
|
+
delete byId[id];
|
|
505
|
+
}
|
|
506
|
+
return {
|
|
507
|
+
status: "changed",
|
|
508
|
+
inverse: [{
|
|
509
|
+
type: "entity.create",
|
|
510
|
+
at: operation.at,
|
|
511
|
+
entries,
|
|
512
|
+
...ids ? { positions } : {}
|
|
513
|
+
}]
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
const exists = Object.prototype.hasOwnProperty.call(byId, operation.id);
|
|
517
|
+
const position = ids?.indexOf(operation.id) ?? -1;
|
|
518
|
+
if (!exists) return rejected$4(operation, "missing-entity", `Entity '${operation.id}' does not exist.`);
|
|
519
|
+
if (!ids || position < 0) return rejected$4(operation, "invalid-collection", `Collection index is missing '${operation.id}'.`);
|
|
520
|
+
if (!valid(ids, operation.anchor)) return rejected$4(operation, "invalid-anchor", "Collection anchor is invalid.");
|
|
521
|
+
if (operation.anchor && ("before" in operation.anchor && operation.anchor.before === operation.id || "after" in operation.anchor && operation.anchor.after === operation.id)) return rejected$4(operation, "invalid-anchor", "An entity cannot be moved relative to itself.");
|
|
522
|
+
const nextIndex = afterRemove(ids, position, operation.anchor);
|
|
523
|
+
if (nextIndex === position) return { status: "unchanged" };
|
|
524
|
+
const inverse = {
|
|
525
|
+
type: "entity.move",
|
|
526
|
+
at: operation.at,
|
|
527
|
+
id: operation.id,
|
|
528
|
+
anchor: at(ids, position)
|
|
529
|
+
};
|
|
530
|
+
ids.splice(position, 1);
|
|
531
|
+
ids.splice(nextIndex, 0, operation.id);
|
|
532
|
+
return {
|
|
533
|
+
status: "changed",
|
|
534
|
+
inverse: [inverse]
|
|
535
|
+
};
|
|
536
|
+
};
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region core/src/mutation/execute-list.ts
|
|
539
|
+
const rejected$3 = (operation, code, message) => ({
|
|
540
|
+
status: "rejected",
|
|
541
|
+
issue: from(operation, code, message)
|
|
542
|
+
});
|
|
543
|
+
const executeList = (target, operation, copyPayload) => {
|
|
544
|
+
if (!Array.isArray(target.value) || target.node.kind !== "list") return rejected$3(operation, "invalid-address", "List target is invalid.");
|
|
545
|
+
const list = target.value;
|
|
546
|
+
const orderedKeys = keys(list, target.node.keyOf);
|
|
547
|
+
const own = (value) => copyPayload ? require_ownership.ownPayload(value) : value;
|
|
548
|
+
if (operation.type === "list.replace") {
|
|
549
|
+
if (operation.keys.length !== operation.value.length) return rejected$3(operation, "invalid-list-keys", "List replacement keys are invalid.");
|
|
550
|
+
const seen = /* @__PURE__ */ new Set();
|
|
551
|
+
for (let index = 0; index < operation.value.length; index += 1) {
|
|
552
|
+
const key = operation.keys[index];
|
|
553
|
+
if (seen.has(key) || target.node.keyOf(operation.value[index]) !== key) return rejected$3(operation, "invalid-list-keys", "List replacement keys are invalid.");
|
|
554
|
+
seen.add(key);
|
|
555
|
+
}
|
|
556
|
+
if (require_ownership.sameStructuralValue(list, operation.value)) return { status: "unchanged" };
|
|
557
|
+
const keys = new Array(list.length);
|
|
558
|
+
for (let index = 0; index < list.length; index += 1) keys[index] = target.node.keyOf(list[index]);
|
|
559
|
+
const inverse = {
|
|
560
|
+
type: "list.replace",
|
|
561
|
+
at: operation.at,
|
|
562
|
+
value: require_ownership.cloneValue(list, "inverse"),
|
|
563
|
+
keys
|
|
564
|
+
};
|
|
565
|
+
const owned = own(operation.value);
|
|
566
|
+
list.length = owned.length;
|
|
567
|
+
for (let index = 0; index < owned.length; index += 1) list[index] = owned[index];
|
|
568
|
+
return {
|
|
569
|
+
status: "changed",
|
|
570
|
+
inverse: [inverse]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (operation.type === "list.insert") {
|
|
574
|
+
if (target.node.keyOf(operation.value) !== operation.key) return rejected$3(operation, "invalid-list-key", "List item key does not match schema.");
|
|
575
|
+
if (orderedKeys.index(operation.key) >= 0) return rejected$3(operation, "duplicate-list-item", `List item '${operation.key}' already exists.`);
|
|
576
|
+
if (!valid(orderedKeys, operation.anchor)) return rejected$3(operation, "invalid-anchor", "List anchor is invalid.");
|
|
577
|
+
list.splice(index(orderedKeys, operation.anchor), 0, own(operation.value));
|
|
578
|
+
return {
|
|
579
|
+
status: "changed",
|
|
580
|
+
inverse: [{
|
|
581
|
+
type: "list.remove",
|
|
582
|
+
at: operation.at,
|
|
583
|
+
key: operation.key
|
|
584
|
+
}]
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
const position = orderedKeys.index(operation.key);
|
|
588
|
+
if (position < 0) return rejected$3(operation, "missing-list-item", `List item '${operation.key}' does not exist.`);
|
|
589
|
+
if (operation.type === "list.remove") {
|
|
590
|
+
const inverse = {
|
|
591
|
+
type: "list.insert",
|
|
592
|
+
at: operation.at,
|
|
593
|
+
key: operation.key,
|
|
594
|
+
value: require_ownership.cloneValue(list[position], "inverse"),
|
|
595
|
+
anchor: at(orderedKeys, position)
|
|
596
|
+
};
|
|
597
|
+
list.splice(position, 1);
|
|
598
|
+
return {
|
|
599
|
+
status: "changed",
|
|
600
|
+
inverse: [inverse]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
if (!valid(orderedKeys, operation.anchor)) return rejected$3(operation, "invalid-anchor", "List anchor is invalid.");
|
|
604
|
+
if (operation.anchor && ("before" in operation.anchor && operation.anchor.before === operation.key || "after" in operation.anchor && operation.anchor.after === operation.key)) return rejected$3(operation, "invalid-anchor", "A list item cannot be moved relative to itself.");
|
|
605
|
+
const nextIndex = afterRemove(orderedKeys, position, operation.anchor);
|
|
606
|
+
if (nextIndex === position) return { status: "unchanged" };
|
|
607
|
+
const inverse = {
|
|
608
|
+
type: "list.move",
|
|
609
|
+
at: operation.at,
|
|
610
|
+
key: operation.key,
|
|
611
|
+
anchor: at(orderedKeys, position)
|
|
612
|
+
};
|
|
613
|
+
const [entry] = list.splice(position, 1);
|
|
614
|
+
list.splice(nextIndex, 0, entry);
|
|
615
|
+
return {
|
|
616
|
+
status: "changed",
|
|
617
|
+
inverse: [inverse]
|
|
618
|
+
};
|
|
619
|
+
};
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region core/src/mutation/tree.ts
|
|
622
|
+
const has$1 = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
623
|
+
const validIndex = (index) => index === void 0 || Number.isInteger(index) && index >= 0;
|
|
624
|
+
const rejected$2 = (code, message) => ({
|
|
625
|
+
status: "rejected",
|
|
626
|
+
code,
|
|
627
|
+
message
|
|
628
|
+
});
|
|
629
|
+
const node = (tree, id) => tree.nodes[id];
|
|
630
|
+
const childIndex = (parent, id) => parent.children.indexOf(id);
|
|
631
|
+
const sameOrder$1 = (left, right) => {
|
|
632
|
+
if (left.length !== right.length) return false;
|
|
633
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
634
|
+
return true;
|
|
635
|
+
};
|
|
636
|
+
const rootPresence = (id) => id === void 0 ? { status: "absent" } : {
|
|
637
|
+
status: "present",
|
|
638
|
+
id
|
|
639
|
+
};
|
|
640
|
+
const capture = (change, id, presence) => {
|
|
641
|
+
if (!change.nodes.has(id)) change.nodes.set(id, presence);
|
|
642
|
+
};
|
|
643
|
+
const without = (values, id) => {
|
|
644
|
+
const result = [];
|
|
645
|
+
for (const value of values) if (value !== id) result.push(value);
|
|
646
|
+
return result;
|
|
647
|
+
};
|
|
648
|
+
const inserted = (values, id, index) => {
|
|
649
|
+
const result = [...values];
|
|
650
|
+
result.splice(Math.min(index, result.length), 0, id);
|
|
651
|
+
return result;
|
|
652
|
+
};
|
|
653
|
+
const moved = (values, id, index) => {
|
|
654
|
+
const result = without(values, id);
|
|
655
|
+
result.splice(Math.min(index, result.length), 0, id);
|
|
656
|
+
return result;
|
|
657
|
+
};
|
|
658
|
+
const parentPresence = (tree, id, children) => {
|
|
659
|
+
const entry = node(tree, id);
|
|
660
|
+
return entry ? {
|
|
661
|
+
status: "present",
|
|
662
|
+
...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
|
|
663
|
+
children,
|
|
664
|
+
value: entry.value
|
|
665
|
+
} : { status: "absent" };
|
|
666
|
+
};
|
|
667
|
+
const restoreIncremental = (value, change) => {
|
|
668
|
+
if (!is(value)) return value;
|
|
669
|
+
if (change.root.status === "absent") delete value.rootId;
|
|
670
|
+
else value.rootId = change.root.id;
|
|
671
|
+
for (const [id, before] of change.nodes) {
|
|
672
|
+
if (before.status === "absent") {
|
|
673
|
+
delete value.nodes[id];
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
value.nodes[id] = {
|
|
677
|
+
...before.parentId === void 0 ? {} : { parentId: before.parentId },
|
|
678
|
+
children: [...before.children],
|
|
679
|
+
value: before.value
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
};
|
|
684
|
+
const restoreChange = (value, change) => change.mode === "whole" ? require_ownership.ownPayload(change.value, "journal") : restoreIncremental(value, change);
|
|
685
|
+
const wholeChange = (inverse, previous) => {
|
|
686
|
+
if (!previous) return {
|
|
687
|
+
mode: "whole",
|
|
688
|
+
value: inverse.value
|
|
689
|
+
};
|
|
690
|
+
if (previous.mode === "whole") return previous;
|
|
691
|
+
return {
|
|
692
|
+
mode: "whole",
|
|
693
|
+
value: restoreIncremental(require_ownership.ownPayload(inverse.value, "journal"), previous)
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
const removedNodePresences = (inverses) => {
|
|
697
|
+
const children = /* @__PURE__ */ new Map();
|
|
698
|
+
for (const inverse of inverses) {
|
|
699
|
+
if (inverse.parentId === void 0) continue;
|
|
700
|
+
const entries = children.get(inverse.parentId);
|
|
701
|
+
const child = {
|
|
702
|
+
id: inverse.treeNodeId,
|
|
703
|
+
index: inverse.index ?? Number.MAX_SAFE_INTEGER
|
|
704
|
+
};
|
|
705
|
+
if (entries) entries.push(child);
|
|
706
|
+
else children.set(inverse.parentId, [child]);
|
|
707
|
+
}
|
|
708
|
+
const result = /* @__PURE__ */ new Map();
|
|
709
|
+
for (const inverse of inverses) {
|
|
710
|
+
const ordered = children.get(inverse.treeNodeId) ?? [];
|
|
711
|
+
ordered.sort((left, right) => left.index - right.index);
|
|
712
|
+
result.set(inverse.treeNodeId, {
|
|
713
|
+
status: "present",
|
|
714
|
+
...inverse.parentId === void 0 ? {} : { parentId: inverse.parentId },
|
|
715
|
+
children: ordered.map((entry) => entry.id),
|
|
716
|
+
value: inverse.value
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
return result;
|
|
720
|
+
};
|
|
721
|
+
const recordChange = (previous, current, forward, inverses) => {
|
|
722
|
+
const replacement = inverses[0];
|
|
723
|
+
if (replacement?.type === "tree.replace") return wholeChange(replacement, previous);
|
|
724
|
+
if (previous?.mode === "whole") return previous;
|
|
725
|
+
const change = previous ?? {
|
|
726
|
+
mode: "incremental",
|
|
727
|
+
root: forward.type === "tree.insert" && forward.parentId === void 0 ? { status: "absent" } : forward.type === "tree.remove" && inverses[0]?.type === "tree.insert" ? rootPresence(inverses[0].parentId === void 0 ? inverses[0].treeNodeId : current.rootId) : rootPresence(current.rootId),
|
|
728
|
+
nodes: /* @__PURE__ */ new Map()
|
|
729
|
+
};
|
|
730
|
+
if (forward.type === "tree.insert") {
|
|
731
|
+
capture(change, forward.treeNodeId, { status: "absent" });
|
|
732
|
+
if (forward.parentId !== void 0) {
|
|
733
|
+
const parentNode = node(current, forward.parentId);
|
|
734
|
+
if (parentNode) capture(change, forward.parentId, parentPresence(current, forward.parentId, without(parentNode.children, forward.treeNodeId)));
|
|
735
|
+
}
|
|
736
|
+
return change;
|
|
737
|
+
}
|
|
738
|
+
if (forward.type === "tree.remove") {
|
|
739
|
+
const inserts = inverses.filter((inverse) => inverse.type === "tree.insert");
|
|
740
|
+
const removed = removedNodePresences(inserts);
|
|
741
|
+
for (const [id, presence] of removed) capture(change, id, presence);
|
|
742
|
+
const first = inserts[0];
|
|
743
|
+
if (first?.parentId !== void 0) {
|
|
744
|
+
const parentNode = node(current, first.parentId);
|
|
745
|
+
if (parentNode) capture(change, first.parentId, parentPresence(current, first.parentId, inserted(parentNode.children, first.treeNodeId, first.index ?? parentNode.children.length)));
|
|
746
|
+
}
|
|
747
|
+
return change;
|
|
748
|
+
}
|
|
749
|
+
if (forward.type === "tree.set") {
|
|
750
|
+
const inverse = inverses[0];
|
|
751
|
+
const entry = node(current, forward.treeNodeId);
|
|
752
|
+
if (entry && inverse?.type === "tree.set") capture(change, forward.treeNodeId, {
|
|
753
|
+
status: "present",
|
|
754
|
+
...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
|
|
755
|
+
children: [...entry.children],
|
|
756
|
+
value: inverse.value
|
|
757
|
+
});
|
|
758
|
+
return change;
|
|
759
|
+
}
|
|
760
|
+
if (forward.type === "tree.move") {
|
|
761
|
+
const inverse = inverses[0];
|
|
762
|
+
const entry = node(current, forward.treeNodeId);
|
|
763
|
+
if (!entry || inverse?.type !== "tree.move" || inverse.parentId === void 0) return change;
|
|
764
|
+
capture(change, forward.treeNodeId, {
|
|
765
|
+
status: "present",
|
|
766
|
+
parentId: inverse.parentId,
|
|
767
|
+
children: [...entry.children],
|
|
768
|
+
value: entry.value
|
|
769
|
+
});
|
|
770
|
+
const oldParent = node(current, inverse.parentId);
|
|
771
|
+
if (oldParent) {
|
|
772
|
+
const children = inverse.parentId === forward.parentId ? moved(oldParent.children, forward.treeNodeId, inverse.index ?? oldParent.children.length) : inserted(oldParent.children, forward.treeNodeId, inverse.index ?? oldParent.children.length);
|
|
773
|
+
capture(change, inverse.parentId, parentPresence(current, inverse.parentId, children));
|
|
774
|
+
}
|
|
775
|
+
if (forward.parentId !== void 0 && forward.parentId !== inverse.parentId) {
|
|
776
|
+
const nextParent = node(current, forward.parentId);
|
|
777
|
+
if (nextParent) capture(change, forward.parentId, parentPresence(current, forward.parentId, without(nextParent.children, forward.treeNodeId)));
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return change;
|
|
781
|
+
};
|
|
782
|
+
const sameNodePresence = (before, value) => {
|
|
783
|
+
if (before.status === "absent") return value === void 0;
|
|
784
|
+
return value !== void 0 && before.parentId === value.parentId && sameOrder$1(before.children, value.children) && require_ownership.sameStructuralValue(before.value, value.value);
|
|
785
|
+
};
|
|
786
|
+
const changeChanged = (change, value) => {
|
|
787
|
+
if (change.mode === "whole") return !require_ownership.sameStructuralValue(change.value, value);
|
|
788
|
+
if (!is(value)) return true;
|
|
789
|
+
if ((change.root.status === "present" ? change.root.id : void 0) !== value.rootId) return true;
|
|
790
|
+
for (const [id, before] of change.nodes) if (!sameNodePresence(before, node(value, id))) return true;
|
|
791
|
+
return false;
|
|
792
|
+
};
|
|
793
|
+
const is = (value) => require_ownership.isRecord(value) && require_ownership.isRecord(value.nodes);
|
|
794
|
+
const contains$1 = (tree, id) => has$1(tree.nodes, id);
|
|
795
|
+
const validate = (value) => {
|
|
796
|
+
if (!is(value)) return false;
|
|
797
|
+
const ids = Object.keys(value.nodes);
|
|
798
|
+
if (ids.length === 0) return value.rootId === void 0;
|
|
799
|
+
if (typeof value.rootId !== "string" || !has$1(value.nodes, value.rootId)) return false;
|
|
800
|
+
for (const id of ids) {
|
|
801
|
+
const entry = value.nodes[id];
|
|
802
|
+
if (!require_ownership.isRecord(entry) || !Array.isArray(entry.children)) return false;
|
|
803
|
+
if (entry.children.some((child) => typeof child !== "string") || new Set(entry.children).size !== entry.children.length) return false;
|
|
804
|
+
if (entry.parentId !== void 0 && typeof entry.parentId !== "string") return false;
|
|
805
|
+
if (id === value.rootId) {
|
|
806
|
+
if (entry.parentId !== void 0) return false;
|
|
807
|
+
} else if (entry.parentId === void 0 || !has$1(value.nodes, entry.parentId)) return false;
|
|
808
|
+
for (const child of entry.children) {
|
|
809
|
+
const childNode = value.nodes[child];
|
|
810
|
+
if (!childNode || childNode.parentId !== id) return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
const seen = /* @__PURE__ */ new Set();
|
|
814
|
+
const stack = [value.rootId];
|
|
815
|
+
while (stack.length) {
|
|
816
|
+
const id = stack.pop();
|
|
817
|
+
if (seen.has(id)) return false;
|
|
818
|
+
seen.add(id);
|
|
819
|
+
const entry = value.nodes[id];
|
|
820
|
+
if (!entry) return false;
|
|
821
|
+
for (const child of entry.children) stack.push(child);
|
|
822
|
+
}
|
|
823
|
+
return seen.size === ids.length;
|
|
824
|
+
};
|
|
825
|
+
const documentTreeError = (node, value, address) => {
|
|
826
|
+
if ("optional" in node && node.optional && value === void 0) return void 0;
|
|
827
|
+
if (node.kind === "tree") return validate(value) ? void 0 : address;
|
|
828
|
+
if (node.kind === "object") {
|
|
829
|
+
if (!require_ownership.isRecord(value)) return void 0;
|
|
830
|
+
for (const [key, child] of Object.entries(node.shape)) {
|
|
831
|
+
const error = documentTreeError(child, value[key], [...address, key]);
|
|
832
|
+
if (error) return error;
|
|
833
|
+
}
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (node.kind === "single") return documentTreeError(node.value, value, address);
|
|
837
|
+
if (node.kind === "variant") {
|
|
838
|
+
if (!require_ownership.isRecord(value) || typeof value[node.tag] !== "string") return void 0;
|
|
839
|
+
const variant = node.variants[value[node.tag]];
|
|
840
|
+
return variant ? documentTreeError(variant, value, address) : void 0;
|
|
841
|
+
}
|
|
842
|
+
if (node.kind === "table") {
|
|
843
|
+
if (!require_ownership.isRecord(value) || !require_ownership.isRecord(value.byId)) return void 0;
|
|
844
|
+
for (const [id, entry] of Object.entries(value.byId)) {
|
|
845
|
+
const error = documentTreeError(node.value, entry, [...address, id]);
|
|
846
|
+
if (error) return error;
|
|
847
|
+
}
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
if (node.kind === "map") {
|
|
851
|
+
if (!require_ownership.isRecord(value)) return void 0;
|
|
852
|
+
for (const [id, entry] of Object.entries(value)) {
|
|
853
|
+
const error = documentTreeError(node.value, entry, [...address, id]);
|
|
854
|
+
if (error) return error;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
};
|
|
858
|
+
const invalidDocument = (schema, value) => documentTreeError({
|
|
859
|
+
kind: "object",
|
|
860
|
+
shape: schema.shape
|
|
861
|
+
}, value, []);
|
|
862
|
+
const replace = (current, operation) => {
|
|
863
|
+
if (!validate(operation.value)) return rejected$2("invalid-tree", "Tree replacement is malformed.");
|
|
864
|
+
if (require_ownership.sameStructuralValue(current, operation.value)) return { status: "unchanged" };
|
|
865
|
+
return {
|
|
866
|
+
status: "changed",
|
|
867
|
+
inverse: [{
|
|
868
|
+
type: "tree.replace",
|
|
869
|
+
at: operation.at,
|
|
870
|
+
value: require_ownership.cloneValue(current, "inverse")
|
|
871
|
+
}]
|
|
872
|
+
};
|
|
873
|
+
};
|
|
874
|
+
const insert = (tree, operation, value) => {
|
|
875
|
+
if (contains$1(tree, operation.treeNodeId)) return rejected$2("duplicate-tree-node", `Tree node '${operation.treeNodeId}' already exists.`);
|
|
876
|
+
if (!validIndex(operation.index)) return rejected$2("invalid-tree-index", "Tree index is invalid.");
|
|
877
|
+
if (tree.rootId === void 0) {
|
|
878
|
+
if (operation.parentId !== void 0) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
|
|
879
|
+
tree.nodes[operation.treeNodeId] = {
|
|
880
|
+
children: [],
|
|
881
|
+
value
|
|
882
|
+
};
|
|
883
|
+
tree.rootId = operation.treeNodeId;
|
|
884
|
+
} else {
|
|
885
|
+
if (operation.parentId === void 0) return rejected$2("invalid-tree", "A non-empty tree has exactly one root.");
|
|
886
|
+
const parentNode = node(tree, operation.parentId);
|
|
887
|
+
if (!parentNode) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
|
|
888
|
+
tree.nodes[operation.treeNodeId] = {
|
|
889
|
+
parentId: operation.parentId,
|
|
890
|
+
children: [],
|
|
891
|
+
value
|
|
892
|
+
};
|
|
893
|
+
parentNode.children.splice(Math.min(operation.index ?? parentNode.children.length, parentNode.children.length), 0, operation.treeNodeId);
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
status: "changed",
|
|
897
|
+
inverse: [{
|
|
898
|
+
type: "tree.remove",
|
|
899
|
+
at: operation.at,
|
|
900
|
+
treeNodeId: operation.treeNodeId
|
|
901
|
+
}]
|
|
902
|
+
};
|
|
903
|
+
};
|
|
904
|
+
const set = (tree, operation, value) => {
|
|
905
|
+
const entry = node(tree, operation.treeNodeId);
|
|
906
|
+
if (!entry) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
|
|
907
|
+
if (Object.is(entry.value, value)) return { status: "unchanged" };
|
|
908
|
+
const inverse = {
|
|
909
|
+
type: "tree.set",
|
|
910
|
+
at: operation.at,
|
|
911
|
+
treeNodeId: operation.treeNodeId,
|
|
912
|
+
value: require_ownership.cloneValue(entry.value, "inverse")
|
|
913
|
+
};
|
|
914
|
+
entry.value = value;
|
|
915
|
+
return {
|
|
916
|
+
status: "changed",
|
|
917
|
+
inverse: [inverse]
|
|
918
|
+
};
|
|
919
|
+
};
|
|
920
|
+
const remove = (tree, operation) => {
|
|
921
|
+
const first = node(tree, operation.treeNodeId);
|
|
922
|
+
if (!first) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
|
|
923
|
+
const parentId = first.parentId;
|
|
924
|
+
const parentNode = parentId === void 0 ? void 0 : node(tree, parentId);
|
|
925
|
+
const originalIndex = parentNode ? childIndex(parentNode, operation.treeNodeId) : 0;
|
|
926
|
+
if (parentId !== void 0 && (!parentNode || originalIndex < 0) || parentId === void 0 && tree.rootId !== operation.treeNodeId) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
|
|
927
|
+
const entries = [];
|
|
928
|
+
const seen = /* @__PURE__ */ new Set();
|
|
929
|
+
const stack = [{
|
|
930
|
+
id: operation.treeNodeId,
|
|
931
|
+
parentId,
|
|
932
|
+
index: originalIndex
|
|
933
|
+
}];
|
|
934
|
+
while (stack.length) {
|
|
935
|
+
const frame = stack.pop();
|
|
936
|
+
const entry = node(tree, frame.id);
|
|
937
|
+
if (!entry || seen.has(frame.id) || entry.parentId !== frame.parentId) return rejected$2("invalid-tree", "Tree contains an invalid descendant.");
|
|
938
|
+
seen.add(frame.id);
|
|
939
|
+
entries.push({
|
|
940
|
+
id: frame.id,
|
|
941
|
+
parentId: frame.parentId,
|
|
942
|
+
index: frame.index,
|
|
943
|
+
value: require_ownership.cloneValue(entry.value, "inverse")
|
|
944
|
+
});
|
|
945
|
+
for (let index = entry.children.length - 1; index >= 0; index -= 1) {
|
|
946
|
+
const childId = entry.children[index];
|
|
947
|
+
const child = node(tree, childId);
|
|
948
|
+
if (!child || child.parentId !== frame.id) return rejected$2("invalid-tree", "Tree child links are inconsistent.");
|
|
949
|
+
stack.push({
|
|
950
|
+
id: childId,
|
|
951
|
+
parentId: frame.id,
|
|
952
|
+
index
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (parentNode) parentNode.children.splice(originalIndex, 1);
|
|
957
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) delete tree.nodes[entries[index].id];
|
|
958
|
+
if (tree.rootId === operation.treeNodeId) delete tree.rootId;
|
|
959
|
+
return {
|
|
960
|
+
status: "changed",
|
|
961
|
+
inverse: entries.map((entry) => ({
|
|
962
|
+
type: "tree.insert",
|
|
963
|
+
at: operation.at,
|
|
964
|
+
treeNodeId: entry.id,
|
|
965
|
+
...entry.parentId === void 0 ? {} : { parentId: entry.parentId },
|
|
966
|
+
index: entry.index,
|
|
967
|
+
value: entry.value
|
|
968
|
+
}))
|
|
969
|
+
};
|
|
970
|
+
};
|
|
971
|
+
const move = (tree, operation) => {
|
|
972
|
+
const entry = node(tree, operation.treeNodeId);
|
|
973
|
+
if (!entry) return rejected$2("missing-tree-node", `Tree node '${operation.treeNodeId}' does not exist.`);
|
|
974
|
+
if (!validIndex(operation.index)) return rejected$2("invalid-tree-index", "Tree index is invalid.");
|
|
975
|
+
if (tree.rootId === operation.treeNodeId) return rejected$2("invalid-tree", "The root cannot be moved.");
|
|
976
|
+
if (operation.parentId === void 0) return rejected$2("invalid-tree", "Only the root can have no parent.");
|
|
977
|
+
const nextParent = node(tree, operation.parentId);
|
|
978
|
+
if (!nextParent) return rejected$2("missing-tree-parent", `Tree parent '${operation.parentId}' does not exist.`);
|
|
979
|
+
if (operation.parentId === operation.treeNodeId) return rejected$2("tree-cycle", "A tree node cannot parent itself.");
|
|
980
|
+
const descendants = [operation.treeNodeId];
|
|
981
|
+
const seen = /* @__PURE__ */ new Set();
|
|
982
|
+
while (descendants.length) {
|
|
983
|
+
const id = descendants.pop();
|
|
984
|
+
if (seen.has(id)) return rejected$2("invalid-tree", "Tree contains a cycle.");
|
|
985
|
+
seen.add(id);
|
|
986
|
+
const current = node(tree, id);
|
|
987
|
+
if (!current) return rejected$2("invalid-tree", "Tree contains a missing node.");
|
|
988
|
+
for (const childId of current.children) {
|
|
989
|
+
const child = node(tree, childId);
|
|
990
|
+
if (!child || child.parentId !== id) return rejected$2("invalid-tree", "Tree child links are inconsistent.");
|
|
991
|
+
if (childId === operation.parentId) return rejected$2("tree-cycle", "Tree move would create a cycle.");
|
|
992
|
+
descendants.push(childId);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
const parentId = entry.parentId;
|
|
996
|
+
const currentParent = parentId === void 0 ? void 0 : node(tree, parentId);
|
|
997
|
+
if (!currentParent || parentId === void 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
|
|
998
|
+
const originalIndex = childIndex(currentParent, operation.treeNodeId);
|
|
999
|
+
if (originalIndex < 0) return rejected$2("invalid-tree", "Tree parent links are inconsistent.");
|
|
1000
|
+
let nextIndex = Math.min(operation.index ?? nextParent.children.length, nextParent.children.length);
|
|
1001
|
+
currentParent.children.splice(originalIndex, 1);
|
|
1002
|
+
if (parentId === operation.parentId && nextIndex > originalIndex) nextIndex -= 1;
|
|
1003
|
+
if (parentId === operation.parentId && nextIndex === originalIndex) {
|
|
1004
|
+
currentParent.children.splice(originalIndex, 0, operation.treeNodeId);
|
|
1005
|
+
return { status: "unchanged" };
|
|
1006
|
+
}
|
|
1007
|
+
nextParent.children.splice(nextIndex, 0, operation.treeNodeId);
|
|
1008
|
+
entry.parentId = operation.parentId;
|
|
1009
|
+
return {
|
|
1010
|
+
status: "changed",
|
|
1011
|
+
inverse: [{
|
|
1012
|
+
type: "tree.move",
|
|
1013
|
+
at: operation.at,
|
|
1014
|
+
treeNodeId: operation.treeNodeId,
|
|
1015
|
+
parentId,
|
|
1016
|
+
index: originalIndex
|
|
1017
|
+
}]
|
|
1018
|
+
};
|
|
1019
|
+
};
|
|
1020
|
+
//#endregion
|
|
1021
|
+
//#region core/src/mutation/execute-tree.ts
|
|
1022
|
+
const outcome = (operation, value) => value.status === "rejected" ? {
|
|
1023
|
+
status: "rejected",
|
|
1024
|
+
issue: from(operation, value.code, value.message)
|
|
1025
|
+
} : value;
|
|
1026
|
+
const executeTree = (root, target, operation, copyPayload) => {
|
|
1027
|
+
if (!is(target.value)) return {
|
|
1028
|
+
status: "rejected",
|
|
1029
|
+
issue: from(operation, "invalid-address", "Tree target is invalid.")
|
|
1030
|
+
};
|
|
1031
|
+
const own = (value) => copyPayload ? require_ownership.ownPayload(value) : value;
|
|
1032
|
+
if (operation.type === "tree.replace") {
|
|
1033
|
+
const result = replace(target.value, operation);
|
|
1034
|
+
if (result.status === "changed") require_dependency.set(root, operation.at, require_ownership.ownPayload(operation.value));
|
|
1035
|
+
return outcome(operation, result);
|
|
1036
|
+
}
|
|
1037
|
+
if (operation.type === "tree.insert") return outcome(operation, insert(target.value, operation, own(operation.value)));
|
|
1038
|
+
if (operation.type === "tree.set") return outcome(operation, set(target.value, operation, own(operation.value)));
|
|
1039
|
+
if (operation.type === "tree.remove") return outcome(operation, remove(target.value, operation));
|
|
1040
|
+
return outcome(operation, move(target.value, operation));
|
|
1041
|
+
};
|
|
1042
|
+
//#endregion
|
|
1043
|
+
//#region core/src/mutation/execute-value.ts
|
|
1044
|
+
const rejected$1 = (operation, code, message) => ({
|
|
1045
|
+
status: "rejected",
|
|
1046
|
+
issue: from(operation, code, message)
|
|
1047
|
+
});
|
|
1048
|
+
const executeValue = (target, operation, copyPayload) => {
|
|
1049
|
+
const own = (value) => copyPayload ? require_ownership.ownPayload(value) : value;
|
|
1050
|
+
if (operation.type === "field.set" || operation.type === "field.clear") {
|
|
1051
|
+
const existed = Object.prototype.hasOwnProperty.call(target.parent, target.key);
|
|
1052
|
+
if (operation.type === "field.clear") {
|
|
1053
|
+
if (target.node.kind !== "field" || !target.node.optional) return rejected$1(operation, "required-field", "Only optional fields can be cleared.");
|
|
1054
|
+
if (!existed) return { status: "unchanged" };
|
|
1055
|
+
const inverse = {
|
|
1056
|
+
type: "field.set",
|
|
1057
|
+
at: operation.at,
|
|
1058
|
+
value: target.value
|
|
1059
|
+
};
|
|
1060
|
+
delete target.parent[target.key];
|
|
1061
|
+
return {
|
|
1062
|
+
status: "changed",
|
|
1063
|
+
inverse: [inverse]
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
if (existed && Object.is(target.value, operation.value)) return { status: "unchanged" };
|
|
1067
|
+
const inverse = existed ? {
|
|
1068
|
+
type: "field.set",
|
|
1069
|
+
at: operation.at,
|
|
1070
|
+
value: target.value
|
|
1071
|
+
} : {
|
|
1072
|
+
type: "field.clear",
|
|
1073
|
+
at: operation.at
|
|
1074
|
+
};
|
|
1075
|
+
target.parent[target.key] = operation.value;
|
|
1076
|
+
return {
|
|
1077
|
+
status: "changed",
|
|
1078
|
+
inverse: [inverse]
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
if (operation.type === "variant.replace") {
|
|
1082
|
+
if (require_ownership.sameStructuralValue(target.value, operation.value)) return { status: "unchanged" };
|
|
1083
|
+
const inverse = {
|
|
1084
|
+
type: "variant.replace",
|
|
1085
|
+
at: operation.at,
|
|
1086
|
+
value: require_ownership.ownPayload(target.value, "inverse")
|
|
1087
|
+
};
|
|
1088
|
+
target.parent[target.key] = own(operation.value);
|
|
1089
|
+
return {
|
|
1090
|
+
status: "changed",
|
|
1091
|
+
inverse: [inverse]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (!require_ownership.isRecord(target.value)) return rejected$1(operation, "invalid-address", "Dictionary target is invalid.");
|
|
1095
|
+
if (operation.type === "dict.replace") {
|
|
1096
|
+
if (require_ownership.sameStructuralValue(target.value, operation.value)) return { status: "unchanged" };
|
|
1097
|
+
const inverse = {
|
|
1098
|
+
type: "dict.replace",
|
|
1099
|
+
at: operation.at,
|
|
1100
|
+
value: require_ownership.cloneValue(target.value, "inverse")
|
|
1101
|
+
};
|
|
1102
|
+
target.parent[target.key] = own(operation.value);
|
|
1103
|
+
return {
|
|
1104
|
+
status: "changed",
|
|
1105
|
+
inverse: [inverse]
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
const existed = Object.prototype.hasOwnProperty.call(target.value, operation.key);
|
|
1109
|
+
if (operation.type === "dict.delete") {
|
|
1110
|
+
if (!existed) return { status: "unchanged" };
|
|
1111
|
+
const inverse = {
|
|
1112
|
+
type: "dict.set",
|
|
1113
|
+
at: operation.at,
|
|
1114
|
+
key: operation.key,
|
|
1115
|
+
value: target.value[operation.key]
|
|
1116
|
+
};
|
|
1117
|
+
delete target.value[operation.key];
|
|
1118
|
+
return {
|
|
1119
|
+
status: "changed",
|
|
1120
|
+
inverse: [inverse]
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
if (existed && Object.is(target.value[operation.key], operation.value)) return { status: "unchanged" };
|
|
1124
|
+
const inverse = existed ? {
|
|
1125
|
+
type: "dict.set",
|
|
1126
|
+
at: operation.at,
|
|
1127
|
+
key: operation.key,
|
|
1128
|
+
value: target.value[operation.key]
|
|
1129
|
+
} : {
|
|
1130
|
+
type: "dict.delete",
|
|
1131
|
+
at: operation.at,
|
|
1132
|
+
key: operation.key
|
|
1133
|
+
};
|
|
1134
|
+
target.value[operation.key] = operation.value;
|
|
1135
|
+
return {
|
|
1136
|
+
status: "changed",
|
|
1137
|
+
inverse: [inverse]
|
|
1138
|
+
};
|
|
1139
|
+
};
|
|
1140
|
+
//#endregion
|
|
1141
|
+
//#region core/src/mutation/execute.ts
|
|
1142
|
+
const invalidTarget = (operation) => ({
|
|
1143
|
+
status: "rejected",
|
|
1144
|
+
issue: from(operation, "invalid-address", `Operation '${operation.type}' does not match the schema address.`)
|
|
1145
|
+
});
|
|
1146
|
+
const execute = (root, operation, target, copyPayload) => {
|
|
1147
|
+
if (operation.type === "field.set" || operation.type === "field.clear") return !target || target.node.kind !== "field" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
|
|
1148
|
+
if (operation.type === "variant.replace") return !target || target.node.kind !== "variant" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
|
|
1149
|
+
if (operation.type === "dict.set" || operation.type === "dict.delete" || operation.type === "dict.replace") return !target || target.node.kind !== "dict" && target.node.kind !== "record" ? invalidTarget(operation) : executeValue(target, operation, copyPayload);
|
|
1150
|
+
if (operation.type === "entity.create" || operation.type === "entity.remove" || operation.type === "entity.move") return !target || target.node.kind !== "table" && target.node.kind !== "map" ? invalidTarget(operation) : executeEntity(target, operation, copyPayload);
|
|
1151
|
+
if (operation.type === "list.insert" || operation.type === "list.move" || operation.type === "list.remove" || operation.type === "list.replace") return !target || target.node.kind !== "list" ? invalidTarget(operation) : executeList(target, operation, copyPayload);
|
|
1152
|
+
if (operation.type === "tree.insert" || operation.type === "tree.move" || operation.type === "tree.remove" || operation.type === "tree.set" || operation.type === "tree.replace") return !target || target.node.kind !== "tree" ? invalidTarget(operation) : executeTree(root, target, operation, copyPayload);
|
|
1153
|
+
return {
|
|
1154
|
+
status: "rejected",
|
|
1155
|
+
issue: from(operation, "unknown-operation", "Unknown document operation.")
|
|
1156
|
+
};
|
|
1157
|
+
};
|
|
1158
|
+
//#endregion
|
|
1159
|
+
//#region core/src/mutation/journal.ts
|
|
1160
|
+
const indexNode = () => ({ children: /* @__PURE__ */ new Map() });
|
|
1161
|
+
const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
1162
|
+
const sameAddress$1 = (left, right) => {
|
|
1163
|
+
if (left.length !== right.length) return false;
|
|
1164
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
1165
|
+
return true;
|
|
1166
|
+
};
|
|
1167
|
+
const sameOrder = (left, right) => {
|
|
1168
|
+
if (left.length !== right.length) return false;
|
|
1169
|
+
for (let index = 0; index < left.length; index += 1) if (left[index] !== right[index]) return false;
|
|
1170
|
+
return true;
|
|
1171
|
+
};
|
|
1172
|
+
const replaceArray = (target, value) => {
|
|
1173
|
+
target.length = value.length;
|
|
1174
|
+
for (let index = 0; index < value.length; index += 1) target[index] = value[index];
|
|
1175
|
+
};
|
|
1176
|
+
const present = (value) => ({
|
|
1177
|
+
status: "present",
|
|
1178
|
+
value
|
|
1179
|
+
});
|
|
1180
|
+
const absent = Object.freeze({ status: "absent" });
|
|
1181
|
+
const inverseMismatch = (operation) => {
|
|
1182
|
+
throw new Error(`Mutation inverse does not match '${operation.type}'.`);
|
|
1183
|
+
};
|
|
1184
|
+
const ownsDescendant = (subject, address) => {
|
|
1185
|
+
if (subject.address.length >= address.length) return false;
|
|
1186
|
+
if (subject.kind === "variant") return true;
|
|
1187
|
+
if (subject.kind !== "entity") return false;
|
|
1188
|
+
return subject.entries.has(address[subject.address.length]);
|
|
1189
|
+
};
|
|
1190
|
+
const nodeAt = (root, address) => {
|
|
1191
|
+
let node = root;
|
|
1192
|
+
for (const segment of address) {
|
|
1193
|
+
node = node.children.get(segment);
|
|
1194
|
+
if (!node) return void 0;
|
|
1195
|
+
}
|
|
1196
|
+
return node;
|
|
1197
|
+
};
|
|
1198
|
+
const indexedSubjects = (root, address) => {
|
|
1199
|
+
const start = nodeAt(root, address);
|
|
1200
|
+
if (!start) return [];
|
|
1201
|
+
const subjects = [];
|
|
1202
|
+
const stack = [start];
|
|
1203
|
+
while (stack.length) {
|
|
1204
|
+
const node = stack.pop();
|
|
1205
|
+
if (!node) break;
|
|
1206
|
+
if (node.subject) subjects.push(node.subject);
|
|
1207
|
+
for (const child of node.children.values()) stack.push(child);
|
|
1208
|
+
}
|
|
1209
|
+
return subjects;
|
|
1210
|
+
};
|
|
1211
|
+
const writePresence = (root, address, before) => {
|
|
1212
|
+
const target = require_dependency.locate(root, address);
|
|
1213
|
+
if (!target) return;
|
|
1214
|
+
if (before.status === "present") target.parent[target.key] = before.value;
|
|
1215
|
+
else if (Array.isArray(target.parent)) target.parent.splice(Number(target.key), 1);
|
|
1216
|
+
else delete target.parent[String(target.key)];
|
|
1217
|
+
};
|
|
1218
|
+
const writeContainer = (root, address, value) => {
|
|
1219
|
+
const target = require_dependency.locate(root, address);
|
|
1220
|
+
if (!target) return;
|
|
1221
|
+
target.parent[target.key] = value;
|
|
1222
|
+
};
|
|
1223
|
+
const restoreDict = (value, state) => {
|
|
1224
|
+
if (state.mode === "whole") {
|
|
1225
|
+
for (const key of Object.keys(value)) delete value[key];
|
|
1226
|
+
for (const [key, entry] of Object.entries(state.value)) value[key] = entry;
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
for (const [key, before] of state.entries) if (before.status === "present") value[key] = before.value;
|
|
1230
|
+
else delete value[key];
|
|
1231
|
+
};
|
|
1232
|
+
const restoreList = (value, subject) => {
|
|
1233
|
+
if (subject.state.mode === "whole") {
|
|
1234
|
+
replaceArray(value, subject.state.value);
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1238
|
+
for (const entry of value) entries.set(subject.node.keyOf(entry), entry);
|
|
1239
|
+
for (const [key, before] of subject.state.items) if (before.status === "present") entries.set(key, before.value);
|
|
1240
|
+
else entries.delete(key);
|
|
1241
|
+
const restored = new Array(subject.state.order.length);
|
|
1242
|
+
for (let index = 0; index < subject.state.order.length; index += 1) restored[index] = entries.get(subject.state.order[index]);
|
|
1243
|
+
replaceArray(value, restored);
|
|
1244
|
+
};
|
|
1245
|
+
const entityCollection = (subject, value) => resolveEntityCollection(subject.node, value);
|
|
1246
|
+
const restoreEntity = (value, subject) => {
|
|
1247
|
+
const resolution = entityCollection(subject, value);
|
|
1248
|
+
if (resolution.status === "invalid") return;
|
|
1249
|
+
const collection = resolution.collection;
|
|
1250
|
+
for (const [id, before] of subject.entries) if (before.status === "present") collection.byId[id] = before.value;
|
|
1251
|
+
else delete collection.byId[id];
|
|
1252
|
+
if (collection.kind !== "table") return;
|
|
1253
|
+
if (subject.order.kind === "tracked") {
|
|
1254
|
+
replaceArray(collection.ids, subject.order.before);
|
|
1255
|
+
return;
|
|
1256
|
+
}
|
|
1257
|
+
if (subject.order.kind === "stable") restoreEntityOrder(collection.ids, subject.order.inverses);
|
|
1258
|
+
};
|
|
1259
|
+
const restoreRootSubject = (value, subject) => {
|
|
1260
|
+
if (subject.kind === "value") return subject.before.status === "present" ? subject.before.value : void 0;
|
|
1261
|
+
if (subject.kind === "variant") return subject.before;
|
|
1262
|
+
if (subject.kind === "dict") {
|
|
1263
|
+
if (require_ownership.isRecord(value)) restoreDict(value, subject.state);
|
|
1264
|
+
return value;
|
|
1265
|
+
}
|
|
1266
|
+
if (subject.kind === "entity") {
|
|
1267
|
+
restoreEntity(value, subject);
|
|
1268
|
+
return value;
|
|
1269
|
+
}
|
|
1270
|
+
if (subject.kind === "list") {
|
|
1271
|
+
if (Array.isArray(value)) restoreList(value, subject);
|
|
1272
|
+
return value;
|
|
1273
|
+
}
|
|
1274
|
+
return restoreChange(value, subject.state);
|
|
1275
|
+
};
|
|
1276
|
+
const restoreSubject = (value, prefix, subject) => {
|
|
1277
|
+
if (sameAddress$1(prefix, subject.address)) return restoreRootSubject(value, subject);
|
|
1278
|
+
const address = subject.address.slice(prefix.length);
|
|
1279
|
+
if (subject.kind === "value") {
|
|
1280
|
+
writePresence(value, address, subject.before);
|
|
1281
|
+
return value;
|
|
1282
|
+
}
|
|
1283
|
+
if (subject.kind === "variant") {
|
|
1284
|
+
writeContainer(value, address, subject.before);
|
|
1285
|
+
return value;
|
|
1286
|
+
}
|
|
1287
|
+
const current = require_dependency.read(value, address);
|
|
1288
|
+
if (subject.kind === "dict") {
|
|
1289
|
+
if (require_ownership.isRecord(current)) restoreDict(current, subject.state);
|
|
1290
|
+
return value;
|
|
1291
|
+
}
|
|
1292
|
+
if (subject.kind === "entity") {
|
|
1293
|
+
restoreEntity(current, subject);
|
|
1294
|
+
return value;
|
|
1295
|
+
}
|
|
1296
|
+
if (subject.kind === "list") {
|
|
1297
|
+
if (Array.isArray(current)) restoreList(current, subject);
|
|
1298
|
+
return value;
|
|
1299
|
+
}
|
|
1300
|
+
const restored = restoreChange(current, subject.state);
|
|
1301
|
+
if (restored !== current) writeContainer(value, address, restored);
|
|
1302
|
+
return value;
|
|
1303
|
+
};
|
|
1304
|
+
const valueChanged = (root, subject) => {
|
|
1305
|
+
const target = require_dependency.locate(root, subject.address);
|
|
1306
|
+
const exists = !!target && hasOwn(target.parent, target.key);
|
|
1307
|
+
if (subject.before.status === "absent") return exists;
|
|
1308
|
+
return !exists || !Object.is(subject.before.value, target?.value);
|
|
1309
|
+
};
|
|
1310
|
+
const dictChanged = (root, subject) => {
|
|
1311
|
+
const value = require_dependency.read(root, subject.address);
|
|
1312
|
+
if (!require_ownership.isRecord(value)) return true;
|
|
1313
|
+
if (subject.state.mode === "whole") return !require_ownership.sameStructuralValue(subject.state.value, value);
|
|
1314
|
+
for (const [key, before] of subject.state.entries) {
|
|
1315
|
+
const exists = hasOwn(value, key);
|
|
1316
|
+
if (before.status === "absent" ? exists : !exists || !Object.is(before.value, value[key])) return true;
|
|
1317
|
+
}
|
|
1318
|
+
return false;
|
|
1319
|
+
};
|
|
1320
|
+
const listChanged = (root, subject) => {
|
|
1321
|
+
const value = require_dependency.read(root, subject.address);
|
|
1322
|
+
if (!Array.isArray(value)) return true;
|
|
1323
|
+
if (subject.state.mode === "whole") return !require_ownership.sameStructuralValue(subject.state.value, value);
|
|
1324
|
+
const positions = /* @__PURE__ */ new Map();
|
|
1325
|
+
const order = new Array(value.length);
|
|
1326
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1327
|
+
const key = subject.node.keyOf(value[index]);
|
|
1328
|
+
order[index] = key;
|
|
1329
|
+
positions.set(key, index);
|
|
1330
|
+
}
|
|
1331
|
+
if (!sameOrder(subject.state.order, order)) return true;
|
|
1332
|
+
for (const [key, before] of subject.state.items) {
|
|
1333
|
+
const position = positions.get(key);
|
|
1334
|
+
if (before.status === "absent") {
|
|
1335
|
+
if (position !== void 0) return true;
|
|
1336
|
+
} else if (position === void 0 || !require_ownership.sameStructuralValue(before.value, value[position])) return true;
|
|
1337
|
+
}
|
|
1338
|
+
return false;
|
|
1339
|
+
};
|
|
1340
|
+
const commonOrderChanged = (before, after) => {
|
|
1341
|
+
const finalPositions = /* @__PURE__ */ new Map();
|
|
1342
|
+
for (let index = 0; index < after.length; index += 1) finalPositions.set(after[index], index);
|
|
1343
|
+
let previous = -1;
|
|
1344
|
+
for (const id of before) {
|
|
1345
|
+
const position = finalPositions.get(id);
|
|
1346
|
+
if (position === void 0) continue;
|
|
1347
|
+
if (position < previous) return true;
|
|
1348
|
+
previous = position;
|
|
1349
|
+
}
|
|
1350
|
+
return false;
|
|
1351
|
+
};
|
|
1352
|
+
const analyzeEntity = (root, subject) => {
|
|
1353
|
+
const resolution = entityCollection(subject, require_dependency.read(root, subject.address));
|
|
1354
|
+
const added = /* @__PURE__ */ new Set();
|
|
1355
|
+
const removed = /* @__PURE__ */ new Set();
|
|
1356
|
+
const updated = /* @__PURE__ */ new Set();
|
|
1357
|
+
if (resolution.status === "invalid") return {
|
|
1358
|
+
changed: true,
|
|
1359
|
+
added,
|
|
1360
|
+
removed,
|
|
1361
|
+
updated,
|
|
1362
|
+
orderChanged: false
|
|
1363
|
+
};
|
|
1364
|
+
const collection = resolution.collection;
|
|
1365
|
+
for (const [id, before] of subject.entries) {
|
|
1366
|
+
const exists = hasOwn(collection.byId, id);
|
|
1367
|
+
if (before.status === "absent") {
|
|
1368
|
+
if (exists) added.add(id);
|
|
1369
|
+
} else if (!exists) removed.add(id);
|
|
1370
|
+
else if (!require_ownership.sameStructuralValue(before.value, collection.byId[id])) updated.add(id);
|
|
1371
|
+
}
|
|
1372
|
+
const beforeOrder = subject.order.kind === "tracked" ? subject.order.before : void 0;
|
|
1373
|
+
const finalOrder = collection.kind === "table" ? collection.ids : [];
|
|
1374
|
+
const netOrderChanged = beforeOrder !== void 0 && !sameOrder(beforeOrder, finalOrder);
|
|
1375
|
+
return {
|
|
1376
|
+
changed: added.size > 0 || removed.size > 0 || updated.size > 0 || netOrderChanged,
|
|
1377
|
+
added,
|
|
1378
|
+
removed,
|
|
1379
|
+
updated,
|
|
1380
|
+
orderChanged: beforeOrder !== void 0 && commonOrderChanged(beforeOrder, finalOrder)
|
|
1381
|
+
};
|
|
1382
|
+
};
|
|
1383
|
+
const subjectChanged = (root, subject) => {
|
|
1384
|
+
if (subject.kind === "value") return valueChanged(root, subject);
|
|
1385
|
+
if (subject.kind === "variant") return !require_ownership.sameStructuralValue(subject.before, require_dependency.read(root, subject.address));
|
|
1386
|
+
if (subject.kind === "dict") return dictChanged(root, subject);
|
|
1387
|
+
if (subject.kind === "list") return listChanged(root, subject);
|
|
1388
|
+
return changeChanged(subject.state, require_dependency.read(root, subject.address));
|
|
1389
|
+
};
|
|
1390
|
+
const listOrderBefore = (value, node, inverse) => {
|
|
1391
|
+
const order = value.map(node.keyOf);
|
|
1392
|
+
if (inverse.type === "list.remove") return order.filter((key) => key !== inverse.key);
|
|
1393
|
+
if (inverse.type === "list.insert") {
|
|
1394
|
+
order.splice(index(order, inverse.anchor), 0, inverse.key);
|
|
1395
|
+
return order;
|
|
1396
|
+
}
|
|
1397
|
+
if (inverse.type === "list.move") {
|
|
1398
|
+
const position = order.indexOf(inverse.key);
|
|
1399
|
+
if (position >= 0) order.splice(position, 1);
|
|
1400
|
+
order.splice(index(order, inverse.anchor), 0, inverse.key);
|
|
1401
|
+
}
|
|
1402
|
+
return order;
|
|
1403
|
+
};
|
|
1404
|
+
const recordListPresence = (state, inverse) => {
|
|
1405
|
+
if (inverse.type === "list.remove" && !state.items.has(inverse.key)) state.items.set(inverse.key, absent);
|
|
1406
|
+
else if (inverse.type === "list.insert" && !state.items.has(inverse.key)) state.items.set(inverse.key, present(inverse.value));
|
|
1407
|
+
};
|
|
1408
|
+
const entityOrderInverse = (operation, inverse) => {
|
|
1409
|
+
if (inverse.type === "entity.remove") return {
|
|
1410
|
+
kind: "remove",
|
|
1411
|
+
ids: inverse.ids
|
|
1412
|
+
};
|
|
1413
|
+
if (inverse.type === "entity.create") {
|
|
1414
|
+
if (!inverse.positions || inverse.positions.length !== inverse.entries.length) return inverseMismatch(operation);
|
|
1415
|
+
return {
|
|
1416
|
+
kind: "restore",
|
|
1417
|
+
ids: inverse.entries.map((entry) => entry.id),
|
|
1418
|
+
positions: inverse.positions
|
|
1419
|
+
};
|
|
1420
|
+
}
|
|
1421
|
+
if (inverse.type === "entity.move") return {
|
|
1422
|
+
kind: "move",
|
|
1423
|
+
id: inverse.id,
|
|
1424
|
+
...inverse.anchor ? { anchor: inverse.anchor } : {}
|
|
1425
|
+
};
|
|
1426
|
+
return inverseMismatch(operation);
|
|
1427
|
+
};
|
|
1428
|
+
const removeEntityIds = (order, ids) => {
|
|
1429
|
+
const removed = new Set(ids);
|
|
1430
|
+
let writeIndex = 0;
|
|
1431
|
+
for (const id of order) if (!removed.has(id)) order[writeIndex++] = id;
|
|
1432
|
+
order.length = writeIndex;
|
|
1433
|
+
};
|
|
1434
|
+
const restoreEntityOrder = (order, inverses) => {
|
|
1435
|
+
for (let index = inverses.length - 1; index >= 0; index -= 1) {
|
|
1436
|
+
const inverse = inverses[index];
|
|
1437
|
+
if (inverse.kind === "remove") {
|
|
1438
|
+
removeEntityIds(order, inverse.ids);
|
|
1439
|
+
continue;
|
|
1440
|
+
}
|
|
1441
|
+
if (inverse.kind === "restore") {
|
|
1442
|
+
restore(order, inverse.ids, inverse.positions);
|
|
1443
|
+
continue;
|
|
1444
|
+
}
|
|
1445
|
+
const position = order.indexOf(inverse.id);
|
|
1446
|
+
if (position < 0 || !valid(order, inverse.anchor)) throw new Error(`Entity order inverse cannot restore '${inverse.id}'.`);
|
|
1447
|
+
const nextIndex = afterRemove(order, position, inverse.anchor);
|
|
1448
|
+
order.splice(position, 1);
|
|
1449
|
+
order.splice(nextIndex, 0, inverse.id);
|
|
1450
|
+
}
|
|
1451
|
+
};
|
|
1452
|
+
const treeOperation = (operation) => operation.type.startsWith("tree.");
|
|
1453
|
+
const createChangeJournal = (root) => {
|
|
1454
|
+
const active = /* @__PURE__ */ new Set();
|
|
1455
|
+
const index = indexNode();
|
|
1456
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1457
|
+
const add = (subject) => {
|
|
1458
|
+
active.add(subject);
|
|
1459
|
+
const candidates = buckets.get(subject.addressHash);
|
|
1460
|
+
if (candidates) candidates.push(subject);
|
|
1461
|
+
else buckets.set(subject.addressHash, [subject]);
|
|
1462
|
+
let node = index;
|
|
1463
|
+
for (const segment of subject.address) {
|
|
1464
|
+
const existing = node.children.get(segment);
|
|
1465
|
+
if (existing) node = existing;
|
|
1466
|
+
else {
|
|
1467
|
+
const created = indexNode();
|
|
1468
|
+
node.children.set(segment, created);
|
|
1469
|
+
node = created;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (node.subject) throw new Error(`Change journal already owns '${subject.address.join(".")}'.`);
|
|
1473
|
+
node.subject = subject;
|
|
1474
|
+
require_ownership.profile.journal.subject();
|
|
1475
|
+
return subject;
|
|
1476
|
+
};
|
|
1477
|
+
const remove = (subject) => {
|
|
1478
|
+
if (!active.delete(subject)) return;
|
|
1479
|
+
const candidates = buckets.get(subject.addressHash);
|
|
1480
|
+
if (candidates) {
|
|
1481
|
+
const position = candidates.indexOf(subject);
|
|
1482
|
+
if (position >= 0) candidates.splice(position, 1);
|
|
1483
|
+
if (candidates.length === 0) buckets.delete(subject.addressHash);
|
|
1484
|
+
}
|
|
1485
|
+
const nodes = [index];
|
|
1486
|
+
let node = index;
|
|
1487
|
+
for (const segment of subject.address) {
|
|
1488
|
+
const child = node.children.get(segment);
|
|
1489
|
+
if (!child) return;
|
|
1490
|
+
nodes.push(child);
|
|
1491
|
+
node = child;
|
|
1492
|
+
}
|
|
1493
|
+
if (node.subject === subject) delete node.subject;
|
|
1494
|
+
for (let position = subject.address.length - 1; position >= 0; position -= 1) {
|
|
1495
|
+
const child = nodes[position + 1];
|
|
1496
|
+
if (child.subject || child.children.size > 0) break;
|
|
1497
|
+
nodes[position].children.delete(subject.address[position]);
|
|
1498
|
+
}
|
|
1499
|
+
};
|
|
1500
|
+
function find(kind, address, addressHash) {
|
|
1501
|
+
const candidates = buckets.get(addressHash) ?? [];
|
|
1502
|
+
for (const subject of candidates) {
|
|
1503
|
+
require_ownership.profile.journal.comparison();
|
|
1504
|
+
if (subject.kind === kind && sameAddress$1(subject.address, address)) return subject;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
const ownerOf = (address) => {
|
|
1508
|
+
let node = index;
|
|
1509
|
+
for (let position = 0; position < address.length; position += 1) {
|
|
1510
|
+
if (node.subject && ownsDescendant(node.subject, address)) return node.subject;
|
|
1511
|
+
const child = node.children.get(address[position]);
|
|
1512
|
+
if (!child) return void 0;
|
|
1513
|
+
node = child;
|
|
1514
|
+
}
|
|
1515
|
+
};
|
|
1516
|
+
const absorb = (value, address) => {
|
|
1517
|
+
const subjects = indexedSubjects(index, address);
|
|
1518
|
+
if (subjects.length === 0) return value;
|
|
1519
|
+
subjects.sort((left, right) => left.address.length - right.address.length);
|
|
1520
|
+
let before = require_ownership.ownPayload(value, "journal");
|
|
1521
|
+
for (const subject of subjects) before = restoreSubject(before, address, subject);
|
|
1522
|
+
for (const subject of subjects) {
|
|
1523
|
+
remove(subject);
|
|
1524
|
+
require_ownership.profile.journal.absorbed();
|
|
1525
|
+
}
|
|
1526
|
+
return before;
|
|
1527
|
+
};
|
|
1528
|
+
const base = (resolved, operation) => ({
|
|
1529
|
+
address: operation.at,
|
|
1530
|
+
addressHash: resolved.addressHash,
|
|
1531
|
+
...resolved.collection ? { collection: resolved.collection } : {}
|
|
1532
|
+
});
|
|
1533
|
+
const recordValue = (resolved, operation, inverse) => {
|
|
1534
|
+
if (find("value", operation.at, resolved.addressHash)) return;
|
|
1535
|
+
const first = inverse[0];
|
|
1536
|
+
if (first?.type !== "field.set" && first?.type !== "field.clear") return inverseMismatch(operation);
|
|
1537
|
+
add({
|
|
1538
|
+
...base(resolved, operation),
|
|
1539
|
+
kind: "value",
|
|
1540
|
+
before: first.type === "field.set" ? present(first.value) : absent
|
|
1541
|
+
});
|
|
1542
|
+
};
|
|
1543
|
+
const recordVariant = (resolved, operation, inverse) => {
|
|
1544
|
+
if (find("variant", operation.at, resolved.addressHash)) return;
|
|
1545
|
+
const first = inverse[0];
|
|
1546
|
+
if (first?.type !== "variant.replace") return inverseMismatch(operation);
|
|
1547
|
+
add({
|
|
1548
|
+
...base(resolved, operation),
|
|
1549
|
+
kind: "variant",
|
|
1550
|
+
before: absorb(first.value, operation.at)
|
|
1551
|
+
});
|
|
1552
|
+
};
|
|
1553
|
+
const recordDict = (resolved, operation, inverse) => {
|
|
1554
|
+
const first = inverse[0];
|
|
1555
|
+
if (!first) return inverseMismatch(operation);
|
|
1556
|
+
const subject = find("dict", operation.at, resolved.addressHash) ?? add({
|
|
1557
|
+
...base(resolved, operation),
|
|
1558
|
+
kind: "dict",
|
|
1559
|
+
state: {
|
|
1560
|
+
mode: "incremental",
|
|
1561
|
+
entries: /* @__PURE__ */ new Map()
|
|
1562
|
+
}
|
|
1563
|
+
});
|
|
1564
|
+
if (subject.state.mode === "whole") return;
|
|
1565
|
+
if (first.type === "dict.replace") {
|
|
1566
|
+
const value = require_ownership.ownPayload(first.value, "journal");
|
|
1567
|
+
restoreDict(value, subject.state);
|
|
1568
|
+
subject.state = {
|
|
1569
|
+
mode: "whole",
|
|
1570
|
+
value
|
|
1571
|
+
};
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
if (first.type !== "dict.set" && first.type !== "dict.delete") return inverseMismatch(operation);
|
|
1575
|
+
if (!subject.state.entries.has(first.key)) subject.state.entries.set(first.key, first.type === "dict.set" ? present(first.value) : absent);
|
|
1576
|
+
};
|
|
1577
|
+
const recordList = (resolved, operation, inverse) => {
|
|
1578
|
+
if (resolved.node.kind !== "list") return inverseMismatch(operation);
|
|
1579
|
+
const first = inverse[0];
|
|
1580
|
+
if (!first) return inverseMismatch(operation);
|
|
1581
|
+
let subject = find("list", operation.at, resolved.addressHash);
|
|
1582
|
+
if (!subject) {
|
|
1583
|
+
if (first.type === "list.replace") {
|
|
1584
|
+
add({
|
|
1585
|
+
...base(resolved, operation),
|
|
1586
|
+
kind: "list",
|
|
1587
|
+
node: resolved.node,
|
|
1588
|
+
state: {
|
|
1589
|
+
mode: "whole",
|
|
1590
|
+
value: first.value
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
return;
|
|
1594
|
+
}
|
|
1595
|
+
const value = require_dependency.read(root, operation.at);
|
|
1596
|
+
if (!Array.isArray(value)) return inverseMismatch(operation);
|
|
1597
|
+
subject = add({
|
|
1598
|
+
...base(resolved, operation),
|
|
1599
|
+
kind: "list",
|
|
1600
|
+
node: resolved.node,
|
|
1601
|
+
state: {
|
|
1602
|
+
mode: "incremental",
|
|
1603
|
+
items: /* @__PURE__ */ new Map(),
|
|
1604
|
+
order: listOrderBefore(value, resolved.node, first)
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
}
|
|
1608
|
+
if (subject.state.mode === "whole") return;
|
|
1609
|
+
if (first.type === "list.replace") {
|
|
1610
|
+
const value = Array.from(require_ownership.ownPayload(first.value, "journal"));
|
|
1611
|
+
restoreList(value, subject);
|
|
1612
|
+
subject.state = {
|
|
1613
|
+
mode: "whole",
|
|
1614
|
+
value
|
|
1615
|
+
};
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (first.type !== "list.insert" && first.type !== "list.move" && first.type !== "list.remove") return inverseMismatch(operation);
|
|
1619
|
+
recordListPresence(subject.state, first);
|
|
1620
|
+
};
|
|
1621
|
+
const trackEntityOrder = (subject, collection) => {
|
|
1622
|
+
if (subject.order.kind !== "stable") return;
|
|
1623
|
+
require_ownership.profile.journal.orderSnapshot(collection.ids.length);
|
|
1624
|
+
const before = [...collection.ids];
|
|
1625
|
+
restoreEntityOrder(before, subject.order.inverses);
|
|
1626
|
+
subject.order = {
|
|
1627
|
+
kind: "tracked",
|
|
1628
|
+
before
|
|
1629
|
+
};
|
|
1630
|
+
};
|
|
1631
|
+
const recordEntity = (resolved, operation, inverse) => {
|
|
1632
|
+
if (resolved.node.kind !== "table" && resolved.node.kind !== "map") return inverseMismatch(operation);
|
|
1633
|
+
const resolution = resolveEntityCollection(resolved.node, require_dependency.read(root, operation.at));
|
|
1634
|
+
if (resolution.status === "invalid") return inverseMismatch(operation);
|
|
1635
|
+
const collection = resolution.collection;
|
|
1636
|
+
const subject = find("entity", operation.at, resolved.addressHash) ?? add({
|
|
1637
|
+
...base(resolved, operation),
|
|
1638
|
+
kind: "entity",
|
|
1639
|
+
node: resolved.node,
|
|
1640
|
+
entries: /* @__PURE__ */ new Map(),
|
|
1641
|
+
order: collection.kind === "table" ? {
|
|
1642
|
+
kind: "stable",
|
|
1643
|
+
inverses: []
|
|
1644
|
+
} : { kind: "unordered" }
|
|
1645
|
+
});
|
|
1646
|
+
const first = inverse[0];
|
|
1647
|
+
if (!first) return inverseMismatch(operation);
|
|
1648
|
+
if (collection.kind === "table" && subject.order.kind === "stable") subject.order.inverses.push(entityOrderInverse(operation, first));
|
|
1649
|
+
if (first.type === "entity.remove") {
|
|
1650
|
+
for (const id of first.ids) if (!subject.entries.has(id)) subject.entries.set(id, absent);
|
|
1651
|
+
if (collection.kind === "table" && first.ids.some((id) => subject.entries.get(id)?.status === "present")) trackEntityOrder(subject, collection);
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
if (first.type === "entity.create") {
|
|
1655
|
+
for (const entry of first.entries) {
|
|
1656
|
+
if (subject.entries.has(entry.id)) continue;
|
|
1657
|
+
const address = [...operation.at, entry.id];
|
|
1658
|
+
subject.entries.set(entry.id, present(absorb(entry.value, address)));
|
|
1659
|
+
}
|
|
1660
|
+
return;
|
|
1661
|
+
}
|
|
1662
|
+
if (first.type === "entity.move") {
|
|
1663
|
+
if (collection.kind === "table" && subject.entries.get(first.id)?.status !== "absent") trackEntityOrder(subject, collection);
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
return inverseMismatch(operation);
|
|
1667
|
+
};
|
|
1668
|
+
const recordTree = (resolved, operation, inverse) => {
|
|
1669
|
+
const current = require_dependency.read(root, operation.at);
|
|
1670
|
+
if (!is(current)) return inverseMismatch(operation);
|
|
1671
|
+
const inverses = inverse.filter(treeOperation);
|
|
1672
|
+
if (inverses.length === 0 || inverses.length !== inverse.length) return inverseMismatch(operation);
|
|
1673
|
+
const subject = find("tree", operation.at, resolved.addressHash);
|
|
1674
|
+
const state = recordChange(subject?.state, current, operation, inverses);
|
|
1675
|
+
if (subject) subject.state = state;
|
|
1676
|
+
else add({
|
|
1677
|
+
...base(resolved, operation),
|
|
1678
|
+
kind: "tree",
|
|
1679
|
+
state
|
|
1680
|
+
});
|
|
1681
|
+
};
|
|
1682
|
+
return {
|
|
1683
|
+
record: (resolved, operation, inverse) => {
|
|
1684
|
+
require_ownership.profile.batch.journalRecord();
|
|
1685
|
+
if (ownerOf(operation.at)) return;
|
|
1686
|
+
if (operation.type === "field.set" || operation.type === "field.clear") {
|
|
1687
|
+
recordValue(resolved, operation, inverse);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
if (operation.type === "variant.replace") {
|
|
1691
|
+
recordVariant(resolved, operation, inverse);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
if (operation.type === "dict.set" || operation.type === "dict.delete" || operation.type === "dict.replace") {
|
|
1695
|
+
recordDict(resolved, operation, inverse);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (operation.type === "entity.create" || operation.type === "entity.remove" || operation.type === "entity.move") {
|
|
1699
|
+
recordEntity(resolved, operation, inverse);
|
|
1700
|
+
return;
|
|
1701
|
+
}
|
|
1702
|
+
if (operation.type === "list.insert" || operation.type === "list.move" || operation.type === "list.remove" || operation.type === "list.replace") {
|
|
1703
|
+
recordList(resolved, operation, inverse);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
recordTree(resolved, operation, inverse);
|
|
1707
|
+
},
|
|
1708
|
+
finish: () => {
|
|
1709
|
+
const changed = [];
|
|
1710
|
+
const entityChanges = /* @__PURE__ */ new Map();
|
|
1711
|
+
for (const subject of active) {
|
|
1712
|
+
require_ownership.profile.journal.comparison();
|
|
1713
|
+
if (subject.kind === "entity") {
|
|
1714
|
+
const analysis = analyzeEntity(root, subject);
|
|
1715
|
+
if (!analysis.changed) continue;
|
|
1716
|
+
entityChanges.set(subject, analysis);
|
|
1717
|
+
changed.push(subject);
|
|
1718
|
+
} else if (subjectChanged(root, subject)) changed.push(subject);
|
|
1719
|
+
}
|
|
1720
|
+
if (changed.length === 0) return { status: "unchanged" };
|
|
1721
|
+
const collections = [];
|
|
1722
|
+
const collectionBuckets = /* @__PURE__ */ new Map();
|
|
1723
|
+
const changeAt = (address, addressHash) => {
|
|
1724
|
+
const candidates = collectionBuckets.get(addressHash) ?? [];
|
|
1725
|
+
collectionBuckets.set(addressHash, candidates);
|
|
1726
|
+
let change = candidates.find((entry) => sameAddress$1(entry.address, address));
|
|
1727
|
+
if (!change) {
|
|
1728
|
+
change = {
|
|
1729
|
+
address,
|
|
1730
|
+
added: /* @__PURE__ */ new Set(),
|
|
1731
|
+
removed: /* @__PURE__ */ new Set(),
|
|
1732
|
+
updated: /* @__PURE__ */ new Set(),
|
|
1733
|
+
orderChanged: false
|
|
1734
|
+
};
|
|
1735
|
+
candidates.push(change);
|
|
1736
|
+
collections.push(change);
|
|
1737
|
+
}
|
|
1738
|
+
return change;
|
|
1739
|
+
};
|
|
1740
|
+
for (const subject of changed) {
|
|
1741
|
+
if (subject.kind === "entity") {
|
|
1742
|
+
const analysis = entityChanges.get(subject);
|
|
1743
|
+
if (analysis) {
|
|
1744
|
+
const change = changeAt(subject.address, subject.addressHash);
|
|
1745
|
+
analysis.added.forEach((id) => change.added.add(id));
|
|
1746
|
+
analysis.removed.forEach((id) => change.removed.add(id));
|
|
1747
|
+
analysis.updated.forEach((id) => change.updated.add(id));
|
|
1748
|
+
if (analysis.orderChanged) change.orderChanged = true;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
for (let collection = subject.collection; collection; collection = collection.parent) changeAt(collection.address, collection.addressHash).updated.add(collection.id);
|
|
1752
|
+
}
|
|
1753
|
+
for (const change of collections) {
|
|
1754
|
+
change.added.forEach((id) => change.updated.delete(id));
|
|
1755
|
+
change.removed.forEach((id) => change.updated.delete(id));
|
|
1756
|
+
}
|
|
1757
|
+
return {
|
|
1758
|
+
status: "changed",
|
|
1759
|
+
paths: Object.freeze(changed.map((subject) => subject.address)),
|
|
1760
|
+
collections
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
};
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region core/src/mutation/operation.ts
|
|
1767
|
+
const has = (value, key) => Object.prototype.hasOwnProperty.call(value, key);
|
|
1768
|
+
const stringArray = (value) => Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
1769
|
+
const positions = (value) => Array.isArray(value) && value.every((entry) => Number.isInteger(entry) && entry >= 0);
|
|
1770
|
+
const address$1 = (value) => stringArray(value);
|
|
1771
|
+
const anchor = (value) => {
|
|
1772
|
+
if (!require_ownership.isRecord(value)) return false;
|
|
1773
|
+
if (Number(has(value, "at")) + Number(has(value, "before")) + Number(has(value, "after")) !== 1) return false;
|
|
1774
|
+
if (has(value, "at")) return value.at === "start" || value.at === "end";
|
|
1775
|
+
return typeof (has(value, "before") ? value.before : value.after) === "string";
|
|
1776
|
+
};
|
|
1777
|
+
const optionalAnchor = (value) => !has(value, "anchor") || value.anchor === void 0 || anchor(value.anchor);
|
|
1778
|
+
const optionalParent = (value) => !has(value, "parentId") || value.parentId === void 0 || typeof value.parentId === "string";
|
|
1779
|
+
const optionalIndex = (value) => !has(value, "index") || value.index === void 0 || Number.isInteger(value.index) && value.index >= 0;
|
|
1780
|
+
const entries = (value) => Array.isArray(value) && value.every((entry) => require_ownership.isRecord(entry) && typeof entry.id === "string" && has(entry, "value"));
|
|
1781
|
+
const decoded = (operation) => ({
|
|
1782
|
+
status: "decoded",
|
|
1783
|
+
operation
|
|
1784
|
+
});
|
|
1785
|
+
const rejected = (code, message) => ({
|
|
1786
|
+
status: "rejected",
|
|
1787
|
+
issue: input(code, message)
|
|
1788
|
+
});
|
|
1789
|
+
const decode = (input) => {
|
|
1790
|
+
if (!require_ownership.isRecord(input)) return rejected("invalid-operation", "Operation must be an object.");
|
|
1791
|
+
if (!address$1(input.at)) return rejected("invalid-address", "Operation address is malformed.");
|
|
1792
|
+
if (typeof input.type !== "string") return rejected("invalid-operation", "Operation type must be a string.");
|
|
1793
|
+
switch (input.type) {
|
|
1794
|
+
case "field.set":
|
|
1795
|
+
case "variant.replace": return has(input, "value") ? decoded(input) : rejected("invalid-operation", "Operation value is required.");
|
|
1796
|
+
case "field.clear": return decoded(input);
|
|
1797
|
+
case "dict.set": return typeof input.key === "string" && has(input, "value") ? decoded(input) : rejected("invalid-operation", "Dictionary set requires a string key and value.");
|
|
1798
|
+
case "dict.delete": return typeof input.key === "string" ? decoded(input) : rejected("invalid-operation", "Dictionary delete requires a string key.");
|
|
1799
|
+
case "dict.replace": return require_ownership.isRecord(input.value) ? decoded(input) : rejected("invalid-operation", "Dictionary replacement must be an object.");
|
|
1800
|
+
case "entity.create": return entries(input.entries) && optionalAnchor(input) && (!has(input, "positions") || input.positions === void 0 || positions(input.positions)) ? decoded(input) : rejected("invalid-operation", "Entity create payload is malformed.");
|
|
1801
|
+
case "entity.remove": return stringArray(input.ids) ? decoded(input) : rejected("invalid-operation", "Entity remove requires string ids.");
|
|
1802
|
+
case "entity.move": return typeof input.id === "string" && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "Entity move payload is malformed.");
|
|
1803
|
+
case "list.insert": return typeof input.key === "string" && has(input, "value") && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "List insert payload is malformed.");
|
|
1804
|
+
case "list.move": return typeof input.key === "string" && optionalAnchor(input) ? decoded(input) : rejected("invalid-operation", "List move payload is malformed.");
|
|
1805
|
+
case "list.remove": return typeof input.key === "string" ? decoded(input) : rejected("invalid-operation", "List remove requires a string key.");
|
|
1806
|
+
case "list.replace": return Array.isArray(input.value) && stringArray(input.keys) ? decoded(input) : rejected("invalid-operation", "List replacement payload is malformed.");
|
|
1807
|
+
case "tree.insert": return typeof input.treeNodeId === "string" && optionalParent(input) && optionalIndex(input) ? decoded(input) : rejected("invalid-operation", "Tree insert payload is malformed.");
|
|
1808
|
+
case "tree.move": return typeof input.treeNodeId === "string" && optionalParent(input) && optionalIndex(input) ? decoded(input) : rejected("invalid-operation", "Tree move payload is malformed.");
|
|
1809
|
+
case "tree.remove": return typeof input.treeNodeId === "string" ? decoded(input) : rejected("invalid-operation", "Tree remove requires a node id.");
|
|
1810
|
+
case "tree.set": return typeof input.treeNodeId === "string" && has(input, "value") ? decoded(input) : rejected("invalid-operation", "Tree set payload is malformed.");
|
|
1811
|
+
case "tree.replace": return has(input, "value") ? decoded(input) : rejected("invalid-operation", "Tree replacement value is required.");
|
|
1812
|
+
default: return rejected("unknown-operation", "Unknown document operation.");
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
const decodeBatch = (input$1) => Array.isArray(input$1) ? {
|
|
1816
|
+
status: "decoded",
|
|
1817
|
+
operations: input$1
|
|
1818
|
+
} : {
|
|
1819
|
+
status: "rejected",
|
|
1820
|
+
issue: input("invalid-operation", "Operation batch must be an array.")
|
|
1821
|
+
};
|
|
1822
|
+
const ownAddress = (value) => {
|
|
1823
|
+
if (Object.isFrozen(value)) return value;
|
|
1824
|
+
require_ownership.profile.address.arrayCopied();
|
|
1825
|
+
return Object.freeze(value.slice());
|
|
1826
|
+
};
|
|
1827
|
+
const structuralPayload = (value) => Array.isArray(value) || require_ownership.isPlainObject(value);
|
|
1828
|
+
const normalize = (operation) => {
|
|
1829
|
+
require_ownership.profile.mutation.normalized();
|
|
1830
|
+
const copy = {
|
|
1831
|
+
...operation,
|
|
1832
|
+
at: ownAddress(operation.at)
|
|
1833
|
+
};
|
|
1834
|
+
if (copy.anchor) copy.anchor = Object.freeze({ ...copy.anchor });
|
|
1835
|
+
if (copy.keys) copy.keys = Object.freeze([...copy.keys]);
|
|
1836
|
+
if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({ ...entry })));
|
|
1837
|
+
if (copy.ids) copy.ids = Object.freeze([...copy.ids]);
|
|
1838
|
+
if (copy.positions) copy.positions = Object.freeze([...copy.positions]);
|
|
1839
|
+
return Object.freeze(copy);
|
|
1840
|
+
};
|
|
1841
|
+
const publish = (operation) => {
|
|
1842
|
+
if (operation.type === "entity.create" && operation.entries.every((entry) => !structuralPayload(entry.value) || require_ownership.isStablePayload(entry.value))) return operation;
|
|
1843
|
+
if ("value" in operation && (!structuralPayload(operation.value) || require_ownership.isStablePayload(operation.value))) return operation;
|
|
1844
|
+
const copy = { ...operation };
|
|
1845
|
+
if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({
|
|
1846
|
+
id: entry.id,
|
|
1847
|
+
value: structuralPayload(entry.value) ? require_ownership.isStablePayload(entry.value) ? entry.value : require_ownership.snapshotPayload(entry.value, "commit") : entry.value
|
|
1848
|
+
})));
|
|
1849
|
+
else if ("value" in copy && structuralPayload(copy.value)) copy.value = require_ownership.isStablePayload(copy.value) ? copy.value : require_ownership.snapshotPayload(copy.value, "commit");
|
|
1850
|
+
return Object.freeze(copy);
|
|
1851
|
+
};
|
|
1852
|
+
const requiresPayloadCopy = (operation) => {
|
|
1853
|
+
if (operation.type === "entity.create") return operation.entries.some((entry) => require_ownership.isStablePayload(entry.value));
|
|
1854
|
+
return "value" in operation && require_ownership.isStablePayload(operation.value);
|
|
1855
|
+
};
|
|
1856
|
+
const inverse = (operation) => {
|
|
1857
|
+
const copy = { ...operation };
|
|
1858
|
+
if (copy.anchor) copy.anchor = Object.freeze({ ...copy.anchor });
|
|
1859
|
+
if (copy.keys) copy.keys = Object.freeze([...copy.keys]);
|
|
1860
|
+
if (copy.entries) copy.entries = Object.freeze(copy.entries.map((entry) => Object.freeze({
|
|
1861
|
+
id: entry.id,
|
|
1862
|
+
value: structuralPayload(entry.value) ? require_ownership.isStablePayload(entry.value) ? entry.value : require_ownership.snapshotPayload(entry.value, "inverse") : entry.value
|
|
1863
|
+
})));
|
|
1864
|
+
if (copy.ids) copy.ids = Object.freeze([...copy.ids]);
|
|
1865
|
+
if (copy.positions) copy.positions = Object.freeze([...copy.positions]);
|
|
1866
|
+
if ("value" in copy && structuralPayload(copy.value)) {
|
|
1867
|
+
const payload = copy;
|
|
1868
|
+
payload.value = require_ownership.isStablePayload(payload.value) ? payload.value : require_ownership.snapshotPayload(payload.value, "inverse");
|
|
1869
|
+
}
|
|
1870
|
+
return Object.freeze(copy);
|
|
1871
|
+
};
|
|
1872
|
+
//#endregion
|
|
1873
|
+
//#region core/src/mutation/session.ts
|
|
1874
|
+
const createMutationSession = (root, schema, options = {}) => {
|
|
1875
|
+
const steps = [];
|
|
1876
|
+
const journal = createChangeJournal(root);
|
|
1877
|
+
let state = "active";
|
|
1878
|
+
const rollback = () => {
|
|
1879
|
+
if (state === "rolled-back") return;
|
|
1880
|
+
for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex -= 1) for (const inverse of steps[stepIndex].inverse) {
|
|
1881
|
+
const result = execute(root, inverse, require_dependency.resolveLocated(schema, root, inverse.at), true);
|
|
1882
|
+
if (result.status === "rejected") throw new Error(`Document rollback failed: ${result.issue.message}`);
|
|
1883
|
+
}
|
|
1884
|
+
state = "rolled-back";
|
|
1885
|
+
};
|
|
1886
|
+
return {
|
|
1887
|
+
apply: (input) => {
|
|
1888
|
+
if (state !== "active") throw new Error("Mutation session is closed.");
|
|
1889
|
+
require_ownership.profile.batch.operation();
|
|
1890
|
+
const decoded = decode(input);
|
|
1891
|
+
if (decoded.status === "rejected") {
|
|
1892
|
+
require_ownership.profile.batch.rejected();
|
|
1893
|
+
return decoded.issue;
|
|
1894
|
+
}
|
|
1895
|
+
const normalized = normalize(decoded.operation);
|
|
1896
|
+
require_ownership.profile.batch.entry(normalized.type === "entity.create" ? normalized.entries.length : normalized.type === "entity.remove" ? normalized.ids.length : 1);
|
|
1897
|
+
const resolved = require_dependency.resolveLocated(schema, root, normalized.at);
|
|
1898
|
+
if (resolved?.collection) require_ownership.profile.batch.collectionResolved();
|
|
1899
|
+
require_ownership.profile.mutation.executed();
|
|
1900
|
+
const result = execute(root, normalized, resolved, options.copyPayload === true || requiresPayloadCopy(normalized));
|
|
1901
|
+
if (result.status === "rejected") {
|
|
1902
|
+
require_ownership.profile.batch.rejected();
|
|
1903
|
+
return result.issue;
|
|
1904
|
+
}
|
|
1905
|
+
if (result.status === "unchanged") return void 0;
|
|
1906
|
+
const published = publish(normalized);
|
|
1907
|
+
const group = [];
|
|
1908
|
+
for (const rawInverse of result.inverse) {
|
|
1909
|
+
const inverse$1 = inverse(rawInverse);
|
|
1910
|
+
group.push(inverse$1);
|
|
1911
|
+
require_ownership.profile.mutation.inverse();
|
|
1912
|
+
require_ownership.profile.batch.inverse();
|
|
1913
|
+
}
|
|
1914
|
+
steps.push({
|
|
1915
|
+
forward: published,
|
|
1916
|
+
inverse: Object.freeze(group)
|
|
1917
|
+
});
|
|
1918
|
+
try {
|
|
1919
|
+
if (!resolved) throw new Error(`Changed operation '${normalized.type}' was not resolved.`);
|
|
1920
|
+
journal.record(resolved, normalized, result.inverse);
|
|
1921
|
+
} catch (error) {
|
|
1922
|
+
rollback();
|
|
1923
|
+
throw error;
|
|
1924
|
+
}
|
|
1925
|
+
},
|
|
1926
|
+
finish: () => {
|
|
1927
|
+
if (state !== "active") throw new Error("Mutation session is closed.");
|
|
1928
|
+
const changes = journal.finish();
|
|
1929
|
+
state = "finished";
|
|
1930
|
+
if (changes.status === "unchanged") return { status: "unchanged" };
|
|
1931
|
+
let inverseCount = 0;
|
|
1932
|
+
for (const step of steps) inverseCount += step.inverse.length;
|
|
1933
|
+
const inverse = new Array(inverseCount);
|
|
1934
|
+
let writeIndex = 0;
|
|
1935
|
+
for (let stepIndex = steps.length - 1; stepIndex >= 0; stepIndex -= 1) for (const entry of steps[stepIndex].inverse) inverse[writeIndex++] = entry;
|
|
1936
|
+
return {
|
|
1937
|
+
status: "changed",
|
|
1938
|
+
operations: Object.freeze(steps.map((step) => step.forward)),
|
|
1939
|
+
inverse: Object.freeze(inverse),
|
|
1940
|
+
paths: changes.paths,
|
|
1941
|
+
collections: changes.collections
|
|
1942
|
+
};
|
|
1943
|
+
},
|
|
1944
|
+
rollback
|
|
1945
|
+
};
|
|
1946
|
+
};
|
|
1947
|
+
const mutateOperations = (root, schema, operations, options = {}) => {
|
|
1948
|
+
const decodedBatch = decodeBatch(operations);
|
|
1949
|
+
if (decodedBatch.status === "rejected") return {
|
|
1950
|
+
status: "rejected",
|
|
1951
|
+
issues: Object.freeze([decodedBatch.issue])
|
|
1952
|
+
};
|
|
1953
|
+
const session = createMutationSession(root, schema, options);
|
|
1954
|
+
for (const input of decodedBatch.operations) {
|
|
1955
|
+
const issue = session.apply(input);
|
|
1956
|
+
if (!issue) continue;
|
|
1957
|
+
session.rollback();
|
|
1958
|
+
return {
|
|
1959
|
+
status: "rejected",
|
|
1960
|
+
issues: Object.freeze([issue])
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
return session.finish();
|
|
1964
|
+
};
|
|
1965
|
+
//#endregion
|
|
1966
|
+
//#region core/src/mutation/footprint.ts
|
|
1967
|
+
const sameAddress = (left, right) => left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
1968
|
+
const containsAddress = (parent, child) => parent.length <= child.length && parent.every((segment, index) => segment === child[index]);
|
|
1969
|
+
const address = (value) => Array.isArray(value) && value.every((segment) => typeof segment === "string") ? Object.freeze(value.slice()) : void 0;
|
|
1970
|
+
const targetKey = (target) => {
|
|
1971
|
+
const at = target.at.join("\0");
|
|
1972
|
+
if (target.kind === "dictionary-key" || target.kind === "list-item") return `${target.kind}:${at}:${target.key}`;
|
|
1973
|
+
if (target.kind === "entity" || target.kind === "tree-node") return `${target.kind}:${at}:${target.id}`;
|
|
1974
|
+
return `${target.kind}:${at}`;
|
|
1975
|
+
};
|
|
1976
|
+
const freezeTarget = (target) => Object.freeze({
|
|
1977
|
+
...target,
|
|
1978
|
+
at: Object.freeze(target.at.slice())
|
|
1979
|
+
});
|
|
1980
|
+
const add = (targets, target) => {
|
|
1981
|
+
const stable = freezeTarget(target);
|
|
1982
|
+
targets.set(targetKey(stable), stable);
|
|
1983
|
+
};
|
|
1984
|
+
const commandFootprint = (operations) => {
|
|
1985
|
+
const targets = /* @__PURE__ */ new Map();
|
|
1986
|
+
for (const operation of operations) switch (operation.type) {
|
|
1987
|
+
case "field.set":
|
|
1988
|
+
case "field.clear":
|
|
1989
|
+
case "variant.replace":
|
|
1990
|
+
add(targets, {
|
|
1991
|
+
kind: "value",
|
|
1992
|
+
at: operation.at
|
|
1993
|
+
});
|
|
1994
|
+
break;
|
|
1995
|
+
case "dict.replace":
|
|
1996
|
+
add(targets, {
|
|
1997
|
+
kind: "dictionary",
|
|
1998
|
+
at: operation.at
|
|
1999
|
+
});
|
|
2000
|
+
break;
|
|
2001
|
+
case "dict.set":
|
|
2002
|
+
case "dict.delete":
|
|
2003
|
+
add(targets, {
|
|
2004
|
+
kind: "dictionary-key",
|
|
2005
|
+
at: operation.at,
|
|
2006
|
+
key: operation.key
|
|
2007
|
+
});
|
|
2008
|
+
break;
|
|
2009
|
+
case "entity.create":
|
|
2010
|
+
add(targets, {
|
|
2011
|
+
kind: "collection",
|
|
2012
|
+
at: operation.at
|
|
2013
|
+
});
|
|
2014
|
+
for (const entry of operation.entries) add(targets, {
|
|
2015
|
+
kind: "entity",
|
|
2016
|
+
at: operation.at,
|
|
2017
|
+
id: entry.id
|
|
2018
|
+
});
|
|
2019
|
+
break;
|
|
2020
|
+
case "entity.remove":
|
|
2021
|
+
add(targets, {
|
|
2022
|
+
kind: "collection",
|
|
2023
|
+
at: operation.at
|
|
2024
|
+
});
|
|
2025
|
+
for (const id of operation.ids) add(targets, {
|
|
2026
|
+
kind: "entity",
|
|
2027
|
+
at: operation.at,
|
|
2028
|
+
id
|
|
2029
|
+
});
|
|
2030
|
+
break;
|
|
2031
|
+
case "entity.move":
|
|
2032
|
+
add(targets, {
|
|
2033
|
+
kind: "collection",
|
|
2034
|
+
at: operation.at
|
|
2035
|
+
});
|
|
2036
|
+
add(targets, {
|
|
2037
|
+
kind: "entity",
|
|
2038
|
+
at: operation.at,
|
|
2039
|
+
id: operation.id
|
|
2040
|
+
});
|
|
2041
|
+
break;
|
|
2042
|
+
case "list.insert":
|
|
2043
|
+
case "list.move":
|
|
2044
|
+
case "list.remove":
|
|
2045
|
+
add(targets, {
|
|
2046
|
+
kind: "list",
|
|
2047
|
+
at: operation.at
|
|
2048
|
+
});
|
|
2049
|
+
add(targets, {
|
|
2050
|
+
kind: "list-item",
|
|
2051
|
+
at: operation.at,
|
|
2052
|
+
key: operation.key
|
|
2053
|
+
});
|
|
2054
|
+
break;
|
|
2055
|
+
case "list.replace":
|
|
2056
|
+
add(targets, {
|
|
2057
|
+
kind: "list",
|
|
2058
|
+
at: operation.at
|
|
2059
|
+
});
|
|
2060
|
+
for (const key of operation.keys) add(targets, {
|
|
2061
|
+
kind: "list-item",
|
|
2062
|
+
at: operation.at,
|
|
2063
|
+
key
|
|
2064
|
+
});
|
|
2065
|
+
break;
|
|
2066
|
+
case "tree.insert":
|
|
2067
|
+
case "tree.move":
|
|
2068
|
+
case "tree.remove":
|
|
2069
|
+
case "tree.set":
|
|
2070
|
+
add(targets, {
|
|
2071
|
+
kind: "tree",
|
|
2072
|
+
at: operation.at
|
|
2073
|
+
});
|
|
2074
|
+
add(targets, {
|
|
2075
|
+
kind: "tree-node",
|
|
2076
|
+
at: operation.at,
|
|
2077
|
+
id: operation.treeNodeId
|
|
2078
|
+
});
|
|
2079
|
+
break;
|
|
2080
|
+
case "tree.replace":
|
|
2081
|
+
add(targets, {
|
|
2082
|
+
kind: "tree",
|
|
2083
|
+
at: operation.at
|
|
2084
|
+
});
|
|
2085
|
+
break;
|
|
2086
|
+
}
|
|
2087
|
+
return Object.freeze(Array.from(targets.values()));
|
|
2088
|
+
};
|
|
2089
|
+
const overlapsValueAndEntity = (value, entity) => containsAddress([...entity.at, entity.id], value.at) || containsAddress(value.at, [...entity.at, entity.id]);
|
|
2090
|
+
const targetsOverlap = (left, right) => {
|
|
2091
|
+
if (left.kind === "value" && right.kind === "value") return containsAddress(left.at, right.at) || containsAddress(right.at, left.at);
|
|
2092
|
+
if (left.kind === "value" && right.kind === "entity") return overlapsValueAndEntity(left, right);
|
|
2093
|
+
if (left.kind === "entity" && right.kind === "value") return overlapsValueAndEntity(right, left);
|
|
2094
|
+
if (left.kind === "dictionary" && right.kind === "dictionary") return sameAddress(left.at, right.at);
|
|
2095
|
+
if (left.kind === "dictionary" && right.kind === "dictionary-key") return sameAddress(left.at, right.at);
|
|
2096
|
+
if (left.kind === "dictionary-key" && right.kind === "dictionary") return sameAddress(left.at, right.at);
|
|
2097
|
+
if (left.kind === "dictionary-key" && right.kind === "dictionary-key") return sameAddress(left.at, right.at) && left.key === right.key;
|
|
2098
|
+
if (left.kind === "collection" && right.kind === "collection") return sameAddress(left.at, right.at);
|
|
2099
|
+
if (left.kind === "collection" && right.kind === "entity") return sameAddress(left.at, right.at);
|
|
2100
|
+
if (left.kind === "entity" && right.kind === "collection") return sameAddress(left.at, right.at);
|
|
2101
|
+
if (left.kind === "entity" && right.kind === "entity") return sameAddress(left.at, right.at) && left.id === right.id;
|
|
2102
|
+
if (left.kind === "list" && (right.kind === "list" || right.kind === "list-item")) return sameAddress(left.at, right.at);
|
|
2103
|
+
if (right.kind === "list" && left.kind === "list-item") return sameAddress(left.at, right.at);
|
|
2104
|
+
if (left.kind === "list-item" && right.kind === "list-item") return sameAddress(left.at, right.at) && left.key === right.key;
|
|
2105
|
+
if (left.kind === "tree" && (right.kind === "tree" || right.kind === "tree-node")) return sameAddress(left.at, right.at);
|
|
2106
|
+
if (right.kind === "tree" && left.kind === "tree-node") return sameAddress(left.at, right.at);
|
|
2107
|
+
if (left.kind === "tree-node" && right.kind === "tree-node") return sameAddress(left.at, right.at) && left.id === right.id;
|
|
2108
|
+
return false;
|
|
2109
|
+
};
|
|
2110
|
+
const footprintsOverlap = (left, right) => left.some((leftTarget) => right.some((rightTarget) => targetsOverlap(leftTarget, rightTarget)));
|
|
2111
|
+
const decodedTarget = (value) => {
|
|
2112
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
2113
|
+
const record = value;
|
|
2114
|
+
const at = address(record.at);
|
|
2115
|
+
if (!at || typeof record.kind !== "string") return void 0;
|
|
2116
|
+
switch (record.kind) {
|
|
2117
|
+
case "value":
|
|
2118
|
+
case "dictionary":
|
|
2119
|
+
case "collection":
|
|
2120
|
+
case "list":
|
|
2121
|
+
case "tree": return freezeTarget({
|
|
2122
|
+
kind: record.kind,
|
|
2123
|
+
at
|
|
2124
|
+
});
|
|
2125
|
+
case "dictionary-key":
|
|
2126
|
+
case "list-item": return typeof record.key === "string" ? freezeTarget({
|
|
2127
|
+
kind: record.kind,
|
|
2128
|
+
at,
|
|
2129
|
+
key: record.key
|
|
2130
|
+
}) : void 0;
|
|
2131
|
+
case "entity":
|
|
2132
|
+
case "tree-node": return typeof record.id === "string" ? freezeTarget({
|
|
2133
|
+
kind: record.kind,
|
|
2134
|
+
at,
|
|
2135
|
+
id: record.id
|
|
2136
|
+
}) : void 0;
|
|
2137
|
+
default: return;
|
|
2138
|
+
}
|
|
2139
|
+
};
|
|
2140
|
+
const decodeCommandFootprint = (value) => {
|
|
2141
|
+
if (!Array.isArray(value)) return void 0;
|
|
2142
|
+
const targets = /* @__PURE__ */ new Map();
|
|
2143
|
+
for (const entry of value) {
|
|
2144
|
+
const target = decodedTarget(entry);
|
|
2145
|
+
if (!target) return void 0;
|
|
2146
|
+
targets.set(targetKey(target), target);
|
|
2147
|
+
}
|
|
2148
|
+
return Object.freeze(Array.from(targets.values()));
|
|
2149
|
+
};
|
|
2150
|
+
//#endregion
|
|
2151
|
+
//#region core/src/runtime/notification.ts
|
|
2152
|
+
const ROOT_BUCKET = Symbol("document-root");
|
|
2153
|
+
const notifications = /* @__PURE__ */ new WeakMap();
|
|
2154
|
+
const key = (value) => require_dependency.bucket(value) ?? ROOT_BUCKET;
|
|
2155
|
+
const createNotification = (runtime) => {
|
|
2156
|
+
const notification = {
|
|
2157
|
+
root: /* @__PURE__ */ new Set(),
|
|
2158
|
+
filtered: /* @__PURE__ */ new Set(),
|
|
2159
|
+
buckets: /* @__PURE__ */ new Map(),
|
|
2160
|
+
processors: [],
|
|
2161
|
+
candidates: /* @__PURE__ */ new Set(),
|
|
2162
|
+
rootSnapshot: [],
|
|
2163
|
+
notifying: false
|
|
2164
|
+
};
|
|
2165
|
+
notifications.set(runtime, notification);
|
|
2166
|
+
return notification;
|
|
2167
|
+
};
|
|
2168
|
+
const notificationOf = (runtime) => {
|
|
2169
|
+
const value = notifications.get(runtime);
|
|
2170
|
+
if (!value) throw new Error("Unknown Doxum runtime.");
|
|
2171
|
+
return value;
|
|
2172
|
+
};
|
|
2173
|
+
const shareNotification = (source, target) => {
|
|
2174
|
+
notifications.set(target, notificationOf(source));
|
|
2175
|
+
};
|
|
2176
|
+
const registerProcessor = (runtime, processor) => {
|
|
2177
|
+
const notification = notificationOf(runtime);
|
|
2178
|
+
const entry = {
|
|
2179
|
+
processor,
|
|
2180
|
+
active: true
|
|
2181
|
+
};
|
|
2182
|
+
notification.processors.push(entry);
|
|
2183
|
+
return () => {
|
|
2184
|
+
if (!entry.active) return;
|
|
2185
|
+
entry.active = false;
|
|
2186
|
+
if (!notification.notifying) {
|
|
2187
|
+
const index = notification.processors.indexOf(entry);
|
|
2188
|
+
if (index >= 0) notification.processors.splice(index, 1);
|
|
2189
|
+
}
|
|
2190
|
+
};
|
|
2191
|
+
};
|
|
2192
|
+
const subscribeRoot = (notification, listener) => {
|
|
2193
|
+
const entry = {
|
|
2194
|
+
listener,
|
|
2195
|
+
active: true
|
|
2196
|
+
};
|
|
2197
|
+
notification.root.add(entry);
|
|
2198
|
+
return () => {
|
|
2199
|
+
entry.active = false;
|
|
2200
|
+
if (!notification.notifying) notification.root.delete(entry);
|
|
2201
|
+
};
|
|
2202
|
+
};
|
|
2203
|
+
const subscribeTargets = (notification, targets, listener) => {
|
|
2204
|
+
const entry = {
|
|
2205
|
+
targets: Object.freeze(targets.slice()),
|
|
2206
|
+
listener,
|
|
2207
|
+
active: true
|
|
2208
|
+
};
|
|
2209
|
+
notification.filtered.add(entry);
|
|
2210
|
+
const keys = new Set(targets.map(key));
|
|
2211
|
+
keys.forEach((key) => {
|
|
2212
|
+
const bucket = notification.buckets.get(key) ?? /* @__PURE__ */ new Set();
|
|
2213
|
+
bucket.add(entry);
|
|
2214
|
+
notification.buckets.set(key, bucket);
|
|
2215
|
+
});
|
|
2216
|
+
return () => {
|
|
2217
|
+
if (!entry.active) return;
|
|
2218
|
+
entry.active = false;
|
|
2219
|
+
if (notification.notifying) return;
|
|
2220
|
+
notification.filtered.delete(entry);
|
|
2221
|
+
keys.forEach((key) => {
|
|
2222
|
+
const bucket = notification.buckets.get(key);
|
|
2223
|
+
bucket?.delete(entry);
|
|
2224
|
+
if (bucket?.size === 0) notification.buckets.delete(key);
|
|
2225
|
+
});
|
|
2226
|
+
};
|
|
2227
|
+
};
|
|
2228
|
+
const notify = (notification, commit) => {
|
|
2229
|
+
notification.notifying = true;
|
|
2230
|
+
const errors = [];
|
|
2231
|
+
const call = (listener) => {
|
|
2232
|
+
try {
|
|
2233
|
+
listener(commit);
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
errors.push(Object.freeze({
|
|
2236
|
+
phase: "listener",
|
|
2237
|
+
error
|
|
2238
|
+
}));
|
|
2239
|
+
}
|
|
2240
|
+
};
|
|
2241
|
+
try {
|
|
2242
|
+
const processorCount = notification.processors.length;
|
|
2243
|
+
for (let index = 0; index < processorCount; index += 1) {
|
|
2244
|
+
const entry = notification.processors[index];
|
|
2245
|
+
if (!entry?.active) continue;
|
|
2246
|
+
try {
|
|
2247
|
+
entry.processor.process(commit);
|
|
2248
|
+
} catch (error) {
|
|
2249
|
+
errors.push(Object.freeze({
|
|
2250
|
+
phase: "processor",
|
|
2251
|
+
error
|
|
2252
|
+
}));
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
for (let index = 0; index < processorCount; index += 1) {
|
|
2256
|
+
const entry = notification.processors[index];
|
|
2257
|
+
if (!entry?.active) continue;
|
|
2258
|
+
try {
|
|
2259
|
+
entry.processor.flush();
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
errors.push(Object.freeze({
|
|
2262
|
+
phase: "flush",
|
|
2263
|
+
error
|
|
2264
|
+
}));
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
const candidates = notification.candidates;
|
|
2268
|
+
candidates.clear();
|
|
2269
|
+
if (commit.impact.kind === "reset") notification.filtered.forEach((entry) => candidates.add(entry));
|
|
2270
|
+
else for (const operation of commit.operations) {
|
|
2271
|
+
const key = operation.at[0] ?? ROOT_BUCKET;
|
|
2272
|
+
notification.buckets.get(key)?.forEach((entry) => candidates.add(entry));
|
|
2273
|
+
notification.buckets.get(ROOT_BUCKET)?.forEach((entry) => candidates.add(entry));
|
|
2274
|
+
}
|
|
2275
|
+
candidates.forEach((entry) => {
|
|
2276
|
+
if (entry.active && entry.targets.some(commit.impact.affects)) call(entry.listener);
|
|
2277
|
+
});
|
|
2278
|
+
const rootSnapshot = notification.rootSnapshot;
|
|
2279
|
+
rootSnapshot.length = 0;
|
|
2280
|
+
notification.root.forEach((entry) => rootSnapshot.push(entry));
|
|
2281
|
+
for (const entry of rootSnapshot) if (entry.active) call(entry.listener);
|
|
2282
|
+
} finally {
|
|
2283
|
+
notification.notifying = false;
|
|
2284
|
+
let writeIndex = 0;
|
|
2285
|
+
for (const entry of notification.processors) if (entry.active) notification.processors[writeIndex++] = entry;
|
|
2286
|
+
notification.processors.length = writeIndex;
|
|
2287
|
+
notification.root.forEach((entry) => {
|
|
2288
|
+
if (!entry.active) notification.root.delete(entry);
|
|
2289
|
+
});
|
|
2290
|
+
notification.filtered.forEach((entry) => {
|
|
2291
|
+
if (!entry.active) notification.filtered.delete(entry);
|
|
2292
|
+
});
|
|
2293
|
+
notification.buckets.forEach((bucket, key) => {
|
|
2294
|
+
bucket.forEach((entry) => {
|
|
2295
|
+
if (!entry.active) bucket.delete(entry);
|
|
2296
|
+
});
|
|
2297
|
+
if (bucket.size === 0) notification.buckets.delete(key);
|
|
2298
|
+
});
|
|
2299
|
+
}
|
|
2300
|
+
return Object.freeze(errors);
|
|
2301
|
+
};
|
|
2302
|
+
const disposeNotification = (notification) => {
|
|
2303
|
+
notification.root.clear();
|
|
2304
|
+
notification.filtered.clear();
|
|
2305
|
+
notification.buckets.clear();
|
|
2306
|
+
notification.processors.length = 0;
|
|
2307
|
+
notification.candidates.clear();
|
|
2308
|
+
notification.rootSnapshot.length = 0;
|
|
2309
|
+
};
|
|
2310
|
+
//#endregion
|
|
2311
|
+
//#region core/src/runtime.ts
|
|
2312
|
+
var RejectedUpdate = class extends Error {
|
|
2313
|
+
issues;
|
|
2314
|
+
constructor(issues) {
|
|
2315
|
+
super("Document update rejected.");
|
|
2316
|
+
this.issues = Object.freeze(issues.slice());
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
const EMPTY = Object.freeze([]);
|
|
2320
|
+
const publishDiagnostic = (value) => Object.freeze({
|
|
2321
|
+
...value,
|
|
2322
|
+
...value.address === void 0 ? {} : { address: Object.freeze(value.address.slice()) }
|
|
2323
|
+
});
|
|
2324
|
+
const createDocument = (input) => {
|
|
2325
|
+
const initialTreeError = invalidDocument(input.schema, input.initial);
|
|
2326
|
+
if (initialTreeError) throw new TypeError(`Initial document contains an invalid tree at '${initialTreeError.join(".")}'.`);
|
|
2327
|
+
require_ownership.profile.clone.initialDocument();
|
|
2328
|
+
const state = {
|
|
2329
|
+
schema: input.schema,
|
|
2330
|
+
document: require_ownership.cloneValue(input.initial, "initial"),
|
|
2331
|
+
disposed: false
|
|
2332
|
+
};
|
|
2333
|
+
let revision = 0;
|
|
2334
|
+
let busy = false;
|
|
2335
|
+
let runtime;
|
|
2336
|
+
let notification;
|
|
2337
|
+
const assertWritable = (intent) => {
|
|
2338
|
+
if (state.disposed) throw new require_dependency.DocumentDisposedError();
|
|
2339
|
+
if (busy) throw new require_dependency.DocumentReentrancyError();
|
|
2340
|
+
require_driver.assertRuntimeWritable(runtime, intent);
|
|
2341
|
+
};
|
|
2342
|
+
const runTransaction = (session, run) => {
|
|
2343
|
+
let reports;
|
|
2344
|
+
let active = true;
|
|
2345
|
+
const transaction = {
|
|
2346
|
+
read: require_dependency.documentReader(input.schema, () => state.document, () => active),
|
|
2347
|
+
write: documentWriter(input.schema, (operation) => {
|
|
2348
|
+
if (!active) throw new Error("Document writer is no longer active.");
|
|
2349
|
+
const rejected = session.apply(operation);
|
|
2350
|
+
if (rejected) throw new RejectedUpdate([rejected]);
|
|
2351
|
+
}),
|
|
2352
|
+
reject: (diagnostic) => {
|
|
2353
|
+
throw new RejectedUpdate((Array.isArray(diagnostic) ? diagnostic : [diagnostic]).map((entry) => publishDiagnostic(entry)));
|
|
2354
|
+
},
|
|
2355
|
+
report: (diagnostic) => {
|
|
2356
|
+
if (!active) throw new Error("Document transaction is no longer active.");
|
|
2357
|
+
(reports ??= []).push(publishDiagnostic(diagnostic));
|
|
2358
|
+
}
|
|
2359
|
+
};
|
|
2360
|
+
try {
|
|
2361
|
+
const value = run(transaction);
|
|
2362
|
+
if (value !== null && typeof value === "object" && typeof value.then === "function") throw new TypeError("Document update callback must be synchronous.");
|
|
2363
|
+
return {
|
|
2364
|
+
value,
|
|
2365
|
+
reports: Object.freeze(reports ? reports.slice() : EMPTY)
|
|
2366
|
+
};
|
|
2367
|
+
} finally {
|
|
2368
|
+
active = false;
|
|
2369
|
+
}
|
|
2370
|
+
};
|
|
2371
|
+
const impactFor = (batch, kind) => createImpact({
|
|
2372
|
+
schema: input.schema,
|
|
2373
|
+
operations: batch.operations,
|
|
2374
|
+
paths: batch.paths,
|
|
2375
|
+
collections: batch.collections,
|
|
2376
|
+
reset: kind === "replace"
|
|
2377
|
+
});
|
|
2378
|
+
const history = createHistory({
|
|
2379
|
+
capacity: input.history === false ? 0 : Math.max(0, input.history?.capacity ?? 100),
|
|
2380
|
+
revision: () => revision,
|
|
2381
|
+
apply: (operations) => applyBatch(operations, "history", false)
|
|
2382
|
+
});
|
|
2383
|
+
const publish = (batch, source, kind, recordHistory) => {
|
|
2384
|
+
revision += 1;
|
|
2385
|
+
const commit = Object.freeze({
|
|
2386
|
+
revision,
|
|
2387
|
+
kind,
|
|
2388
|
+
source,
|
|
2389
|
+
operations: batch.operations,
|
|
2390
|
+
inverse: batch.inverse,
|
|
2391
|
+
impact: impactFor(batch, kind)
|
|
2392
|
+
});
|
|
2393
|
+
if (kind === "replace" || source === "remote") history.invalidate();
|
|
2394
|
+
else if (recordHistory && (source === "local" || source === "system")) history.record(batch.operations, batch.inverse);
|
|
2395
|
+
busy = true;
|
|
2396
|
+
let observerErrors;
|
|
2397
|
+
try {
|
|
2398
|
+
observerErrors = notify(notification, commit);
|
|
2399
|
+
} finally {
|
|
2400
|
+
busy = false;
|
|
2401
|
+
}
|
|
2402
|
+
return {
|
|
2403
|
+
status: "committed",
|
|
2404
|
+
commit,
|
|
2405
|
+
observerErrors
|
|
2406
|
+
};
|
|
2407
|
+
};
|
|
2408
|
+
function applyBatch(operations, source, recordHistory) {
|
|
2409
|
+
assertWritable({
|
|
2410
|
+
kind: "apply",
|
|
2411
|
+
source
|
|
2412
|
+
});
|
|
2413
|
+
const batch = mutateOperations(state.document, input.schema, operations, { copyPayload: source === "history" });
|
|
2414
|
+
if (batch.status === "rejected") return {
|
|
2415
|
+
status: "rejected",
|
|
2416
|
+
issues: batch.issues,
|
|
2417
|
+
revision
|
|
2418
|
+
};
|
|
2419
|
+
if (batch.status === "unchanged") return {
|
|
2420
|
+
status: "unchanged",
|
|
2421
|
+
revision
|
|
2422
|
+
};
|
|
2423
|
+
return publish(batch, source, "operations", recordHistory);
|
|
2424
|
+
}
|
|
2425
|
+
runtime = {
|
|
2426
|
+
schema: input.schema,
|
|
2427
|
+
address: {
|
|
2428
|
+
resolve: (address) => require_dependency.resolveAddress(input.schema, address, state.document),
|
|
2429
|
+
read: (address) => require_dependency.read(state.document, address),
|
|
2430
|
+
contains: require_dependency.contains,
|
|
2431
|
+
overlaps: require_dependency.overlaps,
|
|
2432
|
+
debugKey: require_dependency.debugKey
|
|
2433
|
+
},
|
|
2434
|
+
revision: () => revision,
|
|
2435
|
+
update: (run, options) => {
|
|
2436
|
+
const source = options?.source ?? "local";
|
|
2437
|
+
assertWritable({
|
|
2438
|
+
kind: "update",
|
|
2439
|
+
source
|
|
2440
|
+
});
|
|
2441
|
+
busy = true;
|
|
2442
|
+
const session = createMutationSession(state.document, input.schema);
|
|
2443
|
+
let committed = false;
|
|
2444
|
+
try {
|
|
2445
|
+
const { value, reports } = runTransaction(session, run);
|
|
2446
|
+
const batch = session.finish();
|
|
2447
|
+
if (batch.status === "unchanged") return {
|
|
2448
|
+
status: "unchanged",
|
|
2449
|
+
value,
|
|
2450
|
+
revision,
|
|
2451
|
+
reports
|
|
2452
|
+
};
|
|
2453
|
+
if (batch.status === "rejected") return {
|
|
2454
|
+
status: "rejected",
|
|
2455
|
+
issues: batch.issues,
|
|
2456
|
+
revision
|
|
2457
|
+
};
|
|
2458
|
+
const result = publish(batch, source, "operations", options?.history ?? true);
|
|
2459
|
+
committed = true;
|
|
2460
|
+
return {
|
|
2461
|
+
status: "committed",
|
|
2462
|
+
value,
|
|
2463
|
+
commit: result.commit,
|
|
2464
|
+
reports,
|
|
2465
|
+
observerErrors: result.observerErrors
|
|
2466
|
+
};
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
try {
|
|
2469
|
+
if (!committed) session.rollback();
|
|
2470
|
+
} finally {
|
|
2471
|
+
busy = false;
|
|
2472
|
+
}
|
|
2473
|
+
if (error instanceof RejectedUpdate) return {
|
|
2474
|
+
status: "rejected",
|
|
2475
|
+
issues: error.issues,
|
|
2476
|
+
revision
|
|
2477
|
+
};
|
|
2478
|
+
throw error;
|
|
2479
|
+
} finally {
|
|
2480
|
+
busy = false;
|
|
2481
|
+
}
|
|
2482
|
+
},
|
|
2483
|
+
prepare: (run) => {
|
|
2484
|
+
assertWritable({ kind: "prepare" });
|
|
2485
|
+
busy = true;
|
|
2486
|
+
const session = createMutationSession(state.document, input.schema);
|
|
2487
|
+
try {
|
|
2488
|
+
const { value, reports } = runTransaction(session, run);
|
|
2489
|
+
const batch = session.finish();
|
|
2490
|
+
if (batch.status === "unchanged") {
|
|
2491
|
+
session.rollback();
|
|
2492
|
+
return {
|
|
2493
|
+
status: "unchanged",
|
|
2494
|
+
value,
|
|
2495
|
+
reports
|
|
2496
|
+
};
|
|
2497
|
+
}
|
|
2498
|
+
if (batch.status === "rejected") {
|
|
2499
|
+
session.rollback();
|
|
2500
|
+
return {
|
|
2501
|
+
status: "rejected",
|
|
2502
|
+
issues: batch.issues
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
const impact = impactFor(batch, "operations");
|
|
2506
|
+
session.rollback();
|
|
2507
|
+
return {
|
|
2508
|
+
status: "prepared",
|
|
2509
|
+
value,
|
|
2510
|
+
operations: batch.operations,
|
|
2511
|
+
inverse: batch.inverse,
|
|
2512
|
+
impact,
|
|
2513
|
+
footprint: commandFootprint(batch.operations),
|
|
2514
|
+
reports
|
|
2515
|
+
};
|
|
2516
|
+
} catch (error) {
|
|
2517
|
+
try {
|
|
2518
|
+
session.rollback();
|
|
2519
|
+
} finally {
|
|
2520
|
+
busy = false;
|
|
2521
|
+
}
|
|
2522
|
+
if (error instanceof RejectedUpdate) return {
|
|
2523
|
+
status: "rejected",
|
|
2524
|
+
issues: error.issues
|
|
2525
|
+
};
|
|
2526
|
+
throw error;
|
|
2527
|
+
} finally {
|
|
2528
|
+
busy = false;
|
|
2529
|
+
}
|
|
2530
|
+
},
|
|
2531
|
+
apply: (operations, options) => applyBatch(operations, options?.source ?? "local", options?.history ?? true),
|
|
2532
|
+
replace: (document, options) => {
|
|
2533
|
+
const source = options?.source ?? "system";
|
|
2534
|
+
assertWritable({
|
|
2535
|
+
kind: "replace",
|
|
2536
|
+
source
|
|
2537
|
+
});
|
|
2538
|
+
const invalidTree = invalidDocument(input.schema, document);
|
|
2539
|
+
if (invalidTree) return {
|
|
2540
|
+
status: "rejected",
|
|
2541
|
+
issues: [at$1(invalidTree, "invalid-tree", "Document replacement contains an invalid tree.")],
|
|
2542
|
+
revision
|
|
2543
|
+
};
|
|
2544
|
+
if (require_ownership.deepEqual(state.document, document)) return {
|
|
2545
|
+
status: "unchanged",
|
|
2546
|
+
revision
|
|
2547
|
+
};
|
|
2548
|
+
state.document = require_ownership.cloneValue(document, "replace");
|
|
2549
|
+
return publish({
|
|
2550
|
+
status: "changed",
|
|
2551
|
+
operations: EMPTY,
|
|
2552
|
+
inverse: EMPTY,
|
|
2553
|
+
paths: EMPTY,
|
|
2554
|
+
collections: EMPTY
|
|
2555
|
+
}, source, "replace", false);
|
|
2556
|
+
},
|
|
2557
|
+
snapshot: () => {
|
|
2558
|
+
if (state.disposed) throw new require_dependency.DocumentDisposedError();
|
|
2559
|
+
return require_ownership.cloneValue(state.document, "snapshot");
|
|
2560
|
+
},
|
|
2561
|
+
subscribe: ((targetOrListener, listener) => {
|
|
2562
|
+
if (state.disposed) throw new require_dependency.DocumentDisposedError();
|
|
2563
|
+
if (typeof targetOrListener === "function") return subscribeRoot(notification, targetOrListener);
|
|
2564
|
+
const targets = Array.isArray(targetOrListener) ? targetOrListener : [targetOrListener];
|
|
2565
|
+
if (targets.length === 0 || !listener) throw new TypeError("Filtered subscribe requires at least one target and a listener.");
|
|
2566
|
+
return subscribeTargets(notification, targets, listener);
|
|
2567
|
+
}),
|
|
2568
|
+
history: history.api,
|
|
2569
|
+
dispose: () => {
|
|
2570
|
+
if (state.disposed) return;
|
|
2571
|
+
state.disposed = true;
|
|
2572
|
+
disposeNotification(notification);
|
|
2573
|
+
history.api.clear();
|
|
2574
|
+
require_driver.disposeRuntimeDriver(runtime);
|
|
2575
|
+
}
|
|
2576
|
+
};
|
|
2577
|
+
require_dependency.bindRuntimeAccess(runtime, state);
|
|
2578
|
+
require_driver.bindRuntimeDriver(runtime);
|
|
2579
|
+
notification = createNotification(runtime);
|
|
2580
|
+
return runtime;
|
|
2581
|
+
};
|
|
2582
|
+
//#endregion
|
|
2583
|
+
//#region core/src/runtime/readable.ts
|
|
2584
|
+
const asReadable = (runtime) => {
|
|
2585
|
+
const readable = {
|
|
2586
|
+
address: runtime.address,
|
|
2587
|
+
revision: runtime.revision,
|
|
2588
|
+
subscribe: runtime.subscribe
|
|
2589
|
+
};
|
|
2590
|
+
require_dependency.bindRuntimeAccess(readable, require_dependency.accessOf(runtime));
|
|
2591
|
+
shareNotification(runtime, readable);
|
|
2592
|
+
return Object.freeze(readable);
|
|
2593
|
+
};
|
|
2594
|
+
//#endregion
|
|
5
2595
|
//#region core/src/projection/select.ts
|
|
6
|
-
const select = (runtime, selector) =>
|
|
2596
|
+
const select = (runtime, selector) => require_dependency.readWith(runtime, selector);
|
|
7
2597
|
//#endregion
|
|
8
2598
|
//#region core/src/projection/collection-view.ts
|
|
9
2599
|
const createCollectionView = (input) => {
|
|
10
|
-
if (
|
|
2600
|
+
if (require_dependency.schemaOf(input.runtime) !== input.source.schema) throw new Error("Collection selector belongs to another schema.");
|
|
11
2601
|
const idsListeners = /* @__PURE__ */ new Set();
|
|
12
2602
|
const allListeners = /* @__PURE__ */ new Set();
|
|
13
2603
|
const itemListeners = /* @__PURE__ */ new Map();
|
|
14
2604
|
const itemReadables = /* @__PURE__ */ new Map();
|
|
15
2605
|
let disposed = false;
|
|
16
|
-
const sourceNode =
|
|
2606
|
+
const sourceNode = require_dependency.nodeAt(input.source.schema, input.source.address);
|
|
17
2607
|
const entryNode = sourceNode?.kind === "table" || sourceNode?.kind === "map" ? sourceNode.value : {
|
|
18
2608
|
kind: "object",
|
|
19
2609
|
shape: {}
|
|
20
2610
|
};
|
|
21
2611
|
const mapEntry = (document, id) => {
|
|
22
|
-
|
|
2612
|
+
require_ownership.profile.collectionView.mapped();
|
|
23
2613
|
const address = [...input.source.address, String(id)];
|
|
24
2614
|
let active = true;
|
|
25
2615
|
try {
|
|
26
|
-
return input.map(id,
|
|
2616
|
+
return input.map(id, require_dependency.readerFor(entryNode, {
|
|
27
2617
|
root: () => document,
|
|
28
2618
|
active: () => active
|
|
29
2619
|
}, address));
|
|
@@ -32,10 +2622,10 @@ const createCollectionView = (input) => {
|
|
|
32
2622
|
}
|
|
33
2623
|
};
|
|
34
2624
|
const build = (previous, changed) => {
|
|
35
|
-
const document =
|
|
36
|
-
const collection =
|
|
2625
|
+
const document = require_dependency.documentOf(input.runtime);
|
|
2626
|
+
const collection = require_dependency.read(document, input.source.address);
|
|
37
2627
|
const ids = collection && typeof collection === "object" && "ids" in collection && Array.isArray(collection.ids) ? [...collection.ids] : Object.keys(collection ?? {});
|
|
38
|
-
|
|
2628
|
+
require_ownership.profile.collectionView.idsScanned(ids.length);
|
|
39
2629
|
const values = previous && changed ? previous.values : /* @__PURE__ */ new Map();
|
|
40
2630
|
if (previous && changed) {
|
|
41
2631
|
const nextIds = new Set(ids);
|
|
@@ -63,7 +2653,7 @@ const createCollectionView = (input) => {
|
|
|
63
2653
|
let state = build();
|
|
64
2654
|
const ensureAll = () => {
|
|
65
2655
|
if (!state.allDirty && state.all) return state.all;
|
|
66
|
-
|
|
2656
|
+
require_ownership.profile.collectionView.arrayCopied();
|
|
67
2657
|
const all = new Array(state.ids.length);
|
|
68
2658
|
for (let index = 0; index < state.ids.length; index += 1) all[index] = state.values.get(state.ids[index]);
|
|
69
2659
|
const stable = Object.freeze(all);
|
|
@@ -80,7 +2670,7 @@ const createCollectionView = (input) => {
|
|
|
80
2670
|
if (impact.kind === "incremental" && impact.added.size === 0 && impact.removed.size === 0 && !impact.orderChanged) {
|
|
81
2671
|
const previous = state;
|
|
82
2672
|
const changes = [];
|
|
83
|
-
const document =
|
|
2673
|
+
const document = require_dependency.documentOf(input.runtime);
|
|
84
2674
|
impact.updated.forEach((id) => {
|
|
85
2675
|
if (!previous.values.has(id)) return;
|
|
86
2676
|
const before = previous.values.get(id);
|
|
@@ -220,7 +2810,7 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
220
2810
|
entry.change = meta?.change;
|
|
221
2811
|
if (commitRevision !== void 0 && meta?.changedRevision === commitRevision) {
|
|
222
2812
|
sourceChanged = true;
|
|
223
|
-
|
|
2813
|
+
require_ownership.profile.materialized.sourceChanged();
|
|
224
2814
|
}
|
|
225
2815
|
}
|
|
226
2816
|
return sourceValuesRecord;
|
|
@@ -267,12 +2857,12 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
267
2857
|
});
|
|
268
2858
|
};
|
|
269
2859
|
const build = () => {
|
|
270
|
-
|
|
2860
|
+
require_ownership.profile.materialized.rebuilt();
|
|
271
2861
|
dependencies.clear();
|
|
272
2862
|
let active = true;
|
|
273
2863
|
try {
|
|
274
2864
|
instance = spec.build({
|
|
275
|
-
read:
|
|
2865
|
+
read: require_dependency.documentReader(require_dependency.schemaOf(runtime), () => require_dependency.documentOf(runtime), () => active),
|
|
276
2866
|
sources: sourceValues()
|
|
277
2867
|
});
|
|
278
2868
|
currentValue = instance.value;
|
|
@@ -284,7 +2874,7 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
284
2874
|
};
|
|
285
2875
|
build();
|
|
286
2876
|
const emit = () => {
|
|
287
|
-
|
|
2877
|
+
require_ownership.profile.materialized.notification();
|
|
288
2878
|
Array.from(listeners).forEach((listener) => listener());
|
|
289
2879
|
};
|
|
290
2880
|
const process = (commit) => {
|
|
@@ -298,19 +2888,19 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
298
2888
|
}
|
|
299
2889
|
const sourceSnapshot = sourceValues(commit.revision);
|
|
300
2890
|
if (!sourceChanged && dependencies.size() > 0 && !dependencies.some(commit.impact.affects)) {
|
|
301
|
-
|
|
2891
|
+
require_ownership.profile.materialized.skipped();
|
|
302
2892
|
revision = commit.revision;
|
|
303
2893
|
writeMeta(void 0, currentValue, commit.revision, false);
|
|
304
2894
|
return;
|
|
305
2895
|
}
|
|
306
2896
|
dependencies.clear();
|
|
307
|
-
|
|
2897
|
+
require_ownership.profile.materialized.updated();
|
|
308
2898
|
let active = true;
|
|
309
2899
|
let result;
|
|
310
2900
|
try {
|
|
311
2901
|
activeImpact = commit.impact;
|
|
312
2902
|
result = instance.update({
|
|
313
|
-
read:
|
|
2903
|
+
read: require_dependency.documentReader(require_dependency.schemaOf(runtime), () => require_dependency.documentOf(runtime), () => active),
|
|
314
2904
|
revision: commit.revision,
|
|
315
2905
|
source: commit.source,
|
|
316
2906
|
impact: trackedImpact,
|
|
@@ -375,7 +2965,7 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
375
2965
|
revision,
|
|
376
2966
|
changedRevision: -1
|
|
377
2967
|
});
|
|
378
|
-
const unregister =
|
|
2968
|
+
const unregister = registerProcessor(runtime, {
|
|
379
2969
|
process,
|
|
380
2970
|
flush: () => {
|
|
381
2971
|
if (!pendingEmit) return;
|
|
@@ -386,38 +2976,38 @@ const createMaterializedView = (runtime, spec) => {
|
|
|
386
2976
|
return view;
|
|
387
2977
|
};
|
|
388
2978
|
//#endregion
|
|
389
|
-
exports.DocumentDisposedError =
|
|
390
|
-
exports.DocumentReentrancyError =
|
|
391
|
-
exports.asReadable =
|
|
392
|
-
exports.commandFootprint =
|
|
393
|
-
exports.contains =
|
|
2979
|
+
exports.DocumentDisposedError = require_dependency.DocumentDisposedError;
|
|
2980
|
+
exports.DocumentReentrancyError = require_dependency.DocumentReentrancyError;
|
|
2981
|
+
exports.asReadable = asReadable;
|
|
2982
|
+
exports.commandFootprint = commandFootprint;
|
|
2983
|
+
exports.contains = require_dependency.contains;
|
|
394
2984
|
exports.createCollectionView = createCollectionView;
|
|
395
|
-
exports.createDocument =
|
|
2985
|
+
exports.createDocument = createDocument;
|
|
396
2986
|
exports.createMaterializedView = createMaterializedView;
|
|
397
|
-
exports.debugKey =
|
|
398
|
-
exports.decodeCommandFootprint =
|
|
399
|
-
exports.dict =
|
|
400
|
-
exports.field =
|
|
401
|
-
exports.footprintsOverlap =
|
|
402
|
-
exports.list =
|
|
403
|
-
exports.map =
|
|
404
|
-
exports.object =
|
|
405
|
-
exports.optional =
|
|
406
|
-
exports.overlaps =
|
|
407
|
-
exports.readAddress =
|
|
408
|
-
exports.record =
|
|
409
|
-
exports.resolveAddress =
|
|
410
|
-
exports.schema =
|
|
2987
|
+
exports.debugKey = require_dependency.debugKey;
|
|
2988
|
+
exports.decodeCommandFootprint = decodeCommandFootprint;
|
|
2989
|
+
exports.dict = require_dependency.dict;
|
|
2990
|
+
exports.field = require_dependency.field;
|
|
2991
|
+
exports.footprintsOverlap = footprintsOverlap;
|
|
2992
|
+
exports.list = require_dependency.list;
|
|
2993
|
+
exports.map = require_dependency.map;
|
|
2994
|
+
exports.object = require_dependency.object;
|
|
2995
|
+
exports.optional = require_dependency.optional;
|
|
2996
|
+
exports.overlaps = require_dependency.overlaps;
|
|
2997
|
+
exports.readAddress = require_dependency.read;
|
|
2998
|
+
exports.record = require_dependency.record;
|
|
2999
|
+
exports.resolveAddress = require_dependency.resolveAddress;
|
|
3000
|
+
exports.schema = require_dependency.schema;
|
|
411
3001
|
exports.select = select;
|
|
412
|
-
exports.single =
|
|
413
|
-
exports.table =
|
|
3002
|
+
exports.single = require_dependency.single;
|
|
3003
|
+
exports.table = require_dependency.table;
|
|
414
3004
|
Object.defineProperty(exports, "target", {
|
|
415
3005
|
enumerable: true,
|
|
416
3006
|
get: function() {
|
|
417
|
-
return
|
|
3007
|
+
return require_dependency.impact_target_exports;
|
|
418
3008
|
}
|
|
419
3009
|
});
|
|
420
|
-
exports.tree =
|
|
421
|
-
exports.variant =
|
|
3010
|
+
exports.tree = require_dependency.tree;
|
|
3011
|
+
exports.variant = require_dependency.variant;
|
|
422
3012
|
|
|
423
3013
|
//# sourceMappingURL=index.cjs.map
|