@forgeax/engine-ddc 0.1.4 → 0.1.7

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.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
- if (record === null) {
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
- async begin(guid, desiredKey) {
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 generation = await this.allocateGeneration();
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: current.revision + 1,
624
+ revision: nextRevision2,
636
625
  generation: lease.generation,
637
626
  ...current.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: current.lastKnownGoodKey },
638
- failure: {
639
- desiredKey: lease.desiredKey,
640
- code: "entry-invalid",
641
- detail: "validated DDC key has no readable entry for this asset"
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
- await withDdcLock(this.root, `head-${lease.guid}`, async () => {
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: current.revision + 1,
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
- /** Restore the last accepted head after a failed multi-owner generation commit. */
721
- async restore(head) {
722
- await withDdcLock(this.root, `head-${head.guid}`, async () => {
723
- const path = headFile(this.heads, head.guid);
724
- if (head.state === "missing") {
725
- await rm(path, { force: true });
726
- return;
727
- }
728
- await this.write({
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.currentKey === void 0 ? {} : { currentKey: head.currentKey },
733
- ...head.lastKnownGoodKey === void 0 ? {} : { lastKnownGoodKey: head.lastKnownGoodKey },
734
- ...head.generation === void 0 ? {} : { generation: head.generation },
735
- ...head.activeLease === void 0 ? {} : { active: head.activeLease },
736
- ...head.state === "stale" ? { stale: true } : {},
737
- ...head.failure === void 0 ? {} : { failure: { desiredKey: head.desiredKey, ...head.failure } }
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
- async allocateGeneration() {
767
- const path = join(this.root, "generations", "counter.json");
768
- await mkdir(join(this.root, "generations"), { recursive: true });
769
- let next = 1;
770
- try {
771
- const record = JSON.parse(await readFile(path, "utf8"));
772
- if (record.schemaVersion === "forgeax-ddc-generation/v2" && Number.isInteger(record.next))
773
- next = Math.max(1, record.next);
774
- } catch {
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
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
777
- await writeFile(
778
- temporary,
779
- JSON.stringify({ schemaVersion: "forgeax-ddc-generation/v2", next: next + 1 })
780
- );
781
- await rename(temporary, path);
782
- return next;
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 Map() };
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.set(candidateKey(candidate.envelope), candidate.envelope);
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(key)) {
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(key);
1274
+ state.staged.delete(candidate.envelope);
1173
1275
  return result;
1174
1276
  },
1175
1277
  discard(sourcePath, candidate) {
1176
- stateFor(sourcePath).staged.delete(candidateKey(candidate));
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
- const head = await this.inspect(guid, desiredKey);
1272
- const lease = await this.lifecycle.begin(guid, desiredKey);
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: head
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.restore(candidate.previousHead);
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 timer = this.heartbeatTimers.get(candidate.lease.attempt);
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(candidate.lease.attempt);
1551
+ this.heartbeatTimers.delete(attempt);
1432
1552
  }
1433
1553
  };
1434
1554