@openlfcp/shared-objects 0.1.5 → 0.2.0-beta.1

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} +2 -0
  5. package/dist/{canonical.js → admission/canonical.js} +9 -2
  6. package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +32 -1
  7. package/dist/admission/framing.js +231 -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 +133 -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,412 @@
1
+ import { type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type SnapshotLimits } from "../admission/limits.js";
3
+ import { type ReplicaIntent, type TaskView } from "../replica.js";
4
+ import type { Task } from "../task.js";
5
+ import type { Json } from "../validate.js";
6
+ import { type SectionValidation } from "./schema.js";
7
+ import { type SectionTree } from "./tree.js";
8
+ import { type ListStyle } from "./values.js";
9
+ /**
10
+ * The writer of a SHARED-SECTIONS-PROFILE-01 document (LFCP-02-012): the
11
+ * section and creation intents of §11, and the SOP Task intents on the
12
+ * section's Tasks (§2), committed as batches.
13
+ *
14
+ * - One batch is one Automerge change (§12), with time 0 and the actor of
15
+ * §2 for (Resource, Principal); the batch is validated as a whole, and
16
+ * one refused intent refuses it with nothing written
17
+ * (SDK-SECTIONS-INTEGRATION-01 §3.1).
18
+ * - The caller allocates every node and Task ID and the SDK uses them;
19
+ * PlacementIds are the SDK's unless the intent names one.
20
+ * - A node, its placement, the placement's entry in the parent's children
21
+ * list and the node's `placement` register are written in one change
22
+ * (§4.3); a Task node and its Task are created together (§4.2).
23
+ * - Writes touch single properties; unknown fields and extensions survive.
24
+ *
25
+ * Moves, lifecycle, Text edits and the effective tree come with LFCP-02-013
26
+ * to 016; batches over the §16.2 budgets are refused until 016 splits them.
27
+ */
28
+ /** Why a batch was refused before anything was written (SDK-SECTIONS-INTEGRATION-01 §3.6). */
29
+ export type SectionIntentCode =
30
+ /** The document is not a valid section (§14.2 section-level problems). */
31
+ "SECTION_INVALID"
32
+ /** section.create on a document that already has a root, or another intent before one. */
33
+ | "SECTION_EXISTS"
34
+ /** The section has no `ready` and this writer is not its creator (§12.1). */
35
+ | "SECTION_IMPORTING"
36
+ /** A new ID is not a canonical UUIDv7 or is already used in the Resource (§3). */
37
+ | "ID_IN_USE"
38
+ /** The parent is missing, invalid, deleted, in conflict or cannot hold content (§4.2, §6). */
39
+ | "INVALID_PARENT"
40
+ /** `after` is not a visible child of the parent, or is the moved node (§6). */
41
+ | "INVALID_PREDECESSOR"
42
+ /** The node is invalid, collides, or has concurrent placements: resolve it first (§7, §8). */
43
+ | "NODE_IN_CONFLICT"
44
+ /** The intent names a node the section does not have. */
45
+ | "UNKNOWN_NODE"
46
+ /** A value outside its domain: an ID, a list style, a timestamp, a string (§3, §4). */
47
+ | "INVALID_INTENT"
48
+ /** The batch is over an authoring budget of §16.2. */
49
+ | "OVER_BUDGET"
50
+ /** A Text position names a base revision the SDK cannot rebase onto the current Text (§10). */
51
+ | "STALE_BASE";
52
+ /** A batch refused before commit, with the intent it concerns (SDK-SECTIONS-INTEGRATION-01 §3.6). */
53
+ export declare class SectionIntentError extends Error {
54
+ readonly code: SectionIntentCode;
55
+ /** The index of the refused intent in its batch. */
56
+ readonly intentIndex: number | undefined;
57
+ /** The node the refusal concerns, when there is one. */
58
+ readonly nodeId: string | undefined;
59
+ constructor(code: SectionIntentCode, message: string, intentIndex?: number, nodeId?: string);
60
+ }
61
+ /** Where a new node goes: under `parent`, right after the visible sibling `after` (null: first). */
62
+ interface Position {
63
+ /** The section ID, or a task or item node. */
64
+ readonly parent: string;
65
+ /** The preceding visible sibling, a node ID; null puts the node first (§6). */
66
+ readonly after: string | null;
67
+ /** The new PlacementId; generated when absent. */
68
+ readonly placementId?: string;
69
+ }
70
+ export type SectionIntent = {
71
+ readonly intent: "section.create";
72
+ readonly sectionId: string;
73
+ readonly title: string;
74
+ readonly createdBy: PrincipalId;
75
+ readonly createdAt?: string;
76
+ /** §12.1: false while an import continues in later changes; the default writes `ready`. */
77
+ readonly ready?: boolean;
78
+ } | {
79
+ readonly intent: "section.set_title";
80
+ readonly title: string;
81
+ }
82
+ /** §12.1: the last change of an import writes `ready`; the creator only, once. */
83
+ | {
84
+ readonly intent: "section.mark_ready";
85
+ } | ({
86
+ readonly intent: "task.create_in_section";
87
+ /** The Task, from createTask (§53); its ID is the node's ID. */
88
+ readonly task: Task;
89
+ readonly listStyle?: ListStyle;
90
+ } & Position) | ({
91
+ readonly intent: "paragraph.create" | "item.create" | "raw.create";
92
+ readonly id: string;
93
+ readonly createdBy: PrincipalId;
94
+ readonly createdAt?: string;
95
+ /** Markdown inline source (§4.2); collaborative Text. */
96
+ readonly text: string;
97
+ /** item.create only. */
98
+ readonly listStyle?: ListStyle;
99
+ } & Position)
100
+ /** §5, §6: a fresh placement under `parent`; the node keeps its identity and subtree. */
101
+ | ({
102
+ readonly intent: "node.move";
103
+ readonly id: string;
104
+ } & Position)
105
+ /**
106
+ * §8: a fresh placement written to a node's register, superseding every
107
+ * placement observed at this causal point, conflicted or not.
108
+ */
109
+ | ({
110
+ readonly intent: "node.resolve_placement";
111
+ readonly id: string;
112
+ } & Position)
113
+ /**
114
+ * §8: relocates several nodes atomically, as for a parent cycle; each
115
+ * move is checked against the moves before it, so the result is acyclic.
116
+ */
117
+ | {
118
+ readonly intent: "structure.resolve";
119
+ readonly moves: readonly ({
120
+ readonly id: string;
121
+ } & Position)[];
122
+ }
123
+ /**
124
+ * §9: the lifecycle of a node: a Task node's Task, any other node itself.
125
+ * Always a fresh causal write, also when the value is already visible;
126
+ * descendants are not rewritten.
127
+ */
128
+ | {
129
+ readonly intent: "node.delete" | "node.restore";
130
+ readonly id: string;
131
+ }
132
+ /**
133
+ * §10: edits of a node's existing Text, in Unicode scalar positions of the
134
+ * Text at `base` (a modelRevision): sorted, not overlapping, each against
135
+ * the base text as CodeMirror changes are. They are rebased onto the
136
+ * current Text; a deleted range that changed since `base` is STALE_BASE.
137
+ */
138
+ | {
139
+ readonly intent: "text.edit";
140
+ readonly id: string;
141
+ readonly base: string;
142
+ readonly edits: readonly TextEdit[];
143
+ }
144
+ /**
145
+ * §10: the node keeps the prefix before `at` (a scalar position at
146
+ * `base`); a new node of the same kind, `newId`, gets the suffix right
147
+ * after it under the same parent. An item's children stay with it.
148
+ */
149
+ | ({
150
+ readonly intent: "paragraph.split" | "item.split";
151
+ readonly id: string;
152
+ readonly base: string;
153
+ readonly at: number;
154
+ readonly newId: string;
155
+ readonly createdBy: PrincipalId;
156
+ } & Pick<Position, "placementId">)
157
+ /**
158
+ * §10: two adjacent paragraphs, or items of one list style, without
159
+ * children: `id` gets `separator` (default LF) and `second`'s current
160
+ * text, and `second` is deleted with its Text history kept.
161
+ */
162
+ | {
163
+ readonly intent: "node.join";
164
+ readonly id: string;
165
+ readonly second: string;
166
+ readonly separator?: string;
167
+ }
168
+ /** §4.2: ordered or bullet list membership, on task and item nodes. */
169
+ | {
170
+ readonly intent: "node.set_list_style";
171
+ readonly id: string;
172
+ readonly listStyle: ListStyle;
173
+ }
174
+ /** A SHARED-OBJECTS-PROFILE-01 Task intent on a Task of this section (§2). */
175
+ | Exclude<ReplicaIntent, {
176
+ intent: "task.create";
177
+ }>;
178
+ /** One Text edit in Unicode scalar positions (§10). */
179
+ export interface TextEdit {
180
+ readonly index: number;
181
+ readonly deleteCount: number;
182
+ readonly insert: string;
183
+ }
184
+ /** §16.2: what one change may carry. */
185
+ export declare const AUTHORING_BUDGET: Readonly<{
186
+ textOperations: 8192;
187
+ createdNodes: 256;
188
+ }>;
189
+ /** One change of a batch, as its Data Unit carries it. */
190
+ export interface SectionChangePart {
191
+ readonly change: Uint8Array;
192
+ /** The §11 Data Unit plaintext [1, change]. */
193
+ readonly plaintext: Uint8Array;
194
+ readonly hash: string;
195
+ readonly seq: number;
196
+ }
197
+ /**
198
+ * A committed batch. A batch over the §16.2 budgets is several changes
199
+ * (§12, §12.3): `parts`, in order, one Data Unit each; `change`, `hash`,
200
+ * `seq` and `plaintext` name the last of them (the only one, usually).
201
+ */
202
+ export interface SectionLocalChange {
203
+ /** The intents of the batch, by name. */
204
+ readonly intents: readonly string[];
205
+ /** Every change of the batch, in order. */
206
+ readonly parts: readonly SectionChangePart[];
207
+ readonly change: Uint8Array;
208
+ /** The §11 Data Unit plaintext [1, change]. */
209
+ readonly plaintext: Uint8Array;
210
+ readonly hash: string;
211
+ readonly seq: number;
212
+ /** Nodes the batch creates or writes; the section ID when it writes the section. */
213
+ readonly affectedNodeIds: readonly string[];
214
+ /** The document heads after the change, sorted, as one string (SDK-SECTIONS-INTEGRATION-01 §3.2). */
215
+ readonly modelRevision: string;
216
+ }
217
+ /** A received change with the Principal that signed its Data Unit (§2). */
218
+ export interface SectionUnit {
219
+ readonly bytes: Uint8Array;
220
+ readonly signer?: PrincipalId;
221
+ }
222
+ /** A received change that was not merged. */
223
+ export interface SectionRefusal {
224
+ /** Its position in the received list. */
225
+ readonly index: number;
226
+ /**
227
+ * Its change hash. A change the SOP checks refused is never decoded: its
228
+ * hash is read from its bytes (SHA-256 of the chunk from the type byte),
229
+ * and is undefined only when the bytes are not one readable change chunk.
230
+ */
231
+ readonly hash: string | undefined;
232
+ /** The §14.1 diagnostic (SOP §74.1 for the inherited checks), or ACTOR_EQUIVOCATION when held. */
233
+ readonly diagnostic: string;
234
+ /** Held (SOP §14.1, POST-001): its actor and sequence belong to another change; retried after a rebuild. */
235
+ readonly held: boolean;
236
+ readonly message: string;
237
+ }
238
+ export interface SectionReceiveResult {
239
+ /** Merged now, in causal order. */
240
+ readonly admitted: readonly string[];
241
+ /** Already in the document. */
242
+ readonly duplicates: readonly string[];
243
+ /** Missing a dependency: not received yet, or refused, or waiting itself. */
244
+ readonly waiting: readonly string[];
245
+ readonly refused: readonly SectionRefusal[];
246
+ }
247
+ /** One node of a snapshot (SectionReplica.snapshot). */
248
+ export interface SectionSnapshotNode {
249
+ readonly kind: string;
250
+ /** The section ID or the parent node of the selected placement; undefined when it has none. */
251
+ readonly parent: string | undefined;
252
+ /** Paragraph, item and raw nodes: the Text, with LF line breaks. */
253
+ readonly text?: string;
254
+ readonly listStyle?: ListStyle;
255
+ /** Task nodes: the Task's ID (the node's own). */
256
+ readonly taskId?: string;
257
+ /** Deleted itself (a Task node: its Task); delete projected lines only for this. */
258
+ readonly deleted: boolean;
259
+ /** Deleted itself or under a deleted ancestor: not shown. */
260
+ readonly hidden: boolean;
261
+ }
262
+ /** One synchronous read of a section on one revision (SectionReplica.snapshot). */
263
+ export interface SectionSnapshot {
264
+ /** The document heads, sorted, as one string: a receipt's modelRevision, text.edit's base. */
265
+ readonly revision: string;
266
+ readonly classification: SectionTree["classification"];
267
+ readonly title: {
268
+ readonly value: string | undefined;
269
+ readonly conflicts: readonly string[];
270
+ };
271
+ /** Every node but colliding ones, by ID. */
272
+ readonly nodes: Readonly<Record<string, SectionSnapshotNode>>;
273
+ /** The visible nodes in projection order (preorder), with parent and depth. */
274
+ readonly order: SectionTree["tree"];
275
+ readonly problems: {
276
+ readonly recovery: SectionTree["recovery"];
277
+ readonly invalid: SectionTree["invalid"];
278
+ readonly collisions: SectionTree["collisions"];
279
+ readonly retainedConcurrentEdits: SectionTree["retainedConcurrentEdits"];
280
+ readonly scalarConflicts: SectionTree["scalarConflicts"];
281
+ };
282
+ }
283
+ /** A batch validated and changed on a copy (SectionReplica.stage): applied only on request. */
284
+ export interface StagedSectionChange {
285
+ readonly change: SectionLocalChange;
286
+ /** Adopts the change; throws if the replica moved on since staging. Idempotent. */
287
+ apply(): void;
288
+ /**
289
+ * Undoes apply() while nothing came after it: the replica holds the
290
+ * document it had before (its storage commit failed, or the operation
291
+ * had committed already). Idempotent.
292
+ */
293
+ revert(): void;
294
+ }
295
+ export interface SectionReplicaOptions {
296
+ readonly resource: ResourceId;
297
+ readonly principal: PrincipalId;
298
+ /**
299
+ * SOP §9: the actor sequence this device already used (a restored
300
+ * checkpoint's). Below it the replica writes nothing: its next change
301
+ * would reuse a sequence number another change of this actor has.
302
+ */
303
+ readonly minSeq?: number;
304
+ }
305
+ export declare class SectionReplica {
306
+ #private;
307
+ readonly resource: ResourceId;
308
+ /** The §2 Automerge actor ID. */
309
+ readonly actorId: Uint8Array;
310
+ private constructor();
311
+ /** This actor's latest change sequence in the document (0 when it has none). */
312
+ get actorSeq(): number;
313
+ /** SOP §9: whether this replica may write (it is not behind its actor's used sequence). */
314
+ get writable(): boolean;
315
+ /**
316
+ * §12.1: "ready" once the section's `ready` is written, "importing"
317
+ * before, undefined while the document has no section. Reads one
318
+ * register; validate() tells whether the section is valid.
319
+ */
320
+ sectionState(): "ready" | "importing" | undefined;
321
+ /** Whether the document holds the change with this hex hash. */
322
+ hasChange(hash: string): boolean;
323
+ /**
324
+ * SOP §14.1 (G-EP7): the replica rebuilt from its changes minus `exclude`
325
+ * (change hashes); changes that depend on an excluded one are unapplied
326
+ * too. An excluded change of this actor is not lost state (§9): the
327
+ * rebuilt replica writes on from its own sequence.
328
+ */
329
+ rebuildWithout(exclude: Iterable<string>): {
330
+ readonly replica: SectionReplica;
331
+ readonly unapplied: readonly string[];
332
+ };
333
+ /**
334
+ * This replica merged with a received Snapshot's full save (SOP §13,
335
+ * §14): every change the save holds is admitted as if received one by
336
+ * one (the SOP checks and A1–A5, §14.1), on an empty replica of this
337
+ * actor, so a Snapshot that holds a change admission refuses is
338
+ * rejected; then this replica's own changes follow, so local work the
339
+ * Snapshot lacks is kept. The §9 sequence carries over. Throws
340
+ * PROFILE_INVALID (with the refused change's diagnostic, or
341
+ * INVALID_AUTOMERGE_BYTES) and keeps this replica unchanged.
342
+ */
343
+ mergeSave(save: Uint8Array, limits?: SnapshotLimits): SectionReplica;
344
+ /** An empty replica of the same actor that keeps the §9 sequence (SNAP-EP reset). */
345
+ emptied(): SectionReplica;
346
+ /** An empty document, for section.create or for receiving. */
347
+ static empty(opts: SectionReplicaOptions): SectionReplica;
348
+ /** Loads a full save; SOP §13.1 limits unless it is this device's own state. */
349
+ static fromSave(save: Uint8Array, opts: SectionReplicaOptions, limits?: SnapshotLimits | "local-state"): SectionReplica;
350
+ /** The replica of an admitted change set; changes missing a dependency are returned unapplied. */
351
+ static fromChanges(changes: Iterable<Uint8Array>, opts: SectionReplicaOptions): {
352
+ readonly replica: SectionReplica;
353
+ readonly unapplied: readonly string[];
354
+ };
355
+ /**
356
+ * Receives changes others wrote, as LFCP accepted them (§14.1): each
357
+ * passes the inherited SOP checks (framing, §11.1 expansion, the signer's
358
+ * actor when `signer` is given, the sequence check, holding a taken
359
+ * actor sequence) and the section rules A1–A5 and §12.1, against its
360
+ * causal history, before the engine applies any. A refused change is
361
+ * never merged; the changes that depend on it wait.
362
+ */
363
+ receiveChanges(items: readonly (Uint8Array | SectionUnit)[]): SectionReceiveResult;
364
+ /**
365
+ * Everything an editor projects, in one synchronous read of one revision:
366
+ * the title (and its concurrent values), each node, the visible order,
367
+ * and the problems to show. `revision` is the modelRevision of a commit
368
+ * receipt and the `base` of text.edit.
369
+ */
370
+ snapshot(): SectionSnapshot;
371
+ /**
372
+ * The Task of a task node (§4.2: stored under `/objects/<id>`, the node's
373
+ * ID) with its conflict metadata, in the form SharedObjectsReplica.task
374
+ * gives (SOP §99); undefined if there is no object or it is not a Task.
375
+ */
376
+ task(id: string): TaskView | undefined;
377
+ /** §3, §4, §14.2. */
378
+ validate(): SectionValidation;
379
+ /** §7, §9, §14.3: the effective tree, hidden nodes and structural facts. */
380
+ tree(): SectionTree;
381
+ /** The document heads, sorted, as one string. */
382
+ revision(): string;
383
+ /**
384
+ * The Shared Objects (by ID) that changes since `revision`, an earlier
385
+ * revision() of this document, wrote: a Task's fields, conflicts
386
+ * included, live there and not in its node. From A.diff, without reading
387
+ * the document; undefined when this document does not hold `revision`
388
+ * (a rebuild, or another document).
389
+ */
390
+ objectsTouchedSince(revision: string): readonly string[] | undefined;
391
+ save(): Uint8Array;
392
+ /** Every change of the document, in causal order. */
393
+ changes(): Uint8Array[];
394
+ /** The current state as JSON: scalar strings and Text read as strings. */
395
+ toJSON(): Json;
396
+ /**
397
+ * Validates `intents` as one batch and commits them as one change.
398
+ * Returns null when the batch writes nothing. Throws SectionIntentError,
399
+ * or ProfileError / ObjectIdCollisionError for a Task the SOP refuses.
400
+ */
401
+ commit(intents: readonly SectionIntent[]): SectionLocalChange | null;
402
+ /**
403
+ * Validates `intents` as one batch and prepares their change without
404
+ * changing this replica: `apply()` adopts it, once the caller has stored
405
+ * it durably (its Data Unit, outbound entry and checkpoint). A batch that
406
+ * is never applied leaves the replica as it was. Null when the batch
407
+ * writes nothing.
408
+ */
409
+ stage(intents: readonly SectionIntent[]): StagedSectionChange | null;
410
+ }
411
+ export {};
412
+ //# sourceMappingURL=replica.d.ts.map