@forgeax/engine-ddc 0.1.6 → 0.1.19
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 +9 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.mjs +213 -93
- package/dist/index.mjs.map +1 -1
- package/dist/lifecycle.d.ts +38 -3
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/publication.d.ts.map +1 -1
- package/dist/session.d.ts +4 -3
- package/dist/session.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/generation-session.integration.test.ts +132 -4
- package/src/__tests__/multiprocess-lifecycle.integration.test.ts +33 -2
- package/src/index.ts +4 -0
- package/src/lifecycle.ts +297 -92
- package/src/publication.ts +12 -10
- package/src/session.ts +40 -13
package/dist/index.mjs
CHANGED
|
@@ -404,9 +404,12 @@ function headFile(heads, guid) {
|
|
|
404
404
|
function lockFile(root, name) {
|
|
405
405
|
return join(root, "locks", `${encodeURIComponent(name)}.lock`);
|
|
406
406
|
}
|
|
407
|
-
function withRevision(result, revision) {
|
|
407
|
+
function withRevision(result, revision, restoreFence) {
|
|
408
408
|
const value = { ...result };
|
|
409
409
|
Object.defineProperty(value, "revision", { value: revision, enumerable: false });
|
|
410
|
+
if (restoreFence !== void 0) {
|
|
411
|
+
Object.defineProperty(value, "restoreFence", { value: restoreFence, enumerable: false });
|
|
412
|
+
}
|
|
410
413
|
return value;
|
|
411
414
|
}
|
|
412
415
|
async function delay(ms) {
|
|
@@ -440,6 +443,7 @@ function isHeadRecord(value) {
|
|
|
440
443
|
return false;
|
|
441
444
|
}
|
|
442
445
|
if (value.stale !== void 0 && typeof value.stale !== "boolean") return false;
|
|
446
|
+
if (value.empty !== void 0 && typeof value.empty !== "boolean") return false;
|
|
443
447
|
if (value.failure !== void 0) {
|
|
444
448
|
if (!isRecord(value.failure)) return false;
|
|
445
449
|
if (!isString(value.failure.desiredKey) || !isString(value.failure.code) || !isString(value.failure.detail)) {
|
|
@@ -541,38 +545,14 @@ var DdcLifecycle = class {
|
|
|
541
545
|
}
|
|
542
546
|
async inspect(guid, desiredKey) {
|
|
543
547
|
const record = await this.read(guid);
|
|
544
|
-
|
|
545
|
-
return {
|
|
546
|
-
guid,
|
|
547
|
-
desiredKey,
|
|
548
|
-
state: "missing",
|
|
549
|
-
currentKey: void 0,
|
|
550
|
-
lastKnownGoodKey: void 0,
|
|
551
|
-
revision: 0
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
const recordedFailure = record.failure?.desiredKey === desiredKey ? { code: record.failure.code, detail: record.failure.detail } : void 0;
|
|
555
|
-
const currentEntry = record.currentKey === void 0 ? null : await this.entries.readChecked(record.currentKey);
|
|
556
|
-
const entryFailure = currentEntry !== null && !currentEntry.ok ? { code: currentEntry.error.code, detail: currentEntry.error.detail } : void 0;
|
|
557
|
-
const failure2 = recordedFailure ?? entryFailure;
|
|
558
|
-
const currentValue = currentEntry?.ok === true ? currentEntry.value : null;
|
|
559
|
-
const state = failure2 !== void 0 ? "failed" : record.currentKey === desiredKey && currentValue?.guid === guid ? "current" : record.stale === true ? "stale" : record.active?.desiredKey === desiredKey ? "cooking" : "stale";
|
|
560
|
-
return {
|
|
561
|
-
guid,
|
|
562
|
-
desiredKey,
|
|
563
|
-
state,
|
|
564
|
-
currentKey: record.currentKey,
|
|
565
|
-
lastKnownGoodKey: record.lastKnownGoodKey,
|
|
566
|
-
revision: record.revision,
|
|
567
|
-
...record.generation === void 0 ? {} : { generation: record.generation },
|
|
568
|
-
...record.active === void 0 ? {} : { activeLease: record.active },
|
|
569
|
-
...failure2 === void 0 ? {} : { failure: failure2 }
|
|
570
|
-
};
|
|
548
|
+
return this.projectHead(guid, desiredKey, record);
|
|
571
549
|
}
|
|
572
|
-
|
|
550
|
+
/** Begin a lease and capture the accepted rollback snapshot under one head lock. */
|
|
551
|
+
async beginWithSnapshot(guid, desiredKey) {
|
|
573
552
|
return withDdcLock(this.root, `head-${guid}`, async () => {
|
|
574
553
|
const previous = await this.read(guid);
|
|
575
|
-
const
|
|
554
|
+
const previousHead = await this.projectHead(guid, desiredKey, previous, false);
|
|
555
|
+
const generation = await this.allocateGeneration(previous?.generation ?? 0);
|
|
576
556
|
const lease = {
|
|
577
557
|
guid,
|
|
578
558
|
desiredKey,
|
|
@@ -597,9 +577,12 @@ var DdcLifecycle = class {
|
|
|
597
577
|
...supersededAttempts.length === 0 ? {} : { supersededAttempts },
|
|
598
578
|
...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
|
|
599
579
|
});
|
|
600
|
-
return lease;
|
|
580
|
+
return { lease, previousHead };
|
|
601
581
|
});
|
|
602
582
|
}
|
|
583
|
+
async begin(guid, desiredKey) {
|
|
584
|
+
return (await this.beginWithSnapshot(guid, desiredKey)).lease;
|
|
585
|
+
}
|
|
603
586
|
async commit(lease, validatedKey) {
|
|
604
587
|
return withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
605
588
|
const current = await this.read(lease.guid);
|
|
@@ -629,19 +612,30 @@ var DdcLifecycle = class {
|
|
|
629
612
|
}
|
|
630
613
|
const entry = await this.entries.read(validatedKey);
|
|
631
614
|
if (entry === null || entry.guid !== lease.guid || entry.receipt.key !== validatedKey) {
|
|
615
|
+
const nextRevision2 = current.revision + 1;
|
|
616
|
+
const failure2 = {
|
|
617
|
+
desiredKey: lease.desiredKey,
|
|
618
|
+
code: "entry-invalid",
|
|
619
|
+
detail: "validated DDC key has no readable entry for this asset"
|
|
620
|
+
};
|
|
632
621
|
await this.write({
|
|
633
622
|
guid: lease.guid,
|
|
634
623
|
desiredKey: lease.desiredKey,
|
|
635
|
-
revision:
|
|
624
|
+
revision: nextRevision2,
|
|
636
625
|
generation: lease.generation,
|
|
637
626
|
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
627
|
+
...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
|
|
628
|
+
failure: failure2
|
|
629
|
+
});
|
|
630
|
+
return withRevision({ result: "invalid", key: validatedKey }, nextRevision2, {
|
|
631
|
+
attempt: lease.attempt,
|
|
632
|
+
generation: lease.generation,
|
|
633
|
+
revision: nextRevision2,
|
|
634
|
+
desiredKey: lease.desiredKey,
|
|
635
|
+
outcome: "invalid",
|
|
636
|
+
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
637
|
+
failure: { code: failure2.code, detail: failure2.detail }
|
|
643
638
|
});
|
|
644
|
-
return withRevision({ result: "invalid", key: validatedKey }, current.revision + 1);
|
|
645
639
|
}
|
|
646
640
|
const lastKnownGoodKey = current.currentKey !== void 0 && current.currentKey !== validatedKey ? current.currentKey : current.lastKnownGoodKey;
|
|
647
641
|
const nextRevision = current.revision + 1;
|
|
@@ -655,22 +649,43 @@ var DdcLifecycle = class {
|
|
|
655
649
|
...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
|
|
656
650
|
...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
|
|
657
651
|
});
|
|
658
|
-
return withRevision({ result: "current", key: validatedKey }, nextRevision
|
|
652
|
+
return withRevision({ result: "current", key: validatedKey }, nextRevision, {
|
|
653
|
+
attempt: lease.attempt,
|
|
654
|
+
generation: lease.generation,
|
|
655
|
+
revision: nextRevision,
|
|
656
|
+
desiredKey: lease.desiredKey,
|
|
657
|
+
outcome: "current",
|
|
658
|
+
key: validatedKey,
|
|
659
|
+
currentKey: validatedKey,
|
|
660
|
+
...lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey }
|
|
661
|
+
});
|
|
659
662
|
});
|
|
660
663
|
}
|
|
661
664
|
async fail(lease, failure2) {
|
|
662
|
-
|
|
665
|
+
return withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
663
666
|
const current = await this.read(lease.guid);
|
|
664
667
|
if (current?.active?.attempt !== lease.attempt) return;
|
|
668
|
+
const nextRevision = current.revision + 1;
|
|
665
669
|
await this.write({
|
|
666
670
|
guid: lease.guid,
|
|
667
671
|
desiredKey: lease.desiredKey,
|
|
668
|
-
revision:
|
|
672
|
+
revision: nextRevision,
|
|
669
673
|
generation: lease.generation,
|
|
670
674
|
...current.currentKey === void 0 ? {} : { currentKey: current.currentKey },
|
|
671
675
|
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
676
|
+
...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
|
|
672
677
|
failure: { desiredKey: lease.desiredKey, ...failure2 }
|
|
673
678
|
});
|
|
679
|
+
return {
|
|
680
|
+
attempt: lease.attempt,
|
|
681
|
+
generation: lease.generation,
|
|
682
|
+
revision: nextRevision,
|
|
683
|
+
desiredKey: lease.desiredKey,
|
|
684
|
+
outcome: "failed",
|
|
685
|
+
...current.currentKey === void 0 ? {} : { currentKey: current.currentKey },
|
|
686
|
+
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
687
|
+
failure: failure2
|
|
688
|
+
};
|
|
674
689
|
});
|
|
675
690
|
}
|
|
676
691
|
async heartbeat(lease) {
|
|
@@ -717,25 +732,62 @@ var DdcLifecycle = class {
|
|
|
717
732
|
});
|
|
718
733
|
});
|
|
719
734
|
}
|
|
720
|
-
/**
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
guid: head.guid,
|
|
730
|
-
desiredKey: head.desiredKey,
|
|
735
|
+
/**
|
|
736
|
+
* Restore accepted content only when this lease still owns the mutable head.
|
|
737
|
+
* A newer active or terminal mutation makes rollback a deliberate no-op.
|
|
738
|
+
*/
|
|
739
|
+
async restore(head, lease, fence) {
|
|
740
|
+
const inferredLease = lease ?? ("activeLease" in head && head.activeLease !== void 0 ? head.activeLease : void 0);
|
|
741
|
+
if (inferredLease === void 0) {
|
|
742
|
+
return {
|
|
743
|
+
result: "not-owner",
|
|
731
744
|
revision: head.revision ?? 0,
|
|
732
|
-
...head.
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
745
|
+
...head.generation === void 0 ? {} : { generation: head.generation }
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const snapshot = this.rollbackSnapshot(head);
|
|
749
|
+
return this.restoreIfCurrent(snapshot, inferredLease, fence);
|
|
750
|
+
}
|
|
751
|
+
async restoreIfCurrent(snapshot, lease, fence) {
|
|
752
|
+
return withDdcLock(this.root, `head-${lease.guid}`, async () => {
|
|
753
|
+
const current = await this.read(lease.guid);
|
|
754
|
+
if (current === null || current.guid !== snapshot.guid) {
|
|
755
|
+
return {
|
|
756
|
+
result: "not-owner",
|
|
757
|
+
revision: current?.revision ?? 0,
|
|
758
|
+
...current?.generation === void 0 ? {} : { generation: current.generation }
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
const ownsActive = current.active?.attempt === lease.attempt && current.active.generation === lease.generation && current.active.desiredKey === lease.desiredKey;
|
|
762
|
+
const ownsTerminal = fence !== void 0 && this.matchesRestoreFence(current, lease, fence);
|
|
763
|
+
if (!ownsActive && !ownsTerminal) {
|
|
764
|
+
return {
|
|
765
|
+
result: "not-owner",
|
|
766
|
+
revision: current.revision,
|
|
767
|
+
...current.generation === void 0 ? {} : { generation: current.generation }
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
const nextRevision = current.revision + 1;
|
|
771
|
+
const generation = Math.max(
|
|
772
|
+
current.generation ?? 0,
|
|
773
|
+
lease.generation,
|
|
774
|
+
snapshot.generation ?? 0
|
|
775
|
+
);
|
|
776
|
+
const supersededAttempts = current.supersededAttempts;
|
|
777
|
+
const restored = {
|
|
778
|
+
guid: snapshot.guid,
|
|
779
|
+
desiredKey: snapshot.desiredKey,
|
|
780
|
+
revision: nextRevision,
|
|
781
|
+
generation,
|
|
782
|
+
...snapshot.currentKey === void 0 ? {} : { currentKey: snapshot.currentKey },
|
|
783
|
+
...snapshot.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: snapshot.lastKnownGoodKey },
|
|
784
|
+
...supersededAttempts === void 0 ? {} : { supersededAttempts },
|
|
785
|
+
...snapshot.state === "missing" ? { empty: true } : {},
|
|
786
|
+
...snapshot.state === "stale" ? { stale: true } : {},
|
|
787
|
+
...snapshot.failure === void 0 ? {} : { failure: { desiredKey: snapshot.desiredKey, ...snapshot.failure } }
|
|
788
|
+
};
|
|
789
|
+
await this.write(restored);
|
|
790
|
+
return { result: "restored", revision: nextRevision, generation };
|
|
739
791
|
});
|
|
740
792
|
}
|
|
741
793
|
async revoke(lease) {
|
|
@@ -752,6 +804,8 @@ var DdcLifecycle = class {
|
|
|
752
804
|
guid,
|
|
753
805
|
desiredKey,
|
|
754
806
|
revision: current.revision + 1,
|
|
807
|
+
...current.generation === void 0 ? {} : { generation: current.generation },
|
|
808
|
+
...current.supersededAttempts === void 0 ? {} : { supersededAttempts: current.supersededAttempts },
|
|
755
809
|
...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
|
|
756
810
|
failure: {
|
|
757
811
|
desiredKey,
|
|
@@ -763,23 +817,75 @@ var DdcLifecycle = class {
|
|
|
763
817
|
return this.inspect(guid, desiredKey);
|
|
764
818
|
});
|
|
765
819
|
}
|
|
766
|
-
|
|
767
|
-
const
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
820
|
+
rollbackSnapshot(head) {
|
|
821
|
+
const { activeLease: _activeLease, ...snapshot } = head;
|
|
822
|
+
if (snapshot.state !== "cooking") return snapshot;
|
|
823
|
+
return {
|
|
824
|
+
...snapshot,
|
|
825
|
+
state: snapshot.currentKey === void 0 ? "missing" : "stale"
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
matchesRestoreFence(current, lease, fence) {
|
|
829
|
+
if (fence.attempt !== lease.attempt || fence.generation !== lease.generation || current.active !== void 0 || current.revision !== fence.revision || current.generation !== fence.generation || current.desiredKey !== fence.desiredKey || current.currentKey !== fence.currentKey || current.lastKnownGoodKey !== fence.lastKnownGoodKey) {
|
|
830
|
+
return false;
|
|
775
831
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
return
|
|
832
|
+
if (fence.outcome === "current") {
|
|
833
|
+
return fence.key !== void 0 && current.currentKey === fence.key && current.failure === void 0 && current.stale !== true;
|
|
834
|
+
}
|
|
835
|
+
if (fence.failure === void 0 || current.failure === void 0 || current.failure.desiredKey !== fence.desiredKey || current.failure.code !== fence.failure.code || current.failure.detail !== fence.failure.detail) {
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
return fence.outcome === "invalid" ? fence.currentKey === void 0 : true;
|
|
839
|
+
}
|
|
840
|
+
async projectHead(guid, desiredKey, record, includeActive = true) {
|
|
841
|
+
if (record === null) {
|
|
842
|
+
return {
|
|
843
|
+
guid,
|
|
844
|
+
desiredKey,
|
|
845
|
+
state: "missing",
|
|
846
|
+
currentKey: void 0,
|
|
847
|
+
lastKnownGoodKey: void 0,
|
|
848
|
+
revision: 0
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
const recordedFailure = record.failure?.desiredKey === desiredKey ? { code: record.failure.code, detail: record.failure.detail } : void 0;
|
|
852
|
+
const currentEntry = record.currentKey === void 0 ? null : await this.entries.readChecked(record.currentKey);
|
|
853
|
+
const entryFailure = currentEntry !== null && !currentEntry.ok ? { code: currentEntry.error.code, detail: currentEntry.error.detail } : void 0;
|
|
854
|
+
const failure2 = recordedFailure ?? entryFailure;
|
|
855
|
+
const currentValue = currentEntry?.ok === true ? currentEntry.value : null;
|
|
856
|
+
const state = failure2 !== void 0 ? "failed" : record.currentKey === desiredKey && currentValue?.guid === guid ? "current" : record.stale === true ? "stale" : includeActive && record.active?.desiredKey === desiredKey ? "cooking" : record.empty === true ? "missing" : "stale";
|
|
857
|
+
return {
|
|
858
|
+
guid,
|
|
859
|
+
desiredKey,
|
|
860
|
+
state,
|
|
861
|
+
currentKey: record.currentKey,
|
|
862
|
+
lastKnownGoodKey: record.lastKnownGoodKey,
|
|
863
|
+
revision: record.revision,
|
|
864
|
+
...record.generation === void 0 ? {} : { generation: record.generation },
|
|
865
|
+
...includeActive && record.active === void 0 ? {} : includeActive && record.active !== void 0 ? { activeLease: record.active } : {},
|
|
866
|
+
...failure2 === void 0 ? {} : { failure: failure2 }
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
async allocateGeneration(minimumExclusive = 0) {
|
|
870
|
+
return withDdcLock(this.root, "generation-counter", async () => {
|
|
871
|
+
const path = join(this.root, "generations", "counter.json");
|
|
872
|
+
await mkdir(join(this.root, "generations"), { recursive: true });
|
|
873
|
+
let next = 1;
|
|
874
|
+
try {
|
|
875
|
+
const record = JSON.parse(await readFile(path, "utf8"));
|
|
876
|
+
if (record.schemaVersion === "forgeax-ddc-generation/v2" && Number.isSafeInteger(record.next) && record.next >= 1)
|
|
877
|
+
next = Math.max(1, record.next);
|
|
878
|
+
} catch {
|
|
879
|
+
}
|
|
880
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
881
|
+
next = Math.max(next, minimumExclusive + 1);
|
|
882
|
+
await writeFile(
|
|
883
|
+
temporary,
|
|
884
|
+
JSON.stringify({ schemaVersion: "forgeax-ddc-generation/v2", next: next + 1 })
|
|
885
|
+
);
|
|
886
|
+
await rename(temporary, path);
|
|
887
|
+
return next;
|
|
888
|
+
});
|
|
783
889
|
}
|
|
784
890
|
async read(guid) {
|
|
785
891
|
const path = headFile(this.heads, guid);
|
|
@@ -1129,13 +1235,10 @@ function createAcceptedPublicationStore() {
|
|
|
1129
1235
|
function stateFor(sourcePath) {
|
|
1130
1236
|
const existing = states.get(sourcePath);
|
|
1131
1237
|
if (existing !== void 0) return existing;
|
|
1132
|
-
const created = { snapshot: {}, staged: /* @__PURE__ */ new
|
|
1238
|
+
const created = { snapshot: {}, staged: /* @__PURE__ */ new Set() };
|
|
1133
1239
|
states.set(sourcePath, created);
|
|
1134
1240
|
return created;
|
|
1135
1241
|
}
|
|
1136
|
-
function candidateKey(candidate) {
|
|
1137
|
-
return `${candidate.generation}\0${candidate.digest}\0${candidate.outputSetDigest}`;
|
|
1138
|
-
}
|
|
1139
1242
|
return {
|
|
1140
1243
|
observe(sourcePath) {
|
|
1141
1244
|
return stateFor(sourcePath).snapshot;
|
|
@@ -1147,13 +1250,12 @@ function createAcceptedPublicationStore() {
|
|
|
1147
1250
|
"publication request was cancelled before DDC commit"
|
|
1148
1251
|
);
|
|
1149
1252
|
if (invalid !== void 0) return err(invalid);
|
|
1150
|
-
stateFor(sourcePath).staged.
|
|
1253
|
+
stateFor(sourcePath).staged.add(candidate.envelope);
|
|
1151
1254
|
return ok(void 0);
|
|
1152
1255
|
},
|
|
1153
1256
|
async commit(sourcePath, candidate, commitRoute) {
|
|
1154
|
-
const key = candidateKey(candidate.envelope);
|
|
1155
1257
|
const state = stateFor(sourcePath);
|
|
1156
|
-
if (!state.staged.has(
|
|
1258
|
+
if (!state.staged.has(candidate.envelope)) {
|
|
1157
1259
|
const current = state.snapshot.current;
|
|
1158
1260
|
const lastKnownGood = state.snapshot.lastKnownGood ?? current;
|
|
1159
1261
|
return err(
|
|
@@ -1169,11 +1271,11 @@ function createAcceptedPublicationStore() {
|
|
|
1169
1271
|
);
|
|
1170
1272
|
}
|
|
1171
1273
|
const result = await publishCandidate(state, candidate, commitRoute);
|
|
1172
|
-
state.staged.delete(
|
|
1274
|
+
state.staged.delete(candidate.envelope);
|
|
1173
1275
|
return result;
|
|
1174
1276
|
},
|
|
1175
1277
|
discard(sourcePath, candidate) {
|
|
1176
|
-
stateFor(sourcePath).staged.delete(
|
|
1278
|
+
stateFor(sourcePath).staged.delete(candidate);
|
|
1177
1279
|
},
|
|
1178
1280
|
restore(sourcePath, snapshot) {
|
|
1179
1281
|
const state = stateFor(sourcePath);
|
|
@@ -1248,6 +1350,7 @@ var DdcGenerationSession = class {
|
|
|
1248
1350
|
lifecycle;
|
|
1249
1351
|
entries;
|
|
1250
1352
|
candidates = /* @__PURE__ */ new Map();
|
|
1353
|
+
restoreFences = /* @__PURE__ */ new WeakMap();
|
|
1251
1354
|
heartbeatTimers = /* @__PURE__ */ new Map();
|
|
1252
1355
|
counters = {
|
|
1253
1356
|
hitCount: 0,
|
|
@@ -1268,12 +1371,14 @@ var DdcGenerationSession = class {
|
|
|
1268
1371
|
this.entries = new DdcEntryStore(root);
|
|
1269
1372
|
}
|
|
1270
1373
|
async beginCandidate(guid, desiredKey) {
|
|
1271
|
-
|
|
1272
|
-
const lease = await this.lifecycle.
|
|
1374
|
+
this.assertOpen();
|
|
1375
|
+
const { lease, previousHead } = await this.lifecycle.beginWithSnapshot(guid, desiredKey);
|
|
1376
|
+
if (previousHead.state === "current") this.counters.hitCount += 1;
|
|
1377
|
+
else this.counters.missCount += 1;
|
|
1273
1378
|
const candidate = {
|
|
1274
1379
|
generation: this.generation,
|
|
1275
1380
|
lease,
|
|
1276
|
-
previousHead
|
|
1381
|
+
previousHead
|
|
1277
1382
|
};
|
|
1278
1383
|
this.candidates.set(lease.attempt, candidate);
|
|
1279
1384
|
this.scheduleHeartbeat(candidate);
|
|
@@ -1293,10 +1398,11 @@ var DdcGenerationSession = class {
|
|
|
1293
1398
|
this.candidates.set(candidate.lease.attempt, entryCandidate);
|
|
1294
1399
|
return entryCandidate;
|
|
1295
1400
|
} catch (error) {
|
|
1296
|
-
await this.lifecycle.fail(candidate.lease, {
|
|
1401
|
+
const fence = await this.lifecycle.fail(candidate.lease, {
|
|
1297
1402
|
code: "ddc-entry-stage-failed",
|
|
1298
1403
|
detail: error instanceof Error ? error.message : String(error)
|
|
1299
1404
|
});
|
|
1405
|
+
if (fence !== void 0) this.restoreFences.set(candidate, fence);
|
|
1300
1406
|
this.stopHeartbeat(candidate);
|
|
1301
1407
|
this.candidates.delete(candidate.lease.attempt);
|
|
1302
1408
|
this.counters.writeFailureCount += 1;
|
|
@@ -1314,6 +1420,8 @@ var DdcGenerationSession = class {
|
|
|
1314
1420
|
const registered = this.assertCandidate(candidate);
|
|
1315
1421
|
try {
|
|
1316
1422
|
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
1423
|
+
if (result.restoreFence !== void 0)
|
|
1424
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
1317
1425
|
if (result.result === "invalid") this.counters.corruptCount += 1;
|
|
1318
1426
|
this.stopHeartbeat(registered);
|
|
1319
1427
|
this.candidates.delete(registered.lease.attempt);
|
|
@@ -1329,15 +1437,18 @@ var DdcGenerationSession = class {
|
|
|
1329
1437
|
try {
|
|
1330
1438
|
await this.entries.publish(candidate.staged);
|
|
1331
1439
|
const result = await this.lifecycle.commit(registered.lease, validatedKey);
|
|
1440
|
+
if (result.restoreFence !== void 0)
|
|
1441
|
+
this.restoreFences.set(registered, result.restoreFence);
|
|
1332
1442
|
this.stopHeartbeat(registered);
|
|
1333
1443
|
this.candidates.delete(registered.lease.attempt);
|
|
1334
1444
|
return result;
|
|
1335
1445
|
} catch (error) {
|
|
1336
1446
|
this.stopHeartbeat(registered);
|
|
1337
|
-
await this.lifecycle.fail(registered.lease, {
|
|
1447
|
+
const fence = await this.lifecycle.fail(registered.lease, {
|
|
1338
1448
|
code: "ddc-entry-publish-failed",
|
|
1339
1449
|
detail: error instanceof Error ? error.message : String(error)
|
|
1340
1450
|
});
|
|
1451
|
+
if (fence !== void 0) this.restoreFences.set(registered, fence);
|
|
1341
1452
|
await this.entries.discard(candidate.staged).catch(() => {
|
|
1342
1453
|
});
|
|
1343
1454
|
this.candidates.delete(registered.lease.attempt);
|
|
@@ -1362,20 +1473,28 @@ var DdcGenerationSession = class {
|
|
|
1362
1473
|
}
|
|
1363
1474
|
async restoreEntry(candidate) {
|
|
1364
1475
|
const registered = this.candidates.get(candidate.lease.attempt);
|
|
1476
|
+
const ownerLease = registered?.lease ?? candidate.lease;
|
|
1477
|
+
const fence = this.restoreFences.get(candidate);
|
|
1478
|
+
this.stopHeartbeat(ownerLease);
|
|
1479
|
+
let result = {
|
|
1480
|
+
result: "not-owner",
|
|
1481
|
+
revision: candidate.previousHead.revision ?? 0,
|
|
1482
|
+
...candidate.previousHead.generation === void 0 ? {} : { generation: candidate.previousHead.generation }
|
|
1483
|
+
};
|
|
1365
1484
|
let restoreError;
|
|
1366
1485
|
try {
|
|
1367
|
-
await this.lifecycle.
|
|
1486
|
+
result = await this.lifecycle.restoreIfCurrent(candidate.previousHead, ownerLease, fence);
|
|
1368
1487
|
} catch (error) {
|
|
1369
1488
|
restoreError = error;
|
|
1370
1489
|
} finally {
|
|
1371
1490
|
await this.entries.discard(candidate.staged).catch(() => {
|
|
1372
1491
|
});
|
|
1373
1492
|
if (registered !== void 0) {
|
|
1374
|
-
this.stopHeartbeat(registered);
|
|
1375
1493
|
this.candidates.delete(registered.lease.attempt);
|
|
1376
1494
|
}
|
|
1377
1495
|
}
|
|
1378
1496
|
if (restoreError !== void 0) throw restoreError;
|
|
1497
|
+
return result;
|
|
1379
1498
|
}
|
|
1380
1499
|
metrics() {
|
|
1381
1500
|
return { ...this.counters };
|
|
@@ -1425,10 +1544,11 @@ var DdcGenerationSession = class {
|
|
|
1425
1544
|
this.heartbeatTimers.set(attempt, timer);
|
|
1426
1545
|
}
|
|
1427
1546
|
stopHeartbeat(candidate) {
|
|
1428
|
-
const
|
|
1547
|
+
const attempt = "lease" in candidate ? candidate.lease.attempt : candidate.attempt;
|
|
1548
|
+
const timer = this.heartbeatTimers.get(attempt);
|
|
1429
1549
|
if (timer === void 0) return;
|
|
1430
1550
|
clearTimeout(timer);
|
|
1431
|
-
this.heartbeatTimers.delete(
|
|
1551
|
+
this.heartbeatTimers.delete(attempt);
|
|
1432
1552
|
}
|
|
1433
1553
|
};
|
|
1434
1554
|
|