@openlfcp/shared-objects 0.1.0-rc.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.
@@ -0,0 +1,457 @@
1
+ // Expansion limits for Automerge chunks (SHARED-OBJECTS-PROFILE-01 §11.1,
2
+ // §13.1; SPEC-PATCH-07). Automerge's columnar format run-length encodes its
3
+ // columns, and a Snapshot's columns may also be deflated, so a few bytes can
4
+ // declare millions of operations or gigabytes of data: a 112-byte change
5
+ // expanded to 1,000,000 operations and 1.2 GiB in Automerge JS 3.5.0. This
6
+ // walker reads a chunk's structure and the run headers of every column,
7
+ // without materialising any value, and refuses a chunk over the limits
8
+ // BEFORE the Automerge engine sees it. It runs in time linear in the input
9
+ // (plus the capped inflation of a Snapshot's deflated columns).
10
+ //
11
+ // Counting rules (the profile states them; both SDKs count the same way):
12
+ // - every column's value count (its rows): an RLE run of n adds n, a
13
+ // literal run of n adds n, a null run of n adds n; a boolean column adds
14
+ // each run length; a raw value column adds nothing;
15
+ // - every group column's sum (the total of its values: pred and succ
16
+ // entries);
17
+ // - expanded string bytes: each string value counted once per row it
18
+ // occupies (an RLE run of n copies of a k-byte string adds n * k);
19
+ // - structure: no two columns with one specification, every actor index
20
+ // below the number of actors, and (a change) no group value above one
21
+ // plus the number of other actors (an op's predecessors have distinct
22
+ // actors);
23
+ // - for a Snapshot, the bytes of all column data after inflation.
24
+ import { Inflate } from "fflate";
25
+ import { ProfileInvalidError } from "./profile-invalid.js";
26
+ /** The exact limits of a change (§11.1): writers stay within them, receivers reject above. */
27
+ export const CHANGE_LIMITS = Object.freeze({
28
+ maxRows: 16_384,
29
+ maxGroupSum: 262_144,
30
+ maxStringBytes: 4 * 1024 * 1024,
31
+ maxDeps: 1_024,
32
+ maxActors: 1_024,
33
+ });
34
+ /**
35
+ * The floor of a Snapshot's limits (§13.1): every receiver accepts at least
36
+ * these. A receiver MAY configure higher limits; above its own it rejects
37
+ * the Snapshot before the engine and falls back to the data round.
38
+ */
39
+ export const SNAPSHOT_LIMITS_FLOOR = Object.freeze({
40
+ maxRows: 262_144,
41
+ maxGroupSum: 262_144,
42
+ maxStringBytes: 32 * 1024 * 1024,
43
+ maxInflatedBytes: 32 * 1024 * 1024,
44
+ maxDeps: 1_024,
45
+ maxActors: 1_024,
46
+ });
47
+ /** §11.2: no object of a document is deeper than this (the root is depth 0). */
48
+ export const MAX_DOCUMENT_DEPTH = 256;
49
+ /** The operation actions that create an object (§11.2): makeMap, makeList, makeText, makeTable. */
50
+ export const OBJECT_ACTIONS = new Set([0, 2, 4, 6]);
51
+ const MAGIC = [0x85, 0x6f, 0x4a, 0x83];
52
+ const CHUNK_DOCUMENT = 0;
53
+ const CHUNK_CHANGE = 1;
54
+ const DEFLATE_BIT = 0x08;
55
+ const TYPE_GROUP = 0;
56
+ const TYPE_ACTOR = 1;
57
+ const TYPE_DELTA = 3;
58
+ const TYPE_BOOLEAN = 4;
59
+ const TYPE_STRING = 5;
60
+ const TYPE_RAW = 7;
61
+ /** Input fed to the inflater per push: each push yields at most ~1,032x its size. */
62
+ const INFLATE_SLICE = 1024;
63
+ const refuse = (why) => {
64
+ throw new ProfileInvalidError("INVALID_AUTOMERGE_BYTES", `Automerge chunk refused (§11.1): ${why}`);
65
+ };
66
+ class Cursor {
67
+ bytes;
68
+ end;
69
+ pos = 0;
70
+ constructor(bytes, end = bytes.length) {
71
+ this.bytes = bytes;
72
+ this.end = end;
73
+ }
74
+ get done() {
75
+ return this.pos >= this.end;
76
+ }
77
+ take(n) {
78
+ if (n > this.end - this.pos)
79
+ refuse("a length runs past the end");
80
+ const out = this.bytes.subarray(this.pos, this.pos + n);
81
+ this.pos += n;
82
+ return out;
83
+ }
84
+ /** Unsigned LEB128 as a number; values at or above 2^53 come back as Infinity (over every limit). */
85
+ uleb() {
86
+ let value = 0;
87
+ let scale = 1;
88
+ for (let i = 0; i < 10; i++) {
89
+ if (this.pos >= this.end)
90
+ refuse("a LEB128 number runs past the end");
91
+ const b = this.bytes[this.pos++];
92
+ value += (b & 0x7f) * scale;
93
+ if ((b & 0x80) === 0)
94
+ return value >= 2 ** 53 ? Number.POSITIVE_INFINITY : value;
95
+ scale *= 128;
96
+ }
97
+ return refuse("a LEB128 number is longer than 10 bytes");
98
+ }
99
+ /** Signed LEB128 as a number; magnitudes at or above 2^53 come back as +/-Infinity. */
100
+ sleb() {
101
+ let value = 0;
102
+ let scale = 1;
103
+ for (let i = 0; i < 10; i++) {
104
+ if (this.pos >= this.end)
105
+ refuse("a LEB128 number runs past the end");
106
+ const b = this.bytes[this.pos++];
107
+ value += (b & 0x7f) * scale;
108
+ scale *= 128;
109
+ if ((b & 0x80) === 0) {
110
+ if (b & 0x40)
111
+ value -= scale;
112
+ return Math.abs(value) >= 2 ** 53 ? Math.sign(value) * Number.POSITIVE_INFINITY : value;
113
+ }
114
+ }
115
+ return refuse("a LEB128 number is longer than 10 bytes");
116
+ }
117
+ }
118
+ class Tally {
119
+ limits;
120
+ maxRows = 0;
121
+ groupSum = 0;
122
+ stringBytes = 0;
123
+ columnBytes = 0;
124
+ constructor(limits) {
125
+ this.limits = limits;
126
+ }
127
+ rows(column, limit) {
128
+ if (column > limit)
129
+ refuse(`a column has more than ${limit} values`);
130
+ if (column > this.maxRows)
131
+ this.maxRows = column;
132
+ }
133
+ group(add) {
134
+ this.groupSum += add;
135
+ if (!(this.groupSum <= this.limits.maxGroupSum))
136
+ refuse(`group columns sum to more than ${this.limits.maxGroupSum}`);
137
+ }
138
+ strings(add) {
139
+ this.stringBytes += add;
140
+ if (!(this.stringBytes <= this.limits.maxStringBytes))
141
+ refuse(`strings expand to more than ${this.limits.maxStringBytes} bytes`);
142
+ }
143
+ }
144
+ /** Counts one column's values (and group sums, string bytes) from its run headers. */
145
+ function countColumn(type, data, tally, bounds) {
146
+ if (type === TYPE_RAW)
147
+ return 0;
148
+ const c = new Cursor(data);
149
+ let rows = 0;
150
+ const add = (n) => {
151
+ rows += n;
152
+ tally.rows(rows, bounds.rows);
153
+ };
154
+ if (type === TYPE_BOOLEAN) {
155
+ while (!c.done)
156
+ add(c.uleb());
157
+ return rows;
158
+ }
159
+ // One value of an RLE column: its group count, its string length, or skipped.
160
+ const value = () => {
161
+ if (type === TYPE_STRING) {
162
+ const len = c.uleb();
163
+ c.take(len);
164
+ return len;
165
+ }
166
+ if (type === TYPE_DELTA)
167
+ return c.sleb();
168
+ return c.uleb();
169
+ };
170
+ const account = (v, times) => {
171
+ if (type === TYPE_GROUP) {
172
+ if (v > bounds.group)
173
+ refuse("an operation has more predecessors than the change has actors");
174
+ tally.group(v * times);
175
+ }
176
+ else if (type === TYPE_STRING)
177
+ tally.strings(v * times);
178
+ else if (type === TYPE_ACTOR && !(v < bounds.actors))
179
+ refuse("an actor index is out of range");
180
+ };
181
+ while (!c.done) {
182
+ const header = c.sleb();
183
+ if (header > 0) {
184
+ add(header);
185
+ account(value(), header);
186
+ }
187
+ else if (header < 0) {
188
+ const n = -header;
189
+ add(n); // refuses before reading an oversized literal run
190
+ for (let i = 0; i < n; i++)
191
+ account(value(), 1);
192
+ }
193
+ else
194
+ add(c.uleb());
195
+ }
196
+ return rows;
197
+ }
198
+ /** Column metadata. The count has no limit of its own (§11.1): each entry takes at least two bytes. */
199
+ function columnMetas(c) {
200
+ const count = c.uleb();
201
+ if (count > (c.end - c.pos) / 2)
202
+ refuse("the column metadata runs past the end");
203
+ const out = [];
204
+ const specs = new Set();
205
+ for (let i = 0; i < count; i++) {
206
+ const spec = c.uleb();
207
+ if (specs.has(spec))
208
+ refuse("two columns share a specification");
209
+ specs.add(spec);
210
+ out.push({ spec, length: c.uleb() });
211
+ }
212
+ return out;
213
+ }
214
+ function lengthPrefixedList(c, limit, what, fixed) {
215
+ const count = c.uleb();
216
+ if (count > limit)
217
+ refuse(`more than ${limit} ${what}`);
218
+ const out = [];
219
+ for (let i = 0; i < count; i++)
220
+ out.push(c.take(fixed ?? c.uleb()));
221
+ return out;
222
+ }
223
+ const toHexString = (b) => Array.from(b, (x) => x.toString(16).padStart(2, "0")).join("");
224
+ /**
225
+ * Each column's value limit: a column that shares its id with a group
226
+ * column holds that group's entries (predecessors, successors, a change's
227
+ * dependencies), so the group sum bounds it; every other column has the
228
+ * row limit.
229
+ */
230
+ function rowLimits(metas, limits) {
231
+ const grouped = new Set(metas.filter((m) => (m.spec & 7) === TYPE_GROUP).map((m) => m.spec >> 4));
232
+ return (spec) => grouped.has(spec >> 4) && (spec & 7) !== TYPE_GROUP ? limits.maxGroupSum : limits.maxRows;
233
+ }
234
+ /** The chunk's single body: magic, checksum, type, length; nothing may follow it. */
235
+ function body(bytes, want, what) {
236
+ const c = new Cursor(bytes);
237
+ const magic = c.take(4);
238
+ if (!MAGIC.every((b, i) => magic[i] === b))
239
+ refuse(`${what} has no Automerge magic bytes`);
240
+ c.take(4); // checksum: verified elsewhere (checkChange; the engine for a Snapshot)
241
+ const type = c.take(1)[0];
242
+ if (type !== want)
243
+ refuse(`${what} is chunk type ${type}, not ${want}`);
244
+ const length = c.uleb();
245
+ if (length !== bytes.length - c.pos)
246
+ refuse(`${what} must be exactly one chunk with nothing after it`);
247
+ return c;
248
+ }
249
+ /**
250
+ * §11.1: checks an uncompressed change chunk (type 1) against the exact
251
+ * change limits before the engine sees it. Throws PROFILE_INVALID /
252
+ * INVALID_AUTOMERGE_BYTES; returns what it expands to.
253
+ */
254
+ export function checkChangeExpansion(bytes) {
255
+ const c = body(bytes, CHUNK_CHANGE, "a change");
256
+ const tally = new Tally(CHANGE_LIMITS);
257
+ lengthPrefixedList(c, CHANGE_LIMITS.maxDeps, "dependencies", 32);
258
+ c.take(c.uleb()); // actor
259
+ c.uleb(); // seq
260
+ c.uleb(); // start op
261
+ c.sleb(); // time
262
+ c.take(c.uleb()); // message
263
+ const otherActors = lengthPrefixedList(c, CHANGE_LIMITS.maxActors, "other actors");
264
+ const others = otherActors.length;
265
+ const metas = columnMetas(c);
266
+ const limitOf = rowLimits(metas, CHANGE_LIMITS);
267
+ const columns = [];
268
+ for (const m of metas) {
269
+ if (m.spec & DEFLATE_BIT)
270
+ refuse("a change column is deflated");
271
+ const bounds = { actors: 1 + others, group: 1 + others, rows: limitOf(m.spec) };
272
+ columns.push({ spec: m.spec, rows: countColumn(m.spec & 7, c.take(m.length), tally, bounds) });
273
+ tally.columnBytes += m.length;
274
+ }
275
+ // The rest of the chunk is the change's extra bytes: kept, not expanded.
276
+ return Object.freeze({
277
+ maxRows: tally.maxRows,
278
+ groupSum: tally.groupSum,
279
+ stringBytes: tally.stringBytes,
280
+ columnBytes: tally.columnBytes,
281
+ columns: Object.freeze(columns),
282
+ otherActors: Object.freeze(otherActors.map(toHexString)),
283
+ maxDepth: 0,
284
+ });
285
+ }
286
+ /** Inflates raw DEFLATE data, refusing as soon as the output passes `budget` bytes. */
287
+ function inflateCapped(input, budget) {
288
+ const parts = [];
289
+ let total = 0;
290
+ let over = false;
291
+ let failed;
292
+ const inflater = new Inflate((chunk) => {
293
+ total += chunk.length;
294
+ if (total > budget)
295
+ over = true;
296
+ else
297
+ parts.push(chunk);
298
+ });
299
+ try {
300
+ for (let i = 0; i < input.length && !over; i += INFLATE_SLICE)
301
+ inflater.push(input.subarray(i, i + INFLATE_SLICE), i + INFLATE_SLICE >= input.length);
302
+ if (input.length === 0)
303
+ inflater.push(new Uint8Array(0), true);
304
+ }
305
+ catch (e) {
306
+ failed = e;
307
+ }
308
+ if (over)
309
+ refuse(`Snapshot columns inflate to more than ${budget} more bytes`);
310
+ if (failed !== undefined)
311
+ refuse("a deflated Snapshot column is not valid DEFLATE");
312
+ const out = new Uint8Array(total);
313
+ let o = 0;
314
+ for (const p of parts) {
315
+ out.set(p, o);
316
+ o += p.length;
317
+ }
318
+ return out;
319
+ }
320
+ /** The values of an actor (1), integer (2) or delta (3) column: null for a null row. */
321
+ function columnValues(type, data) {
322
+ const c = new Cursor(data);
323
+ const out = [];
324
+ let acc = 0;
325
+ const one = () => {
326
+ if (type === TYPE_DELTA) {
327
+ acc += c.sleb();
328
+ return acc;
329
+ }
330
+ return c.uleb();
331
+ };
332
+ while (!c.done) {
333
+ const header = c.sleb();
334
+ if (header > 0) {
335
+ if (type === TYPE_DELTA) {
336
+ const delta = c.sleb();
337
+ for (let i = 0; i < header; i++) {
338
+ acc += delta;
339
+ out.push(acc);
340
+ }
341
+ }
342
+ else {
343
+ const v = c.uleb();
344
+ for (let i = 0; i < header; i++)
345
+ out.push(v);
346
+ }
347
+ }
348
+ else if (header < 0)
349
+ for (let i = 0; i < -header; i++)
350
+ out.push(one());
351
+ else {
352
+ const nulls = c.uleb();
353
+ for (let i = 0; i < nulls; i++)
354
+ out.push(null);
355
+ }
356
+ }
357
+ return out;
358
+ }
359
+ /**
360
+ * §11.2: the deepest object of a document chunk, from its operation
361
+ * columns (object, operation ID, action), computed without recursion.
362
+ * Refuses an object deeper than MAX_DOCUMENT_DEPTH, a cycle, or an object
363
+ * written into one the document never created.
364
+ */
365
+ function documentDepth(columns) {
366
+ const col = (spec) => {
367
+ const data = columns.get(spec);
368
+ return data === undefined ? [] : columnValues(spec & 7, data);
369
+ };
370
+ const action = col((4 << 4) | 2);
371
+ const objActor = col((0 << 4) | 1);
372
+ const objCtr = col((0 << 4) | 2);
373
+ const idActor = col((2 << 4) | 1);
374
+ const idCtr = col((2 << 4) | 3);
375
+ const ROOT = "_root";
376
+ const parent = new Map();
377
+ action.forEach((a, i) => {
378
+ if (a === null || !OBJECT_ACTIONS.has(a))
379
+ return;
380
+ const id = `${idCtr[i]}@${idActor[i]}`;
381
+ const oc = objCtr[i] ?? null;
382
+ parent.set(id, oc === null ? ROOT : `${oc}@${objActor[i]}`);
383
+ });
384
+ const depth = new Map([[ROOT, 0]]);
385
+ let deepest = 0;
386
+ for (const start of parent.keys()) {
387
+ const path = [];
388
+ let at = start;
389
+ while (!depth.has(at)) {
390
+ path.push(at);
391
+ if (path.length > MAX_DOCUMENT_DEPTH)
392
+ refuse(`an object is deeper than ${MAX_DOCUMENT_DEPTH} levels (§11.2)`);
393
+ const up = parent.get(at);
394
+ if (up === undefined)
395
+ refuse("an object is written into one the document never created (§11.2)");
396
+ at = up;
397
+ }
398
+ let d = depth.get(at);
399
+ for (let k = path.length - 1; k >= 0; k--) {
400
+ d += 1;
401
+ if (d > MAX_DOCUMENT_DEPTH)
402
+ refuse(`an object is deeper than ${MAX_DOCUMENT_DEPTH} levels (§11.2)`);
403
+ depth.set(path[k], d);
404
+ }
405
+ if (d > deepest)
406
+ deepest = d;
407
+ }
408
+ return deepest;
409
+ }
410
+ /**
411
+ * §13.1: checks a Snapshot's save (exactly one document chunk) against
412
+ * `limits` (at least SNAPSHOT_LIMITS_FLOOR) before the engine sees it,
413
+ * inflating deflated columns under a running cap. Throws PROFILE_INVALID /
414
+ * INVALID_AUTOMERGE_BYTES.
415
+ */
416
+ export function checkSnapshotExpansion(bytes, limits = SNAPSHOT_LIMITS_FLOOR) {
417
+ const c = body(bytes, CHUNK_DOCUMENT, "a Snapshot");
418
+ const tally = new Tally(limits);
419
+ const actors = lengthPrefixedList(c, limits.maxActors, "actors").length;
420
+ lengthPrefixedList(c, limits.maxDeps, "heads", 32);
421
+ const changeMetas = columnMetas(c);
422
+ const opMetas = columnMetas(c);
423
+ const changeLimit = rowLimits(changeMetas, limits);
424
+ const opLimit = rowLimits(opMetas, limits);
425
+ let inflated = 0;
426
+ const columns = [];
427
+ const opData = new Map();
428
+ for (const [m, limitOf] of [
429
+ ...changeMetas.map((m) => [m, changeLimit]),
430
+ ...opMetas.map((m) => [m, opLimit]),
431
+ ]) {
432
+ let data = c.take(m.length);
433
+ if (m.spec & DEFLATE_BIT)
434
+ data = inflateCapped(data, limits.maxInflatedBytes - inflated);
435
+ inflated += data.length;
436
+ if (inflated > limits.maxInflatedBytes)
437
+ refuse(`Snapshot columns hold more than ${limits.maxInflatedBytes} bytes`);
438
+ const bounds = { actors, group: Number.POSITIVE_INFINITY, rows: limitOf(m.spec) };
439
+ columns.push({ spec: m.spec, rows: countColumn(m.spec & 7, data, tally, bounds) });
440
+ if (limitOf === opLimit)
441
+ opData.set(m.spec & ~DEFLATE_BIT, data);
442
+ }
443
+ // §11.2: depths from the operation columns, once their sizes are known to be bounded.
444
+ const maxDepth = documentDepth(opData);
445
+ tally.columnBytes = inflated;
446
+ // The rest is the document's head indices.
447
+ return Object.freeze({
448
+ maxRows: tally.maxRows,
449
+ groupSum: tally.groupSum,
450
+ stringBytes: tally.stringBytes,
451
+ columnBytes: tally.columnBytes,
452
+ columns: Object.freeze(columns),
453
+ otherActors: Object.freeze([]),
454
+ maxDepth,
455
+ });
456
+ }
457
+ //# sourceMappingURL=chunk-limits.js.map
@@ -0,0 +1,159 @@
1
+ import { type DataUnitId, type PrincipalId, type ResourceId } from "@openlfcp/core";
2
+ import { type CheckedChange } from "./automerge-bytes.js";
3
+ import { type ObjectChange, type ReplicaOptions, SharedObjectsReplica } from "./replica.js";
4
+ /**
5
+ * The Shared Objects Data Profile handler (LFCP-033): what a profile-
6
+ * agnostic Data Unit applier (@openlfcp/client DataUnitApplier) calls once
7
+ * LFCP has accepted a unit. It matches the client's DataProfileHandler
8
+ * structurally; this package depends on neither client nor wire.
9
+ *
10
+ * - decode: the §11 plaintext is one checked Automerge change, written by
11
+ * the §8 actor of the unit's signing Principal (§8, §11, SO-SEC1);
12
+ * - apply: the change merges into the replica, or waits in a buffer until
13
+ * the changes it depends on arrive in other units (profile-pending), and
14
+ * every buffered change it unblocks merges with it;
15
+ * - one object becoming profile-invalid is a diagnostic, never a refusal:
16
+ * the other objects stay usable (§77);
17
+ * - exclude (§14.1, G-EP7): rebuilds the replica without given units.
18
+ */
19
+ /** An accepted unit as the applier passes it (structurally the client's ProfileUnit). */
20
+ export interface SharedObjectsUnit {
21
+ readonly unitId: DataUnitId;
22
+ }
23
+ export interface SharedObjectsDiagnostic {
24
+ readonly objectId?: string;
25
+ readonly code: string;
26
+ readonly diagnostic?: string;
27
+ readonly pointer?: string;
28
+ readonly message: string;
29
+ }
30
+ export interface SharedObjectsApplyResult {
31
+ readonly merged: readonly DataUnitId[];
32
+ readonly objects: readonly string[];
33
+ readonly diagnostics: readonly SharedObjectsDiagnostic[];
34
+ readonly pending?: string;
35
+ }
36
+ /** The outcome of applyBatch: per unit merged, pending or rejected, and the objects once. */
37
+ export interface SharedObjectsBatchResult {
38
+ /** Units merged now: the batch's and buffered ones it released. */
39
+ readonly merged: readonly DataUnitId[];
40
+ /** Units buffered for Automerge dependencies (the batch's and earlier ones still waiting). */
41
+ readonly pending: readonly DataUnitId[];
42
+ /** Units whose change was refused (ACTOR_EQUIVOCATION, INVALID_AUTOMERGE_BYTES, …). */
43
+ readonly rejected: readonly {
44
+ readonly unitId: DataUnitId;
45
+ readonly code: string;
46
+ readonly message: string;
47
+ }[];
48
+ readonly objects: readonly string[];
49
+ readonly diagnostics: readonly SharedObjectsDiagnostic[];
50
+ }
51
+ export interface SharedObjectsExcludeResult {
52
+ readonly objects: readonly string[];
53
+ readonly pending: readonly DataUnitId[];
54
+ }
55
+ /** A persisted handler state (structurally the storage ProfileCheckpoint). */
56
+ export interface SharedObjectsCheckpoint {
57
+ readonly resourceId: ResourceId;
58
+ readonly dataProfile: string;
59
+ readonly state: Uint8Array;
60
+ readonly actorSeq: number;
61
+ readonly units: readonly {
62
+ readonly unitId: DataUnitId;
63
+ readonly ref: string;
64
+ }[];
65
+ }
66
+ /** The §13 Snapshot codec (structurally the wire DataProfileCodec): plaintext ↔ full save. */
67
+ export interface SharedObjectsSnapshotCodec {
68
+ readonly dataProfile: string;
69
+ encode(save: Uint8Array): Uint8Array;
70
+ decode(plaintext: Uint8Array): Uint8Array;
71
+ }
72
+ /** The §11 codec of one unit (structurally the wire DataProfileCodec). */
73
+ export interface SharedObjectsCodec {
74
+ readonly dataProfile: string;
75
+ encode(change: CheckedChange): Uint8Array;
76
+ decode(plaintext: Uint8Array): CheckedChange;
77
+ }
78
+ export declare class SharedObjectsDataProfile {
79
+ #private;
80
+ readonly dataProfile = "org.openlfcp.shared-objects.v1";
81
+ constructor(replica: SharedObjectsReplica);
82
+ /**
83
+ * The local state to persist (structurally the storage ProfileCheckpoint,
84
+ * LFCP-034/035): the Automerge full save, this actor's sequence (the §9
85
+ * minSeq on restore) and which unit carried which merged change (for a
86
+ * G-EP7 rebuild). Units buffered for Automerge dependencies are not in it:
87
+ * they stay "profile-pending" in storage and are offered again on restore.
88
+ */
89
+ checkpoint(): SharedObjectsCheckpoint;
90
+ /**
91
+ * The handler of a persisted checkpoint. The replica refuses local writes
92
+ * if its actor is behind the checkpoint's sequence (§9).
93
+ */
94
+ static restore(checkpoint: SharedObjectsCheckpoint, options: Pick<ReplicaOptions, "resource" | "principal">): SharedObjectsDataProfile;
95
+ /** The current replica (a G-EP7 rebuild replaces it). */
96
+ get replica(): SharedObjectsReplica;
97
+ /** §98, §100: object change notifications for remote merges and rebuilds. */
98
+ onObjectChanged(listener: (change: ObjectChange) => void): () => void;
99
+ /**
100
+ * Records which change one of this client's OWN units carries (the unit
101
+ * was created from a local change, so it is merged already). A later
102
+ * G-EP7 exclusion of that unit then rebuilds the replica without it, and
103
+ * the checkpoint keeps the reference. Use it as createQueuedDataUnit's
104
+ * onCreated.
105
+ */
106
+ recordLocal(unitId: DataUnitId, change: CheckedChange): void;
107
+ /**
108
+ * The §13 Snapshot codec (structurally the wire DataProfileCodec): an
109
+ * Automerge full save framed as [1, save]; decoding requires a document
110
+ * chunk. For receiveSnapshot and createSnapshot.
111
+ */
112
+ snapshotCodec(): SharedObjectsSnapshotCodec;
113
+ /** The state to publish as a Snapshot: the replica's full save. */
114
+ snapshotState(): Uint8Array;
115
+ /**
116
+ * Loads a received Snapshot's full save (§13, §66 step 3): the replica
117
+ * becomes the save merged with everything it held, and buffered units
118
+ * whose dependencies the Snapshot brings are merged. Returns the objects
119
+ * that changed and the units merged from the buffer.
120
+ */
121
+ loadSnapshot(save: Uint8Array): {
122
+ readonly objects: readonly string[];
123
+ readonly merged: readonly DataUnitId[];
124
+ };
125
+ /**
126
+ * Forgets the whole state (an empty replica that keeps the §9 sequence,
127
+ * no merged or buffered units), so it can be rebuilt from accepted units
128
+ * only (SNAP-EP: Snapshot-derived state dropped). Notifies the changes.
129
+ */
130
+ reset(): void;
131
+ /** Whether this handler holds the unit's change (merged, recorded or buffered). */
132
+ has(unitId: DataUnitId): boolean;
133
+ /** The units waiting for Automerge dependencies. */
134
+ pendingUnits(): DataUnitId[];
135
+ codecFor(unit: {
136
+ readonly resourceId: ResourceId;
137
+ readonly actor: PrincipalId;
138
+ }): SharedObjectsCodec;
139
+ apply(unit: SharedObjectsUnit, change: CheckedChange): SharedObjectsApplyResult;
140
+ /**
141
+ * Applies many accepted units at once (catch-up, store replay, a
142
+ * Snapshot's remainder): their changes and every buffered one go to the
143
+ * replica in one receiveChanges call, which hands Automerge one batch
144
+ * (§14.1 admission per change; one change per call is quadratic). A
145
+ * refused change rejects only its unit; units missing a dependency are
146
+ * buffered. Diagnostics are computed once, for the objects that changed.
147
+ */
148
+ applyBatch(units: readonly {
149
+ readonly unit: SharedObjectsUnit;
150
+ readonly value: CheckedChange;
151
+ }[]): SharedObjectsBatchResult;
152
+ /**
153
+ * §14.1 (G-EP7): the state without `unitIds`, rebuilt from the
154
+ * remaining changes. Merged units whose changes build on an excluded one
155
+ * go back to the buffer and are reported as pending.
156
+ */
157
+ exclude(unitIds: readonly DataUnitId[]): SharedObjectsExcludeResult;
158
+ }
159
+ //# sourceMappingURL=data-profile.d.ts.map