@openlfcp/shared-objects 0.1.4 → 0.1.5

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/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export declare const PACKAGE = "@openlfcp/shared-objects";
7
7
  export { type CheckedChange, checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
8
8
  export { CHANGE_LIMITS, type ChunkExpansion, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, type SnapshotLimits, } from "./chunk-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
- 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";
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";
12
12
  export { DIAGNOSTIC_ORDER, firstPerField, isMap, type Json, objectProblems, type ProfileDiagnostic, ProfileInvalidError, type ProfileProblem, pointerToken, type RootValidation, validateRoot, validateTransition, } from "./validate.js";
13
13
  export { deriveActorId, FRAMING_VERSION, frameProfilePayload, isLocalDate, isNamespacedValue, isPrincipalRef, isReverseDomain, isUtcTimestamp, PROFILE_ID, type PrincipalRef, parsePrincipalRef, principalRef, unframeProfilePayload, } from "./values.js";
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ export const PACKAGE = "@openlfcp/shared-objects";
7
7
  export { checkChange, checkSaveHeader, frameChange, frameSnapshot, unframeChange, unframeSnapshot, } from "./automerge-bytes.js";
8
8
  export { CHANGE_LIMITS, checkChangeExpansion, checkSnapshotExpansion, MAX_DOCUMENT_DEPTH, SNAPSHOT_LIMITS_FLOOR, } from "./chunk-limits.js";
9
9
  export { SharedObjectsDataProfile, } from "./data-profile.js";
10
- export { ObjectIdCollisionError, resolveFieldConflict, SCALAR_FIELDS, SharedObjectsReplica, } from "./replica.js";
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";
12
12
  export { DIAGNOSTIC_ORDER, firstPerField, isMap, objectProblems, ProfileInvalidError, pointerToken, validateRoot, validateTransition, } from "./validate.js";
13
13
  export { deriveActorId, FRAMING_VERSION, frameProfilePayload, isLocalDate, isNamespacedValue, isPrincipalRef, isReverseDomain, isUtcTimestamp, PROFILE_ID, parsePrincipalRef, principalRef, unframeProfilePayload, } from "./values.js";
@@ -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";
4
+ export type ReferenceRule = "R1" | "R2" | "R3" | "R4" | "R5" | "R6" | "R7" | "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
@@ -137,6 +137,11 @@ export class ReferenceHistory {
137
137
  };
138
138
  for (const [i, op] of change.ops.entries()) {
139
139
  const id = `${change.startOp + i}@${change.actor}`;
140
+ // R10: no operation makes a table. Automerge JS 3.5.0 cannot write a
141
+ // table back out and aborts applying one (differential fuzzing D1); a
142
+ // writer of this profile never makes one.
143
+ if (op.action === 6)
144
+ return "R10";
140
145
  // R3: the root, or an object an operation made; its key form.
141
146
  let sequence;
142
147
  if (op.obj === "_root")
package/dist/replica.d.ts CHANGED
@@ -143,6 +143,12 @@ export interface BuiltReplica {
143
143
  type Doc = A.Doc<Record<string, unknown>>;
144
144
  /** §30: an object's maps and lists nest at most this many levels (the field's own map is 1). */
145
145
  export declare const MAX_VALUE_DEPTH = 64;
146
+ /**
147
+ * Whether a thrown value is a wasm trap (the engine's module is dead, not
148
+ * the change refused): it is rethrown as it is, never turned into
149
+ * INVALID_AUTOMERGE_BYTES, so a crash-loop breaker sees it.
150
+ */
151
+ export declare function isWasmTrap(e: unknown): boolean;
146
152
  /**
147
153
  * Applies one change that passed the receive checks. Automerge can throw
148
154
  * after changing the document in place (a change that skips a sequence
package/dist/replica.js CHANGED
@@ -314,7 +314,7 @@ const actorHex = (opts) => toHex(deriveActorId(opts.resource, opts.principal));
314
314
  * the change refused): it is rethrown as it is, never turned into
315
315
  * INVALID_AUTOMERGE_BYTES, so a crash-loop breaker sees it.
316
316
  */
317
- function isWasmTrap(e) {
317
+ export function isWasmTrap(e) {
318
318
  const trap = globalThis
319
319
  .WebAssembly?.RuntimeError;
320
320
  if (trap !== undefined && e instanceof trap)
@@ -330,7 +330,18 @@ function isWasmTrap(e) {
330
330
  * is: the module is dead, and only a restart recovers. Exported for tests.
331
331
  */
332
332
  export function applyChecked(doc, bytes) {
333
+ // Capture the document's changes BEFORE the apply: a change the engine
334
+ // aborts on (D1: a makeTable it cannot write back) stays in the document's
335
+ // graph or aborts the apply mid-way, so A.getAllChanges(doc) AFTER the
336
+ // error still holds the bad change and rebuilding from it re-aborts
337
+ // ("could not be restored"). Rebuilding from the changes it held before
338
+ // always gives its prior state.
339
+ //
340
+ // Do NOT "simplify" this by applying to A.clone(doc): A.clone gives the
341
+ // clone a new actor id, which breaks local writes (§8) and convergence.
333
342
  const before = [...A.getHeads(doc)].sort().join();
343
+ const actor = A.getActorId(doc);
344
+ const good = A.getAllChanges(doc);
334
345
  try {
335
346
  return { next: A.applyChanges(doc, [bytes])[0] };
336
347
  }
@@ -338,17 +349,21 @@ export function applyChecked(doc, bytes) {
338
349
  if (isWasmTrap(e))
339
350
  throw e;
340
351
  const error = e instanceof Error ? e : new Error(String(e));
341
- let restored;
342
- try {
343
- [restored] = A.applyChanges(A.init({ actor: A.getActorId(doc) }), A.getAllChanges(doc));
344
- }
345
- catch (r) {
346
- throw new Error(`the replica could not be restored after an Automerge error: ${String(r)}`);
347
- }
348
- if ([...A.getHeads(restored)].sort().join() !== before)
349
- throw new Error("the replica could not be restored after an Automerge error: heads differ");
350
- return { restored, error };
352
+ return { restored: restoreFrom(good, actor, before), error };
353
+ }
354
+ }
355
+ /** Rebuilds a document from the changes it held before an aborted apply (never from the aborted one). */
356
+ function restoreFrom(good, actor, before) {
357
+ let restored;
358
+ try {
359
+ [restored] = A.applyChanges(A.init({ actor }), [...good]);
360
+ }
361
+ catch (r) {
362
+ throw new Error(`the replica could not be restored after an Automerge error: ${String(r)}`);
351
363
  }
364
+ if ([...A.getHeads(restored)].sort().join() !== before)
365
+ throw new Error("the replica could not be restored after an Automerge error: heads differ");
366
+ return restored;
352
367
  }
353
368
  /**
354
369
  * applyChecked for a batch of admitted changes, in one engine call. On any
@@ -357,7 +372,10 @@ export function applyChecked(doc, bytes) {
357
372
  * heads it had. Exported for tests.
358
373
  */
359
374
  export function applyBatchChecked(doc, batch) {
360
- const heads = A.getHeads(doc);
375
+ // See applyChecked: capture the changes before the apply, restore from them.
376
+ const before = [...A.getHeads(doc)].sort().join();
377
+ const actor = A.getActorId(doc);
378
+ const good = A.getAllChanges(doc);
361
379
  try {
362
380
  return {
363
381
  next: A.applyChanges(doc, batch.map((c) => c.bytes))[0],
@@ -367,22 +385,8 @@ export function applyBatchChecked(doc, batch) {
367
385
  if (isWasmTrap(e))
368
386
  throw e;
369
387
  const error = e instanceof Error ? e : new Error(String(e));
370
- return { restored: restoreWithout(doc, batch, heads), error };
371
- }
372
- }
373
- function restoreWithout(doc, batch, heads) {
374
- const out = new Set(batch.map((c) => c.hash));
375
- const kept = A.getAllChanges(doc).filter((c) => !out.has(A.decodeChange(c).hash));
376
- let restored;
377
- try {
378
- [restored] = A.applyChanges(A.init({ actor: A.getActorId(doc) }), kept);
388
+ return { restored: restoreFrom(good, actor, before), error };
379
389
  }
380
- catch (r) {
381
- throw new Error(`the replica could not be restored after an Automerge error: ${String(r)}`);
382
- }
383
- if ([...A.getHeads(restored)].sort().join() !== [...heads].sort().join())
384
- throw new Error("the replica could not be restored after an Automerge error: heads differ");
385
- return restored;
386
390
  }
387
391
  function loadFailure(e, what) {
388
392
  if (e instanceof LfcpError)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openlfcp/shared-objects",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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/crypto": "^0.1.4",
49
- "@openlfcp/core": "^0.1.4"
48
+ "@openlfcp/core": "^0.1.5",
49
+ "@openlfcp/crypto": "^0.1.5"
50
50
  },
51
51
  "devDependencies": {
52
52
  "fast-check": "4.10.2"