@openlfcp/shared-objects 0.1.5 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/dist/admission/actor.d.ts +16 -0
- package/dist/admission/actor.js +35 -0
- package/dist/{canonical.d.ts → admission/canonical.d.ts} +9 -0
- package/dist/{canonical.js → admission/canonical.js} +43 -3
- package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +38 -1
- package/dist/admission/framing.js +232 -0
- package/dist/admission/index.d.ts +26 -0
- package/dist/admission/index.js +26 -0
- package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
- package/dist/{chunk-limits.js → admission/limits.js} +25 -10
- package/dist/{references.d.ts → admission/references.d.ts} +1 -1
- package/dist/{references.js → admission/references.js} +9 -3
- package/dist/admission/sequence.d.ts +81 -0
- package/dist/admission/sequence.js +147 -0
- package/dist/data-profile.d.ts +1 -1
- package/dist/data-profile.js +3 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/replica.d.ts +32 -2
- package/dist/replica.js +154 -195
- package/dist/sections/admission.d.ts +35 -0
- package/dist/sections/admission.js +328 -0
- package/dist/sections/data-profile.d.ts +195 -0
- package/dist/sections/data-profile.js +383 -0
- package/dist/sections/dispatch.d.ts +41 -0
- package/dist/sections/dispatch.js +33 -0
- package/dist/sections/document.d.ts +54 -0
- package/dist/sections/document.js +101 -0
- package/dist/sections/index.d.ts +18 -0
- package/dist/sections/index.js +18 -0
- package/dist/sections/replica.d.ts +412 -0
- package/dist/sections/replica.js +1149 -0
- package/dist/sections/schema.d.ts +77 -0
- package/dist/sections/schema.js +548 -0
- package/dist/sections/tree.d.ts +83 -0
- package/dist/sections/tree.js +290 -0
- package/dist/sections/values.d.ts +30 -0
- package/dist/sections/values.js +36 -0
- package/dist/values.d.ts +3 -18
- package/dist/values.js +7 -95
- package/package.json +12 -4
- package/dist/automerge-bytes.js +0 -111
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import * as A from "@automerge/automerge";
|
|
2
|
+
import { LfcpError, toHex } from "@openlfcp/core";
|
|
3
|
+
import { decodedOf } from "../admission/framing.js";
|
|
4
|
+
import { MAX_DOCUMENT_DEPTH } from "../admission/limits.js";
|
|
5
|
+
import { parsePrincipalRef } from "../values.js";
|
|
6
|
+
import { deriveSectionActorId } from "./values.js";
|
|
7
|
+
/**
|
|
8
|
+
* The structural admission of SHARED-SECTIONS-PROFILE-01 §14.1 (LFCP-02-017):
|
|
9
|
+
* rules A1–A5 and the readiness rule of §12.1, decided from the change's own
|
|
10
|
+
* operations and the objects they write into, in time linear in the change.
|
|
11
|
+
* It runs after the inherited SOP checks (framing, limits, actor, sequence)
|
|
12
|
+
* and before the engine applies anything; a refused change is never merged.
|
|
13
|
+
*
|
|
14
|
+
* Objects of the document are identified by their path from the root
|
|
15
|
+
* (Automerge objInfo); objects created by changes admitted earlier in the
|
|
16
|
+
* same batch, or earlier in the change, by the operation that creates them.
|
|
17
|
+
*/
|
|
18
|
+
/** §14.1: the admission diagnostics, in their order of precedence. */
|
|
19
|
+
export const ADMISSION_ORDER = Object.freeze([
|
|
20
|
+
"INVALID_AUTOMERGE_BYTES",
|
|
21
|
+
"CHANGE_ACTOR_MISMATCH",
|
|
22
|
+
"CONTAINER_REPLACED",
|
|
23
|
+
"CHILDREN_LIST_MUTATED",
|
|
24
|
+
"PLACEMENT_NOT_ATOMIC",
|
|
25
|
+
"IMMUTABLE_FIELD_MUTATED",
|
|
26
|
+
"INVALID_FIELD_TYPE",
|
|
27
|
+
]);
|
|
28
|
+
/** A change refused at admission: PROFILE_INVALID with its §14.1 diagnostic. */
|
|
29
|
+
export class SectionAdmissionError extends LfcpError {
|
|
30
|
+
diagnostic;
|
|
31
|
+
constructor(diagnostic, message) {
|
|
32
|
+
super("PROFILE_INVALID", message);
|
|
33
|
+
this.name = "SectionAdmissionError";
|
|
34
|
+
this.diagnostic = diagnostic;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const ROOT_CONTAINERS = new Set([
|
|
38
|
+
"profile",
|
|
39
|
+
"section",
|
|
40
|
+
"objects",
|
|
41
|
+
"nodes",
|
|
42
|
+
"placements",
|
|
43
|
+
"extensions",
|
|
44
|
+
]);
|
|
45
|
+
const NODE_IMMUTABLE = new Set(["id", "kind", "created_by", "task_id"]);
|
|
46
|
+
const TASK_IMMUTABLE = new Set(["id", "type", "created_by"]);
|
|
47
|
+
const NODE_CONTAINERS = new Set(["children", "text", "extensions"]);
|
|
48
|
+
const MAKE = new Set(["makeMap", "makeList", "makeText", "makeTable"]);
|
|
49
|
+
export class SectionAdmission {
|
|
50
|
+
/** The document with every change admitted so far: each change is read at its own deps. */
|
|
51
|
+
#work;
|
|
52
|
+
#backend;
|
|
53
|
+
#resource;
|
|
54
|
+
constructor(doc, resource) {
|
|
55
|
+
this.#work = doc;
|
|
56
|
+
this.#backend = A.getBackend(doc);
|
|
57
|
+
this.#resource = resource;
|
|
58
|
+
}
|
|
59
|
+
/** Admitted changes not yet applied to the working document. */
|
|
60
|
+
#pending = [];
|
|
61
|
+
/**
|
|
62
|
+
* Applies the admitted changes still pending, in one engine call. Done
|
|
63
|
+
* only when a check reads the state they may change, so a long linear run
|
|
64
|
+
* of Text edits is applied once, not change by change.
|
|
65
|
+
*/
|
|
66
|
+
#flush() {
|
|
67
|
+
if (this.#pending.length === 0)
|
|
68
|
+
return;
|
|
69
|
+
const batch = this.#pending;
|
|
70
|
+
this.#pending = [];
|
|
71
|
+
[this.#work] = A.applyChanges(this.#work, batch);
|
|
72
|
+
this.#backend = A.getBackend(this.#work);
|
|
73
|
+
for (const [obj, k] of this.#paths)
|
|
74
|
+
if (k === undefined)
|
|
75
|
+
this.#paths.delete(obj);
|
|
76
|
+
}
|
|
77
|
+
/** The document with the admitted changes applied. */
|
|
78
|
+
get document() {
|
|
79
|
+
this.#flush();
|
|
80
|
+
return this.#work;
|
|
81
|
+
}
|
|
82
|
+
/** Paths of the document's objects, read once each: objects never move. */
|
|
83
|
+
#paths = new Map();
|
|
84
|
+
/** Path and depth of an object of the document; undefined when it is unknown. */
|
|
85
|
+
#known(obj) {
|
|
86
|
+
if (obj === "_root")
|
|
87
|
+
return { path: [], depth: 0 };
|
|
88
|
+
if (this.#paths.get(obj) !== undefined)
|
|
89
|
+
return this.#paths.get(obj);
|
|
90
|
+
this.#flush();
|
|
91
|
+
let known;
|
|
92
|
+
try {
|
|
93
|
+
const path = this.#backend.objInfo(obj).path;
|
|
94
|
+
known = { path, depth: path === undefined ? Number.NaN : path.length };
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
known = undefined;
|
|
98
|
+
}
|
|
99
|
+
this.#paths.set(obj, known);
|
|
100
|
+
return known;
|
|
101
|
+
}
|
|
102
|
+
/** Whether `key` of `obj` has a value in the change's causal history (`heads`). */
|
|
103
|
+
#has(obj, key, heads) {
|
|
104
|
+
this.#flush();
|
|
105
|
+
try {
|
|
106
|
+
return this.#backend.getAll(obj, key, heads).length > 0;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** The section's scalar `key` in the document (its ID and creator never change). */
|
|
113
|
+
/** Section fields already read: they never change once written. */
|
|
114
|
+
#sectionFields = new Map();
|
|
115
|
+
#section(key) {
|
|
116
|
+
const cached = this.#sectionFields.get(key);
|
|
117
|
+
if (cached !== undefined)
|
|
118
|
+
return cached;
|
|
119
|
+
this.#flush();
|
|
120
|
+
const section = this.#work.section;
|
|
121
|
+
const v = section?.[key];
|
|
122
|
+
if (v === undefined)
|
|
123
|
+
return undefined;
|
|
124
|
+
this.#sectionFields.set(key, String(v));
|
|
125
|
+
return String(v);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Admits `change` against its causal history, or throws
|
|
129
|
+
* SectionAdmissionError. An admitted change is applied to the working
|
|
130
|
+
* document at once, so the changes after it in the batch see it.
|
|
131
|
+
*/
|
|
132
|
+
accept(change) {
|
|
133
|
+
const decoded = decodedOf(change);
|
|
134
|
+
const ops = decoded.ops;
|
|
135
|
+
const found = new Set();
|
|
136
|
+
const local = new Map();
|
|
137
|
+
const knownOf = (obj) => local.get(obj) ?? this.#known(obj);
|
|
138
|
+
// Objects this change did not create existed in its causal history.
|
|
139
|
+
const existed = (obj) => !local.has(obj) && obj !== "_root";
|
|
140
|
+
const heads = decoded.deps;
|
|
141
|
+
const has = (obj, key) => this.#has(obj, key, heads);
|
|
142
|
+
// Created placements of this change: their fields, list entries and register writes.
|
|
143
|
+
const placements = new Map();
|
|
144
|
+
const registers = new Map(); // node → PlacementIds written to its register
|
|
145
|
+
const insertedInto = [];
|
|
146
|
+
let readyWrite;
|
|
147
|
+
let creatorInChange;
|
|
148
|
+
ops.forEach((op, i) => {
|
|
149
|
+
const parent = knownOf(op.obj);
|
|
150
|
+
const path = parent?.path;
|
|
151
|
+
const key = op.key;
|
|
152
|
+
const own = (p, k) => p !== undefined && p.length === k ? p : undefined;
|
|
153
|
+
if (MAKE.has(op.action)) {
|
|
154
|
+
const id = `${decoded.startOp + i}@${decoded.actor}`;
|
|
155
|
+
if (parent === undefined)
|
|
156
|
+
throw new SectionAdmissionError("INVALID_AUTOMERGE_BYTES", `the change writes into object ${op.obj}, which the document does not have (SOP §11.2)`);
|
|
157
|
+
const depth = parent.depth + 1;
|
|
158
|
+
if (depth > MAX_DOCUMENT_DEPTH)
|
|
159
|
+
throw new SectionAdmissionError("INVALID_AUTOMERGE_BYTES", `the change creates an object deeper than ${MAX_DOCUMENT_DEPTH} levels (SOP §11.2)`);
|
|
160
|
+
const childPath = path === undefined ? undefined : key !== undefined ? [...path, key] : [...path, -1];
|
|
161
|
+
local.set(id, { path: childPath, depth });
|
|
162
|
+
}
|
|
163
|
+
// The root: its containers are created once (A4).
|
|
164
|
+
if (op.obj === "_root") {
|
|
165
|
+
if (key !== undefined && ROOT_CONTAINERS.has(key) && has("_root", key))
|
|
166
|
+
found.add("CONTAINER_REPLACED");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (path === undefined)
|
|
170
|
+
return; // an object without a known place: nothing structural
|
|
171
|
+
const [top, second] = path;
|
|
172
|
+
if (top === "section" && path.length === 1) {
|
|
173
|
+
if (!existed(op.obj)) {
|
|
174
|
+
if (key === "created_by" && typeof op.value === "string")
|
|
175
|
+
creatorInChange = op.value;
|
|
176
|
+
}
|
|
177
|
+
else if (key === "id" || key === "created_by")
|
|
178
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
179
|
+
if ((key === "children" || key === "extensions") && existed(op.obj) && has(op.obj, key))
|
|
180
|
+
found.add("CONTAINER_REPLACED");
|
|
181
|
+
if (key === "ready")
|
|
182
|
+
readyWrite = {
|
|
183
|
+
value: op.value,
|
|
184
|
+
del: op.action === "del",
|
|
185
|
+
again: (readyWrite?.again ?? false) || (existed(op.obj) && has(op.obj, "ready")),
|
|
186
|
+
};
|
|
187
|
+
else if (op.action === "makeText")
|
|
188
|
+
found.add("INVALID_FIELD_TYPE");
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
// Children lists: insert-only, entries are scalar PlacementIds (A1, A2, A5).
|
|
192
|
+
const owner = own(path, 2)?.[0] === "section" && path[1] === "children"
|
|
193
|
+
? "section"
|
|
194
|
+
: own(path, 3)?.[0] === "nodes" && path[2] === "children"
|
|
195
|
+
? String(second)
|
|
196
|
+
: undefined;
|
|
197
|
+
if (owner !== undefined) {
|
|
198
|
+
const isNew = !existed(op.obj);
|
|
199
|
+
if (op.insert) {
|
|
200
|
+
if (op.action !== "set" || typeof op.value !== "string")
|
|
201
|
+
found.add("INVALID_FIELD_TYPE");
|
|
202
|
+
else
|
|
203
|
+
insertedInto.push({ owner, value: op.value });
|
|
204
|
+
}
|
|
205
|
+
else if (!isNew || op.action === "del" || op.action === "set") {
|
|
206
|
+
found.add("CHILDREN_LIST_MUTATED");
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (top === "nodes" && path.length === 1) {
|
|
211
|
+
// A node map is created once; replacing or deleting it is A4.
|
|
212
|
+
if (key !== undefined && has(op.obj, key) && existed(op.obj))
|
|
213
|
+
found.add("CONTAINER_REPLACED");
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (top === "nodes" && path.length === 2) {
|
|
217
|
+
const node = String(second);
|
|
218
|
+
const isNew = !existed(op.obj);
|
|
219
|
+
if (key === undefined)
|
|
220
|
+
return;
|
|
221
|
+
if (!isNew) {
|
|
222
|
+
if (NODE_CONTAINERS.has(key) && has(op.obj, key))
|
|
223
|
+
found.add("CONTAINER_REPLACED");
|
|
224
|
+
if (NODE_IMMUTABLE.has(key))
|
|
225
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
226
|
+
}
|
|
227
|
+
if (key === "text") {
|
|
228
|
+
if (op.action !== "makeText" && op.action !== "del")
|
|
229
|
+
found.add("INVALID_FIELD_TYPE");
|
|
230
|
+
}
|
|
231
|
+
else if (op.action === "makeText")
|
|
232
|
+
found.add("INVALID_FIELD_TYPE");
|
|
233
|
+
if (key === "placement" && op.action === "set" && typeof op.value === "string") {
|
|
234
|
+
const set = registers.get(node) ?? new Set();
|
|
235
|
+
set.add(op.value);
|
|
236
|
+
registers.set(node, set);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (top === "placements" && path.length === 1) {
|
|
241
|
+
if (key !== undefined && op.action === "makeMap" && !has(op.obj, key))
|
|
242
|
+
placements.set(key, { inserted: [] });
|
|
243
|
+
else if (key !== undefined)
|
|
244
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (top === "placements" && path.length === 2) {
|
|
248
|
+
if (existed(op.obj)) {
|
|
249
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const p = placements.get(String(second));
|
|
253
|
+
if (p !== undefined && op.action === "set" && typeof op.value === "string") {
|
|
254
|
+
if (key === "node_id")
|
|
255
|
+
p.node = op.value;
|
|
256
|
+
if (key === "parent_id")
|
|
257
|
+
p.parent = op.value;
|
|
258
|
+
}
|
|
259
|
+
if (op.action === "makeText")
|
|
260
|
+
found.add("INVALID_FIELD_TYPE");
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (top === "objects" && path.length >= 2) {
|
|
264
|
+
// SOP §30: every string of a Task is a scalar; §75: its identity is immutable.
|
|
265
|
+
if (op.action === "makeText")
|
|
266
|
+
found.add("INVALID_FIELD_TYPE");
|
|
267
|
+
if (path.length === 2 && existed(op.obj) && key !== undefined && TASK_IMMUTABLE.has(key))
|
|
268
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
// §12.1: ready is the scalar true, written once, by the creator's actor.
|
|
273
|
+
if (readyWrite !== undefined) {
|
|
274
|
+
const creator = creatorInChange ?? this.#section("created_by");
|
|
275
|
+
let creatorActor;
|
|
276
|
+
try {
|
|
277
|
+
creatorActor =
|
|
278
|
+
creator === undefined
|
|
279
|
+
? undefined
|
|
280
|
+
: toHex(deriveSectionActorId(this.#resource, parsePrincipalRef(creator)));
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
creatorActor = undefined;
|
|
284
|
+
}
|
|
285
|
+
if (readyWrite.del ||
|
|
286
|
+
readyWrite.again ||
|
|
287
|
+
readyWrite.value !== true ||
|
|
288
|
+
creatorActor !== decoded.actor)
|
|
289
|
+
found.add("IMMUTABLE_FIELD_MUTATED");
|
|
290
|
+
}
|
|
291
|
+
// A2: every inserted entry is a placement this change creates under that list's
|
|
292
|
+
// owner; every created placement is inserted exactly once and assigned to its node.
|
|
293
|
+
const sectionId = insertedInto.length > 0 ? this.#sectionId(ops, decoded.startOp, decoded.actor) : undefined;
|
|
294
|
+
for (const { owner, value } of insertedInto) {
|
|
295
|
+
const p = placements.get(value);
|
|
296
|
+
if (p === undefined)
|
|
297
|
+
found.add("PLACEMENT_NOT_ATOMIC");
|
|
298
|
+
else
|
|
299
|
+
p.inserted.push(owner === "section" ? (sectionId ?? "section") : owner);
|
|
300
|
+
}
|
|
301
|
+
for (const [pid, p] of placements) {
|
|
302
|
+
if (p.inserted.length !== 1 || p.inserted[0] !== p.parent)
|
|
303
|
+
found.add("PLACEMENT_NOT_ATOMIC");
|
|
304
|
+
if (p.node === undefined || !(registers.get(p.node)?.has(pid) ?? false))
|
|
305
|
+
found.add("PLACEMENT_NOT_ATOMIC");
|
|
306
|
+
}
|
|
307
|
+
const first = ADMISSION_ORDER.find((d) => found.has(d));
|
|
308
|
+
if (first !== undefined)
|
|
309
|
+
throw new SectionAdmissionError(first, `the change breaks SHARED-SECTIONS-PROFILE-01 §14.1: ${first}`);
|
|
310
|
+
// Admitted: the changes after it in the batch see it.
|
|
311
|
+
// Admitted: applied before the next check that reads the document.
|
|
312
|
+
this.#pending.push(change.bytes);
|
|
313
|
+
}
|
|
314
|
+
/** The SectionId: from the document, or from the change that creates the section. */
|
|
315
|
+
#sectionId(ops, startOp, actor) {
|
|
316
|
+
const known = this.#section("id");
|
|
317
|
+
if (known !== undefined)
|
|
318
|
+
return known;
|
|
319
|
+
let sectionObj;
|
|
320
|
+
ops.forEach((op, i) => {
|
|
321
|
+
if (op.obj === "_root" && op.key === "section" && op.action === "makeMap")
|
|
322
|
+
sectionObj = `${startOp + i}@${actor}`;
|
|
323
|
+
});
|
|
324
|
+
const set = ops.find((op) => op.obj === sectionObj && op.key === "id" && op.action === "set");
|
|
325
|
+
return typeof set?.value === "string" ? set.value : undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
//# sourceMappingURL=admission.js.map
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { type DataUnitId, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
+
import { type CheckedChange } from "../admission/framing.js";
|
|
3
|
+
import { SectionReplica } from "./replica.js";
|
|
4
|
+
/**
|
|
5
|
+
* The Shared Sections Data Profile handler (LFCP-02-025): what the
|
|
6
|
+
* profile-agnostic Data Unit applier of @openlfcp/client calls once LFCP
|
|
7
|
+
* has accepted a unit of a org.openlfcp.shared-sections.v1 Resource. It
|
|
8
|
+
* matches the client's DataProfileHandler structurally; this package
|
|
9
|
+
* depends on neither client nor wire.
|
|
10
|
+
*
|
|
11
|
+
* - decode: the plaintext is [1, change], a change of the section actor of
|
|
12
|
+
* the unit's signer (SHARED-SECTIONS-PROFILE-01 §2), else
|
|
13
|
+
* CHANGE_ACTOR_MISMATCH: the refusal is the unit's, never the change's;
|
|
14
|
+
* - apply: the change goes through the section admission (§14.1,
|
|
15
|
+
* SectionReplica.receiveChanges); it merges, waits for the changes it
|
|
16
|
+
* depends on, is held (a taken actor sequence, SOP §14.1) or is refused;
|
|
17
|
+
* - exclude (G-EP7): the replica is rebuilt without given units;
|
|
18
|
+
* - checkpoint/restore: the persisted state, with the units of every merged
|
|
19
|
+
* change and this actor's sequence (§9).
|
|
20
|
+
*/
|
|
21
|
+
/** An accepted unit as the applier passes it (structurally the client's ProfileUnit). */
|
|
22
|
+
export interface SectionsUnit {
|
|
23
|
+
readonly unitId: DataUnitId;
|
|
24
|
+
}
|
|
25
|
+
/** The §11 codec of one unit (structurally the wire DataProfileCodec). */
|
|
26
|
+
export interface SectionsCodec {
|
|
27
|
+
readonly dataProfile: string;
|
|
28
|
+
encode(change: CheckedChange): Uint8Array;
|
|
29
|
+
decode(plaintext: Uint8Array): CheckedChange;
|
|
30
|
+
}
|
|
31
|
+
/** The Snapshot codec: a full save framed as SOP §13 frames it (structurally DataProfileCodec). */
|
|
32
|
+
export interface SectionsSnapshotCodec {
|
|
33
|
+
readonly dataProfile: string;
|
|
34
|
+
encode(save: Uint8Array): Uint8Array;
|
|
35
|
+
decode(plaintext: Uint8Array): Uint8Array;
|
|
36
|
+
}
|
|
37
|
+
/** The Snapshot binding of a sync client (structurally the client's SnapshotBinding). */
|
|
38
|
+
export interface SectionsSnapshotBinding {
|
|
39
|
+
readonly codec: SectionsSnapshotCodec;
|
|
40
|
+
load(save: Uint8Array): void;
|
|
41
|
+
current(): Uint8Array;
|
|
42
|
+
}
|
|
43
|
+
/** The persisted state (structurally the storage ProfileCheckpoint). */
|
|
44
|
+
export interface SectionsCheckpoint {
|
|
45
|
+
readonly resourceId: ResourceId;
|
|
46
|
+
readonly dataProfile: string;
|
|
47
|
+
readonly state: Uint8Array;
|
|
48
|
+
readonly actorSeq: number;
|
|
49
|
+
readonly units: readonly {
|
|
50
|
+
readonly unitId: DataUnitId;
|
|
51
|
+
readonly ref: string;
|
|
52
|
+
}[];
|
|
53
|
+
}
|
|
54
|
+
export interface SectionsApplyResult {
|
|
55
|
+
readonly merged: readonly DataUnitId[];
|
|
56
|
+
/** Nodes (and the section) whose projected state changed. */
|
|
57
|
+
readonly objects: readonly string[];
|
|
58
|
+
readonly diagnostics: readonly never[];
|
|
59
|
+
readonly pending?: string;
|
|
60
|
+
readonly held?: string;
|
|
61
|
+
}
|
|
62
|
+
export interface SectionsBatchResult {
|
|
63
|
+
readonly merged: readonly DataUnitId[];
|
|
64
|
+
readonly pending: readonly DataUnitId[];
|
|
65
|
+
readonly held: readonly DataUnitId[];
|
|
66
|
+
readonly rejected: readonly {
|
|
67
|
+
readonly unitId: DataUnitId;
|
|
68
|
+
readonly code: string;
|
|
69
|
+
readonly diagnostic: string;
|
|
70
|
+
readonly message: string;
|
|
71
|
+
}[];
|
|
72
|
+
readonly objects: readonly string[];
|
|
73
|
+
readonly diagnostics: readonly never[];
|
|
74
|
+
}
|
|
75
|
+
export interface SectionsExcludeResult {
|
|
76
|
+
readonly objects: readonly string[];
|
|
77
|
+
readonly pending: readonly DataUnitId[];
|
|
78
|
+
readonly released: readonly DataUnitId[];
|
|
79
|
+
}
|
|
80
|
+
/** A batch staged for commit (structurally the client's StagedOperation). */
|
|
81
|
+
export interface SectionsStagedOperation {
|
|
82
|
+
readonly values: readonly CheckedChange[];
|
|
83
|
+
readonly affectedNodeIds: readonly string[];
|
|
84
|
+
readonly modelRevision: string;
|
|
85
|
+
apply(): void;
|
|
86
|
+
revert(): void;
|
|
87
|
+
writes(units: readonly {
|
|
88
|
+
readonly unitId: DataUnitId;
|
|
89
|
+
}[]): readonly {
|
|
90
|
+
readonly op: "put-profile-checkpoint";
|
|
91
|
+
readonly checkpoint: SectionsCheckpoint;
|
|
92
|
+
}[];
|
|
93
|
+
committed(unitIds: readonly DataUnitId[]): void;
|
|
94
|
+
}
|
|
95
|
+
/** How a sync client commits section batches (structurally the client's CommitBinding). */
|
|
96
|
+
export interface SectionsCommitBinding {
|
|
97
|
+
readonly codec: SectionsCodec;
|
|
98
|
+
stage(intents: readonly unknown[]): SectionsStagedOperation | null;
|
|
99
|
+
/** SDK-SECTIONS-INTEGRATION-01 §4.3: ready or importing; undefined before section.create. */
|
|
100
|
+
section(): "ready" | "importing" | undefined;
|
|
101
|
+
}
|
|
102
|
+
/** SDK-SECTIONS-INTEGRATION-01 §5 nodes-changed: what changed, and why. */
|
|
103
|
+
export interface SectionsNodesChanged {
|
|
104
|
+
readonly nodeIds: readonly string[];
|
|
105
|
+
readonly origin: "local" | "remote" | "rebuild";
|
|
106
|
+
readonly modelRevision: string;
|
|
107
|
+
}
|
|
108
|
+
export declare class SharedSectionsDataProfile {
|
|
109
|
+
#private;
|
|
110
|
+
readonly dataProfile = "org.openlfcp.shared-sections.v1";
|
|
111
|
+
constructor(replica: SectionReplica);
|
|
112
|
+
/** The current replica (a rebuild replaces it). */
|
|
113
|
+
get replica(): SectionReplica;
|
|
114
|
+
/** §5 nodes-changed for remote merges, rebuilds and recorded local changes. */
|
|
115
|
+
onNodesChanged(listener: (event: SectionsNodesChanged) => void): () => void;
|
|
116
|
+
/** The codec of one unit, bound to its signer's section actor (§2). */
|
|
117
|
+
codecFor(unit: {
|
|
118
|
+
readonly resourceId: ResourceId;
|
|
119
|
+
readonly actor: PrincipalId;
|
|
120
|
+
}): SectionsCodec;
|
|
121
|
+
/**
|
|
122
|
+
* SDK-SECTIONS-INTEGRATION-01 §3.1: the binding through which a sync
|
|
123
|
+
* client commits this Principal's batches (structurally the client's
|
|
124
|
+
* CommitBinding): `stage` validates the intents and changes a copy of the
|
|
125
|
+
* replica, splitting what one change cannot carry (§12); the staged
|
|
126
|
+
* operation adopts it, writes the checkpoint with the new units and
|
|
127
|
+
* records them once committed.
|
|
128
|
+
*/
|
|
129
|
+
commitBinding(principal: PrincipalId): SectionsCommitBinding;
|
|
130
|
+
/**
|
|
131
|
+
* Records which change one of this client's own units carries: the change
|
|
132
|
+
* is in the replica already (SectionReplica.stage, then apply). A later
|
|
133
|
+
* exclusion of the unit rebuilds without it, and the checkpoint keeps it.
|
|
134
|
+
*/
|
|
135
|
+
recordLocal(unitId: DataUnitId, change: {
|
|
136
|
+
readonly hash: string;
|
|
137
|
+
readonly affectedNodeIds?: readonly string[];
|
|
138
|
+
}): void;
|
|
139
|
+
/** Merges one accepted unit's change; throws when the section admission refuses it. */
|
|
140
|
+
apply(unit: SectionsUnit, change: CheckedChange): SectionsApplyResult;
|
|
141
|
+
/**
|
|
142
|
+
* Merges many accepted units at once, with every unit still waiting: one
|
|
143
|
+
* admission and one engine call for a catch-up. A refused change rejects
|
|
144
|
+
* only its unit; the units that depend on it wait.
|
|
145
|
+
*/
|
|
146
|
+
applyBatch(units: readonly {
|
|
147
|
+
readonly unit: SectionsUnit;
|
|
148
|
+
readonly value: CheckedChange;
|
|
149
|
+
}[]): SectionsBatchResult;
|
|
150
|
+
/**
|
|
151
|
+
* G-EP7: the state without `unitIds`, rebuilt from the remaining changes.
|
|
152
|
+
* Merged units whose changes build on an excluded one wait again; held
|
|
153
|
+
* units are retried, since the rebuild may free their actor sequence.
|
|
154
|
+
*/
|
|
155
|
+
exclude(unitIds: readonly DataUnitId[]): SectionsExcludeResult;
|
|
156
|
+
/** Whether this handler holds the unit's change (merged, recorded, waiting or held). */
|
|
157
|
+
has(unitId: DataUnitId): boolean;
|
|
158
|
+
heldUnits(): DataUnitId[];
|
|
159
|
+
pendingUnits(): DataUnitId[];
|
|
160
|
+
/** The Snapshot codec (SOP §13): the full save, framed. */
|
|
161
|
+
snapshotCodec(): SectionsSnapshotCodec;
|
|
162
|
+
/** The state to publish as a Snapshot: the replica's full save. */
|
|
163
|
+
snapshotState(): Uint8Array;
|
|
164
|
+
/**
|
|
165
|
+
* Loads a received Snapshot's full save (SOP §13, §14; LFCP-02-028):
|
|
166
|
+
* every change it holds passes the section admission (§14.1), so a
|
|
167
|
+
* Snapshot holding a refused change is rejected and nothing changes;
|
|
168
|
+
* this replica's changes, local work the Snapshot lacks included, are
|
|
169
|
+
* kept on top. Buffered units whose dependencies it brings are merged.
|
|
170
|
+
* Returns the nodes that changed and the units merged from the buffer.
|
|
171
|
+
*/
|
|
172
|
+
loadSnapshot(save: Uint8Array): {
|
|
173
|
+
readonly nodeIds: readonly string[];
|
|
174
|
+
readonly merged: readonly DataUnitId[];
|
|
175
|
+
};
|
|
176
|
+
/** The binding through which a sync client loads and publishes Snapshots. */
|
|
177
|
+
snapshotBinding(): SectionsSnapshotBinding;
|
|
178
|
+
/** Forgets the whole state, keeping the §9 sequence (SNAP-EP). */
|
|
179
|
+
reset(): void;
|
|
180
|
+
/**
|
|
181
|
+
* The state to persist: the full save, this actor's sequence and each
|
|
182
|
+
* merged unit's change, with `local`, this client's units committed in
|
|
183
|
+
* the same transaction as the checkpoint.
|
|
184
|
+
*/
|
|
185
|
+
checkpoint(local?: readonly {
|
|
186
|
+
readonly unitId: DataUnitId;
|
|
187
|
+
readonly ref: string;
|
|
188
|
+
}[]): SectionsCheckpoint;
|
|
189
|
+
/** The handler of a persisted checkpoint; its replica writes nothing below the checkpoint's sequence (§9). */
|
|
190
|
+
static restore(checkpoint: SectionsCheckpoint, options: {
|
|
191
|
+
readonly resource: ResourceId;
|
|
192
|
+
readonly principal: PrincipalId;
|
|
193
|
+
}): SharedSectionsDataProfile;
|
|
194
|
+
}
|
|
195
|
+
//# sourceMappingURL=data-profile.d.ts.map
|