@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.
Files changed (43) hide show
  1. package/README.md +15 -2
  2. package/dist/admission/actor.d.ts +16 -0
  3. package/dist/admission/actor.js +35 -0
  4. package/dist/{canonical.d.ts → admission/canonical.d.ts} +9 -0
  5. package/dist/{canonical.js → admission/canonical.js} +43 -3
  6. package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +38 -1
  7. package/dist/admission/framing.js +232 -0
  8. package/dist/admission/index.d.ts +26 -0
  9. package/dist/admission/index.js +26 -0
  10. package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
  11. package/dist/{chunk-limits.js → admission/limits.js} +25 -10
  12. package/dist/{references.d.ts → admission/references.d.ts} +1 -1
  13. package/dist/{references.js → admission/references.js} +9 -3
  14. package/dist/admission/sequence.d.ts +81 -0
  15. package/dist/admission/sequence.js +147 -0
  16. package/dist/data-profile.d.ts +1 -1
  17. package/dist/data-profile.js +3 -7
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.js +2 -2
  20. package/dist/replica.d.ts +32 -2
  21. package/dist/replica.js +154 -195
  22. package/dist/sections/admission.d.ts +35 -0
  23. package/dist/sections/admission.js +328 -0
  24. package/dist/sections/data-profile.d.ts +195 -0
  25. package/dist/sections/data-profile.js +383 -0
  26. package/dist/sections/dispatch.d.ts +41 -0
  27. package/dist/sections/dispatch.js +33 -0
  28. package/dist/sections/document.d.ts +54 -0
  29. package/dist/sections/document.js +101 -0
  30. package/dist/sections/index.d.ts +18 -0
  31. package/dist/sections/index.js +18 -0
  32. package/dist/sections/replica.d.ts +412 -0
  33. package/dist/sections/replica.js +1149 -0
  34. package/dist/sections/schema.d.ts +77 -0
  35. package/dist/sections/schema.js +548 -0
  36. package/dist/sections/tree.d.ts +83 -0
  37. package/dist/sections/tree.js +290 -0
  38. package/dist/sections/values.d.ts +30 -0
  39. package/dist/sections/values.js +36 -0
  40. package/dist/values.d.ts +3 -18
  41. package/dist/values.js +7 -95
  42. package/package.json +12 -4
  43. package/dist/automerge-bytes.js +0 -111
@@ -0,0 +1,383 @@
1
+ import { LfcpError, toHex, } from "@openlfcp/core";
2
+ import { checkChangeActor } from "../admission/actor.js";
3
+ import { checkChange, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "../admission/framing.js";
4
+ import { SectionReplica } from "./replica.js";
5
+ import { deriveSectionActorId, SECTIONS_PROFILE_ID } from "./values.js";
6
+ /** Each node's (and the title's) projected state, to tell which ones a merge changed. */
7
+ function fingerprints(s) {
8
+ const out = new Map();
9
+ for (const [id, n] of Object.entries(s.nodes))
10
+ out.set(id, JSON.stringify(n));
11
+ out.set("", JSON.stringify(s.title));
12
+ s.order.forEach((e, i) => {
13
+ out.set(e.id, `${out.get(e.id) ?? ""}|${i}:${e.parent}:${e.depth}`);
14
+ });
15
+ return out;
16
+ }
17
+ function changed(before, after) {
18
+ const ids = new Set([...before.keys(), ...after.keys()]);
19
+ return [...ids].filter((id) => id !== "" && before.get(id) !== after.get(id)).sort();
20
+ }
21
+ const stateOf = (replica) => ({
22
+ replica,
23
+ revision: replica.revision(),
24
+ fingerprints: fingerprints(replica.snapshot()),
25
+ });
26
+ /**
27
+ * The nodes that changed from `before` to `to`, by the rule of a receipt's
28
+ * affectedNodeIds (SDK-SECTIONS-INTEGRATION-01 §3): a node whose fields,
29
+ * Text, placement or lifecycle changed, a task node whose Task's fields
30
+ * changed (they live in the Task object, not in the node), and the section
31
+ * when its title changed.
32
+ */
33
+ function nodesChanged(before, to) {
34
+ const after = fingerprints(to.snapshot());
35
+ const ids = new Set(changed(before.fingerprints, after));
36
+ if (before.fingerprints.get("") !== after.get("")) {
37
+ const section = to.validate().sectionId ?? before.replica.validate().sectionId;
38
+ if (section !== undefined)
39
+ ids.add(section);
40
+ }
41
+ // A task node's ID is its Task's (§4.2): the Tasks the changes wrote, by A.diff where the
42
+ // document still holds the earlier revision, else (a rebuild) by comparing each Task.
43
+ const isNode = (id) => before.fingerprints.has(id) || after.has(id);
44
+ const nodes = [...new Set([...before.fingerprints.keys(), ...after.keys()])].filter((id) => id !== "");
45
+ const tasks = to.objectsTouchedSince(before.revision) ??
46
+ // The same replica changed in place without its earlier revision: no state
47
+ // to compare with, so every task node is reported, never one missed.
48
+ (before.replica === to
49
+ ? nodes.filter((id) => to.task(id) !== undefined)
50
+ : nodes.filter((id) => JSON.stringify(before.replica.task(id) ?? null) !== JSON.stringify(to.task(id) ?? null)));
51
+ for (const id of tasks)
52
+ if (isNode(id))
53
+ ids.add(id);
54
+ return [...ids].sort();
55
+ }
56
+ export class SharedSectionsDataProfile {
57
+ dataProfile = SECTIONS_PROFILE_ID;
58
+ #replica;
59
+ /** Merged units: unit ID hex → change hash. */
60
+ #merged = new Map();
61
+ /** LFCP-accepted units waiting for Automerge dependencies, by unit ID hex. */
62
+ #pending = new Map();
63
+ /** LFCP-accepted units whose actor sequence a different change has (SOP §14.1), by unit ID hex. */
64
+ #held = new Map();
65
+ #listeners = new Set();
66
+ constructor(replica) {
67
+ this.#replica = replica;
68
+ }
69
+ /** The current replica (a rebuild replaces it). */
70
+ get replica() {
71
+ return this.#replica;
72
+ }
73
+ /** §5 nodes-changed for remote merges, rebuilds and recorded local changes. */
74
+ onNodesChanged(listener) {
75
+ this.#listeners.add(listener);
76
+ return () => this.#listeners.delete(listener);
77
+ }
78
+ #emit(nodeIds, origin) {
79
+ if (nodeIds.length === 0)
80
+ return;
81
+ const event = Object.freeze({ nodeIds, origin, modelRevision: this.#replica.revision() });
82
+ for (const l of this.#listeners)
83
+ l(event);
84
+ }
85
+ /** The codec of one unit, bound to its signer's section actor (§2). */
86
+ codecFor(unit) {
87
+ const actor = toHex(deriveSectionActorId(unit.resourceId, unit.actor));
88
+ const bound = (c) => checkChangeActor(c, actor);
89
+ return {
90
+ dataProfile: SECTIONS_PROFILE_ID,
91
+ encode: (change) => frameChange(bound(change).bytes),
92
+ decode: (plaintext) => bound(unframeChange(plaintext)),
93
+ };
94
+ }
95
+ /**
96
+ * SDK-SECTIONS-INTEGRATION-01 §3.1: the binding through which a sync
97
+ * client commits this Principal's batches (structurally the client's
98
+ * CommitBinding): `stage` validates the intents and changes a copy of the
99
+ * replica, splitting what one change cannot carry (§12); the staged
100
+ * operation adopts it, writes the checkpoint with the new units and
101
+ * records them once committed.
102
+ */
103
+ commitBinding(principal) {
104
+ const codec = this.codecFor({ resourceId: this.#replica.resource, actor: principal });
105
+ return {
106
+ codec,
107
+ section: () => this.#replica.sectionState(),
108
+ stage: (intents) => {
109
+ const staged = this.#replica.stage(intents);
110
+ if (staged === null)
111
+ return null;
112
+ const parts = staged.change.parts;
113
+ return {
114
+ values: parts.map((p) => checkChange(p.change)),
115
+ affectedNodeIds: staged.change.affectedNodeIds,
116
+ modelRevision: staged.change.modelRevision,
117
+ apply: () => staged.apply(),
118
+ revert: () => staged.revert(),
119
+ writes: (units) => [
120
+ {
121
+ op: "put-profile-checkpoint",
122
+ checkpoint: this.checkpoint(units.map((u, i) => ({
123
+ unitId: u.unitId,
124
+ ref: parts[i].hash,
125
+ }))),
126
+ },
127
+ ],
128
+ committed: (unitIds) => {
129
+ unitIds.forEach((unitId, i) => {
130
+ const part = parts[i];
131
+ if (part !== undefined)
132
+ this.#merged.set(toHex(unitId), { unitId, hash: part.hash });
133
+ });
134
+ this.#emit(staged.change.affectedNodeIds, "local");
135
+ },
136
+ };
137
+ },
138
+ };
139
+ }
140
+ /**
141
+ * Records which change one of this client's own units carries: the change
142
+ * is in the replica already (SectionReplica.stage, then apply). A later
143
+ * exclusion of the unit rebuilds without it, and the checkpoint keeps it.
144
+ */
145
+ recordLocal(unitId, change) {
146
+ if (!this.#replica.hasChange(change.hash))
147
+ throw new LfcpError("PROFILE_INVALID", "the change is not in this replica; record only local changes");
148
+ this.#merged.set(toHex(unitId), { unitId, hash: change.hash });
149
+ if (change.affectedNodeIds !== undefined)
150
+ this.#emit(change.affectedNodeIds, "local");
151
+ }
152
+ /** Merges one accepted unit's change; throws when the section admission refuses it. */
153
+ apply(unit, change) {
154
+ const key = toHex(unit.unitId);
155
+ const r = this.applyBatch([{ unit, value: change }]);
156
+ const own = r.rejected.find((x) => toHex(x.unitId) === key);
157
+ if (own !== undefined) {
158
+ const e = new LfcpError("PROFILE_INVALID", own.message);
159
+ e.diagnostic = own.diagnostic;
160
+ throw e;
161
+ }
162
+ const base = { objects: r.objects, diagnostics: r.diagnostics };
163
+ if (this.#held.has(key))
164
+ return Object.freeze({
165
+ merged: r.merged,
166
+ ...base,
167
+ held: `held: a different change has actor ${change.actor} sequence ${change.seq} (SOP §14.1)`,
168
+ });
169
+ if (!r.merged.some((id) => toHex(id) === key))
170
+ return Object.freeze({
171
+ merged: r.merged,
172
+ ...base,
173
+ pending: "waiting for the changes it depends on",
174
+ });
175
+ return Object.freeze({
176
+ merged: [unit.unitId, ...r.merged.filter((id) => toHex(id) !== key)],
177
+ ...base,
178
+ });
179
+ }
180
+ /**
181
+ * Merges many accepted units at once, with every unit still waiting: one
182
+ * admission and one engine call for a catch-up. A refused change rejects
183
+ * only its unit; the units that depend on it wait.
184
+ */
185
+ applyBatch(units) {
186
+ for (const { unit, value } of units)
187
+ this.#pending.set(toHex(unit.unitId), { unitId: unit.unitId, change: value });
188
+ return this.#offer(false);
189
+ }
190
+ /** Offers every waiting (and, with `retryHeld`, held) unit to the replica. */
191
+ #offer(retryHeld) {
192
+ if (retryHeld) {
193
+ for (const [key, b] of this.#held)
194
+ this.#pending.set(key, b);
195
+ this.#held.clear();
196
+ }
197
+ const offered = [...this.#pending.values()];
198
+ const unitsOf = new Map();
199
+ for (const b of offered)
200
+ unitsOf.set(b.change.hash, [...(unitsOf.get(b.change.hash) ?? []), b]);
201
+ const before = stateOf(this.#replica);
202
+ const out = this.#replica.receiveChanges(offered.map((b) => b.change.bytes));
203
+ const merged = [];
204
+ const settle = (hash, to) => {
205
+ for (const b of unitsOf.get(hash) ?? []) {
206
+ const key = toHex(b.unitId);
207
+ this.#pending.delete(key);
208
+ if (to === "merged") {
209
+ this.#merged.set(key, { unitId: b.unitId, hash });
210
+ merged.push(b.unitId);
211
+ }
212
+ else if (to === "held")
213
+ this.#held.set(key, b);
214
+ else
215
+ this.#pending.set(key, b);
216
+ }
217
+ };
218
+ for (const h of [...out.admitted, ...out.duplicates])
219
+ settle(h, "merged");
220
+ const rejected = [];
221
+ for (const r of out.refused) {
222
+ const hash = r.hash ?? offered[r.index]?.change.hash;
223
+ if (hash === undefined)
224
+ continue;
225
+ if (r.held)
226
+ settle(hash, "held");
227
+ else
228
+ for (const b of unitsOf.get(hash) ?? []) {
229
+ this.#pending.delete(toHex(b.unitId));
230
+ rejected.push({
231
+ unitId: b.unitId,
232
+ code: "PROFILE_INVALID",
233
+ diagnostic: r.diagnostic,
234
+ message: r.message,
235
+ });
236
+ }
237
+ }
238
+ const objects = merged.length > 0 ? nodesChanged(before, this.#replica) : [];
239
+ this.#emit(objects, "remote");
240
+ return Object.freeze({
241
+ merged: Object.freeze(merged),
242
+ pending: Object.freeze([...this.#pending.values()].map((b) => b.unitId)),
243
+ held: Object.freeze([...this.#held.values()].map((b) => b.unitId)),
244
+ rejected: Object.freeze(rejected),
245
+ objects: Object.freeze(objects),
246
+ diagnostics: Object.freeze([]),
247
+ });
248
+ }
249
+ /**
250
+ * G-EP7: the state without `unitIds`, rebuilt from the remaining changes.
251
+ * Merged units whose changes build on an excluded one wait again; held
252
+ * units are retried, since the rebuild may free their actor sequence.
253
+ */
254
+ exclude(unitIds) {
255
+ const hashes = [];
256
+ for (const id of unitIds) {
257
+ const key = toHex(id);
258
+ this.#pending.delete(key);
259
+ this.#held.delete(key);
260
+ const m = this.#merged.get(key);
261
+ if (m !== undefined) {
262
+ hashes.push(m.hash);
263
+ this.#merged.delete(key);
264
+ }
265
+ }
266
+ if (hashes.length === 0)
267
+ return Object.freeze({ objects: [], pending: [], released: [] });
268
+ const before = stateOf(this.#replica);
269
+ const { replica, unapplied } = this.#replica.rebuildWithout(hashes);
270
+ this.#replica = replica;
271
+ const waiting = new Set(unapplied);
272
+ const pending = [];
273
+ for (const [key, m] of [...this.#merged]) {
274
+ if (!waiting.has(m.hash))
275
+ continue;
276
+ this.#merged.delete(key);
277
+ pending.push(m.unitId);
278
+ }
279
+ const objects = nodesChanged(before, this.#replica);
280
+ this.#emit(objects, "rebuild");
281
+ const heldBefore = new Set(this.#held.keys());
282
+ const retried = heldBefore.size > 0 ? this.#offer(true) : undefined;
283
+ const released = (retried?.merged ?? []).filter((id) => heldBefore.has(toHex(id)));
284
+ return Object.freeze({
285
+ objects: Object.freeze(objects),
286
+ pending: Object.freeze(pending),
287
+ released: Object.freeze(released),
288
+ });
289
+ }
290
+ /** Whether this handler holds the unit's change (merged, recorded, waiting or held). */
291
+ has(unitId) {
292
+ const key = toHex(unitId);
293
+ return this.#merged.has(key) || this.#pending.has(key) || this.#held.has(key);
294
+ }
295
+ heldUnits() {
296
+ return [...this.#held.values()].map((b) => b.unitId);
297
+ }
298
+ pendingUnits() {
299
+ return [...this.#pending.values()].map((b) => b.unitId);
300
+ }
301
+ /** The Snapshot codec (SOP §13): the full save, framed. */
302
+ snapshotCodec() {
303
+ return {
304
+ dataProfile: SECTIONS_PROFILE_ID,
305
+ encode: (save) => frameSnapshot(save),
306
+ decode: (plaintext) => unframeSnapshot(plaintext),
307
+ };
308
+ }
309
+ /** The state to publish as a Snapshot: the replica's full save. */
310
+ snapshotState() {
311
+ return this.#replica.save();
312
+ }
313
+ /**
314
+ * Loads a received Snapshot's full save (SOP §13, §14; LFCP-02-028):
315
+ * every change it holds passes the section admission (§14.1), so a
316
+ * Snapshot holding a refused change is rejected and nothing changes;
317
+ * this replica's changes, local work the Snapshot lacks included, are
318
+ * kept on top. Buffered units whose dependencies it brings are merged.
319
+ * Returns the nodes that changed and the units merged from the buffer.
320
+ */
321
+ loadSnapshot(save) {
322
+ const before = stateOf(this.#replica);
323
+ this.#replica = this.#replica.mergeSave(save);
324
+ const merged = [...this.#offer(false).merged];
325
+ const nodeIds = nodesChanged(before, this.#replica);
326
+ this.#emit(nodeIds, "remote");
327
+ return Object.freeze({ nodeIds: Object.freeze(nodeIds), merged: Object.freeze(merged) });
328
+ }
329
+ /** The binding through which a sync client loads and publishes Snapshots. */
330
+ snapshotBinding() {
331
+ return {
332
+ codec: this.snapshotCodec(),
333
+ load: (save) => {
334
+ this.loadSnapshot(save);
335
+ },
336
+ current: () => this.snapshotState(),
337
+ };
338
+ }
339
+ /** Forgets the whole state, keeping the §9 sequence (SNAP-EP). */
340
+ reset() {
341
+ const before = stateOf(this.#replica);
342
+ this.#replica = this.#replica.emptied();
343
+ this.#merged.clear();
344
+ this.#pending.clear();
345
+ this.#held.clear();
346
+ this.#emit(nodesChanged(before, this.#replica), "rebuild");
347
+ }
348
+ /**
349
+ * The state to persist: the full save, this actor's sequence and each
350
+ * merged unit's change, with `local`, this client's units committed in
351
+ * the same transaction as the checkpoint.
352
+ */
353
+ checkpoint(local = []) {
354
+ const units = new Map([...this.#merged.values()].map((m) => [toHex(m.unitId), { unitId: m.unitId, ref: m.hash }]));
355
+ // Own units being committed with this checkpoint (recordLocal once committed).
356
+ for (const u of local)
357
+ units.set(toHex(u.unitId), { unitId: u.unitId, ref: u.ref });
358
+ return Object.freeze({
359
+ resourceId: this.#replica.resource,
360
+ dataProfile: SECTIONS_PROFILE_ID,
361
+ state: this.#replica.save(),
362
+ actorSeq: this.#replica.actorSeq,
363
+ units: Object.freeze([...units.values()]
364
+ .map((m) => Object.freeze({ unitId: m.unitId, ref: m.ref }))
365
+ .sort((a, b) => (toHex(a.unitId) < toHex(b.unitId) ? -1 : 1))),
366
+ });
367
+ }
368
+ /** The handler of a persisted checkpoint; its replica writes nothing below the checkpoint's sequence (§9). */
369
+ static restore(checkpoint, options) {
370
+ if (checkpoint.dataProfile !== SECTIONS_PROFILE_ID)
371
+ throw new LfcpError("DATA_PROFILE_MISMATCH", `a ${checkpoint.dataProfile} checkpoint is not ${SECTIONS_PROFILE_ID}`);
372
+ // This device's own persisted state: not held to the Snapshot limits (SOP §13.1).
373
+ const replica = SectionReplica.fromSave(checkpoint.state, { ...options, minSeq: checkpoint.actorSeq }, "local-state");
374
+ const profile = new SharedSectionsDataProfile(replica);
375
+ for (const u of checkpoint.units) {
376
+ if (!replica.hasChange(u.ref))
377
+ throw new LfcpError("PROFILE_INVALID", `the checkpoint names change ${u.ref}, which its state lacks`);
378
+ profile.#merged.set(toHex(u.unitId), { unitId: u.unitId, hash: u.ref });
379
+ }
380
+ return profile;
381
+ }
382
+ }
383
+ //# sourceMappingURL=data-profile.js.map
@@ -0,0 +1,41 @@
1
+ import { PROFILE_ID } from "../values.js";
2
+ import { SECTIONS_PROFILE_ID } from "./values.js";
3
+ /**
4
+ * Profile dispatch (SHARED-SECTIONS-PROFILE-01 §1, §17): a Resource's
5
+ * Genesis names its Data Profile, and that profile alone selects the model
6
+ * that reads and writes it. A Resource created as Shared Objects stays one;
7
+ * nothing is inferred from the document's contents, and a Task reference
8
+ * (`#task:`) is read with the model of the Resource it names, not by its
9
+ * textual shape.
10
+ */
11
+ /** The models this package implements, by Data Profile. */
12
+ export declare const SUPPORTED_DATA_PROFILES: readonly ["org.openlfcp.shared-objects.v1", "org.openlfcp.shared-sections.v1"];
13
+ export type ProfileModel = {
14
+ readonly kind: "shared-objects";
15
+ readonly dataProfile: typeof PROFILE_ID;
16
+ } | {
17
+ readonly kind: "shared-sections";
18
+ readonly dataProfile: typeof SECTIONS_PROFILE_ID;
19
+ }
20
+ /**
21
+ * LFCP-WIRE-01 §62: no model here reads this profile. The Resource is
22
+ * neither interpreted nor written; nothing is created or changed.
23
+ */
24
+ | {
25
+ readonly kind: "profile-unsupported";
26
+ readonly code: "PROFILE_UNSUPPORTED";
27
+ readonly dataProfile: string;
28
+ };
29
+ /**
30
+ * The model of a Resource whose validated Genesis (its Control Chain state)
31
+ * names `dataProfile`. Exact match only: no prefix, version or case folding.
32
+ */
33
+ export declare function profileModel(dataProfile: string): ProfileModel;
34
+ /**
35
+ * §17: the model that reads a Task reference into a Resource, chosen by that
36
+ * Resource's Data Profile. A standalone Task in a section Resource is a
37
+ * section Task; the same reference shape in a Shared Objects Resource is a
38
+ * Shared Objects Task.
39
+ */
40
+ export declare function taskRefModel(resourceDataProfile: string): ProfileModel;
41
+ //# sourceMappingURL=dispatch.d.ts.map
@@ -0,0 +1,33 @@
1
+ import { PROFILE_ID } from "../values.js";
2
+ import { SECTIONS_PROFILE_ID } from "./values.js";
3
+ /**
4
+ * Profile dispatch (SHARED-SECTIONS-PROFILE-01 §1, §17): a Resource's
5
+ * Genesis names its Data Profile, and that profile alone selects the model
6
+ * that reads and writes it. A Resource created as Shared Objects stays one;
7
+ * nothing is inferred from the document's contents, and a Task reference
8
+ * (`#task:`) is read with the model of the Resource it names, not by its
9
+ * textual shape.
10
+ */
11
+ /** The models this package implements, by Data Profile. */
12
+ export const SUPPORTED_DATA_PROFILES = Object.freeze([PROFILE_ID, SECTIONS_PROFILE_ID]);
13
+ /**
14
+ * The model of a Resource whose validated Genesis (its Control Chain state)
15
+ * names `dataProfile`. Exact match only: no prefix, version or case folding.
16
+ */
17
+ export function profileModel(dataProfile) {
18
+ if (dataProfile === PROFILE_ID)
19
+ return Object.freeze({ kind: "shared-objects", dataProfile });
20
+ if (dataProfile === SECTIONS_PROFILE_ID)
21
+ return Object.freeze({ kind: "shared-sections", dataProfile });
22
+ return Object.freeze({ kind: "profile-unsupported", code: "PROFILE_UNSUPPORTED", dataProfile });
23
+ }
24
+ /**
25
+ * §17: the model that reads a Task reference into a Resource, chosen by that
26
+ * Resource's Data Profile. A standalone Task in a section Resource is a
27
+ * section Task; the same reference shape in a Shared Objects Resource is a
28
+ * Shared Objects Task.
29
+ */
30
+ export function taskRefModel(resourceDataProfile) {
31
+ return profileModel(resourceDataProfile);
32
+ }
33
+ //# sourceMappingURL=dispatch.js.map
@@ -0,0 +1,54 @@
1
+ import { type SnapshotLimits } from "../admission/limits.js";
2
+ import type { Json } from "../validate.js";
3
+ import { type SectionValidation } from "./schema.js";
4
+ import { type SectionTree } from "./tree.js";
5
+ /**
6
+ * A loaded SHARED-SECTIONS-PROFILE-01 document, read-only: what a Resource
7
+ * whose Genesis names org.openlfcp.shared-sections.v1 is read as (§1,
8
+ * profileModel). Loading checks the inherited SOP framing and limits before
9
+ * the engine sees any byte (§2, §16.1).
10
+ *
11
+ * Not yet here: the section admission rules A1–A5 (§14.1), the effective
12
+ * tree (§7) and the intents (§11), which later tasks add. fromChanges
13
+ * therefore loads changes a receiver has already admitted, such as its own
14
+ * persisted history; it is not a receive path.
15
+ */
16
+ /** An Automerge datatype as the backend reports it: "str", "text", "map", "list", "boolean", "int", … */
17
+ export type ValueType = string;
18
+ export declare class SectionDocument {
19
+ #private;
20
+ private constructor();
21
+ /**
22
+ * Loads an Automerge full save: a Snapshot image checked against `limits`
23
+ * (SOP §13.1, at least the floor), or this device's own persisted state
24
+ * ("local-state"). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES.
25
+ */
26
+ static fromSave(save: Uint8Array, limits?: SnapshotLimits | "local-state"): SectionDocument;
27
+ /**
28
+ * The document of exactly an admitted change set, in any order. Each
29
+ * change passes the SOP §11 and §11.1 byte checks first (throws
30
+ * PROFILE_INVALID / INVALID_AUTOMERGE_BYTES). Changes whose dependencies
31
+ * are not in the set are returned by hash as unapplied.
32
+ */
33
+ static fromChanges(changes: Iterable<Uint8Array>): {
34
+ readonly document: SectionDocument;
35
+ readonly unapplied: readonly string[];
36
+ };
37
+ /** The current heads, sorted. */
38
+ heads(): string[];
39
+ /** The full save of the document. */
40
+ save(): Uint8Array;
41
+ /** §3, §4, §14.2: the schema validation of the current state. */
42
+ validate(): SectionValidation;
43
+ /** §7, §9, §14.3: the effective tree, hidden nodes and structural facts. */
44
+ tree(): SectionTree;
45
+ /**
46
+ * The datatype of every concurrent value at `path` from the root (map
47
+ * keys and list indexes), none when absent. Tells a scalar string ("str")
48
+ * from collaborative Text ("text"), which a JSON reading cannot (§2).
49
+ */
50
+ valueTypes(path: readonly (string | number)[]): ValueType[];
51
+ /** The current state as JSON: scalar strings and Text both read as strings. */
52
+ toJSON(): Json;
53
+ }
54
+ //# sourceMappingURL=document.d.ts.map
@@ -0,0 +1,101 @@
1
+ import * as A from "@automerge/automerge";
2
+ import { LfcpError } from "@openlfcp/core";
3
+ import { checkChange, checkSaveHeader } from "../admission/framing.js";
4
+ import { checkChangeExpansion, checkSnapshotExpansion, SNAPSHOT_LIMITS_FLOOR, } from "../admission/limits.js";
5
+ import { ProfileInvalidError } from "../profile-invalid.js";
6
+ import { validateSection } from "./schema.js";
7
+ import { deriveTree } from "./tree.js";
8
+ export class SectionDocument {
9
+ #doc;
10
+ constructor(doc) {
11
+ this.#doc = doc;
12
+ }
13
+ /**
14
+ * Loads an Automerge full save: a Snapshot image checked against `limits`
15
+ * (SOP §13.1, at least the floor), or this device's own persisted state
16
+ * ("local-state"). Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES.
17
+ */
18
+ static fromSave(save, limits = SNAPSHOT_LIMITS_FLOOR) {
19
+ checkSaveHeader(save);
20
+ if (limits !== "local-state")
21
+ checkSnapshotExpansion(save, limits);
22
+ try {
23
+ return new SectionDocument(A.load(save));
24
+ }
25
+ catch (e) {
26
+ if (e instanceof LfcpError)
27
+ throw e;
28
+ throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `invalid Automerge save (SOP §13): ${e.message}`);
29
+ }
30
+ }
31
+ /**
32
+ * The document of exactly an admitted change set, in any order. Each
33
+ * change passes the SOP §11 and §11.1 byte checks first (throws
34
+ * PROFILE_INVALID / INVALID_AUTOMERGE_BYTES). Changes whose dependencies
35
+ * are not in the set are returned by hash as unapplied.
36
+ */
37
+ static fromChanges(changes) {
38
+ const checked = [...changes].map((bytes) => {
39
+ checkChangeExpansion(bytes);
40
+ return checkChange(bytes);
41
+ });
42
+ const [doc] = A.applyChanges(A.init(), checked.map((c) => c.bytes));
43
+ const unapplied = checked.filter((c) => !A.hasHeads(doc, [c.hash])).map((c) => c.hash);
44
+ return Object.freeze({
45
+ document: new SectionDocument(doc),
46
+ unapplied: Object.freeze(unapplied),
47
+ });
48
+ }
49
+ /** The current heads, sorted. */
50
+ heads() {
51
+ return [...A.getHeads(this.#doc)].sort();
52
+ }
53
+ /** The full save of the document. */
54
+ save() {
55
+ return A.save(this.#doc);
56
+ }
57
+ /** §3, §4, §14.2: the schema validation of the current state. */
58
+ validate() {
59
+ return validateSection(this.#doc);
60
+ }
61
+ /** §7, §9, §14.3: the effective tree, hidden nodes and structural facts. */
62
+ tree() {
63
+ return deriveTree(this.#doc);
64
+ }
65
+ /**
66
+ * The datatype of every concurrent value at `path` from the root (map
67
+ * keys and list indexes), none when absent. Tells a scalar string ("str")
68
+ * from collaborative Text ("text"), which a JSON reading cannot (§2).
69
+ */
70
+ valueTypes(path) {
71
+ const backend = A.getBackend(this.#doc);
72
+ const heads = A.getHeads(this.#doc);
73
+ let obj = "_root";
74
+ for (const [i, prop] of path.entries()) {
75
+ const values = backend.getAll(obj, prop, heads);
76
+ if (i === path.length - 1)
77
+ return values.map((v) => v[0]);
78
+ const only = values.length === 1 ? values[0] : undefined;
79
+ if (only === undefined || !["map", "list", "text"].includes(only[0]))
80
+ return [];
81
+ obj = only[1];
82
+ }
83
+ return ["map"];
84
+ }
85
+ /** The current state as JSON: scalar strings and Text both read as strings. */
86
+ toJSON() {
87
+ return plain(this.#doc);
88
+ }
89
+ }
90
+ function plain(value) {
91
+ if (A.isImmutableString(value))
92
+ return value.toString();
93
+ if (value instanceof A.Counter)
94
+ return value.value;
95
+ if (Array.isArray(value))
96
+ return value.map(plain);
97
+ if (value !== null && typeof value === "object" && !(value instanceof Uint8Array))
98
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, plain(v)]));
99
+ return value;
100
+ }
101
+ //# sourceMappingURL=document.js.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SHARED-SECTIONS-PROFILE-01 (org.openlfcp.shared-sections.v1, Working
3
+ * Draft 0.3): profile dispatch, the profile's values and actor binding,
4
+ * schema validation of a section document (LFCP-02-011), and its writer
5
+ * with the section and creation intents (LFCP-02-012 to 016), and the
6
+ * admission of received changes (LFCP-02-017).
7
+ * `@openlfcp/shared-objects/sections`. Its admission (SOP §§7–18) is the
8
+ * shared `@openlfcp/shared-objects/admission` with SECTIONS_ACTOR_DOMAIN.
9
+ */
10
+ export { ADMISSION_ORDER, type AdmissionDiagnostic, SectionAdmissionError, } from "./admission.js";
11
+ export { type SectionsApplyResult, type SectionsBatchResult, type SectionsCheckpoint, type SectionsCodec, type SectionsCommitBinding, type SectionsExcludeResult, type SectionsNodesChanged, type SectionsSnapshotBinding, type SectionsSnapshotCodec, type SectionsStagedOperation, type SectionsUnit, SharedSectionsDataProfile, } from "./data-profile.js";
12
+ export { type ProfileModel, profileModel, SUPPORTED_DATA_PROFILES, taskRefModel, } from "./dispatch.js";
13
+ export { SectionDocument, type ValueType } from "./document.js";
14
+ export { AUTHORING_BUDGET, type SectionIntent, type SectionIntentCode, SectionIntentError, type SectionLocalChange, type SectionReceiveResult, type SectionRefusal, SectionReplica, type SectionReplicaOptions, type SectionSnapshot, type SectionSnapshotNode, type SectionUnit, type StagedSectionChange, type TextEdit, } from "./replica.js";
15
+ export { SECTION_DIAGNOSTIC_ORDER, type SectionDiagnostic, type SectionProblem, type SectionValidation, validateSection, } from "./schema.js";
16
+ export { deriveTree, type SectionTree, type StructuralFact, type TreeEntry, } from "./tree.js";
17
+ export { deriveSectionActorId, LIST_STYLES, type ListStyle, NODE_KINDS, type NodeKind, PARENT_KINDS, ROOT_MAPS, SECTIONS_ACTOR_DOMAIN, SECTIONS_PROFILE_ID, TEXT_KINDS, } from "./values.js";
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,18 @@
1
+ /**
2
+ * SHARED-SECTIONS-PROFILE-01 (org.openlfcp.shared-sections.v1, Working
3
+ * Draft 0.3): profile dispatch, the profile's values and actor binding,
4
+ * schema validation of a section document (LFCP-02-011), and its writer
5
+ * with the section and creation intents (LFCP-02-012 to 016), and the
6
+ * admission of received changes (LFCP-02-017).
7
+ * `@openlfcp/shared-objects/sections`. Its admission (SOP §§7–18) is the
8
+ * shared `@openlfcp/shared-objects/admission` with SECTIONS_ACTOR_DOMAIN.
9
+ */
10
+ export { ADMISSION_ORDER, SectionAdmissionError, } from "./admission.js";
11
+ export { SharedSectionsDataProfile, } from "./data-profile.js";
12
+ export { profileModel, SUPPORTED_DATA_PROFILES, taskRefModel, } from "./dispatch.js";
13
+ export { SectionDocument } from "./document.js";
14
+ export { AUTHORING_BUDGET, SectionIntentError, SectionReplica, } from "./replica.js";
15
+ export { SECTION_DIAGNOSTIC_ORDER, validateSection, } from "./schema.js";
16
+ export { deriveTree, } from "./tree.js";
17
+ export { deriveSectionActorId, LIST_STYLES, NODE_KINDS, PARENT_KINDS, ROOT_MAPS, SECTIONS_ACTOR_DOMAIN, SECTIONS_PROFILE_ID, TEXT_KINDS, } from "./values.js";
18
+ //# sourceMappingURL=index.js.map