@indigoai-us/hq-cloud 6.15.78 → 6.15.80

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.
Files changed (95) hide show
  1. package/dist/bin/area-ledger-migration.d.ts +13 -0
  2. package/dist/bin/area-ledger-migration.d.ts.map +1 -0
  3. package/dist/bin/area-ledger-migration.js +64 -0
  4. package/dist/bin/area-ledger-migration.js.map +1 -0
  5. package/dist/bin/area-ledger-migration.test.d.ts +2 -0
  6. package/dist/bin/area-ledger-migration.test.d.ts.map +1 -0
  7. package/dist/bin/area-ledger-migration.test.js +55 -0
  8. package/dist/bin/area-ledger-migration.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +6 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +6 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.js +47 -0
  14. package/dist/bin/sync-runner-company.test.js.map +1 -1
  15. package/dist/cli/share.d.ts +8 -0
  16. package/dist/cli/share.d.ts.map +1 -1
  17. package/dist/cli/share.js +184 -10
  18. package/dist/cli/share.js.map +1 -1
  19. package/dist/cli/share.test.js +330 -7
  20. package/dist/cli/share.test.js.map +1 -1
  21. package/dist/cli/sync.d.ts +7 -0
  22. package/dist/cli/sync.d.ts.map +1 -1
  23. package/dist/cli/sync.js +11 -1
  24. package/dist/cli/sync.js.map +1 -1
  25. package/dist/cli/sync.test.js +25 -0
  26. package/dist/cli/sync.test.js.map +1 -1
  27. package/dist/index.d.ts +2 -0
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/index.test.js +11 -1
  32. package/dist/index.test.js.map +1 -1
  33. package/dist/journal-store.test.js +42 -1
  34. package/dist/journal-store.test.js.map +1 -1
  35. package/dist/journal.d.ts +13 -1
  36. package/dist/journal.d.ts.map +1 -1
  37. package/dist/journal.js +104 -3
  38. package/dist/journal.js.map +1 -1
  39. package/dist/sync/area-ledger-migration.d.ts +116 -0
  40. package/dist/sync/area-ledger-migration.d.ts.map +1 -0
  41. package/dist/sync/area-ledger-migration.js +505 -0
  42. package/dist/sync/area-ledger-migration.js.map +1 -0
  43. package/dist/sync/area-ledger-migration.test.d.ts +2 -0
  44. package/dist/sync/area-ledger-migration.test.d.ts.map +1 -0
  45. package/dist/sync/area-ledger-migration.test.js +274 -0
  46. package/dist/sync/area-ledger-migration.test.js.map +1 -0
  47. package/dist/sync/area-ledger.d.ts +93 -0
  48. package/dist/sync/area-ledger.d.ts.map +1 -0
  49. package/dist/sync/area-ledger.js +460 -0
  50. package/dist/sync/area-ledger.js.map +1 -0
  51. package/dist/sync/area-ledger.test.d.ts +2 -0
  52. package/dist/sync/area-ledger.test.d.ts.map +1 -0
  53. package/dist/sync/area-ledger.test.js +199 -0
  54. package/dist/sync/area-ledger.test.js.map +1 -0
  55. package/dist/sync/event-sync.d.ts +14 -4
  56. package/dist/sync/event-sync.d.ts.map +1 -1
  57. package/dist/sync/event-sync.js +177 -61
  58. package/dist/sync/event-sync.js.map +1 -1
  59. package/dist/sync/event-sync.test.js +140 -10
  60. package/dist/sync/event-sync.test.js.map +1 -1
  61. package/dist/sync/index.d.ts +2 -0
  62. package/dist/sync/index.d.ts.map +1 -1
  63. package/dist/sync/index.js +1 -0
  64. package/dist/sync/index.js.map +1 -1
  65. package/dist/sync/parallel-sync-coordinator.d.ts.map +1 -1
  66. package/dist/sync/parallel-sync-coordinator.js +22 -4
  67. package/dist/sync/parallel-sync-coordinator.js.map +1 -1
  68. package/dist/sync/parallel-sync-coordinator.test.js +45 -0
  69. package/dist/sync/parallel-sync-coordinator.test.js.map +1 -1
  70. package/dist/sync/reconcile-cursor.d.ts +97 -0
  71. package/dist/sync/reconcile-cursor.d.ts.map +1 -0
  72. package/dist/sync/reconcile-cursor.js +215 -0
  73. package/dist/sync/reconcile-cursor.js.map +1 -0
  74. package/dist/sync/reconcile-cursor.test.d.ts +2 -0
  75. package/dist/sync/reconcile-cursor.test.d.ts.map +1 -0
  76. package/dist/sync/reconcile-cursor.test.js +139 -0
  77. package/dist/sync/reconcile-cursor.test.js.map +1 -0
  78. package/dist/sync/state-store.d.ts +76 -0
  79. package/dist/sync/state-store.d.ts.map +1 -1
  80. package/dist/sync/state-store.js +317 -3
  81. package/dist/sync/state-store.js.map +1 -1
  82. package/dist/sync/state-store.test.js +39 -1
  83. package/dist/sync/state-store.test.js.map +1 -1
  84. package/dist/sync/sync-work.d.ts +2 -1
  85. package/dist/sync/sync-work.d.ts.map +1 -1
  86. package/dist/sync/sync-work.js.map +1 -1
  87. package/dist/types.d.ts +6 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/dist/vault-client.d.ts +9 -2
  90. package/dist/vault-client.d.ts.map +1 -1
  91. package/dist/vault-client.js +3 -2
  92. package/dist/vault-client.js.map +1 -1
  93. package/dist/vault-client.test.js +26 -0
  94. package/dist/vault-client.test.js.map +1 -1
  95. package/package.json +3 -2
@@ -20,6 +20,8 @@ export declare function setStateStorePositionalWriteForTest(write: ((fd: number,
20
20
  export declare function setStateStoreRotationClaimWriteHookForTest(hook: (() => void) | undefined): void;
21
21
  /** Test-only seam for proving same-process async lock waiters queue cooperatively. */
22
22
  export declare function setStateStoreScopeLockAcquiredHookForTest(hook: (() => void | Promise<void>) | undefined): void;
23
+ /** Test-only seam for a failure after a batch lock file has been created. */
24
+ export declare function setStateStoreScopeLockWriteHookForTest(hook: (() => void) | undefined): void;
23
25
  /** Test-only seam for exercising async lock deadlines without a minute-long wait. */
24
26
  export declare function setStateStoreAsyncLockTimeoutForTest(timeoutMs: number | undefined): void;
25
27
  /** Test-only seam for exercising synchronous lock contention without a five-second wait. */
@@ -55,12 +57,53 @@ export interface StateStoreRefreshResult {
55
57
  export interface StateStoreAppendResult extends StateStoreRecord {
56
58
  refresh: StateStoreRefreshResult;
57
59
  }
60
+ /**
61
+ * One durable append participating in a short, ordered multi-scope commit.
62
+ *
63
+ * This is deliberately an internal-storage primitive rather than a general
64
+ * transaction manager: callers still own their recovery protocol. It gives
65
+ * them one important invariant that a sequence of `append()` calls cannot:
66
+ * every live lock is acquired in a stable order and a contender releases all
67
+ * acquired locks before waiting to retry.
68
+ */
69
+ export interface StateStoreBatchAppend<T> {
70
+ /** Stable caller-owned order key. `@meta` is always acquired last. */
71
+ areaId: string;
72
+ store: StateStore<T>;
73
+ type: number;
74
+ payload: unknown;
75
+ flags?: number;
76
+ }
58
77
  export interface StateStoreRecoveryStats {
59
78
  /** Bytes in the selected generation's WAL, including its header. */
60
79
  walBytes: number;
61
80
  /** Largest single snapshot payload or WAL frame buffered during recovery. */
62
81
  maxReadBytes: number;
63
82
  }
83
+ /** A stable identity for one complete snapshot/WAL generation. */
84
+ export interface StateStoreGenerationIdentity {
85
+ generation: number;
86
+ lastRecordSequence: number;
87
+ walBytes: number;
88
+ }
89
+ /**
90
+ * A durable lease on an immutable source generation.
91
+ *
92
+ * Rotation normally keeps only a short rollback history. Migration uses this
93
+ * lease to make an older source generation part of that history until the
94
+ * copy/catch-up transaction has finished (or has explicitly been abandoned).
95
+ */
96
+ export interface StateStoreGenerationPin {
97
+ readonly identity: StateStoreGenerationIdentity;
98
+ /** Durable pin file, persisted by long-running migration control state. */
99
+ readonly path: string;
100
+ release(): void;
101
+ }
102
+ /** One bounded value yielded from a keyed state projection. */
103
+ export interface StateStoreStateEntry {
104
+ key: string;
105
+ value: unknown;
106
+ }
64
107
  export interface StateStoreRepairResult {
65
108
  recoveredGeneration: number;
66
109
  recoveredWalBytes: number;
@@ -112,6 +155,17 @@ export declare class StateStore<T> {
112
155
  static open<T>(options: StateStoreOptions<T>): StateStore<T>;
113
156
  /** Yielding first-open variant for callback-originated projections. */
114
157
  static openAsync<T>(options: StateStoreOptions<T>): Promise<StateStore<T>>;
158
+ /**
159
+ * Append one record to each independent store under lexical, all-or-none
160
+ * locks. The caller's `areaId` is the stable order key; hashed on-disk
161
+ * directories must never decide lock order. `@meta` intentionally follows
162
+ * all data areas so a watermark can only publish after its data is durable.
163
+ *
164
+ * A process death between records is still possible, by design. Consumers
165
+ * that need cross-area atomic visibility must use a data-first recovery
166
+ * protocol (AreaLedger does this with prepare records and a meta watermark).
167
+ */
168
+ static appendBatch<T>(entries: readonly StateStoreBatchAppend<T>[]): StateStoreAppendResult[];
115
169
  /**
116
170
  * Repair an over-budget generation without making appenders wait for snapshot
117
171
  * I/O. Recovery captures a stable generation while holding the scope lock,
@@ -123,6 +177,19 @@ export declare class StateStore<T> {
123
177
  getGeneration(): number;
124
178
  getLastRecordSequence(): number;
125
179
  getRecoveryStats(): StateStoreRecoveryStats;
180
+ getGenerationIdentity(): StateStoreGenerationIdentity;
181
+ /**
182
+ * Persist a generation pin while holding append.lock. The pin is a tiny
183
+ * checksummed file in the store directory, so a concurrent compactor sees it
184
+ * before deciding which immutable generations may be pruned.
185
+ */
186
+ pinCurrentGeneration(): StateStoreGenerationPin;
187
+ /**
188
+ * Iterate a keyed projection without exposing its mutable backing object.
189
+ * Each yielded row is cloned independently, allowing migration clients to
190
+ * keep projected WAL records bounded even when the source journal is huge.
191
+ */
192
+ iterateStateEntries(path: readonly string[]): IterableIterator<StateStoreStateEntry>;
126
193
  append(type: number, payload: unknown, flags?: number): StateStoreAppendResult;
127
194
  /**
128
195
  * Async lock acquisition for callback-originated work. The write itself
@@ -147,6 +214,15 @@ export declare class StateStore<T> {
147
214
  * and retain the prior compact copy.
148
215
  */
149
216
  replaceState(nextState: T): void;
217
+ /**
218
+ * Atomically derive a compact replacement from the newest durable state.
219
+ *
220
+ * Callers that fold a projection must not calculate their replacement before
221
+ * taking the append lock: another writer could otherwise be lost between the
222
+ * read and publication. The transform runs under that lock and publication
223
+ * still validates the source generation after the off-lock snapshot stage.
224
+ */
225
+ replaceStateWith(transform: (state: T) => T): void;
150
226
  private replaceStateAttempt;
151
227
  /** Create G+1 without ever merging two generations during recovery. */
152
228
  compact(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"state-store.d.ts","sourceRoot":"","sources":["../../src/sync/state-store.ts"],"names":[],"mappings":"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"}
@@ -60,6 +60,7 @@ let reflinkCopyHookForTest;
60
60
  let positionalWriteForTest;
61
61
  let rotationClaimWriteHookForTest;
62
62
  let scopeLockAcquiredHookForTest;
63
+ let scopeLockWriteHookForTest;
63
64
  let asyncLockTimeoutMsForTest;
64
65
  let syncLockTimeoutMsForTest;
65
66
  export function resetStateStoreSnapshotRecoveryCountForTest() {
@@ -112,6 +113,10 @@ export function setStateStoreRotationClaimWriteHookForTest(hook) {
112
113
  export function setStateStoreScopeLockAcquiredHookForTest(hook) {
113
114
  scopeLockAcquiredHookForTest = hook;
114
115
  }
116
+ /** Test-only seam for a failure after a batch lock file has been created. */
117
+ export function setStateStoreScopeLockWriteHookForTest(hook) {
118
+ scopeLockWriteHookForTest = hook;
119
+ }
115
120
  /** Test-only seam for exercising async lock deadlines without a minute-long wait. */
116
121
  export function setStateStoreAsyncLockTimeoutForTest(timeoutMs) {
117
122
  asyncLockTimeoutMsForTest = timeoutMs;
@@ -245,6 +250,80 @@ export class StateStore {
245
250
  return created;
246
251
  }
247
252
  }
253
+ /**
254
+ * Append one record to each independent store under lexical, all-or-none
255
+ * locks. The caller's `areaId` is the stable order key; hashed on-disk
256
+ * directories must never decide lock order. `@meta` intentionally follows
257
+ * all data areas so a watermark can only publish after its data is durable.
258
+ *
259
+ * A process death between records is still possible, by design. Consumers
260
+ * that need cross-area atomic visibility must use a data-first recovery
261
+ * protocol (AreaLedger does this with prepare records and a meta watermark).
262
+ */
263
+ static appendBatch(entries) {
264
+ if (entries.length === 0)
265
+ return [];
266
+ const seenAreas = new Set();
267
+ const seenStores = new Set();
268
+ const ordered = entries.map((entry, index) => {
269
+ if (!entry.areaId)
270
+ throw new Error("state-store: batch areaId is required");
271
+ if (seenAreas.has(entry.areaId))
272
+ throw new Error(`state-store: duplicate batch area ${entry.areaId}`);
273
+ if (seenStores.has(entry.store.stateDir))
274
+ throw new Error(`state-store: duplicate batch state directory ${entry.store.stateDir}`);
275
+ seenAreas.add(entry.areaId);
276
+ seenStores.add(entry.store.stateDir);
277
+ assertUint16(entry.type, "record type");
278
+ assertUint16(entry.flags ?? 0, "record flags");
279
+ return { ...entry, index };
280
+ }).sort((left, right) => {
281
+ if (left.areaId === "@meta")
282
+ return right.areaId === "@meta" ? 0 : 1;
283
+ if (right.areaId === "@meta")
284
+ return -1;
285
+ return left.areaId.localeCompare(right.areaId);
286
+ });
287
+ let recovered = false;
288
+ while (true) {
289
+ const result = withScopeLocks(ordered.map((entry) => entry.store.stateDir), () => {
290
+ // Refresh every participant before appending any record. If a
291
+ // generation changed, release the full set and recover off-lock; a
292
+ // partial batch must never retry and duplicate an earlier append.
293
+ const refreshes = new Map();
294
+ for (const entry of ordered) {
295
+ const refresh = entry.store.refreshFromDisk();
296
+ if (!refresh)
297
+ return undefined;
298
+ refreshes.set(entry, refresh);
299
+ }
300
+ const appended = [];
301
+ for (const entry of ordered) {
302
+ appended.push({
303
+ entry,
304
+ result: entry.store.appendUnlocked(entry.type, entry.payload, entry.flags ?? 0, recovered ? { records: [], recovered: true } : refreshes.get(entry)),
305
+ });
306
+ }
307
+ return appended;
308
+ });
309
+ if (!result) {
310
+ for (const entry of ordered)
311
+ entry.store.recoverNewestGenerationOutsideLock();
312
+ recovered = true;
313
+ continue;
314
+ }
315
+ const compact = new Set();
316
+ for (const { entry } of result)
317
+ if (entry.store.needsCompaction())
318
+ compact.add(entry.store);
319
+ for (const store of compact)
320
+ store.compact();
321
+ const byIndex = [];
322
+ for (const appended of result)
323
+ byIndex[appended.entry.index] = appended.result;
324
+ return byIndex;
325
+ }
326
+ }
248
327
  /**
249
328
  * Repair an over-budget generation without making appenders wait for snapshot
250
329
  * I/O. Recovery captures a stable generation while holding the scope lock,
@@ -355,6 +434,70 @@ export class StateStore {
355
434
  getRecoveryStats() {
356
435
  return { walBytes: this.walBytes, maxReadBytes: this.maxRecoveryReadBytes };
357
436
  }
437
+ getGenerationIdentity() {
438
+ return {
439
+ generation: this.generation,
440
+ lastRecordSequence: this.lastRecordSequence,
441
+ walBytes: this.walBytes,
442
+ };
443
+ }
444
+ /**
445
+ * Persist a generation pin while holding append.lock. The pin is a tiny
446
+ * checksummed file in the store directory, so a concurrent compactor sees it
447
+ * before deciding which immutable generations may be pruned.
448
+ */
449
+ pinCurrentGeneration() {
450
+ while (true) {
451
+ const pin = withScopeLock(this.stateDir, () => {
452
+ const refresh = this.refreshFromDisk();
453
+ if (!refresh)
454
+ return undefined;
455
+ const identity = this.getGenerationIdentity();
456
+ const pinPath = writeGenerationPin(this.stateDir, this.scopeDigest, identity);
457
+ let released = false;
458
+ return {
459
+ identity,
460
+ path: pinPath,
461
+ release: () => {
462
+ if (released)
463
+ return;
464
+ released = true;
465
+ try {
466
+ fs.unlinkSync(pinPath);
467
+ fsyncDirectory(this.stateDir);
468
+ }
469
+ catch (error) {
470
+ if (!isMissing(error))
471
+ throw new Error(`state-store: cannot release generation pin ${pinPath}`, { cause: error });
472
+ }
473
+ },
474
+ };
475
+ });
476
+ if (pin)
477
+ return pin;
478
+ this.recoverNewestGenerationOutsideLock();
479
+ }
480
+ }
481
+ /**
482
+ * Iterate a keyed projection without exposing its mutable backing object.
483
+ * Each yielded row is cloned independently, allowing migration clients to
484
+ * keep projected WAL records bounded even when the source journal is huge.
485
+ */
486
+ *iterateStateEntries(path) {
487
+ let current = this.state;
488
+ for (const segment of path) {
489
+ if (typeof current !== "object" || current === null || Array.isArray(current) || !Object.hasOwn(current, segment)) {
490
+ throw new Error(`state-store: state path ${JSON.stringify(path)} is not an object map`);
491
+ }
492
+ current = current[segment];
493
+ }
494
+ if (typeof current !== "object" || current === null || Array.isArray(current)) {
495
+ throw new Error(`state-store: state path ${JSON.stringify(path)} is not an object map`);
496
+ }
497
+ for (const [key, value] of Object.entries(current)) {
498
+ yield { key, value: structuredClone(value) };
499
+ }
500
+ }
358
501
  append(type, payload, flags = 0) {
359
502
  assertUint16(type, "record type");
360
503
  assertUint16(flags, "record flags");
@@ -494,16 +637,28 @@ export class StateStore {
494
637
  * and retain the prior compact copy.
495
638
  */
496
639
  replaceState(nextState) {
640
+ this.replaceStateWith(() => nextState);
641
+ }
642
+ /**
643
+ * Atomically derive a compact replacement from the newest durable state.
644
+ *
645
+ * Callers that fold a projection must not calculate their replacement before
646
+ * taking the append lock: another writer could otherwise be lost between the
647
+ * read and publication. The transform runs under that lock and publication
648
+ * still validates the source generation after the off-lock snapshot stage.
649
+ */
650
+ replaceStateWith(transform) {
497
651
  for (let attempt = 0; attempt < REPLACE_STATE_MAX_ATTEMPTS; attempt++) {
498
- if (this.replaceStateAttempt(nextState))
652
+ if (this.replaceStateAttempt(transform))
499
653
  return;
500
654
  }
501
655
  throw new StateStoreLockError(`cannot replace state after ${REPLACE_STATE_MAX_ATTEMPTS} concurrent writes`);
502
656
  }
503
- replaceStateAttempt(nextState) {
657
+ replaceStateAttempt(transform) {
504
658
  const plan = withScopeLock(this.stateDir, () => {
505
659
  if (!this.refreshFromDisk())
506
660
  return undefined;
661
+ const nextState = transform(this.state);
507
662
  const previousGeneration = this.generation;
508
663
  const previousWalHasRecords = this.walBytes > WAL_HEADER_BYTES;
509
664
  const first = {
@@ -1542,6 +1697,121 @@ function withScopeLock(dir, action) {
1542
1697
  throw actionError;
1543
1698
  return result;
1544
1699
  }
1700
+ /**
1701
+ * Acquire every scope lock in caller-provided stable order, or acquire none.
1702
+ *
1703
+ * Waiting while holding an earlier lock would create the classic A→B/B→A
1704
+ * deadlock across independent StateStores. This optimistic loop instead
1705
+ * drops the partial set and retries after the ordinary bounded backoff. It is
1706
+ * only for tiny durable publication sections; staging, recovery, callbacks,
1707
+ * and network work remain outside these locks.
1708
+ */
1709
+ function withScopeLocks(dirs, action) {
1710
+ const deadline = Date.now() + (syncLockTimeoutMsForTest ?? SYNC_LOCK_TIMEOUT_MS);
1711
+ const acquired = [];
1712
+ while (true) {
1713
+ let unavailable = false;
1714
+ try {
1715
+ for (const dir of dirs) {
1716
+ const held = tryAcquireScopeLock(dir);
1717
+ if (!held) {
1718
+ unavailable = true;
1719
+ break;
1720
+ }
1721
+ acquired.push(held);
1722
+ }
1723
+ if (!unavailable)
1724
+ break;
1725
+ }
1726
+ catch (error) {
1727
+ releaseScopeLocks(acquired);
1728
+ throw error;
1729
+ }
1730
+ releaseScopeLocks(acquired);
1731
+ if (Date.now() >= deadline) {
1732
+ throw new StateStoreLockError("timed out acquiring ordered scope locks");
1733
+ }
1734
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, LOCK_RETRY_MS);
1735
+ }
1736
+ let result;
1737
+ let actionError;
1738
+ try {
1739
+ result = action();
1740
+ }
1741
+ catch (error) {
1742
+ actionError = error;
1743
+ }
1744
+ releaseScopeLocks(acquired);
1745
+ if (actionError !== undefined)
1746
+ throw actionError;
1747
+ return result;
1748
+ }
1749
+ function tryAcquireScopeLock(dir) {
1750
+ const lockPath = path.join(dir, "append.lock");
1751
+ if (inProcessAsyncScopeLocks.has(path.resolve(dir))) {
1752
+ throw new StateStoreLockError(`in-process async holder for ${lockPath}`, {
1753
+ reason: "in-process-async-holder",
1754
+ });
1755
+ }
1756
+ let fd;
1757
+ try {
1758
+ fd = fs.openSync(lockPath, "wx");
1759
+ scopeLockWriteHookForTest?.();
1760
+ fs.writeFileSync(fd, String(process.pid));
1761
+ fs.fdatasyncSync(fd);
1762
+ return { lockPath, fd };
1763
+ }
1764
+ catch (error) {
1765
+ // `openSync(..., "wx")` has already made this process the lock owner.
1766
+ // A failed PID write or sync must not leave that live-PID lock behind,
1767
+ // because no stale breaker will remove it while this process remains up.
1768
+ if (fd !== undefined) {
1769
+ let cleanupError;
1770
+ try {
1771
+ fs.closeSync(fd);
1772
+ }
1773
+ catch (closeError) {
1774
+ cleanupError = closeError;
1775
+ }
1776
+ try {
1777
+ fs.unlinkSync(lockPath);
1778
+ }
1779
+ catch (unlinkError) {
1780
+ if (!isMissing(unlinkError))
1781
+ cleanupError ??= unlinkError;
1782
+ }
1783
+ if (cleanupError !== undefined)
1784
+ throw new StateStoreLockError(`cannot clean up ${lockPath}`, { cause: cleanupError });
1785
+ }
1786
+ if (!isAlreadyExists(error)) {
1787
+ throw new StateStoreLockError(`cannot acquire ${lockPath}`, { cause: error });
1788
+ }
1789
+ if (removeDeadLock(lockPath))
1790
+ return tryAcquireScopeLock(dir);
1791
+ return undefined;
1792
+ }
1793
+ }
1794
+ function releaseScopeLocks(acquired) {
1795
+ let releaseError;
1796
+ while (acquired.length > 0) {
1797
+ const held = acquired.pop();
1798
+ try {
1799
+ fs.closeSync(held.fd);
1800
+ }
1801
+ catch (error) {
1802
+ releaseError ??= error;
1803
+ }
1804
+ try {
1805
+ fs.unlinkSync(held.lockPath);
1806
+ }
1807
+ catch (error) {
1808
+ releaseError ??= error;
1809
+ }
1810
+ }
1811
+ if (releaseError !== undefined) {
1812
+ throw new StateStoreLockError("cannot release ordered scope locks", { cause: releaseError });
1813
+ }
1814
+ }
1545
1815
  /**
1546
1816
  * A best-effort, non-blocking claim for off-lock snapshot materialisation.
1547
1817
  * Appenders deliberately ignore it; it only prevents several stale handles
@@ -1966,7 +2236,7 @@ function retainedGenerationHistoryNeedsRepair(dir, newestGeneration, currentGene
1966
2236
  return false;
1967
2237
  }
1968
2238
  function pruneGenerations(dir, retain) {
1969
- const retained = new Set(retain);
2239
+ const retained = new Set([...retain, ...pinnedGenerations(dir)]);
1970
2240
  for (const name of fs.readdirSync(dir)) {
1971
2241
  const match = /^(?:snapshot|wal)-(\d+)\.bin$/.exec(name);
1972
2242
  if (!match)
@@ -1983,6 +2253,50 @@ function pruneGenerations(dir, retain) {
1983
2253
  }
1984
2254
  fsyncDirectory(dir);
1985
2255
  }
2256
+ function writeGenerationPin(dir, scopeDigest, identity) {
2257
+ const body = {
2258
+ generation: identity.generation,
2259
+ lastRecordSequence: identity.lastRecordSequence,
2260
+ walBytes: identity.walBytes,
2261
+ scopeDigest: scopeDigest.toString("hex"),
2262
+ };
2263
+ const checksum = sha256(Buffer.from(canonicalJson(body), "utf8")).toString("hex");
2264
+ const pinPath = path.join(dir, `generation-pin-${process.pid}-${crypto.randomBytes(8).toString("hex")}.json`);
2265
+ writeNewDurably(pinPath, Buffer.from(`${canonicalJson({ ...body, checksum })}\n`, "utf8"));
2266
+ fsyncDirectory(dir);
2267
+ return pinPath;
2268
+ }
2269
+ /** Invalid pins are retained conservatively: pruning is never allowed to win a crash race. */
2270
+ function pinnedGenerations(dir) {
2271
+ const pins = new Set();
2272
+ for (const name of fs.readdirSync(dir)) {
2273
+ if (!/^generation-pin-[A-Za-z0-9-]+\.json$/.test(name))
2274
+ continue;
2275
+ try {
2276
+ const raw = JSON.parse(fs.readFileSync(path.join(dir, name), "utf8"));
2277
+ const { checksum, ...body } = raw;
2278
+ if (typeof checksum !== "string" ||
2279
+ checksum !== sha256(Buffer.from(canonicalJson(body), "utf8")).toString("hex") ||
2280
+ !Number.isSafeInteger(body.generation) ||
2281
+ body.generation < 1) {
2282
+ // A torn pin represents an in-flight migration; retain every local
2283
+ // generation rather than guessing which source it meant to protect.
2284
+ return fs.readdirSync(dir)
2285
+ .map((candidate) => /^(?:snapshot|wal)-(\d+)\.bin$/.exec(candidate)?.[1])
2286
+ .filter((value) => value !== undefined)
2287
+ .map(Number);
2288
+ }
2289
+ pins.add(body.generation);
2290
+ }
2291
+ catch {
2292
+ return fs.readdirSync(dir)
2293
+ .map((candidate) => /^(?:snapshot|wal)-(\d+)\.bin$/.exec(candidate)?.[1])
2294
+ .filter((value) => value !== undefined)
2295
+ .map(Number);
2296
+ }
2297
+ }
2298
+ return [...pins];
2299
+ }
1986
2300
  function cleanupUnpublishedInitialGeneration(dir) {
1987
2301
  if (fs.existsSync(path.join(dir, "current")))
1988
2302
  return;