arborkit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/dist/addressing.d.ts +22 -0
- package/dist/addressing.js +56 -0
- package/dist/addressing.js.map +1 -0
- package/dist/ag-ui.d.ts +45 -0
- package/dist/ag-ui.js +50 -0
- package/dist/ag-ui.js.map +1 -0
- package/dist/arbor.d.ts +73 -0
- package/dist/arbor.js +1542 -0
- package/dist/arbor.js.map +1 -0
- package/dist/artifact-tree.d.ts +66 -0
- package/dist/artifact-tree.js +285 -0
- package/dist/artifact-tree.js.map +1 -0
- package/dist/clock.d.ts +15 -0
- package/dist/clock.js +23 -0
- package/dist/clock.js.map +1 -0
- package/dist/decompose.d.ts +18 -0
- package/dist/decompose.js +22 -0
- package/dist/decompose.js.map +1 -0
- package/dist/delta-storage.d.ts +55 -0
- package/dist/delta-storage.js +106 -0
- package/dist/delta-storage.js.map +1 -0
- package/dist/delta.d.ts +55 -0
- package/dist/delta.js +740 -0
- package/dist/delta.js.map +1 -0
- package/dist/embedding-port.d.ts +17 -0
- package/dist/embedding-port.js +21 -0
- package/dist/embedding-port.js.map +1 -0
- package/dist/embedding-text.d.ts +14 -0
- package/dist/embedding-text.js +21 -0
- package/dist/embedding-text.js.map +1 -0
- package/dist/errors.d.ts +37 -0
- package/dist/errors.js +59 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-log.d.ts +75 -0
- package/dist/event-log.js +82 -0
- package/dist/event-log.js.map +1 -0
- package/dist/file-storage.d.ts +22 -0
- package/dist/file-storage.js +42 -0
- package/dist/file-storage.js.map +1 -0
- package/dist/ids.d.ts +17 -0
- package/dist/ids.js +22 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +1826 -0
- package/dist/index.js.map +1 -0
- package/dist/json-edit.d.ts +18 -0
- package/dist/json-edit.js +85 -0
- package/dist/json-edit.js.map +1 -0
- package/dist/jsonpointer.d.ts +14 -0
- package/dist/jsonpointer.js +33 -0
- package/dist/jsonpointer.js.map +1 -0
- package/dist/mutator.d.ts +59 -0
- package/dist/mutator.js +244 -0
- package/dist/mutator.js.map +1 -0
- package/dist/navigator.d.ts +85 -0
- package/dist/navigator.js +192 -0
- package/dist/navigator.js.map +1 -0
- package/dist/path-glob.d.ts +13 -0
- package/dist/path-glob.js +40 -0
- package/dist/path-glob.js.map +1 -0
- package/dist/registry-validator.d.ts +15 -0
- package/dist/registry-validator.js +11 -0
- package/dist/registry-validator.js.map +1 -0
- package/dist/replay.d.ts +38 -0
- package/dist/replay.js +183 -0
- package/dist/replay.js.map +1 -0
- package/dist/semantic-index.d.ts +88 -0
- package/dist/semantic-index.js +226 -0
- package/dist/semantic-index.js.map +1 -0
- package/dist/storage.d.ts +39 -0
- package/dist/storage.js +378 -0
- package/dist/storage.js.map +1 -0
- package/dist/toolset.d.ts +78 -0
- package/dist/toolset.js +306 -0
- package/dist/toolset.js.map +1 -0
- package/dist/type-aware-decision.d.ts +8 -0
- package/dist/type-aware-decision.js +17 -0
- package/dist/type-aware-decision.js.map +1 -0
- package/dist/type-registry.d.ts +20 -0
- package/dist/type-registry.js +17 -0
- package/dist/type-registry.js.map +1 -0
- package/dist/types.d.ts +28 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index-port.d.ts +34 -0
- package/dist/vector-index-port.js +49 -0
- package/dist/vector-index-port.js.map +1 -0
- package/dist/zod-adapter.d.ts +13 -0
- package/dist/zod-adapter.js +34 -0
- package/dist/zod-adapter.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
// src/decompose.ts
|
|
2
|
+
function kindOf(value, opaque) {
|
|
3
|
+
if (opaque) return "leaf";
|
|
4
|
+
return Array.isArray(value) ? "array" : "object";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/errors.ts
|
|
8
|
+
var ArborError = class extends Error {
|
|
9
|
+
constructor(code, message) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.code = code;
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
}
|
|
14
|
+
code;
|
|
15
|
+
};
|
|
16
|
+
var InvalidOpError = class extends ArborError {
|
|
17
|
+
constructor(message) {
|
|
18
|
+
super("INVALID_OP", message);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// src/artifact-tree.ts
|
|
23
|
+
var ArtifactTree = class _ArtifactTree {
|
|
24
|
+
constructor(deps) {
|
|
25
|
+
this.deps = deps;
|
|
26
|
+
}
|
|
27
|
+
deps;
|
|
28
|
+
nodes = /* @__PURE__ */ new Map();
|
|
29
|
+
rootId;
|
|
30
|
+
/** Lazily built per-parent key→childId maps for O(1) object-child lookup.
|
|
31
|
+
* A cache OUTSIDE the nodes (ArbNode/StoredArtifact stay byte-identical);
|
|
32
|
+
* invalidated on every child-set change, rebuilt on read. Arrays never
|
|
33
|
+
* populate it (index lookup is already O(1)). */
|
|
34
|
+
keyMaps = /* @__PURE__ */ new Map();
|
|
35
|
+
static fromJson(json, deps) {
|
|
36
|
+
const tree = new _ArtifactTree(deps);
|
|
37
|
+
tree.rootId = tree.build(json, null, null);
|
|
38
|
+
return tree;
|
|
39
|
+
}
|
|
40
|
+
build(value, parentId, key, type) {
|
|
41
|
+
const opaque = this.deps.decision.isOpaque(value, type);
|
|
42
|
+
const kind = kindOf(value, opaque);
|
|
43
|
+
const id = this.deps.idGen.next();
|
|
44
|
+
const node = {
|
|
45
|
+
id,
|
|
46
|
+
parentId,
|
|
47
|
+
key,
|
|
48
|
+
kind,
|
|
49
|
+
content: kind === "leaf" ? value : null,
|
|
50
|
+
childIds: [],
|
|
51
|
+
meta: { version: 0, updatedAt: this.deps.clock.now(), embedding: { state: "none" } }
|
|
52
|
+
};
|
|
53
|
+
if (type !== void 0) node.type = type;
|
|
54
|
+
this.nodes.set(id, node);
|
|
55
|
+
if (kind === "object") {
|
|
56
|
+
for (const [k, v] of Object.entries(value)) {
|
|
57
|
+
node.childIds.push(this.build(v, id, k));
|
|
58
|
+
}
|
|
59
|
+
} else if (kind === "array") {
|
|
60
|
+
value.forEach((v, i) => {
|
|
61
|
+
node.childIds.push(this.build(v, id, i));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return id;
|
|
65
|
+
}
|
|
66
|
+
get(id) {
|
|
67
|
+
return this.nodes.get(id);
|
|
68
|
+
}
|
|
69
|
+
root() {
|
|
70
|
+
return this.nodes.get(this.rootId);
|
|
71
|
+
}
|
|
72
|
+
rootIdValue() {
|
|
73
|
+
return this.rootId;
|
|
74
|
+
}
|
|
75
|
+
/** O(1) child lookup: arrays by index, objects via a lazily built key map. */
|
|
76
|
+
childByKey(parentId, key) {
|
|
77
|
+
const parent = this.nodes.get(parentId);
|
|
78
|
+
if (!parent) return void 0;
|
|
79
|
+
if (parent.kind === "array") {
|
|
80
|
+
if (!/^(0|[1-9]\d*)$/.test(key)) return void 0;
|
|
81
|
+
const i = Number(key);
|
|
82
|
+
if (i >= parent.childIds.length) return void 0;
|
|
83
|
+
return this.nodes.get(parent.childIds[i]);
|
|
84
|
+
}
|
|
85
|
+
let map = this.keyMaps.get(parentId);
|
|
86
|
+
if (!map) {
|
|
87
|
+
map = /* @__PURE__ */ new Map();
|
|
88
|
+
for (const cid2 of parent.childIds) map.set(String(this.nodes.get(cid2).key), cid2);
|
|
89
|
+
this.keyMaps.set(parentId, map);
|
|
90
|
+
}
|
|
91
|
+
const cid = map.get(key);
|
|
92
|
+
return cid === void 0 ? void 0 : this.nodes.get(cid);
|
|
93
|
+
}
|
|
94
|
+
children(id) {
|
|
95
|
+
const n = this.nodes.get(id);
|
|
96
|
+
if (!n) return [];
|
|
97
|
+
return n.childIds.map((cid) => this.nodes.get(cid));
|
|
98
|
+
}
|
|
99
|
+
has(id) {
|
|
100
|
+
return this.nodes.has(id);
|
|
101
|
+
}
|
|
102
|
+
size() {
|
|
103
|
+
return this.nodes.size;
|
|
104
|
+
}
|
|
105
|
+
/** Reconstruct the JSON value rooted at `id` (defaults to the tree root). */
|
|
106
|
+
toJson(id = this.rootId) {
|
|
107
|
+
const n = this.nodes.get(id);
|
|
108
|
+
if (!n) throw new Error(`Unknown node: ${id}`);
|
|
109
|
+
if (n.kind === "leaf") return n.content;
|
|
110
|
+
if (n.kind === "array") return n.childIds.map((cid) => this.toJson(cid));
|
|
111
|
+
const obj = {};
|
|
112
|
+
for (const cid of n.childIds) {
|
|
113
|
+
const c = this.nodes.get(cid);
|
|
114
|
+
obj[String(c.key)] = this.toJson(cid);
|
|
115
|
+
}
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
/** Replace the subtree value at `id` in place, keeping the node's id/key/parentId.
|
|
119
|
+
* `clearType` explicitly un-types the node (used by type-aware revert). */
|
|
120
|
+
replaceValue(id, value, type, clearType = false) {
|
|
121
|
+
const node = this.nodes.get(id);
|
|
122
|
+
if (!node) throw new InvalidOpError(`Unknown node: ${id}`);
|
|
123
|
+
this.deleteDescendants(id);
|
|
124
|
+
this.keyMaps.delete(id);
|
|
125
|
+
const opaque = this.deps.decision.isOpaque(value, type);
|
|
126
|
+
const kind = kindOf(value, opaque);
|
|
127
|
+
node.kind = kind;
|
|
128
|
+
node.content = kind === "leaf" ? value : null;
|
|
129
|
+
node.childIds = [];
|
|
130
|
+
if (clearType) node.type = void 0;
|
|
131
|
+
else if (type !== void 0) node.type = type;
|
|
132
|
+
if (kind === "object") {
|
|
133
|
+
for (const [k, v] of Object.entries(value)) {
|
|
134
|
+
node.childIds.push(this.build(v, id, k));
|
|
135
|
+
}
|
|
136
|
+
} else if (kind === "array") {
|
|
137
|
+
value.forEach((v, i) => {
|
|
138
|
+
node.childIds.push(this.build(v, id, i));
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Recursively remove all descendants of `id` from the node map (keeps `id` itself). */
|
|
143
|
+
deleteDescendants(id) {
|
|
144
|
+
const node = this.nodes.get(id);
|
|
145
|
+
if (!node) return;
|
|
146
|
+
for (const cid of node.childIds) {
|
|
147
|
+
this.deleteDescendants(cid);
|
|
148
|
+
this.nodes.delete(cid);
|
|
149
|
+
this.keyMaps.delete(cid);
|
|
150
|
+
}
|
|
151
|
+
node.childIds = [];
|
|
152
|
+
}
|
|
153
|
+
/** Deep, independent copy of the tree state for transaction rollback.
|
|
154
|
+
* `restore` consumes the snapshot; do not reuse it afterwards. */
|
|
155
|
+
snapshot() {
|
|
156
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const [id, node] of this.nodes) {
|
|
158
|
+
nodes.set(id, structuredClone(node));
|
|
159
|
+
}
|
|
160
|
+
return { nodes, rootId: this.rootId };
|
|
161
|
+
}
|
|
162
|
+
/** Replace the tree state with a previously taken snapshot. The snapshot's
|
|
163
|
+
* nodes are adopted BY REFERENCE — restore consumes the snapshot; do not
|
|
164
|
+
* reuse it afterwards. */
|
|
165
|
+
restore(snap) {
|
|
166
|
+
this.nodes.clear();
|
|
167
|
+
this.keyMaps.clear();
|
|
168
|
+
for (const [id, node] of snap.nodes) {
|
|
169
|
+
this.nodes.set(id, node);
|
|
170
|
+
}
|
|
171
|
+
this.rootId = snap.rootId;
|
|
172
|
+
}
|
|
173
|
+
/** Insert a decomposed `value` as a child of `parentId`. For objects `keyOrIndex` is the string key; for arrays it is the insert index. Returns the new child's id. */
|
|
174
|
+
insertChild(parentId, keyOrIndex, value, type) {
|
|
175
|
+
const parent = this.nodes.get(parentId);
|
|
176
|
+
if (!parent) throw new InvalidOpError(`Unknown node: ${parentId}`);
|
|
177
|
+
if (parent.kind === "object") {
|
|
178
|
+
if (typeof keyOrIndex !== "string") {
|
|
179
|
+
throw new InvalidOpError("object insert requires a string key");
|
|
180
|
+
}
|
|
181
|
+
if (parent.childIds.some((cid2) => this.nodes.get(cid2).key === keyOrIndex)) {
|
|
182
|
+
throw new InvalidOpError(`key already exists: ${keyOrIndex}`);
|
|
183
|
+
}
|
|
184
|
+
const cid = this.build(value, parentId, keyOrIndex, type);
|
|
185
|
+
parent.childIds.push(cid);
|
|
186
|
+
this.keyMaps.delete(parentId);
|
|
187
|
+
return cid;
|
|
188
|
+
}
|
|
189
|
+
if (parent.kind === "array") {
|
|
190
|
+
if (typeof keyOrIndex !== "number") {
|
|
191
|
+
throw new InvalidOpError("array insert requires a numeric index");
|
|
192
|
+
}
|
|
193
|
+
const at = Math.max(0, Math.min(keyOrIndex, parent.childIds.length));
|
|
194
|
+
const cid = this.build(value, parentId, at, type);
|
|
195
|
+
parent.childIds.splice(at, 0, cid);
|
|
196
|
+
this.renumberArray(parentId);
|
|
197
|
+
return cid;
|
|
198
|
+
}
|
|
199
|
+
throw new InvalidOpError("cannot insert into a leaf node");
|
|
200
|
+
}
|
|
201
|
+
/** Remove `childId` (and its subtree) from `parentId`. Renumbers array siblings. */
|
|
202
|
+
removeChild(parentId, childId) {
|
|
203
|
+
const parent = this.nodes.get(parentId);
|
|
204
|
+
if (!parent) throw new InvalidOpError(`Unknown node: ${parentId}`);
|
|
205
|
+
const idx = parent.childIds.indexOf(childId);
|
|
206
|
+
if (idx < 0) throw new InvalidOpError(`${childId} is not a child of ${parentId}`);
|
|
207
|
+
this.deleteDescendants(childId);
|
|
208
|
+
this.nodes.delete(childId);
|
|
209
|
+
this.keyMaps.delete(childId);
|
|
210
|
+
parent.childIds.splice(idx, 1);
|
|
211
|
+
this.keyMaps.delete(parentId);
|
|
212
|
+
if (parent.kind === "array") this.renumberArray(parentId);
|
|
213
|
+
}
|
|
214
|
+
/** Move `id` under `newParentId` at `keyOrIndex`, preserving `id`. Renumbers affected arrays.
|
|
215
|
+
* ALL validation happens before any mutation — a rejected move leaves the tree untouched. */
|
|
216
|
+
moveNode(id, newParentId, keyOrIndex) {
|
|
217
|
+
const node = this.nodes.get(id);
|
|
218
|
+
if (!node) throw new InvalidOpError(`Unknown node: ${id}`);
|
|
219
|
+
if (node.parentId === null) throw new InvalidOpError("cannot move the root");
|
|
220
|
+
const newParent = this.nodes.get(newParentId);
|
|
221
|
+
if (!newParent) throw new InvalidOpError(`Unknown node: ${newParentId}`);
|
|
222
|
+
if (newParent.kind === "leaf") throw new InvalidOpError("cannot move into a leaf node");
|
|
223
|
+
let anc = newParentId;
|
|
224
|
+
while (anc !== null) {
|
|
225
|
+
if (anc === id) throw new InvalidOpError("cannot move a node into itself or its own subtree");
|
|
226
|
+
anc = this.nodes.get(anc)?.parentId ?? null;
|
|
227
|
+
}
|
|
228
|
+
if (newParent.kind === "object") {
|
|
229
|
+
if (typeof keyOrIndex !== "string") throw new InvalidOpError("object move requires a string key");
|
|
230
|
+
if (newParent.childIds.some((cid) => cid !== id && this.nodes.get(cid).key === keyOrIndex)) {
|
|
231
|
+
throw new InvalidOpError(`key already exists: ${keyOrIndex}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const oldParent = this.nodes.get(node.parentId);
|
|
235
|
+
const oldIdx = oldParent.childIds.indexOf(id);
|
|
236
|
+
oldParent.childIds.splice(oldIdx, 1);
|
|
237
|
+
this.keyMaps.delete(oldParent.id);
|
|
238
|
+
this.keyMaps.delete(newParentId);
|
|
239
|
+
if (oldParent.kind === "array") this.renumberArray(oldParent.id);
|
|
240
|
+
if (newParent.kind === "object") {
|
|
241
|
+
node.parentId = newParentId;
|
|
242
|
+
node.key = keyOrIndex;
|
|
243
|
+
newParent.childIds.push(id);
|
|
244
|
+
} else {
|
|
245
|
+
const at = typeof keyOrIndex === "number" ? Math.max(0, Math.min(keyOrIndex, newParent.childIds.length)) : newParent.childIds.length;
|
|
246
|
+
node.parentId = newParentId;
|
|
247
|
+
newParent.childIds.splice(at, 0, id);
|
|
248
|
+
this.renumberArray(newParentId);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
/** Set each array child's `key` to its current position. */
|
|
252
|
+
renumberArray(parentId) {
|
|
253
|
+
const parent = this.nodes.get(parentId);
|
|
254
|
+
if (!parent) return;
|
|
255
|
+
parent.childIds.forEach((cid, i) => {
|
|
256
|
+
this.nodes.get(cid).key = i;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
/** All nodes in the tree (for serialization). */
|
|
260
|
+
allNodes() {
|
|
261
|
+
return [...this.nodes.values()];
|
|
262
|
+
}
|
|
263
|
+
/** Rebuild a tree from previously serialized nodes, preserving their ids. */
|
|
264
|
+
static fromStored(nodes, rootId, deps) {
|
|
265
|
+
const tree = new _ArtifactTree(deps);
|
|
266
|
+
for (const node of nodes) tree.nodes.set(node.id, node);
|
|
267
|
+
tree.rootId = rootId;
|
|
268
|
+
return tree;
|
|
269
|
+
}
|
|
270
|
+
/** All transitive descendant ids of `id` (depth-first), excluding `id` itself. */
|
|
271
|
+
descendantIds(id) {
|
|
272
|
+
const out = [];
|
|
273
|
+
const node = this.nodes.get(id);
|
|
274
|
+
if (!node) return out;
|
|
275
|
+
for (const cid of node.childIds) {
|
|
276
|
+
out.push(cid);
|
|
277
|
+
out.push(...this.descendantIds(cid));
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
export {
|
|
283
|
+
ArtifactTree
|
|
284
|
+
};
|
|
285
|
+
//# sourceMappingURL=artifact-tree.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/decompose.ts","../src/errors.ts","../src/artifact-tree.ts"],"sourcesContent":["import type { Json, NodeKind } from \"./types\";\r\n\r\n/** Policy deciding whether a value is stored whole (opaque leaf) or split into child nodes. */\r\nexport interface DecomposeDecision {\r\n /** `type` is the optional registered node type (used by the by-type override in a later milestone). */\r\n isOpaque(value: Json, type?: string): boolean;\r\n}\r\n\r\n/** UTF-8 byte length of the JSON serialization of a value. */\r\nexport function byteSize(value: Json): number {\r\n return Buffer.byteLength(JSON.stringify(value), \"utf8\");\r\n}\r\n\r\n/** Structural kind of a value given whether it is being stored opaquely. */\r\nexport function kindOf(value: Json, opaque: boolean): NodeKind {\r\n if (opaque) return \"leaf\";\r\n return Array.isArray(value) ? \"array\" : \"object\";\r\n}\r\n\r\n/**\r\n * Default policy: scalars are always opaque leaves; containers stay opaque\r\n * while their serialized size is within `maxOpaqueBytes`, otherwise they split.\r\n */\r\nexport function sizeBasedDecision(maxOpaqueBytes: number): DecomposeDecision {\r\n return {\r\n isOpaque(value: Json): boolean {\r\n if (value === null || typeof value !== \"object\") return true;\r\n return byteSize(value) <= maxOpaqueBytes;\r\n },\r\n };\r\n}\r\n","import type { NodeId } from \"./types\";\r\n\r\n/** A reference to a node: by stable id or by JSON Pointer path. */\r\nexport type Ref = { id: NodeId } | { path: string };\r\n\r\nexport class ArborError extends Error {\r\n constructor(\r\n public readonly code: string,\r\n message: string,\r\n ) {\r\n super(message);\r\n this.name = new.target.name;\r\n }\r\n}\r\n\r\nexport class NodeNotFoundError extends ArborError {\r\n constructor(public readonly ref: Ref) {\r\n super(\"NODE_NOT_FOUND\", `Node not found: ${JSON.stringify(ref)}`);\r\n }\r\n}\r\n\r\nexport class ScopeViolationError extends ArborError {\r\n constructor(\r\n public readonly targetPath: string,\r\n public readonly scope: string,\r\n ) {\r\n super(\"SCOPE_VIOLATION\", `Access outside scope: ${targetPath} (scope: ${scope})`);\r\n }\r\n}\r\n\r\nexport class StaleVersionError extends ArborError {\r\n constructor(\r\n public readonly id: NodeId,\r\n public readonly expected: number,\r\n public readonly actual: number,\r\n ) {\r\n super(\"STALE_VERSION\", `Stale version for ${id}: expected ${expected}, actual ${actual}`);\r\n }\r\n}\r\n\r\nexport class InvalidOpError extends ArborError {\r\n constructor(message: string) {\r\n super(\"INVALID_OP\", message);\r\n }\r\n}\r\n\r\nexport class ValidationError extends ArborError {\r\n constructor(\r\n public readonly type: string | undefined,\r\n public readonly details: string,\r\n ) {\r\n super(\"VALIDATION_ERROR\", `Validation failed${type ? ` for type ${type}` : \"\"}: ${details}`);\r\n }\r\n}\r\n","import type { ArbNode, Json, NodeId } from \"./types\";\r\nimport type { IdGen } from \"./ids\";\r\nimport type { Clock } from \"./clock\";\r\nimport { type DecomposeDecision, kindOf } from \"./decompose\";\r\nimport { InvalidOpError } from \"./errors\";\r\n\r\nexport interface TreeDeps {\r\n idGen: IdGen;\r\n clock: Clock;\r\n decision: DecomposeDecision;\r\n}\r\n\r\nexport interface TreeSnapshot {\r\n nodes: Map<NodeId, ArbNode>;\r\n rootId: NodeId;\r\n}\r\n\r\nexport class ArtifactTree {\r\n private readonly nodes = new Map<NodeId, ArbNode>();\r\n private rootId!: NodeId;\r\n /** Lazily built per-parent key→childId maps for O(1) object-child lookup.\r\n * A cache OUTSIDE the nodes (ArbNode/StoredArtifact stay byte-identical);\r\n * invalidated on every child-set change, rebuilt on read. Arrays never\r\n * populate it (index lookup is already O(1)). */\r\n private readonly keyMaps = new Map<NodeId, Map<string, NodeId>>();\r\n\r\n private constructor(private readonly deps: TreeDeps) {}\r\n\r\n static fromJson(json: Json, deps: TreeDeps): ArtifactTree {\r\n const tree = new ArtifactTree(deps);\r\n tree.rootId = tree.build(json, null, null);\r\n return tree;\r\n }\r\n\r\n private build(value: Json, parentId: NodeId | null, key: string | number | null, type?: string): NodeId {\r\n const opaque = this.deps.decision.isOpaque(value, type);\r\n const kind = kindOf(value, opaque);\r\n const id = this.deps.idGen.next();\r\n const node: ArbNode = {\r\n id,\r\n parentId,\r\n key,\r\n kind,\r\n content: kind === \"leaf\" ? value : null,\r\n childIds: [],\r\n meta: { version: 0, updatedAt: this.deps.clock.now(), embedding: { state: \"none\" } },\r\n };\r\n if (type !== undefined) node.type = type;\r\n this.nodes.set(id, node);\r\n\r\n if (kind === \"object\") {\r\n for (const [k, v] of Object.entries(value as Record<string, Json>)) {\r\n node.childIds.push(this.build(v, id, k));\r\n }\r\n } else if (kind === \"array\") {\r\n (value as Json[]).forEach((v, i) => {\r\n node.childIds.push(this.build(v, id, i));\r\n });\r\n }\r\n return id;\r\n }\r\n\r\n get(id: NodeId): ArbNode | undefined {\r\n return this.nodes.get(id);\r\n }\r\n\r\n root(): ArbNode {\r\n return this.nodes.get(this.rootId)!;\r\n }\r\n\r\n rootIdValue(): NodeId {\r\n return this.rootId;\r\n }\r\n\r\n /** O(1) child lookup: arrays by index, objects via a lazily built key map. */\r\n childByKey(parentId: NodeId, key: string): ArbNode | undefined {\r\n const parent = this.nodes.get(parentId);\r\n if (!parent) return undefined;\r\n if (parent.kind === \"array\") {\r\n if (!/^(0|[1-9]\\d*)$/.test(key)) return undefined; // canonical RFC 6901 index: digits only, no leading zeros\r\n const i = Number(key);\r\n if (i >= parent.childIds.length) return undefined;\r\n return this.nodes.get(parent.childIds[i]);\r\n }\r\n let map = this.keyMaps.get(parentId);\r\n if (!map) {\r\n map = new Map();\r\n for (const cid of parent.childIds) map.set(String(this.nodes.get(cid)!.key), cid);\r\n this.keyMaps.set(parentId, map);\r\n }\r\n const cid = map.get(key);\r\n return cid === undefined ? undefined : this.nodes.get(cid);\r\n }\r\n\r\n children(id: NodeId): ArbNode[] {\r\n const n = this.nodes.get(id);\r\n if (!n) return [];\r\n return n.childIds.map((cid) => this.nodes.get(cid)!);\r\n }\r\n\r\n has(id: NodeId): boolean {\r\n return this.nodes.has(id);\r\n }\r\n\r\n size(): number {\r\n return this.nodes.size;\r\n }\r\n\r\n /** Reconstruct the JSON value rooted at `id` (defaults to the tree root). */\r\n toJson(id: NodeId = this.rootId): Json {\r\n const n = this.nodes.get(id);\r\n if (!n) throw new Error(`Unknown node: ${id}`);\r\n if (n.kind === \"leaf\") return n.content;\r\n if (n.kind === \"array\") return n.childIds.map((cid) => this.toJson(cid));\r\n const obj: Record<string, Json> = {};\r\n for (const cid of n.childIds) {\r\n const c = this.nodes.get(cid)!;\r\n obj[String(c.key)] = this.toJson(cid);\r\n }\r\n return obj;\r\n }\r\n\r\n /** Replace the subtree value at `id` in place, keeping the node's id/key/parentId.\r\n * `clearType` explicitly un-types the node (used by type-aware revert). */\r\n replaceValue(id: NodeId, value: Json, type?: string, clearType = false): void {\r\n const node = this.nodes.get(id);\r\n if (!node) throw new InvalidOpError(`Unknown node: ${id}`);\r\n this.deleteDescendants(id);\r\n this.keyMaps.delete(id);\r\n const opaque = this.deps.decision.isOpaque(value, type);\r\n const kind = kindOf(value, opaque);\r\n node.kind = kind;\r\n node.content = kind === \"leaf\" ? value : null;\r\n node.childIds = [];\r\n if (clearType) node.type = undefined;\r\n else if (type !== undefined) node.type = type;\r\n if (kind === \"object\") {\r\n for (const [k, v] of Object.entries(value as Record<string, Json>)) {\r\n node.childIds.push(this.build(v, id, k));\r\n }\r\n } else if (kind === \"array\") {\r\n (value as Json[]).forEach((v, i) => {\r\n node.childIds.push(this.build(v, id, i));\r\n });\r\n }\r\n }\r\n\r\n /** Recursively remove all descendants of `id` from the node map (keeps `id` itself). */\r\n private deleteDescendants(id: NodeId): void {\r\n const node = this.nodes.get(id);\r\n if (!node) return;\r\n for (const cid of node.childIds) {\r\n this.deleteDescendants(cid);\r\n this.nodes.delete(cid);\r\n this.keyMaps.delete(cid);\r\n }\r\n node.childIds = [];\r\n }\r\n\r\n /** Deep, independent copy of the tree state for transaction rollback.\r\n * `restore` consumes the snapshot; do not reuse it afterwards. */\r\n snapshot(): TreeSnapshot {\r\n const nodes = new Map<NodeId, ArbNode>();\r\n for (const [id, node] of this.nodes) {\r\n nodes.set(id, structuredClone(node));\r\n }\r\n return { nodes, rootId: this.rootId };\r\n }\r\n\r\n /** Replace the tree state with a previously taken snapshot. The snapshot's\r\n * nodes are adopted BY REFERENCE — restore consumes the snapshot; do not\r\n * reuse it afterwards. */\r\n restore(snap: TreeSnapshot): void {\r\n this.nodes.clear();\r\n this.keyMaps.clear();\r\n for (const [id, node] of snap.nodes) {\r\n this.nodes.set(id, node);\r\n }\r\n this.rootId = snap.rootId;\r\n }\r\n\r\n /** Insert a decomposed `value` as a child of `parentId`. For objects `keyOrIndex` is the string key; for arrays it is the insert index. Returns the new child's id. */\r\n insertChild(parentId: NodeId, keyOrIndex: string | number, value: Json, type?: string): NodeId {\r\n const parent = this.nodes.get(parentId);\r\n if (!parent) throw new InvalidOpError(`Unknown node: ${parentId}`);\r\n if (parent.kind === \"object\") {\r\n if (typeof keyOrIndex !== \"string\") {\r\n throw new InvalidOpError(\"object insert requires a string key\");\r\n }\r\n if (parent.childIds.some((cid) => this.nodes.get(cid)!.key === keyOrIndex)) {\r\n throw new InvalidOpError(`key already exists: ${keyOrIndex}`);\r\n }\r\n const cid = this.build(value, parentId, keyOrIndex, type);\r\n parent.childIds.push(cid);\r\n this.keyMaps.delete(parentId);\r\n return cid;\r\n }\r\n if (parent.kind === \"array\") {\r\n if (typeof keyOrIndex !== \"number\") {\r\n throw new InvalidOpError(\"array insert requires a numeric index\");\r\n }\r\n const at = Math.max(0, Math.min(keyOrIndex, parent.childIds.length));\r\n const cid = this.build(value, parentId, at, type);\r\n parent.childIds.splice(at, 0, cid);\r\n this.renumberArray(parentId);\r\n return cid;\r\n }\r\n throw new InvalidOpError(\"cannot insert into a leaf node\");\r\n }\r\n\r\n /** Remove `childId` (and its subtree) from `parentId`. Renumbers array siblings. */\r\n removeChild(parentId: NodeId, childId: NodeId): void {\r\n const parent = this.nodes.get(parentId);\r\n if (!parent) throw new InvalidOpError(`Unknown node: ${parentId}`);\r\n const idx = parent.childIds.indexOf(childId);\r\n if (idx < 0) throw new InvalidOpError(`${childId} is not a child of ${parentId}`);\r\n this.deleteDescendants(childId);\r\n this.nodes.delete(childId);\r\n this.keyMaps.delete(childId);\r\n parent.childIds.splice(idx, 1);\r\n this.keyMaps.delete(parentId);\r\n if (parent.kind === \"array\") this.renumberArray(parentId);\r\n }\r\n\r\n /** Move `id` under `newParentId` at `keyOrIndex`, preserving `id`. Renumbers affected arrays.\r\n * ALL validation happens before any mutation — a rejected move leaves the tree untouched. */\r\n moveNode(id: NodeId, newParentId: NodeId, keyOrIndex: string | number): void {\r\n const node = this.nodes.get(id);\r\n if (!node) throw new InvalidOpError(`Unknown node: ${id}`);\r\n if (node.parentId === null) throw new InvalidOpError(\"cannot move the root\");\r\n const newParent = this.nodes.get(newParentId);\r\n if (!newParent) throw new InvalidOpError(`Unknown node: ${newParentId}`);\r\n if (newParent.kind === \"leaf\") throw new InvalidOpError(\"cannot move into a leaf node\");\r\n // Moving into itself or its own subtree would create a parent-chain cycle:\r\n // toJson silently drops the subtree and pathOf never terminates.\r\n let anc: NodeId | null = newParentId;\r\n while (anc !== null) {\r\n if (anc === id) throw new InvalidOpError(\"cannot move a node into itself or its own subtree\");\r\n anc = this.nodes.get(anc)?.parentId ?? null;\r\n }\r\n if (newParent.kind === \"object\") {\r\n if (typeof keyOrIndex !== \"string\") throw new InvalidOpError(\"object move requires a string key\");\r\n // Mirrors insertChild: a duplicate key silently shadows the existing child.\r\n if (newParent.childIds.some((cid) => cid !== id && this.nodes.get(cid)!.key === keyOrIndex)) {\r\n throw new InvalidOpError(`key already exists: ${keyOrIndex}`);\r\n }\r\n }\r\n\r\n const oldParent = this.nodes.get(node.parentId)!;\r\n const oldIdx = oldParent.childIds.indexOf(id);\r\n oldParent.childIds.splice(oldIdx, 1);\r\n this.keyMaps.delete(oldParent.id);\r\n this.keyMaps.delete(newParentId);\r\n if (oldParent.kind === \"array\") this.renumberArray(oldParent.id);\r\n\r\n if (newParent.kind === \"object\") {\r\n node.parentId = newParentId;\r\n node.key = keyOrIndex;\r\n newParent.childIds.push(id);\r\n } else {\r\n const at = typeof keyOrIndex === \"number\" ? Math.max(0, Math.min(keyOrIndex, newParent.childIds.length)) : newParent.childIds.length;\r\n node.parentId = newParentId;\r\n newParent.childIds.splice(at, 0, id);\r\n this.renumberArray(newParentId);\r\n }\r\n }\r\n\r\n /** Set each array child's `key` to its current position. */\r\n private renumberArray(parentId: NodeId): void {\r\n const parent = this.nodes.get(parentId);\r\n if (!parent) return;\r\n parent.childIds.forEach((cid, i) => {\r\n this.nodes.get(cid)!.key = i;\r\n });\r\n }\r\n\r\n /** All nodes in the tree (for serialization). */\r\n allNodes(): ArbNode[] {\r\n return [...this.nodes.values()];\r\n }\r\n\r\n /** Rebuild a tree from previously serialized nodes, preserving their ids. */\r\n static fromStored(nodes: ArbNode[], rootId: NodeId, deps: TreeDeps): ArtifactTree {\r\n const tree = new ArtifactTree(deps);\r\n for (const node of nodes) tree.nodes.set(node.id, node);\r\n tree.rootId = rootId;\r\n return tree;\r\n }\r\n\r\n /** All transitive descendant ids of `id` (depth-first), excluding `id` itself. */\r\n descendantIds(id: NodeId): NodeId[] {\r\n const out: NodeId[] = [];\r\n const node = this.nodes.get(id);\r\n if (!node) return out;\r\n for (const cid of node.childIds) {\r\n out.push(cid);\r\n out.push(...this.descendantIds(cid));\r\n }\r\n return out;\r\n }\r\n}\r\n"],"mappings":"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","names":["cid"]}
|
package/dist/clock.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface Clock {
|
|
2
|
+
now(): number;
|
|
3
|
+
}
|
|
4
|
+
declare class SystemClock implements Clock {
|
|
5
|
+
now(): number;
|
|
6
|
+
}
|
|
7
|
+
/** Deterministic test double: constant value, manually advanced. */
|
|
8
|
+
declare class FixedClock implements Clock {
|
|
9
|
+
private t;
|
|
10
|
+
constructor(t?: number);
|
|
11
|
+
now(): number;
|
|
12
|
+
advance(ms: number): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export { type Clock, FixedClock, SystemClock };
|
package/dist/clock.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/clock.ts
|
|
2
|
+
var SystemClock = class {
|
|
3
|
+
now() {
|
|
4
|
+
return Date.now();
|
|
5
|
+
}
|
|
6
|
+
};
|
|
7
|
+
var FixedClock = class {
|
|
8
|
+
constructor(t = 0) {
|
|
9
|
+
this.t = t;
|
|
10
|
+
}
|
|
11
|
+
t;
|
|
12
|
+
now() {
|
|
13
|
+
return this.t;
|
|
14
|
+
}
|
|
15
|
+
advance(ms) {
|
|
16
|
+
this.t += ms;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
export {
|
|
20
|
+
FixedClock,
|
|
21
|
+
SystemClock
|
|
22
|
+
};
|
|
23
|
+
//# sourceMappingURL=clock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/clock.ts"],"sourcesContent":["export interface Clock {\r\n now(): number; // epoch milliseconds\r\n}\r\n\r\nexport class SystemClock implements Clock {\r\n now(): number {\r\n return Date.now();\r\n }\r\n}\r\n\r\n/** Deterministic test double: constant value, manually advanced. */\r\nexport class FixedClock implements Clock {\r\n constructor(private t = 0) {}\r\n now(): number {\r\n return this.t;\r\n }\r\n advance(ms: number): void {\r\n this.t += ms;\r\n }\r\n}\r\n"],"mappings":";AAIO,IAAM,cAAN,MAAmC;AAAA,EACxC,MAAc;AACZ,WAAO,KAAK,IAAI;AAAA,EAClB;AACF;AAGO,IAAM,aAAN,MAAkC;AAAA,EACvC,YAAoB,IAAI,GAAG;AAAP;AAAA,EAAQ;AAAA,EAAR;AAAA,EACpB,MAAc;AACZ,WAAO,KAAK;AAAA,EACd;AAAA,EACA,QAAQ,IAAkB;AACxB,SAAK,KAAK;AAAA,EACZ;AACF;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Json, NodeKind } from './types.js';
|
|
2
|
+
|
|
3
|
+
/** Policy deciding whether a value is stored whole (opaque leaf) or split into child nodes. */
|
|
4
|
+
interface DecomposeDecision {
|
|
5
|
+
/** `type` is the optional registered node type (used by the by-type override in a later milestone). */
|
|
6
|
+
isOpaque(value: Json, type?: string): boolean;
|
|
7
|
+
}
|
|
8
|
+
/** UTF-8 byte length of the JSON serialization of a value. */
|
|
9
|
+
declare function byteSize(value: Json): number;
|
|
10
|
+
/** Structural kind of a value given whether it is being stored opaquely. */
|
|
11
|
+
declare function kindOf(value: Json, opaque: boolean): NodeKind;
|
|
12
|
+
/**
|
|
13
|
+
* Default policy: scalars are always opaque leaves; containers stay opaque
|
|
14
|
+
* while their serialized size is within `maxOpaqueBytes`, otherwise they split.
|
|
15
|
+
*/
|
|
16
|
+
declare function sizeBasedDecision(maxOpaqueBytes: number): DecomposeDecision;
|
|
17
|
+
|
|
18
|
+
export { type DecomposeDecision, byteSize, kindOf, sizeBasedDecision };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// src/decompose.ts
|
|
2
|
+
function byteSize(value) {
|
|
3
|
+
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
4
|
+
}
|
|
5
|
+
function kindOf(value, opaque) {
|
|
6
|
+
if (opaque) return "leaf";
|
|
7
|
+
return Array.isArray(value) ? "array" : "object";
|
|
8
|
+
}
|
|
9
|
+
function sizeBasedDecision(maxOpaqueBytes) {
|
|
10
|
+
return {
|
|
11
|
+
isOpaque(value) {
|
|
12
|
+
if (value === null || typeof value !== "object") return true;
|
|
13
|
+
return byteSize(value) <= maxOpaqueBytes;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
byteSize,
|
|
19
|
+
kindOf,
|
|
20
|
+
sizeBasedDecision
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=decompose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/decompose.ts"],"sourcesContent":["import type { Json, NodeKind } from \"./types\";\r\n\r\n/** Policy deciding whether a value is stored whole (opaque leaf) or split into child nodes. */\r\nexport interface DecomposeDecision {\r\n /** `type` is the optional registered node type (used by the by-type override in a later milestone). */\r\n isOpaque(value: Json, type?: string): boolean;\r\n}\r\n\r\n/** UTF-8 byte length of the JSON serialization of a value. */\r\nexport function byteSize(value: Json): number {\r\n return Buffer.byteLength(JSON.stringify(value), \"utf8\");\r\n}\r\n\r\n/** Structural kind of a value given whether it is being stored opaquely. */\r\nexport function kindOf(value: Json, opaque: boolean): NodeKind {\r\n if (opaque) return \"leaf\";\r\n return Array.isArray(value) ? \"array\" : \"object\";\r\n}\r\n\r\n/**\r\n * Default policy: scalars are always opaque leaves; containers stay opaque\r\n * while their serialized size is within `maxOpaqueBytes`, otherwise they split.\r\n */\r\nexport function sizeBasedDecision(maxOpaqueBytes: number): DecomposeDecision {\r\n return {\r\n isOpaque(value: Json): boolean {\r\n if (value === null || typeof value !== \"object\") return true;\r\n return byteSize(value) <= maxOpaqueBytes;\r\n },\r\n };\r\n}\r\n"],"mappings":";AASO,SAAS,SAAS,OAAqB;AAC5C,SAAO,OAAO,WAAW,KAAK,UAAU,KAAK,GAAG,MAAM;AACxD;AAGO,SAAS,OAAO,OAAa,QAA2B;AAC7D,MAAI,OAAQ,QAAO;AACnB,SAAO,MAAM,QAAQ,KAAK,IAAI,UAAU;AAC1C;AAMO,SAAS,kBAAkB,gBAA2C;AAC3E,SAAO;AAAA,IACL,SAAS,OAAsB;AAC7B,UAAI,UAAU,QAAQ,OAAO,UAAU,SAAU,QAAO;AACxD,aAAO,SAAS,KAAK,KAAK;AAAA,IAC5B;AAAA,EACF;AACF;","names":[]}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { MutationEvent } from './event-log.js';
|
|
2
|
+
import { StoredArtifact } from './storage.js';
|
|
3
|
+
import './types.js';
|
|
4
|
+
import './artifact-tree.js';
|
|
5
|
+
import './ids.js';
|
|
6
|
+
import './clock.js';
|
|
7
|
+
import './decompose.js';
|
|
8
|
+
import './vector-index-port.js';
|
|
9
|
+
|
|
10
|
+
/** A checkpoint snapshot plus the events journaled after it. */
|
|
11
|
+
interface DeltaBundle {
|
|
12
|
+
checkpoint: StoredArtifact | null;
|
|
13
|
+
journal: MutationEvent[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Append-oriented persistence: a periodic full **checkpoint** + an appendable **journal**
|
|
17
|
+
* of events after it. `appendEvents` is O(new events) — the per-save win over `StoragePort`,
|
|
18
|
+
* which rewrites the whole artifact. Restore = checkpoint + forward-replayed journal
|
|
19
|
+
* (see `restoreFromDelta`). Opt-in and independent of `StoragePort`.
|
|
20
|
+
*/
|
|
21
|
+
interface DeltaStoragePort {
|
|
22
|
+
/** Replace the checkpoint and clear the journal (the new checkpoint already embeds
|
|
23
|
+
* every event up to its version). */
|
|
24
|
+
writeCheckpoint(artifact: StoredArtifact): Promise<void>;
|
|
25
|
+
/** Append events to the journal (O(events)). */
|
|
26
|
+
appendEvents(events: readonly MutationEvent[]): Promise<void>;
|
|
27
|
+
/** The current checkpoint (or null) plus journaled events with `seq >=` the checkpoint
|
|
28
|
+
* version — stale pre-checkpoint events are filtered, making writeCheckpoint+clear
|
|
29
|
+
* crash-safe. */
|
|
30
|
+
loadDelta(): Promise<DeltaBundle>;
|
|
31
|
+
}
|
|
32
|
+
/** In-memory DeltaStoragePort (deep-clones on the boundary). */
|
|
33
|
+
declare class MemoryDeltaStorage implements DeltaStoragePort {
|
|
34
|
+
private checkpoint;
|
|
35
|
+
private journal;
|
|
36
|
+
writeCheckpoint(artifact: StoredArtifact): Promise<void>;
|
|
37
|
+
appendEvents(events: readonly MutationEvent[]): Promise<void>;
|
|
38
|
+
loadDelta(): Promise<DeltaBundle>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* File-backed DeltaStoragePort: the checkpoint is a JSON file (atomic + validated, via
|
|
42
|
+
* `FileStorage`); the journal is an append-only NDJSON file (one event per line).
|
|
43
|
+
* `writeCheckpoint` clears the journal; a torn final journal line (crash mid-append) is
|
|
44
|
+
* treated as a truncated tail and ignored.
|
|
45
|
+
*/
|
|
46
|
+
declare class FileDeltaStorage implements DeltaStoragePort {
|
|
47
|
+
private readonly journalPath;
|
|
48
|
+
private readonly checkpointStore;
|
|
49
|
+
constructor(checkpointPath: string, journalPath: string);
|
|
50
|
+
writeCheckpoint(artifact: StoredArtifact): Promise<void>;
|
|
51
|
+
appendEvents(events: readonly MutationEvent[]): Promise<void>;
|
|
52
|
+
loadDelta(): Promise<DeltaBundle>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export { type DeltaBundle, type DeltaStoragePort, FileDeltaStorage, MemoryDeltaStorage };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// src/delta-storage.ts
|
|
2
|
+
import { readFile as readFile2, writeFile as writeFile2, appendFile } from "fs/promises";
|
|
3
|
+
|
|
4
|
+
// src/file-storage.ts
|
|
5
|
+
import { readFile, writeFile, rename } from "fs/promises";
|
|
6
|
+
function isStoredArtifact(v) {
|
|
7
|
+
if (typeof v !== "object" || v === null) return false;
|
|
8
|
+
const a = v;
|
|
9
|
+
return (a["version"] === 1 || a["version"] === 2) && typeof a["rootId"] === "string" && Array.isArray(a["nodes"]) && Array.isArray(a["events"]) && Array.isArray(a["vectors"]);
|
|
10
|
+
}
|
|
11
|
+
var FileStorage = class {
|
|
12
|
+
constructor(path) {
|
|
13
|
+
this.path = path;
|
|
14
|
+
}
|
|
15
|
+
path;
|
|
16
|
+
async save(artifact) {
|
|
17
|
+
const tmp = this.path + ".tmp";
|
|
18
|
+
await writeFile(tmp, JSON.stringify(artifact), "utf8");
|
|
19
|
+
await rename(tmp, this.path);
|
|
20
|
+
}
|
|
21
|
+
async load() {
|
|
22
|
+
let text;
|
|
23
|
+
try {
|
|
24
|
+
text = await readFile(this.path, "utf8");
|
|
25
|
+
} catch (err) {
|
|
26
|
+
if (err.code === "ENOENT") return null;
|
|
27
|
+
throw err;
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(text);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
34
|
+
throw new Error(`FileStorage: corrupt artifact file ${this.path}: ${detail}`);
|
|
35
|
+
}
|
|
36
|
+
if (!isStoredArtifact(parsed)) {
|
|
37
|
+
throw new Error(`FileStorage: invalid artifact file ${this.path} (unrecognized shape)`);
|
|
38
|
+
}
|
|
39
|
+
return parsed;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/delta-storage.ts
|
|
44
|
+
function checkpointVersion(c) {
|
|
45
|
+
return c ? (c.baseSeq ?? 0) + c.events.length : 0;
|
|
46
|
+
}
|
|
47
|
+
var MemoryDeltaStorage = class {
|
|
48
|
+
checkpoint = null;
|
|
49
|
+
journal = [];
|
|
50
|
+
async writeCheckpoint(artifact) {
|
|
51
|
+
this.checkpoint = structuredClone(artifact);
|
|
52
|
+
this.journal = [];
|
|
53
|
+
}
|
|
54
|
+
async appendEvents(events) {
|
|
55
|
+
for (const e of events) this.journal.push(structuredClone(e));
|
|
56
|
+
}
|
|
57
|
+
async loadDelta() {
|
|
58
|
+
const v = checkpointVersion(this.checkpoint);
|
|
59
|
+
return {
|
|
60
|
+
checkpoint: this.checkpoint ? structuredClone(this.checkpoint) : null,
|
|
61
|
+
journal: this.journal.filter((e) => e.seq >= v).map((e) => structuredClone(e))
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var FileDeltaStorage = class {
|
|
66
|
+
constructor(checkpointPath, journalPath) {
|
|
67
|
+
this.journalPath = journalPath;
|
|
68
|
+
this.checkpointStore = new FileStorage(checkpointPath);
|
|
69
|
+
}
|
|
70
|
+
journalPath;
|
|
71
|
+
checkpointStore;
|
|
72
|
+
async writeCheckpoint(artifact) {
|
|
73
|
+
await this.checkpointStore.save(artifact);
|
|
74
|
+
await writeFile2(this.journalPath, "", "utf8");
|
|
75
|
+
}
|
|
76
|
+
async appendEvents(events) {
|
|
77
|
+
if (events.length === 0) return;
|
|
78
|
+
const lines = "\n" + events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
79
|
+
await appendFile(this.journalPath, lines, "utf8");
|
|
80
|
+
}
|
|
81
|
+
async loadDelta() {
|
|
82
|
+
const checkpoint = await this.checkpointStore.load();
|
|
83
|
+
const v = checkpointVersion(checkpoint);
|
|
84
|
+
let text = "";
|
|
85
|
+
try {
|
|
86
|
+
text = await readFile2(this.journalPath, "utf8");
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (err.code !== "ENOENT") throw err;
|
|
89
|
+
}
|
|
90
|
+
const journal = [];
|
|
91
|
+
for (const line of text.split("\n")) {
|
|
92
|
+
if (!line) continue;
|
|
93
|
+
try {
|
|
94
|
+
journal.push(JSON.parse(line));
|
|
95
|
+
} catch {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { checkpoint, journal: journal.filter((e) => e.seq >= v) };
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
export {
|
|
103
|
+
FileDeltaStorage,
|
|
104
|
+
MemoryDeltaStorage
|
|
105
|
+
};
|
|
106
|
+
//# sourceMappingURL=delta-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/delta-storage.ts","../src/file-storage.ts"],"sourcesContent":["import { readFile, writeFile, appendFile } from \"node:fs/promises\";\r\nimport type { MutationEvent } from \"./event-log\";\r\nimport type { StoredArtifact } from \"./storage\";\r\nimport { FileStorage } from \"./file-storage\";\r\n\r\n/** A checkpoint snapshot plus the events journaled after it. */\r\nexport interface DeltaBundle {\r\n checkpoint: StoredArtifact | null;\r\n journal: MutationEvent[];\r\n}\r\n\r\n/**\r\n * Append-oriented persistence: a periodic full **checkpoint** + an appendable **journal**\r\n * of events after it. `appendEvents` is O(new events) — the per-save win over `StoragePort`,\r\n * which rewrites the whole artifact. Restore = checkpoint + forward-replayed journal\r\n * (see `restoreFromDelta`). Opt-in and independent of `StoragePort`.\r\n */\r\nexport interface DeltaStoragePort {\r\n /** Replace the checkpoint and clear the journal (the new checkpoint already embeds\r\n * every event up to its version). */\r\n writeCheckpoint(artifact: StoredArtifact): Promise<void>;\r\n /** Append events to the journal (O(events)). */\r\n appendEvents(events: readonly MutationEvent[]): Promise<void>;\r\n /** The current checkpoint (or null) plus journaled events with `seq >=` the checkpoint\r\n * version — stale pre-checkpoint events are filtered, making writeCheckpoint+clear\r\n * crash-safe. */\r\n loadDelta(): Promise<DeltaBundle>;\r\n}\r\n\r\n/** The next-seq a checkpoint covers (absolute): baseSeq + embedded event count. */\r\nfunction checkpointVersion(c: StoredArtifact | null): number {\r\n return c ? (c.baseSeq ?? 0) + c.events.length : 0;\r\n}\r\n\r\n/** In-memory DeltaStoragePort (deep-clones on the boundary). */\r\nexport class MemoryDeltaStorage implements DeltaStoragePort {\r\n private checkpoint: StoredArtifact | null = null;\r\n private journal: MutationEvent[] = [];\r\n\r\n async writeCheckpoint(artifact: StoredArtifact): Promise<void> {\r\n this.checkpoint = structuredClone(artifact);\r\n this.journal = [];\r\n }\r\n\r\n async appendEvents(events: readonly MutationEvent[]): Promise<void> {\r\n for (const e of events) this.journal.push(structuredClone(e));\r\n }\r\n\r\n async loadDelta(): Promise<DeltaBundle> {\r\n const v = checkpointVersion(this.checkpoint);\r\n return {\r\n checkpoint: this.checkpoint ? structuredClone(this.checkpoint) : null,\r\n journal: this.journal.filter((e) => e.seq >= v).map((e) => structuredClone(e)),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * File-backed DeltaStoragePort: the checkpoint is a JSON file (atomic + validated, via\r\n * `FileStorage`); the journal is an append-only NDJSON file (one event per line).\r\n * `writeCheckpoint` clears the journal; a torn final journal line (crash mid-append) is\r\n * treated as a truncated tail and ignored.\r\n */\r\nexport class FileDeltaStorage implements DeltaStoragePort {\r\n private readonly checkpointStore: FileStorage;\r\n\r\n constructor(\r\n checkpointPath: string,\r\n private readonly journalPath: string,\r\n ) {\r\n this.checkpointStore = new FileStorage(checkpointPath);\r\n }\r\n\r\n async writeCheckpoint(artifact: StoredArtifact): Promise<void> {\r\n await this.checkpointStore.save(artifact);\r\n await writeFile(this.journalPath, \"\", \"utf8\");\r\n }\r\n\r\n async appendEvents(events: readonly MutationEvent[]): Promise<void> {\r\n if (events.length === 0) return;\r\n // The leading \"\\n\" isolates any torn tail left by a crash mid-append: new events\r\n // always start on a fresh line, and blank lines are skipped on load.\r\n const lines = \"\\n\" + events.map((e) => JSON.stringify(e)).join(\"\\n\") + \"\\n\";\r\n await appendFile(this.journalPath, lines, \"utf8\");\r\n }\r\n\r\n async loadDelta(): Promise<DeltaBundle> {\r\n const checkpoint = await this.checkpointStore.load();\r\n const v = checkpointVersion(checkpoint);\r\n let text = \"\";\r\n try {\r\n text = await readFile(this.journalPath, \"utf8\");\r\n } catch (err) {\r\n if ((err as NodeJS.ErrnoException).code !== \"ENOENT\") throw err;\r\n }\r\n const journal: MutationEvent[] = [];\r\n for (const line of text.split(\"\\n\")) {\r\n if (!line) continue;\r\n try {\r\n journal.push(JSON.parse(line) as MutationEvent);\r\n } catch {\r\n continue; // torn/garbage line from a crash mid-append — skip it; restore validates contiguity\r\n }\r\n }\r\n return { checkpoint, journal: journal.filter((e) => e.seq >= v) };\r\n }\r\n}\r\n","import { readFile, writeFile, rename } from \"node:fs/promises\";\r\nimport type { StoredArtifact, StoragePort } from \"./storage\";\r\n\r\nfunction isStoredArtifact(v: unknown): v is StoredArtifact {\r\n if (typeof v !== \"object\" || v === null) return false;\r\n const a = v as Record<string, unknown>;\r\n return (\r\n (a[\"version\"] === 1 || a[\"version\"] === 2) &&\r\n typeof a[\"rootId\"] === \"string\" &&\r\n Array.isArray(a[\"nodes\"]) &&\r\n Array.isArray(a[\"events\"]) &&\r\n Array.isArray(a[\"vectors\"])\r\n );\r\n}\r\n\r\n/**\r\n * File-backed StoragePort: one JSON file per artifact. `load` returns null if the\r\n * file is absent. Saves are atomic (write tmp, then rename) so a crash mid-write\r\n * never corrupts an existing artifact; loads validate the parsed shape.\r\n */\r\nexport class FileStorage implements StoragePort {\r\n constructor(private readonly path: string) {}\r\n\r\n async save(artifact: StoredArtifact): Promise<void> {\r\n const tmp = this.path + \".tmp\";\r\n await writeFile(tmp, JSON.stringify(artifact), \"utf8\");\r\n await rename(tmp, this.path);\r\n }\r\n\r\n async load(): Promise<StoredArtifact | null> {\r\n let text: string;\r\n try {\r\n text = await readFile(this.path, \"utf8\");\r\n } catch (err) {\r\n if ((err as NodeJS.ErrnoException).code === \"ENOENT\") return null;\r\n throw err;\r\n }\r\n let parsed: unknown;\r\n try {\r\n parsed = JSON.parse(text);\r\n } catch (err) {\r\n const detail = err instanceof Error ? err.message : String(err);\r\n throw new Error(`FileStorage: corrupt artifact file ${this.path}: ${detail}`);\r\n }\r\n if (!isStoredArtifact(parsed)) {\r\n throw new Error(`FileStorage: invalid artifact file ${this.path} (unrecognized shape)`);\r\n }\r\n return parsed;\r\n }\r\n}\r\n"],"mappings":"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","names":["readFile","writeFile","writeFile","readFile"]}
|