@openlfcp/shared-objects 0.2.0-beta.7 → 0.2.0-beta.9
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/sections/replica.js +152 -94
- package/package.json +3 -3
package/dist/sections/replica.js
CHANGED
|
@@ -100,6 +100,8 @@ const S = (s) => new A.ImmutableString(s);
|
|
|
100
100
|
const str = (v) => A.isImmutableString(v) ? v.toString() : typeof v === "string" ? v : undefined;
|
|
101
101
|
const isListStyle = (s) => typeof s === "string" && LIST_STYLES.includes(s);
|
|
102
102
|
const revisionOf = (doc) => [...A.getHeads(doc)].sort().join(",");
|
|
103
|
+
/** `changes` without those of `batch` (by hash), in their order. */
|
|
104
|
+
const withoutBatch = (changes, batch) => changes.filter((c) => !batch.has(A.decodeChange(c).hash));
|
|
103
105
|
/** Rebuilds a section document from the changes it held before an aborted apply. */
|
|
104
106
|
function restoreSection(changes, actor, revision) {
|
|
105
107
|
let restored;
|
|
@@ -517,6 +519,9 @@ export class SectionReplica {
|
|
|
517
519
|
if (A.getActorId(this.#doc) !== this.#actor)
|
|
518
520
|
throw new Error("the document actor is not the §2 actor");
|
|
519
521
|
const affected = new Set();
|
|
522
|
+
// The validation's own document, freed on every exit (#7: an Automerge
|
|
523
|
+
// document left to the garbage collector holds its whole history in
|
|
524
|
+
// wasm memory until the collector happens to run).
|
|
520
525
|
let scratch = A.clone(this.#doc);
|
|
521
526
|
// The validation of the document before the batch: nodes the batch
|
|
522
527
|
// creates are valid by construction, so it is computed again only when
|
|
@@ -531,106 +536,159 @@ export class SectionReplica {
|
|
|
531
536
|
const chunks = [[]];
|
|
532
537
|
const used = { text: 0, nodes: 0 };
|
|
533
538
|
let i = -1;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
539
|
+
try {
|
|
540
|
+
for (const step of steps) {
|
|
541
|
+
if (step.intent !== "text.continue")
|
|
542
|
+
i = Math.min(i + 1, intents.length - 1);
|
|
543
|
+
const cost = { text: 0, nodes: 0 };
|
|
544
|
+
const write = this.#prepare(scratch, state, step, i, affected, cost);
|
|
545
|
+
if (cost.text > AUTHORING_BUDGET.textOperations ||
|
|
546
|
+
cost.nodes > AUTHORING_BUDGET.createdNodes)
|
|
547
|
+
throw new SectionIntentError("OVER_BUDGET", `one intent is over the §16.2 budgets (${cost.text} Text operations, ${cost.nodes} nodes)`, i);
|
|
548
|
+
const current = chunks[chunks.length - 1];
|
|
549
|
+
if (current.length > 0 &&
|
|
550
|
+
(used.text + cost.text > AUTHORING_BUDGET.textOperations ||
|
|
551
|
+
used.nodes + cost.nodes > AUTHORING_BUDGET.createdNodes)) {
|
|
552
|
+
chunks.push([write]);
|
|
553
|
+
used.text = cost.text;
|
|
554
|
+
used.nodes = cost.nodes;
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
current.push(write);
|
|
558
|
+
used.text += cost.text;
|
|
559
|
+
used.nodes += cost.nodes;
|
|
560
|
+
}
|
|
561
|
+
scratch = A.change(scratch, { time: 0 }, write);
|
|
562
|
+
if (step.intent === "section.create" || step.intent === "section.mark_ready")
|
|
563
|
+
state.v = undefined;
|
|
564
|
+
// Only creations keep every verified ancestor chain as it was.
|
|
565
|
+
if (!KEEPS_CHAINS.has(step.intent))
|
|
566
|
+
state.chainOk.clear();
|
|
553
567
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
// Only creations keep every verified ancestor chain as it was.
|
|
558
|
-
if (!KEEPS_CHAINS.has(step.intent))
|
|
559
|
-
state.chainOk.clear();
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
A.free(scratch);
|
|
560
571
|
}
|
|
561
572
|
const message = intents.map((x) => x.intent).join(",");
|
|
562
573
|
const before = A.getHeads(this.#doc);
|
|
563
|
-
//
|
|
564
|
-
//
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
574
|
+
// The batch's changes are made on a fork with this replica's §2 actor;
|
|
575
|
+
// apply() gives #doc the same changes by applyChanges, so #doc keeps its
|
|
576
|
+
// one Automerge document. (Adopting the fork as #doc kept, through each
|
|
577
|
+
// object the new document shared with the old one, one old document
|
|
578
|
+
// alive per unchanged node: a section's memory grew with the number of
|
|
579
|
+
// Tasks times its history, #7.) The fork is freed on every exit.
|
|
580
|
+
let work = A.clone(this.#doc, { actor: this.#actor });
|
|
581
|
+
let handedOver = false;
|
|
582
|
+
try {
|
|
583
|
+
const parts = [];
|
|
584
|
+
const partChanges = [];
|
|
585
|
+
for (const chunk of chunks) {
|
|
586
|
+
if (chunk.length === 0)
|
|
587
|
+
continue;
|
|
588
|
+
const heads = A.getHeads(work).join();
|
|
589
|
+
work = A.change(work, { message, time: 0 }, (d) => {
|
|
590
|
+
for (const w of chunk)
|
|
591
|
+
w(d);
|
|
592
|
+
});
|
|
593
|
+
if (A.getHeads(work).join() === heads)
|
|
594
|
+
continue;
|
|
595
|
+
const bytes = A.getLastLocalChange(work);
|
|
596
|
+
let checked;
|
|
597
|
+
try {
|
|
598
|
+
checkChangeExpansion(bytes);
|
|
599
|
+
checked = checkChange(bytes);
|
|
600
|
+
}
|
|
601
|
+
catch (e) {
|
|
602
|
+
throw new SectionIntentError("OVER_BUDGET", `a change of the batch is larger than one change may be (SOP §11.1: ${e.message})`);
|
|
603
|
+
}
|
|
604
|
+
partChanges.push(checked);
|
|
605
|
+
parts.push(Object.freeze({
|
|
606
|
+
change: checked.bytes,
|
|
607
|
+
plaintext: frameProfilePayload(checked.bytes),
|
|
608
|
+
hash: checked.hash,
|
|
609
|
+
seq: checked.seq,
|
|
610
|
+
}));
|
|
586
611
|
}
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
});
|
|
605
|
-
const base = revisionOf(this.#doc);
|
|
606
|
-
const prior = this.#doc;
|
|
607
|
-
const priorSeqs = this.#seqs === undefined ? undefined : new Map(this.#seqs);
|
|
608
|
-
let applied = false;
|
|
609
|
-
return Object.freeze({
|
|
610
|
-
change,
|
|
611
|
-
apply: () => {
|
|
612
|
-
if (applied)
|
|
613
|
-
return;
|
|
614
|
-
if (revisionOf(this.#doc) !== base)
|
|
615
|
-
throw new Error("the replica changed since the batch was staged; stage it again");
|
|
616
|
-
this.#doc = next;
|
|
617
|
-
if (this.#seqs !== undefined && last.seq > (this.#seqs.get(this.#actor) ?? 0))
|
|
618
|
-
this.#seqs.set(this.#actor, last.seq);
|
|
619
|
-
for (const c of partChanges)
|
|
620
|
-
this.#refs?.add(c.hash, parsedOf(c));
|
|
621
|
-
applied = true;
|
|
622
|
-
},
|
|
623
|
-
revert: () => {
|
|
624
|
-
if (!applied)
|
|
612
|
+
if (parts.length === 0 || A.getHeads(work).join() === before.join())
|
|
613
|
+
return null;
|
|
614
|
+
const last = parts[parts.length - 1];
|
|
615
|
+
const change = Object.freeze({
|
|
616
|
+
intents: Object.freeze(intents.map((x) => x.intent)),
|
|
617
|
+
parts: Object.freeze(parts),
|
|
618
|
+
...last,
|
|
619
|
+
affectedNodeIds: Object.freeze([...affected].sort()),
|
|
620
|
+
modelRevision: revisionOf(work),
|
|
621
|
+
});
|
|
622
|
+
const base = revisionOf(this.#doc);
|
|
623
|
+
const batch = new Set(partChanges.map((c) => c.hash));
|
|
624
|
+
const priorSeqs = this.#seqs === undefined ? undefined : new Map(this.#seqs);
|
|
625
|
+
const fork = work;
|
|
626
|
+
let forkFreed = false;
|
|
627
|
+
const freeFork = () => {
|
|
628
|
+
if (forkFreed)
|
|
625
629
|
return;
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
630
|
+
forkFreed = true;
|
|
631
|
+
A.free(fork);
|
|
632
|
+
};
|
|
633
|
+
let applied = false;
|
|
634
|
+
handedOver = true;
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
change,
|
|
637
|
+
apply: () => {
|
|
638
|
+
if (applied)
|
|
639
|
+
return;
|
|
640
|
+
if (forkFreed)
|
|
641
|
+
throw new Error("the batch could not be applied before; stage it again");
|
|
642
|
+
if (revisionOf(this.#doc) !== base)
|
|
643
|
+
throw new Error("the replica changed since the batch was staged; stage it again");
|
|
644
|
+
try {
|
|
645
|
+
try {
|
|
646
|
+
this.#doc = A.applyChanges(this.#doc, partChanges.map((c) => c.bytes))[0];
|
|
647
|
+
if (revisionOf(this.#doc) !== change.modelRevision)
|
|
648
|
+
throw new Error("the batch did not apply as it was staged");
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (isWasmTrap(e))
|
|
652
|
+
throw e;
|
|
653
|
+
// The document is not trusted after a failed apply: rebuilt from
|
|
654
|
+
// the changes the fork held before the batch, never from it.
|
|
655
|
+
this.#doc = this.#rebuilt(withoutBatch(A.getAllChanges(fork), batch), base);
|
|
656
|
+
throw e;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
finally {
|
|
660
|
+
freeFork();
|
|
661
|
+
}
|
|
662
|
+
if (this.#seqs !== undefined && last.seq > (this.#seqs.get(this.#actor) ?? 0))
|
|
663
|
+
this.#seqs.set(this.#actor, last.seq);
|
|
664
|
+
for (const c of partChanges)
|
|
665
|
+
this.#refs?.add(c.hash, parsedOf(c));
|
|
666
|
+
applied = true;
|
|
667
|
+
},
|
|
668
|
+
revert: () => {
|
|
669
|
+
if (!applied)
|
|
670
|
+
return;
|
|
671
|
+
if (revisionOf(this.#doc) !== change.modelRevision)
|
|
672
|
+
throw new Error("the replica changed after the batch was applied; it cannot be reverted");
|
|
673
|
+
// The document as it was before the batch: its changes without the batch's.
|
|
674
|
+
this.#doc = this.#rebuilt(withoutBatch(A.getAllChanges(this.#doc), batch), base);
|
|
675
|
+
this.#seqs = priorSeqs === undefined ? undefined : new Map(priorSeqs);
|
|
676
|
+
this.#refs = null;
|
|
677
|
+
applied = false;
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
finally {
|
|
682
|
+
if (!handedOver)
|
|
683
|
+
A.free(work);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
/** A document of exactly `changes`, with this replica's §2 actor, at `revision` (checked). */
|
|
687
|
+
#rebuilt(changes, revision) {
|
|
688
|
+
const doc = restoreSection(changes, this.#actor, revision);
|
|
689
|
+
if (A.getActorId(doc) !== this.#actor)
|
|
690
|
+
throw new Error("the section replica could not be restored: another actor");
|
|
691
|
+
return doc;
|
|
634
692
|
}
|
|
635
693
|
/**
|
|
636
694
|
* Checks one intent against `doc`, the document with the batch so far,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlfcp/shared-objects",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.9",
|
|
4
4
|
"description": "SHARED-OBJECTS-PROFILE-01 (org.openlfcp.shared-objects.v1) implementation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openlfcp",
|
|
@@ -53,8 +53,8 @@
|
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@automerge/automerge": "3.5.0",
|
|
55
55
|
"fflate": "0.8.3",
|
|
56
|
-
"@openlfcp/core": "^0.2.0-beta.
|
|
57
|
-
"@openlfcp/crypto": "^0.2.0-beta.
|
|
56
|
+
"@openlfcp/core": "^0.2.0-beta.9",
|
|
57
|
+
"@openlfcp/crypto": "^0.2.0-beta.9"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"fast-check": "4.10.2"
|