@openlfcp/shared-objects 0.1.1 → 0.1.3
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 +2 -2
- package/dist/data-profile.d.ts +14 -3
- package/dist/data-profile.js +43 -5
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ reaches the replica (§95). The replica has no Markdown or editor concepts.
|
|
|
20
20
|
|
|
21
21
|
Part of the OpenLFCP TypeScript SDK, which implements the OpenLFCP MVP 0.1
|
|
22
22
|
subset of [LFCP-WIRE-01](https://github.com/openlfcp/spec/blob/main/wire/LFCP-WIRE-01.md)
|
|
23
|
-
at `mvp-0.1-baseline.
|
|
23
|
+
at `mvp-0.1-baseline.9`, not every deferred WIRE-01 feature. It does not
|
|
24
24
|
claim full LFCP-WIRE-01 conformance. This is the MVP 0.1 release
|
|
25
|
-
(`0.1.
|
|
25
|
+
(`0.1.3`, npm dist-tag `latest`). Until 1.0, minor versions may change APIs.
|
|
26
26
|
|
|
27
27
|
## Automerge and WebAssembly
|
|
28
28
|
|
package/dist/data-profile.d.ts
CHANGED
|
@@ -14,7 +14,10 @@ import { type ObjectChange, type ReplicaOptions, SharedObjectsReplica } from "./
|
|
|
14
14
|
* every buffered change it unblocks merges with it;
|
|
15
15
|
* - one object becoming profile-invalid is a diagnostic, never a refusal:
|
|
16
16
|
* the other objects stay usable (§77);
|
|
17
|
-
* - exclude (§14.1, G-EP7): rebuilds the replica without given units
|
|
17
|
+
* - exclude (§14.1, G-EP7): rebuilds the replica without given units;
|
|
18
|
+
* - a change whose actor and sequence number another change of the
|
|
19
|
+
* document already has is held (§14.1, POST-001): never merged, not
|
|
20
|
+
* refused, and retried after every exclude.
|
|
18
21
|
*/
|
|
19
22
|
/** An accepted unit as the applier passes it (structurally the client's ProfileUnit). */
|
|
20
23
|
export interface SharedObjectsUnit {
|
|
@@ -32,6 +35,8 @@ export interface SharedObjectsApplyResult {
|
|
|
32
35
|
readonly objects: readonly string[];
|
|
33
36
|
readonly diagnostics: readonly SharedObjectsDiagnostic[];
|
|
34
37
|
readonly pending?: string;
|
|
38
|
+
/** Why the unit is held (§14.1): its actor and sequence number belong to a different change. */
|
|
39
|
+
readonly held?: string;
|
|
35
40
|
}
|
|
36
41
|
/** The outcome of applyBatch: per unit merged, pending or rejected, and the objects once. */
|
|
37
42
|
export interface SharedObjectsBatchResult {
|
|
@@ -39,7 +44,9 @@ export interface SharedObjectsBatchResult {
|
|
|
39
44
|
readonly merged: readonly DataUnitId[];
|
|
40
45
|
/** Units buffered for Automerge dependencies (the batch's and earlier ones still waiting). */
|
|
41
46
|
readonly pending: readonly DataUnitId[];
|
|
42
|
-
/** Units
|
|
47
|
+
/** Units held because a different change has their actor and sequence number (§14.1), all of them. */
|
|
48
|
+
readonly held: readonly DataUnitId[];
|
|
49
|
+
/** Units whose change was refused (INVALID_AUTOMERGE_BYTES, …). */
|
|
43
50
|
readonly rejected: readonly {
|
|
44
51
|
readonly unitId: DataUnitId;
|
|
45
52
|
readonly code: string;
|
|
@@ -51,6 +58,8 @@ export interface SharedObjectsBatchResult {
|
|
|
51
58
|
export interface SharedObjectsExcludeResult {
|
|
52
59
|
readonly objects: readonly string[];
|
|
53
60
|
readonly pending: readonly DataUnitId[];
|
|
61
|
+
/** Held units that merged after the rebuild (§14.1): their actor and sequence number were free again. */
|
|
62
|
+
readonly released: readonly DataUnitId[];
|
|
54
63
|
}
|
|
55
64
|
/** A persisted handler state (structurally the storage ProfileCheckpoint). */
|
|
56
65
|
export interface SharedObjectsCheckpoint {
|
|
@@ -128,8 +137,10 @@ export declare class SharedObjectsDataProfile {
|
|
|
128
137
|
* only (SNAP-EP: Snapshot-derived state dropped). Notifies the changes.
|
|
129
138
|
*/
|
|
130
139
|
reset(): void;
|
|
131
|
-
/** Whether this handler holds the unit's change (merged, recorded or
|
|
140
|
+
/** Whether this handler holds the unit's change (merged, recorded, buffered or held). */
|
|
132
141
|
has(unitId: DataUnitId): boolean;
|
|
142
|
+
/** The units held because a different change has their actor and sequence number (§14.1). */
|
|
143
|
+
heldUnits(): DataUnitId[];
|
|
133
144
|
/** The units waiting for Automerge dependencies. */
|
|
134
145
|
pendingUnits(): DataUnitId[];
|
|
135
146
|
codecFor(unit: {
|
package/dist/data-profile.js
CHANGED
|
@@ -10,6 +10,8 @@ export class SharedObjectsDataProfile {
|
|
|
10
10
|
#merged = new Map();
|
|
11
11
|
/** LFCP-accepted units waiting for Automerge dependencies, by unit ID hex. */
|
|
12
12
|
#pending = new Map();
|
|
13
|
+
/** LFCP-accepted units whose actor and sequence number a different change has (§14.1), by unit ID hex. */
|
|
14
|
+
#held = new Map();
|
|
13
15
|
#listeners = new Set();
|
|
14
16
|
constructor(replica) {
|
|
15
17
|
this.#replica = replica;
|
|
@@ -116,12 +118,17 @@ export class SharedObjectsDataProfile {
|
|
|
116
118
|
this.#replica = before.emptied();
|
|
117
119
|
this.#merged.clear();
|
|
118
120
|
this.#pending.clear();
|
|
121
|
+
this.#held.clear();
|
|
119
122
|
this.#emit(rebuildChanges(before, this.#replica));
|
|
120
123
|
}
|
|
121
|
-
/** Whether this handler holds the unit's change (merged, recorded or
|
|
124
|
+
/** Whether this handler holds the unit's change (merged, recorded, buffered or held). */
|
|
122
125
|
has(unitId) {
|
|
123
126
|
const key = toHex(unitId);
|
|
124
|
-
return this.#merged.has(key) || this.#pending.has(key);
|
|
127
|
+
return this.#merged.has(key) || this.#pending.has(key) || this.#held.has(key);
|
|
128
|
+
}
|
|
129
|
+
/** The units held because a different change has their actor and sequence number (§14.1). */
|
|
130
|
+
heldUnits() {
|
|
131
|
+
return [...this.#held.values()].map((b) => b.unitId);
|
|
125
132
|
}
|
|
126
133
|
/** The units waiting for Automerge dependencies. */
|
|
127
134
|
pendingUnits() {
|
|
@@ -149,6 +156,13 @@ export class SharedObjectsDataProfile {
|
|
|
149
156
|
const own = r.refused.get(key);
|
|
150
157
|
if (own !== undefined)
|
|
151
158
|
throw own;
|
|
159
|
+
if (this.#held.has(key))
|
|
160
|
+
return Object.freeze({
|
|
161
|
+
merged: r.result.merged,
|
|
162
|
+
objects: r.result.objects,
|
|
163
|
+
diagnostics: r.result.diagnostics,
|
|
164
|
+
held: `held: a different change has actor ${change.actor} sequence ${change.seq} (§14.1)`,
|
|
165
|
+
});
|
|
152
166
|
const self = r.result.merged.some((id) => toHex(id) === key);
|
|
153
167
|
if (!self)
|
|
154
168
|
return Object.freeze({
|
|
@@ -175,9 +189,15 @@ export class SharedObjectsDataProfile {
|
|
|
175
189
|
applyBatch(units) {
|
|
176
190
|
return this.#applyBatch(units).result;
|
|
177
191
|
}
|
|
178
|
-
#applyBatch(units) {
|
|
192
|
+
#applyBatch(units, retryHeld = false) {
|
|
179
193
|
const byHash = new Map();
|
|
180
194
|
const offer = (unitId, change) => byHash.set(change.hash, [...(byHash.get(change.hash) ?? []), unitId]);
|
|
195
|
+
// §14.1: after a rebuild, held changes are offered again with the rest.
|
|
196
|
+
if (retryHeld) {
|
|
197
|
+
for (const [key, b] of this.#held)
|
|
198
|
+
this.#pending.set(key, b);
|
|
199
|
+
this.#held.clear();
|
|
200
|
+
}
|
|
181
201
|
for (const b of this.#pending.values())
|
|
182
202
|
offer(b.unitId, b.change);
|
|
183
203
|
for (const { unit, value } of units) {
|
|
@@ -197,6 +217,11 @@ export class SharedObjectsDataProfile {
|
|
|
197
217
|
for (const { change, error } of r.refused)
|
|
198
218
|
for (const unitId of byHash.get(change.hash) ?? []) {
|
|
199
219
|
this.#pending.delete(toHex(unitId));
|
|
220
|
+
// §14.1 (POST-001): another change has its actor and sequence number: held, not refused.
|
|
221
|
+
if (error.code === "ACTOR_EQUIVOCATION") {
|
|
222
|
+
this.#held.set(toHex(unitId), { unitId, change });
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
200
225
|
rejected.push({ unitId, code: error.code, message: error.message });
|
|
201
226
|
refused.set(toHex(unitId), error);
|
|
202
227
|
}
|
|
@@ -209,6 +234,7 @@ export class SharedObjectsDataProfile {
|
|
|
209
234
|
result: Object.freeze({
|
|
210
235
|
merged: Object.freeze(merged),
|
|
211
236
|
pending: Object.freeze([...this.#pending.values()].map((b) => b.unitId)),
|
|
237
|
+
held: Object.freeze(this.heldUnits()),
|
|
212
238
|
rejected: Object.freeze(rejected),
|
|
213
239
|
objects: Object.freeze(objects),
|
|
214
240
|
diagnostics: Object.freeze(this.#diagnostics(objects)),
|
|
@@ -251,6 +277,7 @@ export class SharedObjectsDataProfile {
|
|
|
251
277
|
for (const id of unitIds) {
|
|
252
278
|
const key = toHex(id);
|
|
253
279
|
this.#pending.delete(key);
|
|
280
|
+
this.#held.delete(key);
|
|
254
281
|
const m = this.#merged.get(key);
|
|
255
282
|
if (m !== undefined) {
|
|
256
283
|
hashes.push(m.hash);
|
|
@@ -258,7 +285,7 @@ export class SharedObjectsDataProfile {
|
|
|
258
285
|
}
|
|
259
286
|
}
|
|
260
287
|
if (hashes.length === 0)
|
|
261
|
-
return Object.freeze({ objects: [], pending: [] });
|
|
288
|
+
return Object.freeze({ objects: [], pending: [], released: [] });
|
|
262
289
|
const before = this.#replica;
|
|
263
290
|
const { replica, unapplied } = before.rebuildWithout(hashes);
|
|
264
291
|
this.#replica = replica;
|
|
@@ -273,7 +300,18 @@ export class SharedObjectsDataProfile {
|
|
|
273
300
|
}
|
|
274
301
|
const changes = rebuildChanges(before, replica);
|
|
275
302
|
this.#emit(changes);
|
|
276
|
-
|
|
303
|
+
// §14.1: the rebuild may have freed held changes' actor sequences.
|
|
304
|
+
const held = new Set(this.#held.keys());
|
|
305
|
+
const retried = held.size > 0 ? this.#applyBatch([], true) : null;
|
|
306
|
+
const released = (retried?.result.merged ?? []).filter((id) => held.has(toHex(id)));
|
|
307
|
+
const objects = [
|
|
308
|
+
...new Set([...changes.map((c) => c.objectId), ...(retried?.result.objects ?? [])]),
|
|
309
|
+
].sort();
|
|
310
|
+
return Object.freeze({
|
|
311
|
+
objects,
|
|
312
|
+
pending: pending.filter((id) => !(retried?.result.merged ?? []).some((m) => toHex(m) === toHex(id))),
|
|
313
|
+
released: Object.freeze(released),
|
|
314
|
+
});
|
|
277
315
|
}
|
|
278
316
|
}
|
|
279
317
|
/** §100 notifications for every object that differs between two replicas of one Resource. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@automerge/automerge": "3.5.0",
|
|
47
47
|
"fflate": "0.8.3",
|
|
48
|
-
"@openlfcp/core": "^0.1.
|
|
49
|
-
"@openlfcp/crypto": "^0.1.
|
|
48
|
+
"@openlfcp/core": "^0.1.3",
|
|
49
|
+
"@openlfcp/crypto": "^0.1.3"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"fast-check": "4.10.2"
|