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

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 (45) 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} +41 -1
  7. package/dist/admission/framing.js +242 -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/concurrent.d.ts +15 -0
  17. package/dist/concurrent.js +55 -0
  18. package/dist/data-profile.d.ts +1 -1
  19. package/dist/data-profile.js +3 -7
  20. package/dist/index.d.ts +2 -2
  21. package/dist/index.js +2 -2
  22. package/dist/replica.d.ts +32 -2
  23. package/dist/replica.js +165 -209
  24. package/dist/sections/admission.d.ts +35 -0
  25. package/dist/sections/admission.js +328 -0
  26. package/dist/sections/data-profile.d.ts +195 -0
  27. package/dist/sections/data-profile.js +383 -0
  28. package/dist/sections/dispatch.d.ts +41 -0
  29. package/dist/sections/dispatch.js +33 -0
  30. package/dist/sections/document.d.ts +54 -0
  31. package/dist/sections/document.js +101 -0
  32. package/dist/sections/index.d.ts +18 -0
  33. package/dist/sections/index.js +18 -0
  34. package/dist/sections/replica.d.ts +406 -0
  35. package/dist/sections/replica.js +1227 -0
  36. package/dist/sections/schema.d.ts +77 -0
  37. package/dist/sections/schema.js +548 -0
  38. package/dist/sections/tree.d.ts +83 -0
  39. package/dist/sections/tree.js +401 -0
  40. package/dist/sections/values.d.ts +30 -0
  41. package/dist/sections/values.js +36 -0
  42. package/dist/values.d.ts +3 -18
  43. package/dist/values.js +7 -95
  44. package/package.json +12 -4
  45. package/dist/automerge-bytes.js +0 -111
@@ -22,7 +22,7 @@
22
22
  // actors);
23
23
  // - for a Snapshot, the bytes of all column data after inflation.
24
24
  import { Inflate } from "fflate";
25
- import { ProfileInvalidError } from "./profile-invalid.js";
25
+ import { ProfileInvalidError } from "../profile-invalid.js";
26
26
  /** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
27
27
  export const CHANGE_LIMITS = Object.freeze({
28
28
  maxRows: 16_384,
@@ -63,6 +63,21 @@ const INFLATE_SLICE = 1024;
63
63
  const refuse = (why) => {
64
64
  throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
65
65
  };
66
+ /**
67
+ * A chunk refused because it cannot be read as one: no magic bytes, a
68
+ * length that does not cover it exactly, a truncated number or field. A
69
+ * chunk refused for anything else (a limit, a deflated or duplicate column,
70
+ * an out-of-range value) was read in full and still names its change hash.
71
+ */
72
+ export class MalformedChunkError extends ProfileInvalidError {
73
+ constructor(why) {
74
+ super("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
75
+ this.name = "MalformedChunkError";
76
+ }
77
+ }
78
+ const malformed = (why) => {
79
+ throw new MalformedChunkError(why);
80
+ };
66
81
  class Cursor {
67
82
  bytes;
68
83
  end;
@@ -76,7 +91,7 @@ class Cursor {
76
91
  }
77
92
  take(n) {
78
93
  if (n > this.end - this.pos)
79
- refuse("a length runs past the end");
94
+ malformed("a length runs past the end");
80
95
  const out = this.bytes.subarray(this.pos, this.pos + n);
81
96
  this.pos += n;
82
97
  return out;
@@ -87,14 +102,14 @@ class Cursor {
87
102
  let scale = 1;
88
103
  for (let i = 0; i < 10; i++) {
89
104
  if (this.pos >= this.end)
90
- refuse("a LEB128 number runs past the end");
105
+ malformed("a LEB128 number runs past the end");
91
106
  const b = this.bytes[this.pos++];
92
107
  value += (b & 0x7f) * scale;
93
108
  if ((b & 0x80) === 0)
94
109
  return value >= 2 ** 53 ? Number.POSITIVE_INFINITY : value;
95
110
  scale *= 128;
96
111
  }
97
- return refuse("a LEB128 number is longer than 10 bytes");
112
+ return malformed("a LEB128 number is longer than 10 bytes");
98
113
  }
99
114
  /** Signed LEB128 as a number; magnitudes at or above 2^53 come back as +/-Infinity. */
100
115
  sleb() {
@@ -102,7 +117,7 @@ class Cursor {
102
117
  let scale = 1;
103
118
  for (let i = 0; i < 10; i++) {
104
119
  if (this.pos >= this.end)
105
- refuse("a LEB128 number runs past the end");
120
+ malformed("a LEB128 number runs past the end");
106
121
  const b = this.bytes[this.pos++];
107
122
  value += (b & 0x7f) * scale;
108
123
  scale *= 128;
@@ -112,7 +127,7 @@ class Cursor {
112
127
  return Math.abs(value) >= 2 ** 53 ? Math.sign(value) * Number.POSITIVE_INFINITY : value;
113
128
  }
114
129
  }
115
- return refuse("a LEB128 number is longer than 10 bytes");
130
+ return malformed("a LEB128 number is longer than 10 bytes");
116
131
  }
117
132
  }
118
133
  class Tally {
@@ -199,7 +214,7 @@ function countColumn(type, data, tally, bounds) {
199
214
  function columnMetas(c) {
200
215
  const count = c.uleb();
201
216
  if (count > (c.end - c.pos) / 2)
202
- refuse("the column metadata runs past the end");
217
+ malformed("the column metadata runs past the end");
203
218
  const out = [];
204
219
  const specs = new Set();
205
220
  for (let i = 0; i < count; i++) {
@@ -236,14 +251,14 @@ function body(bytes, want, what) {
236
251
  const c = new Cursor(bytes);
237
252
  const magic = c.take(4);
238
253
  if (!MAGIC.every((b, i) => magic[i] === b))
239
- refuse(`${what} has no Automerge magic bytes`);
254
+ malformed(`${what} has no Automerge magic bytes`);
240
255
  c.take(4); // checksum: verified elsewhere (checkChange; the engine for a Snapshot)
241
256
  const type = c.take(1)[0];
242
257
  if (type !== want)
243
258
  refuse(`${what} is chunk type ${type}, not ${want}`);
244
259
  const length = c.uleb();
245
260
  if (length !== bytes.length - c.pos)
246
- refuse(`${what} must be exactly one chunk with nothing after it`);
261
+ malformed(`${what} must be exactly one chunk with nothing after it`);
247
262
  return c;
248
263
  }
249
264
  /**
@@ -454,4 +469,4 @@ export function checkSnapshotExpansion(bytes, limits = SNAPSHOT_LIMITS_FLOOR) {
454
469
  maxDepth,
455
470
  });
456
471
  }
457
- //# sourceMappingURL=chunk-limits.js.map
472
+ //# sourceMappingURL=limits.js.map
@@ -1,7 +1,7 @@
1
1
  import * as A from "@automerge/automerge";
2
2
  import { type ParsedChange } from "./canonical.js";
3
3
  /** A §11.4 rule a change breaks. */
4
- export type ReferenceRule = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R10";
4
+ export type ReferenceRule = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "R8" | "R9" | "R10";
5
5
  /**
6
6
  * The changes of a document as §11.4 reads them. A replica keeps it current
7
7
  * as changes are admitted and committed, or builds it again from its
@@ -31,6 +31,7 @@ const targetOf = (op, id) => ({
31
31
  insert: op.insert,
32
32
  del: op.action === 3,
33
33
  make: MAKE.has(op.action) ? op.action : null,
34
+ counterPut: op.action === 1 && op.valueType === 8,
34
35
  });
35
36
  function splitId(id) {
36
37
  const at = id.indexOf("@");
@@ -137,9 +138,10 @@ export class ReferenceHistory {
137
138
  };
138
139
  for (const [i, op] of change.ops.entries()) {
139
140
  const id = `${change.startOp + i}@${change.actor}`;
140
- // R10: no operation makes a table. Automerge JS 3.5.0 cannot write a
141
- // table back out and aborts applying one (differential fuzzing D1); a
142
- // writer of this profile never makes one.
141
+ // R9: no operation is a mark. R10: no operation makes a table (an engine
142
+ // cannot write a table back out, and writing into one aborts the apply).
143
+ if (op.action === 7)
144
+ return "R9";
143
145
  if (op.action === 6)
144
146
  return "R10";
145
147
  // R3: the root, or an object an operation made; its key form.
@@ -176,6 +178,10 @@ export class ReferenceHistory {
176
178
  // R7: a deletion has a predecessor.
177
179
  if (op.action === 3 && op.pred.length === 0)
178
180
  return "R7";
181
+ // R8: an increment names at least one put of a counter, and only such puts (R6 put
182
+ // them on the same object and key).
183
+ if (op.action === 5 && (op.pred.length === 0 || op.pred.some((p) => !target(p)?.counterPut)))
184
+ return "R8";
179
185
  own.push(targetOf(op, id));
180
186
  }
181
187
  return null;
@@ -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,147 @@
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 if (c.beginsWithAuthor && c.seq !== 1) {
73
+ refused.push({
74
+ change: c,
75
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change's extra bytes begin with an author but its sequence number is ${c.seq}, not 1 (§14.1)`),
76
+ held: false,
77
+ });
78
+ continue;
79
+ }
80
+ else {
81
+ const references = referenceRefusal(c, doc);
82
+ if (references !== null) {
83
+ refused.push({ change: c, error: references, held: false });
84
+ continue;
85
+ }
86
+ try {
87
+ accept(c);
88
+ }
89
+ catch (e) {
90
+ refused.push({ change: c, error: e, held: false });
91
+ continue;
92
+ }
93
+ seqs.set(c.actor, c.seq);
94
+ doc.references?.add(c.hash, parsedOf(c));
95
+ admitted.push(c);
96
+ }
97
+ for (const child of children.get(c.hash) ?? []) {
98
+ blocking[child] = blocking[child] - 1;
99
+ if (blocking[child] === 0 && !unreachable[child])
100
+ ready.push(child);
101
+ }
102
+ }
103
+ return Object.freeze({
104
+ admitted: Object.freeze(admitted),
105
+ duplicates: Object.freeze(duplicates),
106
+ waiting: Object.freeze(all.filter((_, i) => !done[i])),
107
+ refused: Object.freeze(refused),
108
+ });
109
+ }
110
+ /**
111
+ * One change: a duplicate, missing dependencies, a held sequence, an actor
112
+ * the document does not know (§11.1) or a skipped sequence (§14.1), checked
113
+ * in that order before the engine sees it; or the actor's next change.
114
+ */
115
+ export function admitChange(change, doc) {
116
+ if (doc.hasChange(change.hash))
117
+ return { kind: "duplicate" };
118
+ const missing = change.deps.filter((d) => !doc.hasChange(d));
119
+ if (missing.length > 0)
120
+ return { kind: "missing", missing: Object.freeze(missing) };
121
+ const latest = doc.latestSeq(change.actor);
122
+ if (change.seq <= latest)
123
+ return { kind: "held", error: held(change) };
124
+ const unknown = change.otherActors.find((a) => doc.latestSeq(a) === 0);
125
+ if (unknown !== undefined)
126
+ return {
127
+ kind: "invalid",
128
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change names actor ${unknown}, unknown to this document (§11.1)`),
129
+ };
130
+ if (change.seq !== latest + 1)
131
+ return {
132
+ kind: "invalid",
133
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `actor ${change.actor} sequence ${change.seq} skips sequence ${latest + 1} (§14.1)`),
134
+ };
135
+ // §14.1: an author belongs only in an actor's first change (finding D5).
136
+ if (change.beginsWithAuthor && change.seq !== 1)
137
+ return {
138
+ kind: "invalid",
139
+ error: new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `the change's extra bytes begin with an author but its sequence number is ${change.seq}, not 1 (§14.1)`),
140
+ };
141
+ // §11.4: checked only; the caller adds the change once the engine applied it.
142
+ const references = referenceRefusal(change, doc);
143
+ if (references !== null)
144
+ return { kind: "invalid", error: references };
145
+ return { kind: "next" };
146
+ }
147
+ //# sourceMappingURL=sequence.js.map
@@ -0,0 +1,15 @@
1
+ import * as A from "@automerge/automerge";
2
+ type AMap = Record<string, unknown>;
3
+ /**
4
+ * The distinct concurrent values of `map[key]` in `doc` (SOP §45,
5
+ * mvp-0.2-baseline.12): none when absent, one when it is not conflicted or
6
+ * when every concurrent value is equal. Two values are equal when they have
7
+ * the same Automerge scalar type and the same value (strings code point by
8
+ * code point); an object (a map, a list or Text) equals no other value. In
9
+ * Automerge's order of the concurrent values, the first of each equal group.
10
+ */
11
+ export declare function distinctValues(doc: A.Doc<unknown>, map: AMap | undefined, key: string): unknown[];
12
+ /** Whether `map[key]` has two or more distinct concurrent values (a conflict, SOP §45). */
13
+ export declare function isConflicted(doc: A.Doc<unknown>, map: AMap, key: string): boolean;
14
+ export {};
15
+ //# sourceMappingURL=concurrent.d.ts.map
@@ -0,0 +1,55 @@
1
+ import * as A from "@automerge/automerge";
2
+ /** Automerge object types: an object value equals no other value (SOP §45). */
3
+ const OBJECTS = new Set(["map", "list", "text", "table"]);
4
+ /** A scalar's identity for equality: its Automerge type and its value. */
5
+ function scalarKey(type, value) {
6
+ if (value instanceof Uint8Array)
7
+ return `${type}:${Array.from(value, (b) => b.toString(16).padStart(2, "0")).join("")}`;
8
+ if (value instanceof Date)
9
+ return `${type}:${value.getTime()}`;
10
+ if (typeof value === "bigint")
11
+ return `${type}:${value}`;
12
+ return `${type}:${JSON.stringify(value)}`;
13
+ }
14
+ /**
15
+ * The distinct concurrent values of `map[key]` in `doc` (SOP §45,
16
+ * mvp-0.2-baseline.12): none when absent, one when it is not conflicted or
17
+ * when every concurrent value is equal. Two values are equal when they have
18
+ * the same Automerge scalar type and the same value (strings code point by
19
+ * code point); an object (a map, a list or Text) equals no other value. In
20
+ * Automerge's order of the concurrent values, the first of each equal group.
21
+ */
22
+ export function distinctValues(doc, map, key) {
23
+ if (map === undefined || !(key in map))
24
+ return [];
25
+ const conflicts = A.getConflicts(map, key);
26
+ if (conflicts === undefined)
27
+ return [map[key]];
28
+ const obj = A.getObjectId(map);
29
+ const all = obj === null ? [] : A.getBackend(doc).getAll(obj, key, A.getHeads(doc));
30
+ const typeOf = new Map();
31
+ for (const e of all) {
32
+ const type = e[0];
33
+ if (OBJECTS.has(type))
34
+ typeOf.set(e[1], { type, value: undefined });
35
+ else
36
+ typeOf.set(e[2], { type, value: e[1] });
37
+ }
38
+ const seen = new Set();
39
+ const out = [];
40
+ for (const [opId, value] of Object.entries(conflicts)) {
41
+ const t = typeOf.get(opId);
42
+ const k = t === undefined || OBJECTS.has(t.type) ? `object:${opId}` : scalarKey(t.type, t.value);
43
+ if (seen.has(k))
44
+ continue;
45
+ seen.add(k);
46
+ out.push(value);
47
+ }
48
+ return out;
49
+ }
50
+ /** Whether `map[key]` has two or more distinct concurrent values (a conflict, SOP §45). */
51
+ export function isConflicted(doc, map, key) {
52
+ return (A.getConflicts(map, key) !== undefined &&
53
+ distinctValues(doc, map, key).length > 1);
54
+ }
55
+ //# sourceMappingURL=concurrent.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