@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.
- package/README.md +15 -2
- package/dist/admission/actor.d.ts +16 -0
- package/dist/admission/actor.js +35 -0
- package/dist/{canonical.d.ts → admission/canonical.d.ts} +9 -0
- package/dist/{canonical.js → admission/canonical.js} +43 -3
- package/dist/{automerge-bytes.d.ts → admission/framing.d.ts} +41 -1
- package/dist/admission/framing.js +242 -0
- package/dist/admission/index.d.ts +26 -0
- package/dist/admission/index.js +26 -0
- package/dist/{chunk-limits.d.ts → admission/limits.d.ts} +11 -1
- package/dist/{chunk-limits.js → admission/limits.js} +25 -10
- package/dist/{references.d.ts → admission/references.d.ts} +1 -1
- package/dist/{references.js → admission/references.js} +9 -3
- package/dist/admission/sequence.d.ts +81 -0
- package/dist/admission/sequence.js +147 -0
- package/dist/concurrent.d.ts +15 -0
- package/dist/concurrent.js +55 -0
- package/dist/data-profile.d.ts +1 -1
- package/dist/data-profile.js +3 -7
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/replica.d.ts +32 -2
- package/dist/replica.js +165 -209
- package/dist/sections/admission.d.ts +35 -0
- package/dist/sections/admission.js +328 -0
- package/dist/sections/data-profile.d.ts +195 -0
- package/dist/sections/data-profile.js +383 -0
- package/dist/sections/dispatch.d.ts +41 -0
- package/dist/sections/dispatch.js +33 -0
- package/dist/sections/document.d.ts +54 -0
- package/dist/sections/document.js +101 -0
- package/dist/sections/index.d.ts +18 -0
- package/dist/sections/index.js +18 -0
- package/dist/sections/replica.d.ts +406 -0
- package/dist/sections/replica.js +1227 -0
- package/dist/sections/schema.d.ts +77 -0
- package/dist/sections/schema.js +548 -0
- package/dist/sections/tree.d.ts +83 -0
- package/dist/sections/tree.js +401 -0
- package/dist/sections/values.d.ts +30 -0
- package/dist/sections/values.js +36 -0
- package/dist/values.d.ts +3 -18
- package/dist/values.js +7 -95
- package/package.json +12 -4
- package/dist/automerge-bytes.js +0 -111
|
@@ -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 "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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
|
-
//
|
|
141
|
-
// table back out and
|
|
142
|
-
|
|
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
|
package/dist/data-profile.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type DataUnitId, type PrincipalId, type ResourceId } from "@openlfcp/core";
|
|
2
|
-
import { type CheckedChange } from "./
|
|
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-
|
package/dist/data-profile.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LfcpError, toHex, } from "@openlfcp/core";
|
|
2
|
-
import {
|
|
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 "./
|
|
8
|
-
export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./
|
|
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 "./
|
|
8
|
-
export { CHANGE_LIMITS, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./
|
|
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 "./
|
|
4
|
-
import { type SnapshotLimits } from "./
|
|
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
|