@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,81 @@
1
+ import { LfcpError } from "@openlfcp/core";
2
+ import { ProfileInvalidError } from "../profile-invalid.js";
3
+ import { type CheckedChange } from "./framing.js";
4
+ import type { ReferenceHistory } from "./references.js";
5
+ /**
6
+ * SHARED-OBJECTS-PROFILE-01 §14.1, the sequence admission, for any profile
7
+ * that inherits it (SHARED-SECTIONS-PROFILE-01 §2): which received changes
8
+ * the engine may apply, in which order, and why the others wait or are
9
+ * refused. It reads the document through `DocumentSequences` and knows no
10
+ * profile semantics; a profile adds its own checks through `accept`.
11
+ */
12
+ /** What admission needs to know of the document. */
13
+ export interface DocumentSequences {
14
+ /** Whether the document holds the change with this hex hash. */
15
+ hasChange(hash: string): boolean;
16
+ /** The actor's latest sequence number in the document; 0 when it has none. */
17
+ latestSeq(actor: string): number;
18
+ /**
19
+ * The document's changes as §11.4 reads them. When given, a change whose
20
+ * operations refer outside its causal history is refused; admitBatch adds
21
+ * the changes it admits (the caller rebuilds it if the engine then fails).
22
+ */
23
+ readonly references?: ReferenceHistory;
24
+ }
25
+ /** A change admission refused: held (§14.1, POST-001) or invalid. */
26
+ export interface RefusedChange {
27
+ readonly change: CheckedChange;
28
+ readonly error: LfcpError;
29
+ /**
30
+ * The actor and sequence number belong to a different change of the
31
+ * document (ACTOR_EQUIVOCATION): a profile holds it and retries it after a
32
+ * rebuild that removes changes; it is not profile-invalid.
33
+ */
34
+ readonly held: boolean;
35
+ }
36
+ export interface BatchAdmission {
37
+ /** Changes to apply, in an order where each comes after its dependencies. */
38
+ readonly admitted: readonly CheckedChange[];
39
+ /** Changes the document holds already. */
40
+ readonly duplicates: readonly CheckedChange[];
41
+ /** Changes whose dependencies are neither in the document nor admitted. */
42
+ readonly waiting: readonly CheckedChange[];
43
+ /** Held and invalid changes, in the order they were decided. */
44
+ readonly refused: readonly RefusedChange[];
45
+ }
46
+ /**
47
+ * Admits many changes at once: each once its dependencies inside the batch
48
+ * are admitted (Kahn's order), against the document and the batch so far.
49
+ * A taken sequence is held, a change naming an actor the document does not
50
+ * know or skipping a sequence is INVALID_AUTOMERGE_BYTES (§11.1, §14.1),
51
+ * and `accept` (the profile's own checks, e.g. §11.2 depth) may refuse by
52
+ * throwing; a refused change does not stop the others, and the changes
53
+ * that depend on it wait. `changes` must hold each hash once.
54
+ */
55
+ export declare function admitBatch(changes: readonly CheckedChange[], doc: DocumentSequences, accept?: (change: CheckedChange) => void): BatchAdmission;
56
+ /** One change against the document (receiveChange): what §14.1 says of it. */
57
+ export type ChangeAdmission = {
58
+ readonly kind: "duplicate";
59
+ } | {
60
+ readonly kind: "missing";
61
+ readonly missing: readonly string[];
62
+ }
63
+ /** Its actor and sequence belong to another change: held (§14.1, POST-001). */
64
+ | {
65
+ readonly kind: "held";
66
+ readonly error: LfcpError;
67
+ } | {
68
+ readonly kind: "invalid";
69
+ readonly error: ProfileInvalidError;
70
+ }
71
+ /** The actor's next change, every dependency present: the engine may apply it. */
72
+ | {
73
+ readonly kind: "next";
74
+ };
75
+ /**
76
+ * One change: a duplicate, missing dependencies, a held sequence, an actor
77
+ * the document does not know (§11.1) or a skipped sequence (§14.1), checked
78
+ * in that order before the engine sees it; or the actor's next change.
79
+ */
80
+ export declare function admitChange(change: CheckedChange, doc: DocumentSequences): ChangeAdmission;
81
+ //# sourceMappingURL=sequence.d.ts.map
@@ -0,0 +1,133 @@
1
+ import { LfcpError } from "@openlfcp/core";
2
+ import { ProfileInvalidError } from "../profile-invalid.js";
3
+ import { parsedOf } from "./framing.js";
4
+ /** §11.4: a refusal for a change whose operations refer outside its history, or null. */
5
+ function referenceRefusal(c, doc) {
6
+ const rule = doc.references?.check(parsedOf(c)) ?? null;
7
+ return rule === null
8
+ ? null
9
+ : new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change's operations refer outside its causal history (§11.4 ${rule})`);
10
+ }
11
+ const held = (c) => new LfcpError("ACTOR_EQUIVOCATION", `actor ${c.actor} sequence ${c.seq} already has a different change (§26.2)`);
12
+ /**
13
+ * Admits many changes at once: each once its dependencies inside the batch
14
+ * are admitted (Kahn's order), against the document and the batch so far.
15
+ * A taken sequence is held, a change naming an actor the document does not
16
+ * know or skipping a sequence is INVALID_AUTOMERGE_BYTES (§11.1, §14.1),
17
+ * and `accept` (the profile's own checks, e.g. §11.2 depth) may refuse by
18
+ * throwing; a refused change does not stop the others, and the changes
19
+ * that depend on it wait. `changes` must hold each hash once.
20
+ */
21
+ export function admitBatch(changes, doc, accept = () => undefined) {
22
+ const all = [...changes];
23
+ const index = new Map(all.map((c, i) => [c.hash, i]));
24
+ const blocking = all.map(() => 0);
25
+ const unreachable = all.map(() => false);
26
+ const children = new Map();
27
+ all.forEach((c, i) => {
28
+ for (const d of c.deps) {
29
+ if (doc.hasChange(d))
30
+ continue;
31
+ if (index.has(d)) {
32
+ blocking[i] = blocking[i] + 1;
33
+ children.set(d, [...(children.get(d) ?? []), i]);
34
+ }
35
+ else
36
+ unreachable[i] = true;
37
+ }
38
+ });
39
+ const seqs = new Map();
40
+ const latest = (actor) => seqs.get(actor) ?? doc.latestSeq(actor);
41
+ const admitted = [];
42
+ const duplicates = [];
43
+ const refused = [];
44
+ const done = all.map(() => false);
45
+ const ready = all.flatMap((_, i) => (blocking[i] === 0 && !unreachable[i] ? [i] : []));
46
+ for (let k = 0; k < ready.length; k++) {
47
+ const i = ready[k];
48
+ const c = all[i];
49
+ done[i] = true;
50
+ if (doc.hasChange(c.hash))
51
+ duplicates.push(c);
52
+ else if (c.seq <= latest(c.actor)) {
53
+ refused.push({ change: c, error: held(c), held: true });
54
+ continue;
55
+ }
56
+ else if (c.otherActors.some((a) => latest(a) === 0)) {
57
+ refused.push({
58
+ change: c,
59
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change names an actor unknown to this document (§11.1)`),
60
+ held: false,
61
+ });
62
+ continue;
63
+ }
64
+ else if (c.seq !== latest(c.actor) + 1) {
65
+ refused.push({
66
+ change: c,
67
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `actor ${c.actor} sequence ${c.seq} skips sequence ${latest(c.actor) + 1} (§14.1)`),
68
+ held: false,
69
+ });
70
+ continue;
71
+ }
72
+ else {
73
+ const references = referenceRefusal(c, doc);
74
+ if (references !== null) {
75
+ refused.push({ change: c, error: references, held: false });
76
+ continue;
77
+ }
78
+ try {
79
+ accept(c);
80
+ }
81
+ catch (e) {
82
+ refused.push({ change: c, error: e, held: false });
83
+ continue;
84
+ }
85
+ seqs.set(c.actor, c.seq);
86
+ doc.references?.add(c.hash, parsedOf(c));
87
+ admitted.push(c);
88
+ }
89
+ for (const child of children.get(c.hash) ?? []) {
90
+ blocking[child] = blocking[child] - 1;
91
+ if (blocking[child] === 0 && !unreachable[child])
92
+ ready.push(child);
93
+ }
94
+ }
95
+ return Object.freeze({
96
+ admitted: Object.freeze(admitted),
97
+ duplicates: Object.freeze(duplicates),
98
+ waiting: Object.freeze(all.filter((_, i) => !done[i])),
99
+ refused: Object.freeze(refused),
100
+ });
101
+ }
102
+ /**
103
+ * One change: a duplicate, missing dependencies, a held sequence, an actor
104
+ * the document does not know (§11.1) or a skipped sequence (§14.1), checked
105
+ * in that order before the engine sees it; or the actor's next change.
106
+ */
107
+ export function admitChange(change, doc) {
108
+ if (doc.hasChange(change.hash))
109
+ return { kind: "duplicate" };
110
+ const missing = change.deps.filter((d) => !doc.hasChange(d));
111
+ if (missing.length > 0)
112
+ return { kind: "missing", missing: Object.freeze(missing) };
113
+ const latest = doc.latestSeq(change.actor);
114
+ if (change.seq <= latest)
115
+ return { kind: "held", error: held(change) };
116
+ const unknown = change.otherActors.find((a) => doc.latestSeq(a) === 0);
117
+ if (unknown !== undefined)
118
+ return {
119
+ kind: "invalid",
120
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change names actor ${unknown}, unknown to this document (§11.1)`),
121
+ };
122
+ if (change.seq !== latest + 1)
123
+ return {
124
+ kind: "invalid",
125
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `actor ${change.actor} sequence ${change.seq} skips sequence ${latest + 1} (§14.1)`),
126
+ };
127
+ // §11.4: checked only; the caller adds the change once the engine applied it.
128
+ const references = referenceRefusal(change, doc);
129
+ if (references !== null)
130
+ return { kind: "invalid", error: references };
131
+ return { kind: "next" };
132
+ }
133
+ //# sourceMappingURL=sequence.js.map
@@ -1,5 +1,5 @@
1
1
  import { type DataUnitId, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
- import { type CheckedChange } from "./automerge-bytes.js";
2
+ import { type CheckedChange } from "./admission/framing.js";
3
3
  import { type ObjectChange, type ReplicaOptions, SharedObjectsReplica } from "./replica.js";
4
4
  /**
5
5
  * The Shared Objects Data Profile handler (LFCP-033): what a profile-
@@ -1,7 +1,7 @@
1
1
  import { LfcpError, toHex, } from "@openlfcp/core";
2
- import { frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
2
+ import { checkChangeActor } from "./admission/actor.js";
3
+ import { frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./admission/framing.js";
3
4
  import { SharedObjectsReplica } from "./replica.js";
4
- import { ProfileInvalidError } from "./validate.js";
5
5
  import { deriveActorId, PROFILE_ID } from "./values.js";
6
6
  export class SharedObjectsDataProfile {
7
7
  dataProfile = PROFILE_ID;
@@ -139,11 +139,7 @@ export class SharedObjectsDataProfile {
139
139
  // §8, §11 (SO-SEC1): a unit carries only changes of its signer's §8
140
140
  // actor, so no Principal can write into another Principal's Automerge
141
141
  // history. Anything else is PROFILE_INVALID / CHANGE_ACTOR_MISMATCH.
142
- const bound = (change) => {
143
- if (change.actor !== actor)
144
- throw new ProfileInvalidError("CHANGE_ACTOR_MISMATCH", `the change's Automerge actor ${change.actor} is not the §8 actor ${actor} of the unit's signer (§11, SO-SEC1)`);
145
- return change;
146
- };
142
+ const bound = (change) => checkChangeActor(change, actor);
147
143
  return {
148
144
  dataProfile: PROFILE_ID,
149
145
  encode: (change) => frameChange(bound(change).bytes),
package/dist/index.d.ts CHANGED
@@ -4,8 +4,8 @@
4
4
  * Automerge binding (LFCP-031).
5
5
  */
6
6
  export declare const PACKAGE = "@openlfcp/shared-objects";
7
- export { type CheckedChange, checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
8
- export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./chunk-limits.js";
7
+ export { type CheckedChange, checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./admission/framing.js";
8
+ export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./admission/limits.js";
9
9
  export { type SharedObjectsApplyResult, type SharedObjectsBatchResult, type SharedObjectsCheckpoint, type SharedObjectsCodec, SharedObjectsDataProfile, type SharedObjectsDiagnostic, type SharedObjectsExcludeResult, type SharedObjectsSnapshotCodec, type SharedObjectsUnit, } from "./data-profile.js";
10
10
  export { applyBatchChecked, applyChecked, type BatchReceiveResult, type BuiltReplica, isWasmTrap, type LocalChange, type ObjectChange, ObjectIdCollisionError, type ObjectStatus, type ReceiveResult, type ReplicaIntent, type ReplicaOptions, type ReplicaValidation, type ResolveFieldConflict, resolveFieldConflict, SCALAR_FIELDS, type ScalarField, type ScalarView, SharedObjectsReplica, type TaskView, } from "./replica.js";
11
11
  export { addTag, assign, cancel, clearDue, clearScheduled, complete, createTask, deleteTask, type NewTask, type ParsedTask, ProfileError, parseTask, removeTag, reopen, restoreTask, setDue, setPriority, setScheduled, setStatus, setTitle, type Task, type TaskChange, type TaskIntent, type TaskPriority, type TaskStatus, taskToJson, unassign, } from "./task.js";
package/dist/index.js CHANGED
@@ -4,8 +4,8 @@
4
4
  * Automerge binding (LFCP-031).
5
5
  */
6
6
  export const PACKAGE = "@openlfcp/shared-objects";
7
- export { checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
8
- export { CHANGE_LIMITS, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./chunk-limits.js";
7
+ export { checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./admission/framing.js";
8
+ export { CHANGE_LIMITS, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./admission/limits.js";
9
9
  export { SharedObjectsDataProfile, } from "./data-profile.js";
10
10
  export { applyBatchChecked, applyChecked, isWasmTrap, ObjectIdCollisionError, resolveFieldConflict, SCALAR_FIELDS, SharedObjectsReplica, } from "./replica.js";
11
11
  export { addTag, assign, cancel, clearDue, clearScheduled, complete, createTask, deleteTask, ProfileError, parseTask, removeTag, reopen, restoreTask, setDue, setPriority, setScheduled, setStatus, setTitle, taskToJson, unassign, } from "./task.js";
package/dist/replica.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as A from "@automerge/automerge";
2
2
  import { LfcpError, type ObjectId, type PrincipalId, type ResourceId } from "@openlfcp/core";
3
- import { type CheckedChange } from "./automerge-bytes.js";
4
- import { type SnapshotLimits } from "./chunk-limits.js";
3
+ import { type CheckedChange } from "./admission/framing.js";
4
+ import { type SnapshotLimits } from "./admission/limits.js";
5
5
  import { type Task, type TaskIntent } from "./task.js";
6
6
  import { type Json, type ProfileProblem, type RootValidation } from "./validate.js";
7
7
  /**
@@ -141,13 +141,43 @@ export interface BuiltReplica {
141
141
  readonly unapplied: readonly CheckedChange[];
142
142
  }
143
143
  type Doc = A.Doc<Record<string, unknown>>;
144
+ type AMap = Record<string, unknown>;
144
145
  /** §30: an object's maps and lists nest at most this many levels (the field's own map is 1). */
145
146
  export declare const MAX_VALUE_DEPTH = 64;
147
+ /**
148
+ * §99: the Task under `id` of `objects` (the document's `objects` map) with
149
+ * conflict metadata, or undefined if there is no object or it is not a Task.
150
+ * Shared by the Shared Objects and the Shared Sections replicas.
151
+ */
152
+ export declare function taskView(doc: A.Doc<unknown>, objects: AMap | undefined, id: string): TaskView | undefined;
153
+ /**
154
+ * §74.1 problems of one object as stored at `/objects/<id>` of `doc`: its
155
+ * logical values, every collaborative Text in it (§30) and every concurrent
156
+ * value of its scalar registers (§45), one diagnostic per field. Shared by
157
+ * every profile whose Tasks are SOP Tasks (SHARED-SECTIONS-PROFILE-01 §2).
158
+ */
159
+ export declare function storedObjectProblems(doc: A.Doc<unknown>, stored: AMap, id: string): ProfileProblem[];
160
+ /** A Task intent checked against the stored objects, ready to write inside a change. */
161
+ export interface PreparedTaskIntent {
162
+ /** The Task it writes. */
163
+ readonly id: string;
164
+ /** Writes the intent into the `objects` map of a change's document. */
165
+ readonly perform: (objects: Record<string, unknown>) => void;
166
+ }
167
+ /**
168
+ * Checks one Task intent against `objects`, the stored objects map of
169
+ * `doc`, before anything is written: §21 no reuse of an Object ID on
170
+ * create, §74 the Task after the intent is valid, and no Text left in a
171
+ * field it writes. Throws ProfileError or ObjectIdCollisionError. Shared by
172
+ * every profile whose Tasks are SOP Tasks (SHARED-SECTIONS-PROFILE-01 §2).
173
+ */
174
+ export declare function prepareTaskIntent(doc: A.Doc<unknown>, objects: Record<string, unknown>, intent: ReplicaIntent): PreparedTaskIntent;
146
175
  /**
147
176
  * Whether a thrown value is a wasm trap (the engine's module is dead, not
148
177
  * the change refused): it is rethrown as it is, never turned into
149
178
  * INVALID_AUTOMERGE_BYTES, so a crash-loop breaker sees it.
150
179
  */
180
+ /** A wasm trap (RuntimeError, or a terminated module): fatal, not a refusable change. */
151
181
  export declare function isWasmTrap(e: unknown): boolean;
152
182
  /**
153
183
  * Applies one change that passed the receive checks. Automerge can throw