@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.
- package/LICENSE +201 -0
- package/README.md +91 -0
- package/dist/automerge-bytes.d.ts +37 -0
- package/dist/automerge-bytes.js +95 -0
- package/dist/chunk-limits.d.ts +58 -0
- package/dist/chunk-limits.js +457 -0
- package/dist/data-profile.d.ts +159 -0
- package/dist/data-profile.js +311 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +15 -0
- package/dist/profile-invalid.d.ts +12 -0
- package/dist/profile-invalid.js +15 -0
- package/dist/replica.d.ts +277 -0
- package/dist/replica.js +1039 -0
- package/dist/task.d.ts +159 -0
- package/dist/task.js +192 -0
- package/dist/validate.d.ts +70 -0
- package/dist/validate.js +196 -0
- package/dist/values.d.ts +52 -0
- package/dist/values.js +163 -0
- package/dist/wasm.d.ts +33 -0
- package/dist/wasm.js +58 -0
- package/package.json +59 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { LfcpError, toHex, } from "@openlfcp/core";
|
|
2
|
+
import { frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
|
|
3
|
+
import { SharedObjectsReplica } from "./replica.js";
|
|
4
|
+
import { ProfileInvalidError } from "./validate.js";
|
|
5
|
+
import { deriveActorId, PROFILE_ID } from "./values.js";
|
|
6
|
+
export class SharedObjectsDataProfile {
|
|
7
|
+
dataProfile = PROFILE_ID;
|
|
8
|
+
#replica;
|
|
9
|
+
/** Merged units: unit ID hex → change hash. */
|
|
10
|
+
#merged = new Map();
|
|
11
|
+
/** LFCP-accepted units waiting for Automerge dependencies, by unit ID hex. */
|
|
12
|
+
#pending = new Map();
|
|
13
|
+
#listeners = new Set();
|
|
14
|
+
constructor(replica) {
|
|
15
|
+
this.#replica = replica;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The local state to persist (structurally the storage ProfileCheckpoint,
|
|
19
|
+
* LFCP-034/035): the Automerge full save, this actor's sequence (the §9
|
|
20
|
+
* minSeq on restore) and which unit carried which merged change (for a
|
|
21
|
+
* G-EP7 rebuild). Units buffered for Automerge dependencies are not in it:
|
|
22
|
+
* they stay "profile-pending" in storage and are offered again on restore.
|
|
23
|
+
*/
|
|
24
|
+
checkpoint() {
|
|
25
|
+
return Object.freeze({
|
|
26
|
+
resourceId: this.#replica.resource,
|
|
27
|
+
dataProfile: PROFILE_ID,
|
|
28
|
+
state: this.#replica.save(),
|
|
29
|
+
actorSeq: this.#replica.actorSeq,
|
|
30
|
+
units: Object.freeze([...this.#merged.values()]
|
|
31
|
+
.map((m) => Object.freeze({ unitId: m.unitId, ref: m.hash }))
|
|
32
|
+
.sort((a, b) => (toHex(a.unitId) < toHex(b.unitId) ? -1 : 1))),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The handler of a persisted checkpoint. The replica refuses local writes
|
|
37
|
+
* if its actor is behind the checkpoint's sequence (§9).
|
|
38
|
+
*/
|
|
39
|
+
static restore(checkpoint, options) {
|
|
40
|
+
if (checkpoint.dataProfile !== PROFILE_ID)
|
|
41
|
+
throw new LfcpError("DATA_PROFILE_MISMATCH", `a ${checkpoint.dataProfile} checkpoint is not ${PROFILE_ID}`);
|
|
42
|
+
// This device's own persisted state: not held to the Snapshot limits (§13.1).
|
|
43
|
+
const replica = SharedObjectsReplica.fromSave(checkpoint.state, { ...options, minSeq: checkpoint.actorSeq }, "local-state");
|
|
44
|
+
const profile = new SharedObjectsDataProfile(replica);
|
|
45
|
+
for (const u of checkpoint.units) {
|
|
46
|
+
if (!replica.hasChange(u.ref))
|
|
47
|
+
throw new LfcpError("PROFILE_INVALID", `the checkpoint names change ${u.ref}, which its state lacks`);
|
|
48
|
+
profile.#merged.set(toHex(u.unitId), { unitId: u.unitId, hash: u.ref });
|
|
49
|
+
}
|
|
50
|
+
return profile;
|
|
51
|
+
}
|
|
52
|
+
/** The current replica (a G-EP7 rebuild replaces it). */
|
|
53
|
+
get replica() {
|
|
54
|
+
return this.#replica;
|
|
55
|
+
}
|
|
56
|
+
/** §98, §100: object change notifications for remote merges and rebuilds. */
|
|
57
|
+
onObjectChanged(listener) {
|
|
58
|
+
this.#listeners.add(listener);
|
|
59
|
+
return () => this.#listeners.delete(listener);
|
|
60
|
+
}
|
|
61
|
+
#emit(changes) {
|
|
62
|
+
for (const c of changes)
|
|
63
|
+
for (const l of this.#listeners)
|
|
64
|
+
l(c);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Records which change one of this client's OWN units carries (the unit
|
|
68
|
+
* was created from a local change, so it is merged already). A later
|
|
69
|
+
* G-EP7 exclusion of that unit then rebuilds the replica without it, and
|
|
70
|
+
* the checkpoint keeps the reference. Use it as createQueuedDataUnit's
|
|
71
|
+
* onCreated.
|
|
72
|
+
*/
|
|
73
|
+
recordLocal(unitId, change) {
|
|
74
|
+
if (!this.#replica.hasChange(change.hash))
|
|
75
|
+
throw new LfcpError("PROFILE_INVALID", "the change is not in this replica; record only local changes");
|
|
76
|
+
this.#merged.set(toHex(unitId), { unitId, hash: change.hash });
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The §13 Snapshot codec (structurally the wire DataProfileCodec): an
|
|
80
|
+
* Automerge full save framed as [1, save]; decoding requires a document
|
|
81
|
+
* chunk. For receiveSnapshot and createSnapshot.
|
|
82
|
+
*/
|
|
83
|
+
snapshotCodec() {
|
|
84
|
+
return {
|
|
85
|
+
dataProfile: PROFILE_ID,
|
|
86
|
+
encode: (save) => frameSnapshot(save),
|
|
87
|
+
decode: (plaintext) => unframeSnapshot(plaintext),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** The state to publish as a Snapshot: the replica's full save. */
|
|
91
|
+
snapshotState() {
|
|
92
|
+
return this.#replica.save();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Loads a received Snapshot's full save (§13, §66 step 3): the replica
|
|
96
|
+
* becomes the save merged with everything it held, and buffered units
|
|
97
|
+
* whose dependencies the Snapshot brings are merged. Returns the objects
|
|
98
|
+
* that changed and the units merged from the buffer.
|
|
99
|
+
*/
|
|
100
|
+
loadSnapshot(save) {
|
|
101
|
+
const before = this.#replica;
|
|
102
|
+
const { replica } = before.mergeSave(save);
|
|
103
|
+
this.#replica = replica;
|
|
104
|
+
const merged = [...this.#applyBatch([]).result.merged];
|
|
105
|
+
const changes = rebuildChanges(before, replica).map((c) => Object.freeze({ ...c, origin: "remote" }));
|
|
106
|
+
this.#emit(changes);
|
|
107
|
+
return Object.freeze({ objects: changes.map((c) => c.objectId), merged });
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Forgets the whole state (an empty replica that keeps the §9 sequence,
|
|
111
|
+
* no merged or buffered units), so it can be rebuilt from accepted units
|
|
112
|
+
* only (SNAP-EP: Snapshot-derived state dropped). Notifies the changes.
|
|
113
|
+
*/
|
|
114
|
+
reset() {
|
|
115
|
+
const before = this.#replica;
|
|
116
|
+
this.#replica = before.emptied();
|
|
117
|
+
this.#merged.clear();
|
|
118
|
+
this.#pending.clear();
|
|
119
|
+
this.#emit(rebuildChanges(before, this.#replica));
|
|
120
|
+
}
|
|
121
|
+
/** Whether this handler holds the unit's change (merged, recorded or buffered). */
|
|
122
|
+
has(unitId) {
|
|
123
|
+
const key = toHex(unitId);
|
|
124
|
+
return this.#merged.has(key) || this.#pending.has(key);
|
|
125
|
+
}
|
|
126
|
+
/** The units waiting for Automerge dependencies. */
|
|
127
|
+
pendingUnits() {
|
|
128
|
+
return [...this.#pending.values()].map((b) => b.unitId);
|
|
129
|
+
}
|
|
130
|
+
codecFor(unit) {
|
|
131
|
+
const actor = toHex(deriveActorId(unit.resourceId, unit.actor));
|
|
132
|
+
// §8, §11 (SO-SEC1): a unit carries only changes of its signer's §8
|
|
133
|
+
// actor, so no Principal can write into another Principal's Automerge
|
|
134
|
+
// history. Anything else is PROFILE_INVALID / CHANGE_ACTOR_MISMATCH.
|
|
135
|
+
const bound = (change) => {
|
|
136
|
+
if (change.actor !== actor)
|
|
137
|
+
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)`);
|
|
138
|
+
return change;
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
dataProfile: PROFILE_ID,
|
|
142
|
+
encode: (change) => frameChange(bound(change).bytes),
|
|
143
|
+
decode: (plaintext) => bound(unframeChange(plaintext)),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
apply(unit, change) {
|
|
147
|
+
const key = toHex(unit.unitId);
|
|
148
|
+
const r = this.#applyBatch([{ unit, value: change }]);
|
|
149
|
+
const own = r.refused.get(key);
|
|
150
|
+
if (own !== undefined)
|
|
151
|
+
throw own;
|
|
152
|
+
const self = r.result.merged.some((id) => toHex(id) === key);
|
|
153
|
+
if (!self)
|
|
154
|
+
return Object.freeze({
|
|
155
|
+
merged: r.result.merged,
|
|
156
|
+
objects: r.result.objects,
|
|
157
|
+
diagnostics: r.result.diagnostics,
|
|
158
|
+
pending: `waiting for Automerge changes ${r.missing.join(", ")}`,
|
|
159
|
+
});
|
|
160
|
+
// The unit first, then the buffered ones it released.
|
|
161
|
+
return Object.freeze({
|
|
162
|
+
merged: [unit.unitId, ...r.result.merged.filter((id) => toHex(id) !== key)],
|
|
163
|
+
objects: r.result.objects,
|
|
164
|
+
diagnostics: r.result.diagnostics,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Applies many accepted units at once (catch-up, store replay, a
|
|
169
|
+
* Snapshot's remainder): their changes and every buffered one go to the
|
|
170
|
+
* replica in one receiveChanges call, which hands Automerge one batch
|
|
171
|
+
* (§14.1 admission per change; one change per call is quadratic). A
|
|
172
|
+
* refused change rejects only its unit; units missing a dependency are
|
|
173
|
+
* buffered. Diagnostics are computed once, for the objects that changed.
|
|
174
|
+
*/
|
|
175
|
+
applyBatch(units) {
|
|
176
|
+
return this.#applyBatch(units).result;
|
|
177
|
+
}
|
|
178
|
+
#applyBatch(units) {
|
|
179
|
+
const byHash = new Map();
|
|
180
|
+
const offer = (unitId, change) => byHash.set(change.hash, [...(byHash.get(change.hash) ?? []), unitId]);
|
|
181
|
+
for (const b of this.#pending.values())
|
|
182
|
+
offer(b.unitId, b.change);
|
|
183
|
+
for (const { unit, value } of units) {
|
|
184
|
+
this.#pending.set(toHex(unit.unitId), { unitId: unit.unitId, change: value });
|
|
185
|
+
offer(unit.unitId, value);
|
|
186
|
+
}
|
|
187
|
+
const r = this.#replica.receiveChanges([...this.#pending.values()].map((b) => b.change));
|
|
188
|
+
const merged = [];
|
|
189
|
+
for (const c of [...r.applied, ...r.duplicates])
|
|
190
|
+
for (const unitId of byHash.get(c.hash) ?? []) {
|
|
191
|
+
this.#pending.delete(toHex(unitId));
|
|
192
|
+
this.#merged.set(toHex(unitId), { unitId, hash: c.hash });
|
|
193
|
+
merged.push(unitId);
|
|
194
|
+
}
|
|
195
|
+
const rejected = [];
|
|
196
|
+
const refused = new Map();
|
|
197
|
+
for (const { change, error } of r.refused)
|
|
198
|
+
for (const unitId of byHash.get(change.hash) ?? []) {
|
|
199
|
+
this.#pending.delete(toHex(unitId));
|
|
200
|
+
rejected.push({ unitId, code: error.code, message: error.message });
|
|
201
|
+
refused.set(toHex(unitId), error);
|
|
202
|
+
}
|
|
203
|
+
this.#emit(r.objects);
|
|
204
|
+
const objects = [...new Set(r.objects.map((c) => c.objectId))].sort();
|
|
205
|
+
const missing = [
|
|
206
|
+
...new Set(r.waiting.flatMap((c) => c.deps.filter((d) => !this.#replica.hasChange(d)))),
|
|
207
|
+
];
|
|
208
|
+
return {
|
|
209
|
+
result: Object.freeze({
|
|
210
|
+
merged: Object.freeze(merged),
|
|
211
|
+
pending: Object.freeze([...this.#pending.values()].map((b) => b.unitId)),
|
|
212
|
+
rejected: Object.freeze(rejected),
|
|
213
|
+
objects: Object.freeze(objects),
|
|
214
|
+
diagnostics: Object.freeze(this.#diagnostics(objects)),
|
|
215
|
+
}),
|
|
216
|
+
refused,
|
|
217
|
+
missing,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/** §74.1 problems and §21 collisions of the given objects (§77: the rest are unaffected). */
|
|
221
|
+
#diagnostics(objects) {
|
|
222
|
+
if (objects.length === 0)
|
|
223
|
+
return [];
|
|
224
|
+
const validation = this.#replica.validate();
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const id of objects) {
|
|
227
|
+
for (const p of validation.objects.get(id) ?? [])
|
|
228
|
+
out.push({
|
|
229
|
+
objectId: id,
|
|
230
|
+
code: p.code,
|
|
231
|
+
diagnostic: p.diagnostic,
|
|
232
|
+
pointer: p.pointer,
|
|
233
|
+
message: p.message,
|
|
234
|
+
});
|
|
235
|
+
if (validation.collisions.includes(id))
|
|
236
|
+
out.push({
|
|
237
|
+
objectId: id,
|
|
238
|
+
code: "OBJECT_ID_COLLISION",
|
|
239
|
+
message: `${id}: concurrent objects share this Object ID (§21)`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* §14.1 (G-EP7): the state without `unitIds`, rebuilt from the
|
|
246
|
+
* remaining changes. Merged units whose changes build on an excluded one
|
|
247
|
+
* go back to the buffer and are reported as pending.
|
|
248
|
+
*/
|
|
249
|
+
exclude(unitIds) {
|
|
250
|
+
const hashes = [];
|
|
251
|
+
for (const id of unitIds) {
|
|
252
|
+
const key = toHex(id);
|
|
253
|
+
this.#pending.delete(key);
|
|
254
|
+
const m = this.#merged.get(key);
|
|
255
|
+
if (m !== undefined) {
|
|
256
|
+
hashes.push(m.hash);
|
|
257
|
+
this.#merged.delete(key);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (hashes.length === 0)
|
|
261
|
+
return Object.freeze({ objects: [], pending: [] });
|
|
262
|
+
const before = this.#replica;
|
|
263
|
+
const { replica, unapplied } = before.rebuildWithout(hashes);
|
|
264
|
+
this.#replica = replica;
|
|
265
|
+
const pending = [];
|
|
266
|
+
for (const change of unapplied) {
|
|
267
|
+
const entry = [...this.#merged].find(([, m]) => m.hash === change.hash);
|
|
268
|
+
if (entry === undefined)
|
|
269
|
+
continue; // a local change: kept out with its dependency
|
|
270
|
+
this.#merged.delete(entry[0]);
|
|
271
|
+
this.#pending.set(entry[0], { unitId: entry[1].unitId, change });
|
|
272
|
+
pending.push(entry[1].unitId);
|
|
273
|
+
}
|
|
274
|
+
const changes = rebuildChanges(before, replica);
|
|
275
|
+
this.#emit(changes);
|
|
276
|
+
return Object.freeze({ objects: changes.map((c) => c.objectId), pending });
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/** §100 notifications for every object that differs between two replicas of one Resource. */
|
|
280
|
+
function rebuildChanges(before, after) {
|
|
281
|
+
const ids = [...new Set([...before.objectIds(), ...after.objectIds()])].sort();
|
|
282
|
+
const beforeConflicts = before.conflicts();
|
|
283
|
+
const afterConflicts = after.conflicts();
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const id of ids) {
|
|
286
|
+
const was = (before.getObject(id) ?? {});
|
|
287
|
+
const now = (after.getObject(id) ?? {});
|
|
288
|
+
const fields = [...new Set([...Object.keys(was), ...Object.keys(now)])]
|
|
289
|
+
.filter((f) => JSON.stringify(was[f]) !== JSON.stringify(now[f]))
|
|
290
|
+
.sort();
|
|
291
|
+
const wasC = Object.keys(beforeConflicts[id] ?? {});
|
|
292
|
+
const nowC = Object.keys(afterConflicts[id] ?? {});
|
|
293
|
+
if (fields.length === 0 && JSON.stringify(wasC) === JSON.stringify(nowC))
|
|
294
|
+
continue;
|
|
295
|
+
out.push(Object.freeze({
|
|
296
|
+
resource: after.resource,
|
|
297
|
+
objectId: id,
|
|
298
|
+
objectType: typeof now.type === "string"
|
|
299
|
+
? now.type
|
|
300
|
+
: typeof was.type === "string"
|
|
301
|
+
? was.type
|
|
302
|
+
: undefined,
|
|
303
|
+
fields: Object.freeze(fields),
|
|
304
|
+
conflictsAppeared: Object.freeze(nowC.filter((f) => !wasC.includes(f)).sort()),
|
|
305
|
+
conflictsDisappeared: Object.freeze(wasC.filter((f) => !nowC.includes(f)).sort()),
|
|
306
|
+
origin: "rebuild",
|
|
307
|
+
}));
|
|
308
|
+
}
|
|
309
|
+
return out;
|
|
310
|
+
}
|
|
311
|
+
//# sourceMappingURL=data-profile.js.map
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1): the Shared
|
|
3
|
+
* Task model and profile validation over logical state (LFCP-030), and its
|
|
4
|
+
* Automerge binding (LFCP-031).
|
|
5
|
+
*/
|
|
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";
|
|
9
|
+
export { type SharedObjectsApplyResult, type SharedObjectsBatchResult, type SharedObjectsCheckpoint, type SharedObjectsCodec, SharedObjectsDataProfile, type SharedObjectsDiagnostic, type SharedObjectsExcludeResult, type SharedObjectsSnapshotCodec, type SharedObjectsUnit, } from "./data-profile.js";
|
|
10
|
+
export { type BatchReceiveResult, type BuiltReplica, 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
|
+
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";
|
|
12
|
+
export { DIAGNOSTIC_ORDER, firstPerField, isMap, type Json, objectProblems, type ProfileDiagnostic, ProfileInvalidError, type ProfileProblem, pointerToken, type RootValidation, validateRoot, validateTransition, } from "./validate.js";
|
|
13
|
+
export { deriveActorId, FRAMING_VERSION, frameProfilePayload, isLocalDate, isNamespacedValue, isPrincipalRef, isReverseDomain, isUtcTimestamp, PROFILE_ID, type PrincipalRef, parsePrincipalRef, principalRef, unframeProfilePayload, } from "./values.js";
|
|
14
|
+
export { initializeAutomerge, isAutomergeInitialized } from "./wasm.js";
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1): the Shared
|
|
3
|
+
* Task model and profile validation over logical state (LFCP-030), and its
|
|
4
|
+
* Automerge binding (LFCP-031).
|
|
5
|
+
*/
|
|
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";
|
|
9
|
+
export { SharedObjectsDataProfile, } from "./data-profile.js";
|
|
10
|
+
export { ObjectIdCollisionError, resolveFieldConflict, SCALAR_FIELDS, SharedObjectsReplica, } from "./replica.js";
|
|
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";
|
|
12
|
+
export { DIAGNOSTIC_ORDER, firstPerField, isMap, objectProblems, ProfileInvalidError, pointerToken, validateRoot, validateTransition, } from "./validate.js";
|
|
13
|
+
export { deriveActorId, FRAMING_VERSION, frameProfilePayload, isLocalDate, isNamespacedValue, isPrincipalRef, isReverseDomain, isUtcTimestamp, PROFILE_ID, parsePrincipalRef, principalRef, unframeProfilePayload, } from "./values.js";
|
|
14
|
+
export { initializeAutomerge, isAutomergeInitialized } from "./wasm.js";
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LfcpError } from "@openlfcp/core";
|
|
2
|
+
import type { ProfileDiagnostic } from "./validate.js";
|
|
3
|
+
/**
|
|
4
|
+
* PROFILE_INVALID thrown for a rejected profile plaintext or value, with
|
|
5
|
+
* its §74.1 diagnostic: CHANGE_ACTOR_MISMATCH (§8, §11) or
|
|
6
|
+
* INVALID_AUTOMERGE_BYTES (§11, §13).
|
|
7
|
+
*/
|
|
8
|
+
export declare class ProfileInvalidError extends LfcpError {
|
|
9
|
+
readonly diagnostic: ProfileDiagnostic;
|
|
10
|
+
constructor(diagnostic: ProfileDiagnostic, message: string);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=profile-invalid.d.ts.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { LfcpError } from "@openlfcp/core";
|
|
2
|
+
/**
|
|
3
|
+
* PROFILE_INVALID thrown for a rejected profile plaintext or value, with
|
|
4
|
+
* its §74.1 diagnostic: CHANGE_ACTOR_MISMATCH (§8, §11) or
|
|
5
|
+
* INVALID_AUTOMERGE_BYTES (§11, §13).
|
|
6
|
+
*/
|
|
7
|
+
export class ProfileInvalidError extends LfcpError {
|
|
8
|
+
diagnostic;
|
|
9
|
+
constructor(diagnostic, message) {
|
|
10
|
+
super("PROFILE_INVALID", message);
|
|
11
|
+
this.name = "ProfileInvalidError";
|
|
12
|
+
this.diagnostic = diagnostic;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=profile-invalid.js.map
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import * as A from "@automerge/automerge";
|
|
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";
|
|
5
|
+
import { type Task, type TaskIntent } from "./task.js";
|
|
6
|
+
import { type Json, type ProfileProblem, type RootValidation } from "./validate.js";
|
|
7
|
+
/**
|
|
8
|
+
* The Automerge binding of SHARED-OBJECTS-PROFILE-01 (LFCP-031): one
|
|
9
|
+
* Resource's replica of the org.openlfcp.shared-objects.v1 document.
|
|
10
|
+
*
|
|
11
|
+
* - The actor is always the §8 actor of (Resource, Principal), set
|
|
12
|
+
* explicitly; §9 actor state safety is enforced with `minSeq`.
|
|
13
|
+
* - One semantic intent is exactly one Automerge change (§10, §12), with
|
|
14
|
+
* time 0 (informational, and a wall clock would leak edit times) and the
|
|
15
|
+
* intent name as message. An intent that changes nothing makes no change.
|
|
16
|
+
* - Writes touch single properties only, so unknown fields, extension
|
|
17
|
+
* namespaces and object types survive (§70-§72). Objects are never
|
|
18
|
+
* removed: deletion is the lifecycle tombstone (§54-§56).
|
|
19
|
+
* - Scalar conflicts stay visible (§44-§47) and are resolved by a new
|
|
20
|
+
* causal write (§69); tags and assignees are add-wins maps (§39-§43).
|
|
21
|
+
*
|
|
22
|
+
* Receiving is pure profile work: LFCP verification, decryption and
|
|
23
|
+
* authorization come first (§95, LFCP-033).
|
|
24
|
+
*/
|
|
25
|
+
/** The conflict-preserving scalar registers of a Task (§44). */
|
|
26
|
+
export declare const SCALAR_FIELDS: readonly ["lifecycle", "title", "status", "due", "scheduled", "completion_date", "priority"];
|
|
27
|
+
export type ScalarField = (typeof SCALAR_FIELDS)[number];
|
|
28
|
+
/** §69 task.resolve_field_conflict: write `value` (null clears a date) after the merged conflicts. */
|
|
29
|
+
export interface ResolveFieldConflict {
|
|
30
|
+
readonly intent: "task.resolve_field_conflict";
|
|
31
|
+
readonly id: ObjectId;
|
|
32
|
+
readonly field: ScalarField;
|
|
33
|
+
readonly value: string | null;
|
|
34
|
+
}
|
|
35
|
+
export type ReplicaIntent = TaskIntent | ResolveFieldConflict;
|
|
36
|
+
/** task.resolve_field_conflict (§69). */
|
|
37
|
+
export declare function resolveFieldConflict(id: ObjectId, field: ScalarField, value: string | null): ResolveFieldConflict;
|
|
38
|
+
/** §21: two concurrent objects under one Object ID: OBJECT_ID_COLLISION, a profile error of its own, not PROFILE_INVALID. */
|
|
39
|
+
export declare class ObjectIdCollisionError extends LfcpError {
|
|
40
|
+
readonly objectId: string;
|
|
41
|
+
constructor(objectId: string);
|
|
42
|
+
}
|
|
43
|
+
/** §99: a scalar register's provisional value and every concurrent value. */
|
|
44
|
+
export interface ScalarView {
|
|
45
|
+
/** Automerge's deterministic visible value; provisional while conflicted. Undefined when absent. */
|
|
46
|
+
readonly value: Json | undefined;
|
|
47
|
+
/** Every concurrent value, sorted by JSON: none when absent, one when resolved. */
|
|
48
|
+
readonly values: readonly Json[];
|
|
49
|
+
readonly conflicted: boolean;
|
|
50
|
+
}
|
|
51
|
+
export type ObjectStatus = "ready" | "profile_invalid" | "object_id_collision";
|
|
52
|
+
/** §99: a Task with its conflict metadata. */
|
|
53
|
+
export interface TaskView {
|
|
54
|
+
readonly id: string;
|
|
55
|
+
readonly status: ObjectStatus;
|
|
56
|
+
readonly problems: readonly ProfileProblem[];
|
|
57
|
+
/** The provisional Task, when every visible value is valid. */
|
|
58
|
+
readonly task: Task | undefined;
|
|
59
|
+
readonly fields: {
|
|
60
|
+
readonly [F in ScalarField]: ScalarView;
|
|
61
|
+
};
|
|
62
|
+
readonly tags: readonly string[];
|
|
63
|
+
readonly assignees: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
/** §100: what one change did to one object. */
|
|
66
|
+
export interface ObjectChange {
|
|
67
|
+
readonly resource: ResourceId;
|
|
68
|
+
readonly objectId: string;
|
|
69
|
+
readonly objectType: string | undefined;
|
|
70
|
+
/** Top-level fields written, sorted. */
|
|
71
|
+
readonly fields: readonly string[];
|
|
72
|
+
readonly conflictsAppeared: readonly string[];
|
|
73
|
+
readonly conflictsDisappeared: readonly string[];
|
|
74
|
+
/** "rebuild": the state was rebuilt without some changes (§14.1, G-EP7). */
|
|
75
|
+
readonly origin: "local" | "remote" | "rebuild";
|
|
76
|
+
}
|
|
77
|
+
/** A local intent's Automerge change, ready to become a Data Unit. */
|
|
78
|
+
export interface LocalChange {
|
|
79
|
+
readonly intent: string;
|
|
80
|
+
/** The exact Automerge change bytes. */
|
|
81
|
+
readonly change: Uint8Array;
|
|
82
|
+
/** §11: the Data Unit plaintext [1, change]. */
|
|
83
|
+
readonly plaintext: Uint8Array;
|
|
84
|
+
readonly hash: string;
|
|
85
|
+
readonly seq: number;
|
|
86
|
+
readonly objects: readonly ObjectChange[];
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The outcome of receiving one change. A change whose dependencies are not
|
|
90
|
+
* all here is not applied and not invalid: the caller buffers it and offers
|
|
91
|
+
* it again later (LFCP-033).
|
|
92
|
+
*/
|
|
93
|
+
export type ReceiveResult = {
|
|
94
|
+
readonly status: "applied";
|
|
95
|
+
readonly change: CheckedChange;
|
|
96
|
+
readonly objects: readonly ObjectChange[];
|
|
97
|
+
} | {
|
|
98
|
+
readonly status: "duplicate";
|
|
99
|
+
readonly change: CheckedChange;
|
|
100
|
+
} | {
|
|
101
|
+
readonly status: "missing_dependencies";
|
|
102
|
+
readonly change: CheckedChange;
|
|
103
|
+
readonly missing: readonly string[];
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The outcome of receiving many changes at once (receiveChanges): the
|
|
107
|
+
* changes merged, those already held, those still missing a dependency
|
|
108
|
+
* (not handed to Automerge, §14.1), and those refused with their error.
|
|
109
|
+
*/
|
|
110
|
+
export interface BatchReceiveResult {
|
|
111
|
+
readonly applied: readonly CheckedChange[];
|
|
112
|
+
readonly duplicates: readonly CheckedChange[];
|
|
113
|
+
readonly waiting: readonly CheckedChange[];
|
|
114
|
+
readonly refused: readonly {
|
|
115
|
+
readonly change: CheckedChange;
|
|
116
|
+
readonly error: LfcpError;
|
|
117
|
+
}[];
|
|
118
|
+
/** §100 notifications for everything the batch changed. */
|
|
119
|
+
readonly objects: readonly ObjectChange[];
|
|
120
|
+
}
|
|
121
|
+
/** Root validation plus §21 collisions and the G-SC3 scalar-string rule. */
|
|
122
|
+
export interface ReplicaValidation extends RootValidation {
|
|
123
|
+
/** Object IDs with concurrent objects (OBJECT_ID_COLLISION, §21). */
|
|
124
|
+
readonly collisions: readonly string[];
|
|
125
|
+
}
|
|
126
|
+
export interface ReplicaOptions {
|
|
127
|
+
readonly resource: ResourceId;
|
|
128
|
+
/** The writing Principal; with the Resource it fixes the §8 actor. */
|
|
129
|
+
readonly principal: PrincipalId;
|
|
130
|
+
/**
|
|
131
|
+
* §9: the highest change sequence this actor is known to have used
|
|
132
|
+
* (persisted by the caller, LFCP-035). If the loaded state is behind it,
|
|
133
|
+
* local writes are refused: they would start an unrelated history under
|
|
134
|
+
* sequences already used.
|
|
135
|
+
*/
|
|
136
|
+
readonly minSeq?: number;
|
|
137
|
+
}
|
|
138
|
+
/** A replica built from a change set; `unapplied` lacks dependencies in that set. */
|
|
139
|
+
export interface BuiltReplica {
|
|
140
|
+
readonly replica: SharedObjectsReplica;
|
|
141
|
+
readonly unapplied: readonly CheckedChange[];
|
|
142
|
+
}
|
|
143
|
+
type Doc = A.Doc<Record<string, unknown>>;
|
|
144
|
+
/** §30: an object's maps and lists nest at most this many levels (the field's own map is 1). */
|
|
145
|
+
export declare const MAX_VALUE_DEPTH = 64;
|
|
146
|
+
/**
|
|
147
|
+
* Applies one change that passed the receive checks. Automerge can throw
|
|
148
|
+
* after changing the document in place (a change that skips a sequence
|
|
149
|
+
* number stays in its graph without its operations), so on any engine error
|
|
150
|
+
* the handle is dropped: the document is rebuilt from the changes it held,
|
|
151
|
+
* which must give back the heads it had. A wasm trap is rethrown as it
|
|
152
|
+
* is: the module is dead, and only a restart recovers. Exported for tests.
|
|
153
|
+
*/
|
|
154
|
+
export declare function applyChecked(doc: Doc, bytes: Uint8Array): {
|
|
155
|
+
readonly next: Doc;
|
|
156
|
+
} | {
|
|
157
|
+
readonly restored: Doc;
|
|
158
|
+
readonly error: Error;
|
|
159
|
+
};
|
|
160
|
+
/**
|
|
161
|
+
* applyChecked for a batch of admitted changes, in one engine call. On any
|
|
162
|
+
* engine error the handle is dropped and the document as it was before is
|
|
163
|
+
* rebuilt from the changes it held minus the batch, checked against the
|
|
164
|
+
* heads it had. Exported for tests.
|
|
165
|
+
*/
|
|
166
|
+
export declare function applyBatchChecked(doc: Doc, batch: readonly CheckedChange[]): {
|
|
167
|
+
readonly next: Doc;
|
|
168
|
+
} | {
|
|
169
|
+
readonly restored: Doc;
|
|
170
|
+
readonly error: Error;
|
|
171
|
+
};
|
|
172
|
+
export declare class SharedObjectsReplica {
|
|
173
|
+
#private;
|
|
174
|
+
readonly resource: ResourceId;
|
|
175
|
+
/** The §8 Automerge actor ID. */
|
|
176
|
+
readonly actorId: Uint8Array;
|
|
177
|
+
private constructor();
|
|
178
|
+
/** §16: a new Resource document. The initial change is the Resource's first Data Unit. */
|
|
179
|
+
static create(opts: ReplicaOptions): {
|
|
180
|
+
replica: SharedObjectsReplica;
|
|
181
|
+
change: LocalChange;
|
|
182
|
+
};
|
|
183
|
+
/** A replica with no state yet, that receives the Resource's changes. */
|
|
184
|
+
static empty(opts: ReplicaOptions): SharedObjectsReplica;
|
|
185
|
+
/**
|
|
186
|
+
* Loads an Automerge full save: a received Snapshot image (§13), checked
|
|
187
|
+
* against `limits` (§13.1, at least the floor) before Automerge loads it,
|
|
188
|
+
* or this device's own persisted state ("local-state", not limited).
|
|
189
|
+
* Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES.
|
|
190
|
+
*/
|
|
191
|
+
static fromSave(save: Uint8Array, opts: ReplicaOptions, limits?: SnapshotLimits | "local-state"): SharedObjectsReplica;
|
|
192
|
+
/** §13: loads a Snapshot plaintext [1, save]; later Data Units are received after it. */
|
|
193
|
+
static fromSnapshot(plaintext: Uint8Array, opts: ReplicaOptions): SharedObjectsReplica;
|
|
194
|
+
/**
|
|
195
|
+
* The replica of exactly an accepted change set: its state depends only
|
|
196
|
+
* on the set, not on the order. Changes whose dependencies are not in the
|
|
197
|
+
* set are returned as unapplied.
|
|
198
|
+
*/
|
|
199
|
+
static fromChanges(changes: Iterable<Uint8Array>, opts: ReplicaOptions): BuiltReplica;
|
|
200
|
+
/**
|
|
201
|
+
* §14.1 (G-EP7): this replica rebuilt from its own changes minus
|
|
202
|
+
* `exclude` (change hashes). Changes that depend on an excluded change are
|
|
203
|
+
* unapplied too. An excluded change of this actor is not lost state (§9):
|
|
204
|
+
* the rebuilt replica writes on from its own sequence, reusing the
|
|
205
|
+
* sequence numbers of the removed changes. A replica that was already
|
|
206
|
+
* behind its persisted sequence stays refused.
|
|
207
|
+
*/
|
|
208
|
+
rebuildWithout(exclude: Iterable<string>): BuiltReplica;
|
|
209
|
+
/**
|
|
210
|
+
* This replica merged with an Automerge full save (a loaded Snapshot,
|
|
211
|
+
* §13): the save's document plus every change of this replica, so local
|
|
212
|
+
* work the Snapshot does not hold is kept. The §9 sequence carries over.
|
|
213
|
+
* Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES when the save does not load.
|
|
214
|
+
*/
|
|
215
|
+
mergeSave(save: Uint8Array): BuiltReplica;
|
|
216
|
+
/** An empty replica of the same Resource and actor that keeps the §9 sequence (nothing reused). */
|
|
217
|
+
emptied(): SharedObjectsReplica;
|
|
218
|
+
/** The highest change sequence of this replica's own actor in its state. */
|
|
219
|
+
get actorSeq(): number;
|
|
220
|
+
/** §9: false when the state is behind the persisted sequence, so local writes are refused. */
|
|
221
|
+
get writable(): boolean;
|
|
222
|
+
/** Hex hashes of the current heads, sorted. */
|
|
223
|
+
heads(): string[];
|
|
224
|
+
hasChange(hash: string): boolean;
|
|
225
|
+
/** Every change of the state, dependencies first. */
|
|
226
|
+
changes(): Uint8Array[];
|
|
227
|
+
/** The Automerge full save of the state. */
|
|
228
|
+
save(): Uint8Array;
|
|
229
|
+
/** §13: the Snapshot plaintext [1, save]. */
|
|
230
|
+
snapshot(): Uint8Array;
|
|
231
|
+
/**
|
|
232
|
+
* The logical root: scalar strings (and Text) read as strings, object keys
|
|
233
|
+
* in canonical order, so JSON.stringify(root()) is comparable across
|
|
234
|
+
* replicas holding the same state.
|
|
235
|
+
*/
|
|
236
|
+
root(): Json;
|
|
237
|
+
/** The logical object stored under `id` (its visible value), if any. */
|
|
238
|
+
getObject(id: string): Json | undefined;
|
|
239
|
+
objectIds(): string[];
|
|
240
|
+
/** §45: every conflicted top-level field of every object, with its concurrent values sorted. */
|
|
241
|
+
conflicts(): Record<string, Record<string, Json[]>>;
|
|
242
|
+
/** §21: Object IDs under which concurrent objects were created. */
|
|
243
|
+
collisions(): string[];
|
|
244
|
+
/** §73-§77 validation of the visible state, every concurrent scalar value and G-SC3, plus §21 collisions. */
|
|
245
|
+
validate(): ReplicaValidation;
|
|
246
|
+
/** §99: the Task under `id` with conflict metadata, or undefined if there is no object or it is not a Task. */
|
|
247
|
+
task(id: string): TaskView | undefined;
|
|
248
|
+
/**
|
|
249
|
+
* Applies one intent as exactly one Automerge change (§10) and returns it,
|
|
250
|
+
* or null when the intent changes nothing (removing an absent tag,
|
|
251
|
+
* clearing an absent date). The resulting object must be profile-valid;
|
|
252
|
+
* an object already invalid or collided is not written (§76, §21) unless
|
|
253
|
+
* the write repairs it.
|
|
254
|
+
*/
|
|
255
|
+
apply(intent: ReplicaIntent): LocalChange | null;
|
|
256
|
+
/** §11: receives a Data Unit plaintext [1, change]. Throws PROFILE_INVALID / INVALID_AUTOMERGE_BYTES for invalid bytes. */
|
|
257
|
+
receive(plaintext: Uint8Array): ReceiveResult;
|
|
258
|
+
/** Receives one bare Automerge change (persisted or reference bytes). */
|
|
259
|
+
receiveChange(bytes: Uint8Array): ReceiveResult;
|
|
260
|
+
/**
|
|
261
|
+
* Receives many changes at once: store replay, catch-up, a Snapshot's
|
|
262
|
+
* remainder. Same rules as receiveChange (§14.1: no change with a
|
|
263
|
+
* missing dependency reaches Automerge; a taken sequence is
|
|
264
|
+
* ACTOR_EQUIVOCATION; a skipped one INVALID_AUTOMERGE_BYTES), but the
|
|
265
|
+
* admitted changes go to Automerge in ONE call: one change per call is
|
|
266
|
+
* quadratic (10 000 changes: ~12 s against ~70 ms as one batch).
|
|
267
|
+
*
|
|
268
|
+
* The changes may come in any order, duplicated: each is admitted once
|
|
269
|
+
* its dependencies inside the batch are (Kahn's order). A refused change
|
|
270
|
+
* does not stop the others; changes that depend on it wait. If Automerge
|
|
271
|
+
* fails anyway, the document is restored and the admitted changes are
|
|
272
|
+
* received one at a time, which isolates the failing one.
|
|
273
|
+
*/
|
|
274
|
+
receiveChanges(changes: Iterable<Uint8Array | CheckedChange>): BatchReceiveResult;
|
|
275
|
+
}
|
|
276
|
+
export {};
|
|
277
|
+
//# sourceMappingURL=replica.d.ts.map
|