@peerbit/native-backbone 0.1.3 → 0.2.0

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 (47) hide show
  1. package/README.md +31 -0
  2. package/dist/src/durability/codec.d.ts +86 -0
  3. package/dist/src/durability/codec.d.ts.map +1 -0
  4. package/dist/src/durability/codec.js +365 -0
  5. package/dist/src/durability/codec.js.map +1 -0
  6. package/dist/src/durability/lease.d.ts +59 -0
  7. package/dist/src/durability/lease.d.ts.map +1 -0
  8. package/dist/src/durability/lease.js +48 -0
  9. package/dist/src/durability/lease.js.map +1 -0
  10. package/dist/src/durability/memory-storage.d.ts +37 -0
  11. package/dist/src/durability/memory-storage.d.ts.map +1 -0
  12. package/dist/src/durability/memory-storage.js +436 -0
  13. package/dist/src/durability/memory-storage.js.map +1 -0
  14. package/dist/src/durability/node-lease.d.ts +14 -0
  15. package/dist/src/durability/node-lease.d.ts.map +1 -0
  16. package/dist/src/durability/node-lease.js +214 -0
  17. package/dist/src/durability/node-lease.js.map +1 -0
  18. package/dist/src/durability/node-storage.d.ts +76 -0
  19. package/dist/src/durability/node-storage.d.ts.map +1 -0
  20. package/dist/src/durability/node-storage.js +1813 -0
  21. package/dist/src/durability/node-storage.js.map +1 -0
  22. package/dist/src/durability/storage.d.ts +224 -0
  23. package/dist/src/durability/storage.d.ts.map +1 -0
  24. package/dist/src/durability/storage.js +343 -0
  25. package/dist/src/durability/storage.js.map +1 -0
  26. package/dist/src/index.d.ts +93 -15
  27. package/dist/src/index.d.ts.map +1 -1
  28. package/dist/src/index.js +717 -199
  29. package/dist/src/index.js.map +1 -1
  30. package/dist/wasm/README.md +31 -0
  31. package/dist/wasm/native_backbone.d.ts +93 -77
  32. package/dist/wasm/native_backbone.js +96 -0
  33. package/dist/wasm/native_backbone_bg.wasm +0 -0
  34. package/dist/wasm/native_backbone_bg.wasm.d.ts +81 -77
  35. package/package.json +4 -3
  36. package/src/append_tx/committed_latest.rs +24 -43
  37. package/src/documents.rs +7 -9
  38. package/src/durability/codec.ts +683 -0
  39. package/src/durability/lease.ts +87 -0
  40. package/src/durability/memory-storage.ts +593 -0
  41. package/src/durability/node-lease.ts +293 -0
  42. package/src/durability/node-storage.ts +2798 -0
  43. package/src/durability/storage.ts +682 -0
  44. package/src/durability.rs +1872 -0
  45. package/src/error.rs +8 -0
  46. package/src/index.ts +1392 -735
  47. package/src/lib.rs +1 -0
@@ -0,0 +1,2798 @@
1
+ import {
2
+ NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH,
3
+ type NativeDurabilityCheckpointTransactionState,
4
+ NativeDurabilityJournalCodec,
5
+ type NativeDurabilityJournalRecord,
6
+ type NativeDurabilityJournalScan,
7
+ type NativeDurabilityJournalValidationContext,
8
+ NativeDurabilityPhase,
9
+ createNativeDurabilityJournalCodec,
10
+ isNativeDurabilityJournalCodec,
11
+ } from "./codec.js";
12
+ import {
13
+ NATIVE_DURABILITY_MAX_U64,
14
+ type NativeDurabilityLease,
15
+ } from "./lease.js";
16
+ import {
17
+ NATIVE_DURABILITY_NODE_LEASE_DIRECTORY_NAME,
18
+ acquireNativeDurabilityNodeLease,
19
+ } from "./node-lease.js";
20
+ import {
21
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES,
22
+ NATIVE_DURABILITY_STORAGE_VERSION,
23
+ type NativeDurabilityCheckpoint,
24
+ type NativeDurabilityCheckpointReceipt,
25
+ type NativeDurabilityCheckpointRequest,
26
+ type NativeDurabilityDeleteReceipt,
27
+ type NativeDurabilityDeleteRequest,
28
+ NativeDurabilityDigestMismatchError,
29
+ NativeDurabilityIncompleteTailMismatchError,
30
+ type NativeDurabilityIncompleteTailReconciliationRequest,
31
+ type NativeDurabilityJournalAppendRequest,
32
+ NativeDurabilityJournalOffsetConflictError,
33
+ type NativeDurabilityJournalReceipt,
34
+ type NativeDurabilityJournalReconciliationReceipt,
35
+ NativeDurabilityMigrationRequiredError,
36
+ type NativeDurabilityOperationScope,
37
+ NativeDurabilityOutcomeUnknownError,
38
+ type NativeDurabilityStageReceipt,
39
+ type NativeDurabilityStageRequest,
40
+ type NativeDurabilityStagedBlockReference,
41
+ type NativeDurabilityStagingManifest,
42
+ type NativeDurabilityStorage,
43
+ NativeDurabilityStorageClosedError,
44
+ NativeDurabilityStorageCorruptionError,
45
+ type NativeDurabilityStorageStats,
46
+ NativeDurabilityStorageUnsupportedError,
47
+ assertNativeDurabilityCheckpointRequest,
48
+ assertNativeDurabilityDeleteRequest,
49
+ assertNativeDurabilityFence,
50
+ assertNativeDurabilityJournalAppendRequest,
51
+ assertNativeDurabilityOperationScope,
52
+ assertNativeDurabilityStageRequest,
53
+ copyNativeDurabilityBytes,
54
+ encodeNativeDurabilityCanonical,
55
+ nativeDurabilityBytesEqual,
56
+ nativeDurabilityDigestFromHex,
57
+ nativeDurabilityDigestHex,
58
+ } from "./storage.js";
59
+
60
+ const STORAGE_DIRECTORY_NAME = "native-durability-v1";
61
+ const STAGING_DIRECTORY_NAME = "staging";
62
+ const CHECKPOINT_DIRECTORY_NAME = "checkpoints";
63
+ const JOURNAL_FILE_NAME = "journal.bin";
64
+ const STAGING_MANIFEST_FILE_NAME = "manifest.json";
65
+ const CHECKPOINT_HIGHWATER_FILE_NAME = "generation-highwater.json";
66
+ const CHECKPOINT_MANIFEST_A = "manifest-a.json";
67
+ const CHECKPOINT_MANIFEST_B = "manifest-b.json";
68
+ const JOURNAL_BASE_MANIFEST = "journal-base.json";
69
+
70
+ type ManifestEnvelope = { payload: string; checksum: string };
71
+
72
+ type NodeFs = typeof import("fs/promises");
73
+ type NodePath = typeof import("path");
74
+ type NodeCreateHash = (typeof import("crypto"))["createHash"];
75
+
76
+ let nodeFs: NodeFs | undefined;
77
+ let nodePath: NodePath | undefined;
78
+ let nodeCreateHash: NodeCreateHash | undefined;
79
+
80
+ const loadNodeModules = async (): Promise<void> => {
81
+ if (nodeFs && nodePath && nodeCreateHash) return;
82
+ const processLike = globalThis as {
83
+ process?: { versions?: { node?: string } };
84
+ };
85
+ if (!processLike.process?.versions?.node) {
86
+ throw new NativeDurabilityStorageUnsupportedError(
87
+ "Native durability strict storage is Node-only; OPFS is not supported",
88
+ );
89
+ }
90
+ const fsModule = "fs/promises";
91
+ const pathModule = "path";
92
+ const cryptoModule = "crypto";
93
+ const [fs, path, crypto] = await Promise.all([
94
+ import(/* @vite-ignore */ fsModule) as Promise<NodeFs>,
95
+ import(/* @vite-ignore */ pathModule) as Promise<NodePath>,
96
+ import(/* @vite-ignore */ cryptoModule) as Promise<typeof import("crypto")>,
97
+ ]);
98
+ nodeFs = fs;
99
+ nodePath = path;
100
+ nodeCreateHash = crypto.createHash;
101
+ };
102
+
103
+ const requireNodeFs = (): NodeFs => {
104
+ if (!nodeFs) throw new Error("Node durability modules have not been loaded");
105
+ return nodeFs;
106
+ };
107
+
108
+ const requireNodePath = (): NodePath => {
109
+ if (!nodePath)
110
+ throw new Error("Node durability modules have not been loaded");
111
+ return nodePath;
112
+ };
113
+
114
+ type DiskFence = { epoch: string; ownerId: string; domainId: string };
115
+ type DiskScope = {
116
+ transactionId: string;
117
+ txSequence: string;
118
+ recordLsn: string;
119
+ };
120
+ type DiskBlockReference = {
121
+ ordinal: number;
122
+ cid: string;
123
+ byteLength: number;
124
+ digest: string;
125
+ };
126
+
127
+ type DiskStagingManifest = {
128
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
129
+ scope: DiskScope;
130
+ fence: DiskFence;
131
+ blocks: DiskBlockReference[];
132
+ };
133
+
134
+ type DiskStagingCoverage = {
135
+ transactionId: string;
136
+ txSequence: string;
137
+ coveredThroughLsn: string;
138
+ stagingManifestDigest: string;
139
+ };
140
+
141
+ type DiskRetainedTransaction = {
142
+ txSequence: string;
143
+ transactionId: string;
144
+ phase: number;
145
+ operationKind: number;
146
+ planDigest: string;
147
+ };
148
+
149
+ type DiskCheckpointManifest = {
150
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
151
+ programId: string;
152
+ generation: string;
153
+ checkpointLsn: string;
154
+ txSequenceHighwater: string;
155
+ file: string;
156
+ byteLength: number;
157
+ digest: string;
158
+ originFence: DiskFence;
159
+ stagingCoverage: DiskStagingCoverage[];
160
+ retainedTransactions: DiskRetainedTransaction[];
161
+ };
162
+
163
+ type DiskGenerationIdentity = {
164
+ generation: string;
165
+ requestDigest: string;
166
+ transactionId: string;
167
+ txSequence: string;
168
+ };
169
+
170
+ type DiskGenerationHighwater = {
171
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
172
+ generation: string;
173
+ pending?: DiskGenerationIdentity;
174
+ completed?: DiskGenerationIdentity;
175
+ };
176
+
177
+ type GenerationIdentity = {
178
+ generation: bigint;
179
+ requestDigest: Uint8Array;
180
+ transactionId: string;
181
+ txSequence: bigint;
182
+ };
183
+
184
+ type GenerationState = {
185
+ generation: bigint;
186
+ pending?: GenerationIdentity;
187
+ completed?: GenerationIdentity;
188
+ };
189
+
190
+ export type NativeDurabilityNodeWritable = {
191
+ write: (
192
+ buffer: Uint8Array,
193
+ offset: number,
194
+ length: number,
195
+ position: number,
196
+ ) => Promise<{ bytesWritten: number }>;
197
+ };
198
+
199
+ export type NativeDurabilityNodeReadable = {
200
+ read: (
201
+ buffer: Uint8Array,
202
+ offset: number,
203
+ length: number,
204
+ position: number,
205
+ ) => Promise<{ bytesRead: number }>;
206
+ };
207
+
208
+ /** `FileHandle.write()` is allowed to complete with a short write. */
209
+ export const writeNativeDurabilityBytesFully = async (
210
+ handle: NativeDurabilityNodeWritable,
211
+ bytes: Uint8Array,
212
+ position: number,
213
+ ): Promise<void> => {
214
+ let written = 0;
215
+ while (written < bytes.byteLength) {
216
+ const result = await handle.write(
217
+ bytes,
218
+ written,
219
+ bytes.byteLength - written,
220
+ position + written,
221
+ );
222
+ if (
223
+ !Number.isSafeInteger(result.bytesWritten) ||
224
+ result.bytesWritten <= 0
225
+ ) {
226
+ throw new Error("Native durability write made no forward progress");
227
+ }
228
+ if (result.bytesWritten > bytes.byteLength - written) {
229
+ throw new Error(
230
+ "Native durability write exceeded the requested byte count",
231
+ );
232
+ }
233
+ written += result.bytesWritten;
234
+ }
235
+ };
236
+
237
+ export const readNativeDurabilityBytesFully = async (
238
+ handle: NativeDurabilityNodeReadable,
239
+ byteLength: number,
240
+ ): Promise<Uint8Array> => {
241
+ if (!Number.isSafeInteger(byteLength) || byteLength < 0) {
242
+ throw new RangeError("Native durability read length must be safe");
243
+ }
244
+ const bytes = new Uint8Array(byteLength);
245
+ let offset = 0;
246
+ while (offset < byteLength) {
247
+ const result = await handle.read(
248
+ bytes,
249
+ offset,
250
+ byteLength - offset,
251
+ offset,
252
+ );
253
+ if (
254
+ !Number.isSafeInteger(result.bytesRead) ||
255
+ result.bytesRead <= 0 ||
256
+ result.bytesRead > byteLength - offset
257
+ ) {
258
+ throw new NativeDurabilityStorageCorruptionError(
259
+ "Native durability file changed during an exact read",
260
+ );
261
+ }
262
+ offset += result.bytesRead;
263
+ }
264
+ return bytes;
265
+ };
266
+
267
+ const sha256 = (bytes: Uint8Array): Uint8Array => {
268
+ if (!nodeCreateHash)
269
+ throw new Error("Node durability modules have not been loaded");
270
+ return new Uint8Array(nodeCreateHash("sha256").update(bytes).digest());
271
+ };
272
+
273
+ const sha256Hex = (bytes: Uint8Array): string =>
274
+ nativeDurabilityDigestHex(sha256(bytes));
275
+
276
+ const bytesToHex = (bytes: Uint8Array): string =>
277
+ Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
278
+
279
+ const bytesFromHex = (value: unknown, subject: string): Uint8Array => {
280
+ if (
281
+ typeof value !== "string" ||
282
+ value.length % 2 !== 0 ||
283
+ !/^[0-9a-f]*$/.test(value)
284
+ ) {
285
+ throw new NativeDurabilityStorageCorruptionError(
286
+ `${subject} must be canonical lowercase hexadecimal bytes`,
287
+ );
288
+ }
289
+ const bytes = new Uint8Array(value.length / 2);
290
+ for (let index = 0; index < bytes.byteLength; index++) {
291
+ bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
292
+ }
293
+ return bytes;
294
+ };
295
+
296
+ const encodeManifest = (payload: unknown): Uint8Array => {
297
+ const payloadText = JSON.stringify(payload);
298
+ return new TextEncoder().encode(
299
+ JSON.stringify({
300
+ payload: payloadText,
301
+ checksum: sha256Hex(new TextEncoder().encode(payloadText)),
302
+ }),
303
+ );
304
+ };
305
+
306
+ const decodeManifest = <T>(bytes: Uint8Array, subject: string): T => {
307
+ try {
308
+ const envelope = JSON.parse(
309
+ new TextDecoder().decode(bytes),
310
+ ) as ManifestEnvelope;
311
+ if (
312
+ typeof envelope.payload !== "string" ||
313
+ typeof envelope.checksum !== "string" ||
314
+ sha256Hex(new TextEncoder().encode(envelope.payload)) !==
315
+ envelope.checksum
316
+ ) {
317
+ throw new Error("checksum mismatch");
318
+ }
319
+ return JSON.parse(envelope.payload) as T;
320
+ } catch (error) {
321
+ throw new NativeDurabilityStorageCorruptionError(
322
+ `Invalid ${subject} manifest`,
323
+ error,
324
+ );
325
+ }
326
+ };
327
+
328
+ const isNotFound = (error: unknown): boolean =>
329
+ (error as { code?: string })?.code === "ENOENT";
330
+
331
+ const syncDirectory = async (path: string): Promise<void> => {
332
+ const handle = await requireNodeFs().open(path, "r");
333
+ try {
334
+ await handle.sync();
335
+ } finally {
336
+ await handle.close();
337
+ }
338
+ };
339
+
340
+ const ensureDirectory = async (path: string, parent: string): Promise<void> => {
341
+ let created = false;
342
+ try {
343
+ await requireNodeFs().mkdir(path);
344
+ created = true;
345
+ } catch (error) {
346
+ if ((error as { code?: string }).code !== "EEXIST") throw error;
347
+ if (!(await requireNodeFs().stat(path)).isDirectory()) throw error;
348
+ }
349
+ if (created) await syncDirectory(parent);
350
+ };
351
+
352
+ const diskFence = (lease: NativeDurabilityLease): DiskFence => ({
353
+ epoch: lease.fence.epoch.toString(),
354
+ ownerId: lease.fence.ownerId,
355
+ domainId: lease.fence.domainId,
356
+ });
357
+
358
+ const diskScope = (scope: NativeDurabilityOperationScope): DiskScope => ({
359
+ transactionId: scope.transactionId,
360
+ txSequence: scope.txSequence.toString(),
361
+ recordLsn: scope.recordLsn.toString(),
362
+ });
363
+
364
+ const blockFileName = (ordinal: number): string =>
365
+ `${ordinal.toString().padStart(12, "0")}.block`;
366
+
367
+ const checkpointFileName = (generation: bigint): string =>
368
+ `checkpoint-${generation}.bin`;
369
+
370
+ const transactionDirectoryName = (transactionId: string): string =>
371
+ `tx-${sha256Hex(new TextEncoder().encode(transactionId))}`;
372
+
373
+ export type NativeDurabilityNodeStorageOptions = {
374
+ directory: string;
375
+ programId: Uint8Array;
376
+ };
377
+
378
+ const readFileIfExists = async (
379
+ path: string,
380
+ ): Promise<Uint8Array | undefined> => {
381
+ try {
382
+ return new Uint8Array(await requireNodeFs().readFile(path));
383
+ } catch (error) {
384
+ if (isNotFound(error)) return undefined;
385
+ throw error;
386
+ }
387
+ };
388
+
389
+ const writeNewFileAndSync = async (
390
+ path: string,
391
+ bytes: Uint8Array,
392
+ ): Promise<void> => {
393
+ const handle = await requireNodeFs().open(path, "wx+");
394
+ try {
395
+ await writeNativeDurabilityBytesFully(handle, bytes, 0);
396
+ await handle.sync();
397
+ } finally {
398
+ await handle.close();
399
+ }
400
+ };
401
+
402
+ const replaceFileAtomicallyAndSync = async (
403
+ path: string,
404
+ bytes: Uint8Array,
405
+ temporarySuffix: string,
406
+ ): Promise<void> => {
407
+ const parent = requireNodePath().dirname(path);
408
+ const temporary = `${path}.tmp-${temporarySuffix}`;
409
+ await requireNodeFs().rm(temporary, { force: true });
410
+ await writeNewFileAndSync(temporary, bytes);
411
+ await requireNodeFs().rename(temporary, path);
412
+ await syncDirectory(parent);
413
+ };
414
+
415
+ const writeImmutableFileAtomicallyAndSync = async (
416
+ path: string,
417
+ bytes: Uint8Array,
418
+ temporarySuffix: string,
419
+ ): Promise<void> => {
420
+ const parent = requireNodePath().dirname(path);
421
+ const temporary = `${path}.tmp-${temporarySuffix}`;
422
+ await requireNodeFs().rm(temporary, { force: true });
423
+ await writeNewFileAndSync(temporary, bytes);
424
+ await requireNodeFs().rename(temporary, path);
425
+ await syncDirectory(parent);
426
+ };
427
+
428
+ const parseUnsignedBigint = (value: unknown, subject: string): bigint => {
429
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) {
430
+ throw new NativeDurabilityStorageCorruptionError(
431
+ `${subject} must be an unsigned decimal bigint`,
432
+ );
433
+ }
434
+ const parsed = BigInt(value);
435
+ if (parsed > NATIVE_DURABILITY_MAX_U64) {
436
+ throw new NativeDurabilityStorageCorruptionError(
437
+ `${subject} exceeds unsigned 64-bit range`,
438
+ );
439
+ }
440
+ return parsed;
441
+ };
442
+
443
+ const parseGenerationIdentity = (
444
+ value: DiskGenerationIdentity | undefined,
445
+ subject: string,
446
+ ): GenerationIdentity | undefined => {
447
+ if (value == null) return undefined;
448
+ if (
449
+ typeof value.transactionId !== "string" ||
450
+ !value.transactionId ||
451
+ new TextEncoder().encode(value.transactionId).byteLength >
452
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES ||
453
+ typeof value.requestDigest !== "string"
454
+ ) {
455
+ throw new NativeDurabilityStorageCorruptionError(
456
+ `${subject} checkpoint identity fields are invalid`,
457
+ );
458
+ }
459
+ const identity = {
460
+ generation: parseUnsignedBigint(
461
+ value.generation,
462
+ `${subject} checkpoint generation`,
463
+ ),
464
+ requestDigest: nativeDurabilityDigestFromHex(value.requestDigest),
465
+ transactionId: value.transactionId,
466
+ txSequence: parseUnsignedBigint(
467
+ value.txSequence,
468
+ `${subject} checkpoint transaction sequence`,
469
+ ),
470
+ };
471
+ if (identity.txSequence === 0n) {
472
+ throw new NativeDurabilityStorageCorruptionError(
473
+ `${subject} checkpoint transaction sequence must be non-zero`,
474
+ );
475
+ }
476
+ return identity;
477
+ };
478
+
479
+ const diskGenerationIdentity = (
480
+ identity: GenerationIdentity,
481
+ ): DiskGenerationIdentity => ({
482
+ generation: identity.generation.toString(),
483
+ requestDigest: nativeDurabilityDigestHex(identity.requestDigest),
484
+ transactionId: identity.transactionId,
485
+ txSequence: identity.txSequence.toString(),
486
+ });
487
+
488
+ const generationIdentityMatches = (
489
+ identity: GenerationIdentity,
490
+ request: NativeDurabilityCheckpointRequest,
491
+ requestDigest: Uint8Array,
492
+ ): boolean =>
493
+ identity.transactionId === request.scope.transactionId &&
494
+ identity.txSequence === request.scope.txSequence &&
495
+ nativeDurabilityBytesEqual(identity.requestDigest, requestDigest);
496
+
497
+ const parseStagingManifest = (
498
+ bytes: Uint8Array,
499
+ expectedTransactionId?: string,
500
+ ): NativeDurabilityStagingManifest => {
501
+ const disk = decodeManifest<DiskStagingManifest>(bytes, "staging");
502
+ if (
503
+ disk.version !== NATIVE_DURABILITY_STORAGE_VERSION ||
504
+ !disk.scope ||
505
+ typeof disk.scope.transactionId !== "string" ||
506
+ !disk.scope.transactionId ||
507
+ (expectedTransactionId != null &&
508
+ disk.scope.transactionId !== expectedTransactionId) ||
509
+ !disk.fence ||
510
+ typeof disk.fence.ownerId !== "string" ||
511
+ typeof disk.fence.domainId !== "string" ||
512
+ !Array.isArray(disk.blocks)
513
+ ) {
514
+ throw new NativeDurabilityStorageCorruptionError(
515
+ "Staging manifest fields are invalid",
516
+ );
517
+ }
518
+ if (
519
+ new TextEncoder().encode(disk.scope.transactionId).byteLength >
520
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES
521
+ ) {
522
+ throw new NativeDurabilityStorageCorruptionError(
523
+ "Staging transaction ID exceeds the journal format limit",
524
+ );
525
+ }
526
+ const blocks = disk.blocks.map((block, index) => {
527
+ if (
528
+ block.ordinal !== index ||
529
+ typeof block.cid !== "string" ||
530
+ !block.cid ||
531
+ !Number.isSafeInteger(block.byteLength) ||
532
+ block.byteLength < 0
533
+ ) {
534
+ throw new NativeDurabilityStorageCorruptionError(
535
+ `Staging block reference ${index} is invalid`,
536
+ );
537
+ }
538
+ return {
539
+ ordinal: block.ordinal,
540
+ cid: block.cid,
541
+ byteLength: block.byteLength,
542
+ digest: nativeDurabilityDigestFromHex(block.digest),
543
+ };
544
+ });
545
+ const manifest: NativeDurabilityStagingManifest = {
546
+ version: NATIVE_DURABILITY_STORAGE_VERSION,
547
+ scope: {
548
+ transactionId: disk.scope.transactionId,
549
+ txSequence: parseUnsignedBigint(disk.scope.txSequence, "txSequence"),
550
+ recordLsn: parseUnsignedBigint(disk.scope.recordLsn, "recordLsn"),
551
+ },
552
+ fence: {
553
+ epoch: parseUnsignedBigint(disk.fence.epoch, "fence epoch"),
554
+ ownerId: disk.fence.ownerId,
555
+ domainId: disk.fence.domainId,
556
+ },
557
+ blocks,
558
+ manifestDigest: sha256(bytes),
559
+ };
560
+ if (manifest.scope.txSequence === 0n || manifest.scope.recordLsn === 0n) {
561
+ throw new NativeDurabilityStorageCorruptionError(
562
+ "Staging manifest sequence and LSN must be non-zero",
563
+ );
564
+ }
565
+ try {
566
+ assertNativeDurabilityFence(manifest.fence);
567
+ } catch (error) {
568
+ throw new NativeDurabilityStorageCorruptionError(
569
+ "Staging manifest fence is invalid",
570
+ error,
571
+ );
572
+ }
573
+ return manifest;
574
+ };
575
+
576
+ const parseDiskFence = (
577
+ value: DiskFence | undefined,
578
+ subject: string,
579
+ ): NativeDurabilityLease["fence"] => {
580
+ if (
581
+ !value ||
582
+ typeof value.ownerId !== "string" ||
583
+ typeof value.domainId !== "string"
584
+ ) {
585
+ throw new NativeDurabilityStorageCorruptionError(
586
+ `${subject} fence is invalid`,
587
+ );
588
+ }
589
+ const fence = {
590
+ epoch: parseUnsignedBigint(value.epoch, `${subject} fence epoch`),
591
+ ownerId: value.ownerId,
592
+ domainId: value.domainId,
593
+ };
594
+ try {
595
+ assertNativeDurabilityFence(fence);
596
+ } catch (error) {
597
+ throw new NativeDurabilityStorageCorruptionError(
598
+ `${subject} fence is invalid`,
599
+ error,
600
+ );
601
+ }
602
+ return fence;
603
+ };
604
+
605
+ const parseDiskStagingCoverage = (
606
+ value: unknown,
607
+ checkpointLsn: bigint,
608
+ ): NativeDurabilityCheckpoint["stagingCoverage"] => {
609
+ if (!Array.isArray(value)) {
610
+ throw new NativeDurabilityStorageCorruptionError(
611
+ "Checkpoint staging coverage is invalid",
612
+ );
613
+ }
614
+ const transactionIds = new Set<string>();
615
+ return value.map((unknownCoverage, index) => {
616
+ const coverage = unknownCoverage as Partial<DiskStagingCoverage>;
617
+ if (
618
+ typeof coverage.transactionId !== "string" ||
619
+ !coverage.transactionId ||
620
+ new TextEncoder().encode(coverage.transactionId).byteLength >
621
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES ||
622
+ transactionIds.has(coverage.transactionId) ||
623
+ typeof coverage.stagingManifestDigest !== "string"
624
+ ) {
625
+ throw new NativeDurabilityStorageCorruptionError(
626
+ `Checkpoint staging coverage ${index} is invalid`,
627
+ );
628
+ }
629
+ transactionIds.add(coverage.transactionId);
630
+ const txSequence = parseUnsignedBigint(
631
+ coverage.txSequence,
632
+ `checkpoint staging coverage ${index} transaction sequence`,
633
+ );
634
+ const coveredThroughLsn = parseUnsignedBigint(
635
+ coverage.coveredThroughLsn,
636
+ `checkpoint staging coverage ${index} LSN`,
637
+ );
638
+ if (
639
+ txSequence === 0n ||
640
+ coveredThroughLsn === 0n ||
641
+ coveredThroughLsn > checkpointLsn
642
+ ) {
643
+ throw new NativeDurabilityStorageCorruptionError(
644
+ `Checkpoint staging coverage ${index} exceeds its checkpoint`,
645
+ );
646
+ }
647
+ return {
648
+ transactionId: coverage.transactionId,
649
+ txSequence,
650
+ coveredThroughLsn,
651
+ stagingManifestDigest: nativeDurabilityDigestFromHex(
652
+ coverage.stagingManifestDigest,
653
+ ),
654
+ };
655
+ });
656
+ };
657
+
658
+ const parseDiskRetainedTransactions = (
659
+ value: unknown,
660
+ txSequenceHighwater: bigint,
661
+ ): NativeDurabilityCheckpointTransactionState[] => {
662
+ if (!Array.isArray(value)) {
663
+ throw new NativeDurabilityStorageCorruptionError(
664
+ "Checkpoint retained transactions are invalid",
665
+ );
666
+ }
667
+ const transactionIds = new Set<string>();
668
+ const transactionSequences = new Set<bigint>();
669
+ return value.map((unknownRetained, index) => {
670
+ const retained = unknownRetained as Partial<DiskRetainedTransaction>;
671
+ const txSequence = parseUnsignedBigint(
672
+ retained.txSequence,
673
+ `retained transaction ${index} sequence`,
674
+ );
675
+ if (
676
+ txSequence === 0n ||
677
+ txSequence > txSequenceHighwater ||
678
+ typeof retained.transactionId !== "string" ||
679
+ !retained.transactionId ||
680
+ new TextEncoder().encode(retained.transactionId).byteLength >
681
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES ||
682
+ transactionIds.has(retained.transactionId) ||
683
+ transactionSequences.has(txSequence) ||
684
+ !Number.isInteger(retained.phase) ||
685
+ (retained.phase as number) < 1 ||
686
+ (retained.phase as number) > 6 ||
687
+ !Number.isInteger(retained.operationKind) ||
688
+ (retained.operationKind as number) < 1 ||
689
+ (retained.operationKind as number) > 5 ||
690
+ typeof retained.planDigest !== "string"
691
+ ) {
692
+ throw new NativeDurabilityStorageCorruptionError(
693
+ `Retained transaction ${index} is invalid`,
694
+ );
695
+ }
696
+ transactionIds.add(retained.transactionId);
697
+ transactionSequences.add(txSequence);
698
+ return {
699
+ txSequence,
700
+ transactionId: retained.transactionId,
701
+ phase: retained.phase,
702
+ operationKind: retained.operationKind,
703
+ planDigest: nativeDurabilityDigestFromHex(retained.planDigest),
704
+ } as NativeDurabilityCheckpointTransactionState;
705
+ });
706
+ };
707
+
708
+ const cloneCheckpoint = (
709
+ checkpoint: NativeDurabilityCheckpoint,
710
+ ): NativeDurabilityCheckpoint => ({
711
+ ...checkpoint,
712
+ bytes: copyNativeDurabilityBytes(checkpoint.bytes),
713
+ digest: copyNativeDurabilityBytes(checkpoint.digest),
714
+ originFence: { ...checkpoint.originFence },
715
+ stagingCoverage: checkpoint.stagingCoverage.map((coverage) => ({
716
+ ...coverage,
717
+ stagingManifestDigest: copyNativeDurabilityBytes(
718
+ coverage.stagingManifestDigest,
719
+ ),
720
+ })),
721
+ retainedTransactions: checkpoint.retainedTransactions.map((retained) => ({
722
+ ...retained,
723
+ planDigest: copyNativeDurabilityBytes(retained.planDigest),
724
+ })),
725
+ });
726
+
727
+ const snapshotStageRequest = (
728
+ request: NativeDurabilityStageRequest,
729
+ ): NativeDurabilityStageRequest => {
730
+ assertNativeDurabilityStageRequest(request);
731
+ return {
732
+ scope: { ...request.scope },
733
+ blocks: [...request.blocks]
734
+ .sort((left, right) => left.ordinal - right.ordinal)
735
+ .map((block) => ({
736
+ ordinal: block.ordinal,
737
+ cid: block.cid,
738
+ bytes: copyNativeDurabilityBytes(block.bytes),
739
+ digest: copyNativeDurabilityBytes(block.digest),
740
+ })),
741
+ };
742
+ };
743
+
744
+ const snapshotJournalRequest = (
745
+ request: NativeDurabilityJournalAppendRequest,
746
+ ): NativeDurabilityJournalAppendRequest => {
747
+ assertNativeDurabilityJournalAppendRequest(request);
748
+ return {
749
+ ...request,
750
+ frames: copyNativeDurabilityBytes(request.frames),
751
+ framesDigest: copyNativeDurabilityBytes(request.framesDigest),
752
+ };
753
+ };
754
+
755
+ const snapshotCheckpointRequest = (
756
+ request: NativeDurabilityCheckpointRequest,
757
+ ): NativeDurabilityCheckpointRequest => {
758
+ assertNativeDurabilityCheckpointRequest(request);
759
+ return {
760
+ ...request,
761
+ scope: { ...request.scope },
762
+ bytes: copyNativeDurabilityBytes(request.bytes),
763
+ digest: copyNativeDurabilityBytes(request.digest),
764
+ stagingCoverage: request.stagingCoverage.map((coverage) => ({
765
+ ...coverage,
766
+ stagingManifestDigest: copyNativeDurabilityBytes(
767
+ coverage.stagingManifestDigest,
768
+ ),
769
+ })),
770
+ retainedTransactions: request.retainedTransactions.map((retained) => ({
771
+ ...retained,
772
+ planDigest: copyNativeDurabilityBytes(retained.planDigest),
773
+ })),
774
+ };
775
+ };
776
+
777
+ const snapshotDeleteRequest = (
778
+ request: NativeDurabilityDeleteRequest,
779
+ ): NativeDurabilityDeleteRequest => {
780
+ assertNativeDurabilityDeleteRequest(request);
781
+ return {
782
+ scope: { ...request.scope },
783
+ targets: request.targets.map((target) => ({ ...target })),
784
+ };
785
+ };
786
+
787
+ const nodeNativeDurabilityStorageConstructionToken = Symbol(
788
+ "NodeNativeDurabilityStorageConstructionToken",
789
+ );
790
+
791
+ export class NodeNativeDurabilityStorage implements NativeDurabilityStorage {
792
+ readonly version = NATIVE_DURABILITY_STORAGE_VERSION;
793
+ readonly kind = "node-fsync" as const;
794
+ readonly crashSafe = true;
795
+ readonly domainId: string;
796
+ readonly fence: NativeDurabilityLease["fence"];
797
+
798
+ private readonly rootDirectory: string;
799
+ private readonly namespaceDirectory: string;
800
+ private readonly stagingDirectory: string;
801
+ private readonly checkpointDirectory: string;
802
+ private readonly journalPath: string;
803
+ private operationTail: Promise<void> = Promise.resolve();
804
+ private closePromise?: Promise<void>;
805
+ private closing = false;
806
+ private closed = false;
807
+ private barrierOrdinal = 0n;
808
+ private strictDeleteCount = 0n;
809
+
810
+ private constructor(
811
+ constructionToken: typeof nodeNativeDurabilityStorageConstructionToken,
812
+ private readonly lease: NativeDurabilityLease,
813
+ private readonly journalCodec: NativeDurabilityJournalCodec,
814
+ private readonly programId: Uint8Array,
815
+ rootDirectory: string,
816
+ ) {
817
+ if (constructionToken !== nodeNativeDurabilityStorageConstructionToken) {
818
+ throw new TypeError(
819
+ "NodeNativeDurabilityStorage must be created by createNodeNativeDurabilityStorage",
820
+ );
821
+ }
822
+ if (!isNativeDurabilityJournalCodec(journalCodec)) {
823
+ throw new TypeError(
824
+ "NodeNativeDurabilityStorage requires the official native durability journal codec",
825
+ );
826
+ }
827
+ assertNativeDurabilityFence(lease.fence);
828
+ this.domainId = lease.fence.domainId;
829
+ this.fence = Object.freeze({ ...lease.fence });
830
+ const path = requireNodePath();
831
+ const root = path.resolve(rootDirectory);
832
+ this.rootDirectory = root;
833
+ this.namespaceDirectory = path.join(root, STORAGE_DIRECTORY_NAME);
834
+ this.stagingDirectory = path.join(
835
+ this.namespaceDirectory,
836
+ STAGING_DIRECTORY_NAME,
837
+ );
838
+ this.checkpointDirectory = path.join(
839
+ this.namespaceDirectory,
840
+ CHECKPOINT_DIRECTORY_NAME,
841
+ );
842
+ this.journalPath = path.join(this.namespaceDirectory, JOURNAL_FILE_NAME);
843
+ }
844
+
845
+ static async create(
846
+ options: NativeDurabilityNodeStorageOptions,
847
+ ): Promise<NodeNativeDurabilityStorage> {
848
+ if (
849
+ !(options?.programId instanceof Uint8Array) ||
850
+ options.programId.byteLength === 0 ||
851
+ options.programId.byteLength >
852
+ NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH
853
+ ) {
854
+ throw new TypeError(
855
+ `programId must contain 1-${NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH} bytes`,
856
+ );
857
+ }
858
+ if (typeof options.directory !== "string" || !options.directory) {
859
+ throw new TypeError("directory must be a non-empty string");
860
+ }
861
+ const requestedDirectory = options.directory;
862
+ const programId = copyNativeDurabilityBytes(options.programId);
863
+ await loadNodeModules();
864
+ const fs = requireNodeFs();
865
+ const canonicalDirectory = await fs.realpath(requestedDirectory);
866
+ if (!(await fs.stat(canonicalDirectory)).isDirectory()) {
867
+ throw new NativeDurabilityStorageUnsupportedError(
868
+ `Native durability root is not a directory: ${canonicalDirectory}`,
869
+ );
870
+ }
871
+ const lease = await acquireNativeDurabilityNodeLease(canonicalDirectory);
872
+ try {
873
+ const initialContext: NativeDurabilityJournalValidationContext = {
874
+ checkpointLsn: 0n,
875
+ checkpointTxSequenceHighwater: 0n,
876
+ expectedProgramId: programId,
877
+ expectedWriterDomainId: lease.fence.domainId,
878
+ checkpointWriterEpoch: 0n,
879
+ currentWriterEpoch: lease.fence.epoch,
880
+ currentWriterOwnerId: lease.fence.ownerId,
881
+ retainedTransactions: [],
882
+ };
883
+ const codec = await createNativeDurabilityJournalCodec(initialContext);
884
+ const storage = new NodeNativeDurabilityStorage(
885
+ nodeNativeDurabilityStorageConstructionToken,
886
+ lease,
887
+ codec,
888
+ programId,
889
+ canonicalDirectory,
890
+ );
891
+ await storage.enqueue(async () => storage.initialize());
892
+ return storage;
893
+ } catch (error) {
894
+ await lease.close();
895
+ throw error;
896
+ }
897
+ }
898
+
899
+ private enqueue<T>(operation: () => Promise<T>): Promise<T> {
900
+ if (this.closing || this.closed) {
901
+ return Promise.reject(new NativeDurabilityStorageClosedError());
902
+ }
903
+ const result = this.operationTail.then(() =>
904
+ this.lease.runWhileHeld(operation),
905
+ );
906
+ const settled = result.then(
907
+ () => undefined,
908
+ () => undefined,
909
+ );
910
+ this.operationTail = settled;
911
+ return result;
912
+ }
913
+
914
+ private async initialize(): Promise<void> {
915
+ const fs = requireNodeFs();
916
+ const entries = await fs.readdir(this.rootDirectory);
917
+ const namespaceExists = entries.includes(STORAGE_DIRECTORY_NAME);
918
+ const legacyEntries = entries.filter(
919
+ (entry) =>
920
+ entry !== STORAGE_DIRECTORY_NAME &&
921
+ entry !== NATIVE_DURABILITY_NODE_LEASE_DIRECTORY_NAME,
922
+ );
923
+ if (!namespaceExists && legacyEntries.length > 0) {
924
+ throw new NativeDurabilityMigrationRequiredError(this.rootDirectory);
925
+ }
926
+ if (namespaceExists && legacyEntries.length > 0) {
927
+ const existingCheckpoints = await this.readValidCheckpointsInternal();
928
+ const existingJournalBase = await this.readJournalBaseCheckpoint();
929
+ if (existingCheckpoints.length === 0 || !existingJournalBase) {
930
+ throw new NativeDurabilityMigrationRequiredError(this.rootDirectory);
931
+ }
932
+ }
933
+ await ensureDirectory(this.namespaceDirectory, this.rootDirectory);
934
+ await ensureDirectory(this.stagingDirectory, this.namespaceDirectory);
935
+ await ensureDirectory(this.checkpointDirectory, this.namespaceDirectory);
936
+ const hasEstablishedAuthority = (
937
+ await Promise.all([
938
+ readFileIfExists(this.checkpointHighwaterPath()),
939
+ readFileIfExists(this.checkpointManifestPath("a")),
940
+ readFileIfExists(this.checkpointManifestPath("b")),
941
+ readFileIfExists(
942
+ requireNodePath().join(
943
+ this.checkpointDirectory,
944
+ JOURNAL_BASE_MANIFEST,
945
+ ),
946
+ ),
947
+ ])
948
+ ).some((bytes) => bytes != null);
949
+ let journalExists = false;
950
+ try {
951
+ journalExists = (await fs.stat(this.journalPath)).isFile();
952
+ } catch (error) {
953
+ if (!isNotFound(error)) throw error;
954
+ }
955
+ if (!journalExists) {
956
+ if (hasEstablishedAuthority) {
957
+ throw new NativeDurabilityStorageCorruptionError(
958
+ "Established native durability authority is missing journal.bin",
959
+ );
960
+ }
961
+ const handle = await fs.open(this.journalPath, "wx+");
962
+ try {
963
+ await handle.sync();
964
+ } finally {
965
+ await handle.close();
966
+ }
967
+ await syncDirectory(this.namespaceDirectory);
968
+ }
969
+ // This is also a feature probe: crash-safe mode is unavailable when the
970
+ // platform/filesystem cannot fsync a directory.
971
+ await syncDirectory(this.namespaceDirectory);
972
+ await this.ensureGenesisCheckpoint(legacyEntries.length === 0);
973
+ await this.reconcileCheckpointGenerationState();
974
+ const journalBase = await this.readJournalBaseCheckpoint();
975
+ if (!journalBase) {
976
+ throw new NativeDurabilityStorageCorruptionError(
977
+ "Native durability journal base is missing after initialization",
978
+ );
979
+ }
980
+ const journalHandle = await fs.open(this.journalPath, "r+");
981
+ let journalScan: NativeDurabilityJournalScan;
982
+ try {
983
+ const journalLength = Number((await journalHandle.stat()).size);
984
+ if (!Number.isSafeInteger(journalLength) || journalLength < 0) {
985
+ throw new NativeDurabilityStorageCorruptionError(
986
+ "Native durability journal size is not a safe byte offset",
987
+ );
988
+ }
989
+ journalScan = this.journalCodec.scan(
990
+ await readNativeDurabilityBytesFully(journalHandle, journalLength),
991
+ this.checkpointValidationContext(journalBase),
992
+ );
993
+ // A reopen that observes complete prepared frames must cross a fresh
994
+ // barrier before recovery can classify them as durable. This also makes
995
+ // a complete-write/before-sync process death recoverable without the
996
+ // original caller retaining its append request.
997
+ await journalHandle.sync();
998
+ } finally {
999
+ await journalHandle.close();
1000
+ }
1001
+ const activeCheckpoint = (await this.readValidCheckpointsInternal())[0];
1002
+ if (
1003
+ !activeCheckpoint ||
1004
+ journalScan.lastRecordLsn < activeCheckpoint.checkpointLsn
1005
+ ) {
1006
+ throw new NativeDurabilityStorageCorruptionError(
1007
+ "Journal authority ends before the active checkpoint watermark",
1008
+ );
1009
+ }
1010
+ await this.reconcileStagingOrphans(journalScan.records);
1011
+ }
1012
+
1013
+ private checkpointManifestPath(slot: "a" | "b"): string {
1014
+ return requireNodePath().join(
1015
+ this.checkpointDirectory,
1016
+ slot === "a" ? CHECKPOINT_MANIFEST_A : CHECKPOINT_MANIFEST_B,
1017
+ );
1018
+ }
1019
+
1020
+ private checkpointHighwaterPath(): string {
1021
+ return requireNodePath().join(
1022
+ this.checkpointDirectory,
1023
+ CHECKPOINT_HIGHWATER_FILE_NAME,
1024
+ );
1025
+ }
1026
+
1027
+ private async readGenerationState(): Promise<GenerationState | undefined> {
1028
+ const bytes = await readFileIfExists(this.checkpointHighwaterPath());
1029
+ if (!bytes) return undefined;
1030
+ const disk = decodeManifest<DiskGenerationHighwater>(
1031
+ bytes,
1032
+ "checkpoint generation highwater",
1033
+ );
1034
+ if (disk.version !== this.version) {
1035
+ throw new NativeDurabilityStorageCorruptionError(
1036
+ "Checkpoint generation highwater version is invalid",
1037
+ );
1038
+ }
1039
+ const generation = parseUnsignedBigint(
1040
+ disk.generation,
1041
+ "checkpoint generation highwater",
1042
+ );
1043
+ const pending = parseGenerationIdentity(disk.pending, "pending");
1044
+ const completed = parseGenerationIdentity(disk.completed, "completed");
1045
+ if (
1046
+ (pending &&
1047
+ (pending.generation !== generation || pending.generation === 0n)) ||
1048
+ (completed &&
1049
+ (completed.generation > generation || completed.generation === 0n)) ||
1050
+ (pending && completed && completed.generation >= pending.generation)
1051
+ ) {
1052
+ throw new NativeDurabilityStorageCorruptionError(
1053
+ "Checkpoint request identity exceeds its generation highwater",
1054
+ );
1055
+ }
1056
+ return { generation, pending, completed };
1057
+ }
1058
+
1059
+ private async writeGenerationState(state: GenerationState): Promise<void> {
1060
+ await replaceFileAtomicallyAndSync(
1061
+ this.checkpointHighwaterPath(),
1062
+ encodeManifest({
1063
+ version: this.version,
1064
+ generation: state.generation.toString(),
1065
+ pending: state.pending
1066
+ ? diskGenerationIdentity(state.pending)
1067
+ : undefined,
1068
+ completed: state.completed
1069
+ ? diskGenerationIdentity(state.completed)
1070
+ : undefined,
1071
+ } satisfies DiskGenerationHighwater),
1072
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1073
+ );
1074
+ }
1075
+
1076
+ private async readCheckpointSlot(
1077
+ slot: "a" | "b",
1078
+ ): Promise<NativeDurabilityCheckpoint | undefined> {
1079
+ return this.readCheckpointManifest(
1080
+ this.checkpointManifestPath(slot),
1081
+ slot,
1082
+ `checkpoint manifest ${slot}`,
1083
+ );
1084
+ }
1085
+
1086
+ private async readCheckpointManifest(
1087
+ path: string,
1088
+ manifestSlot: "a" | "b",
1089
+ subject: string,
1090
+ ): Promise<NativeDurabilityCheckpoint | undefined> {
1091
+ const manifestBytes = await readFileIfExists(path);
1092
+ if (!manifestBytes) return undefined;
1093
+ const disk = decodeManifest<DiskCheckpointManifest>(manifestBytes, subject);
1094
+ if (
1095
+ disk.version !== this.version ||
1096
+ typeof disk.file !== "string" ||
1097
+ !Number.isSafeInteger(disk.byteLength) ||
1098
+ disk.byteLength < 0 ||
1099
+ typeof disk.digest !== "string"
1100
+ ) {
1101
+ throw new NativeDurabilityStorageCorruptionError(
1102
+ `${subject} fields are invalid`,
1103
+ );
1104
+ }
1105
+ const persistedProgramId = bytesFromHex(
1106
+ disk.programId,
1107
+ `${subject} program ID`,
1108
+ );
1109
+ if (!nativeDurabilityBytesEqual(persistedProgramId, this.programId)) {
1110
+ throw new NativeDurabilityStorageCorruptionError(
1111
+ `${subject} belongs to another program`,
1112
+ );
1113
+ }
1114
+ const generation = parseUnsignedBigint(
1115
+ disk.generation,
1116
+ `${subject} generation`,
1117
+ );
1118
+ const checkpointLsn = parseUnsignedBigint(
1119
+ disk.checkpointLsn,
1120
+ `${subject} LSN`,
1121
+ );
1122
+ const txSequenceHighwater = parseUnsignedBigint(
1123
+ disk.txSequenceHighwater,
1124
+ `${subject} transaction highwater`,
1125
+ );
1126
+ if (disk.file !== checkpointFileName(generation)) {
1127
+ throw new NativeDurabilityStorageCorruptionError(
1128
+ `${subject} has a noncanonical generation file`,
1129
+ );
1130
+ }
1131
+ const originFence = parseDiskFence(disk.originFence, subject);
1132
+ if (
1133
+ originFence.domainId !== this.domainId ||
1134
+ originFence.epoch > this.lease.fence.epoch ||
1135
+ (originFence.epoch === this.lease.fence.epoch &&
1136
+ originFence.ownerId !== this.lease.fence.ownerId)
1137
+ ) {
1138
+ throw new NativeDurabilityStorageCorruptionError(
1139
+ `${subject} has an invalid durability fence`,
1140
+ );
1141
+ }
1142
+ const stagingCoverage = parseDiskStagingCoverage(
1143
+ disk.stagingCoverage,
1144
+ checkpointLsn,
1145
+ );
1146
+ const retainedTransactions = parseDiskRetainedTransactions(
1147
+ disk.retainedTransactions,
1148
+ txSequenceHighwater,
1149
+ );
1150
+ if (
1151
+ (generation === 0n &&
1152
+ (checkpointLsn !== 0n ||
1153
+ txSequenceHighwater !== 0n ||
1154
+ stagingCoverage.length !== 0 ||
1155
+ retainedTransactions.length !== 0 ||
1156
+ disk.byteLength !== 0)) ||
1157
+ (generation > 0n &&
1158
+ (checkpointLsn === 0n || txSequenceHighwater === 0n)) ||
1159
+ stagingCoverage.some(
1160
+ (coverage) => coverage.txSequence > txSequenceHighwater,
1161
+ )
1162
+ ) {
1163
+ throw new NativeDurabilityStorageCorruptionError(
1164
+ `${subject} checkpoint watermarks are inconsistent`,
1165
+ );
1166
+ }
1167
+ for (const coverage of stagingCoverage) {
1168
+ const retained = retainedTransactions.find(
1169
+ (candidate) => candidate.transactionId === coverage.transactionId,
1170
+ );
1171
+ if (
1172
+ !retained ||
1173
+ retained.txSequence !== coverage.txSequence ||
1174
+ retained.phase !== NativeDurabilityPhase.Clean
1175
+ ) {
1176
+ throw new NativeDurabilityStorageCorruptionError(
1177
+ `${subject} staging coverage lacks an exact retained CLEAN transaction`,
1178
+ );
1179
+ }
1180
+ }
1181
+ const checkpointBytes = await readFileIfExists(
1182
+ requireNodePath().join(this.checkpointDirectory, disk.file),
1183
+ );
1184
+ const digest = nativeDurabilityDigestFromHex(disk.digest);
1185
+ if (
1186
+ !checkpointBytes ||
1187
+ checkpointBytes.byteLength !== disk.byteLength ||
1188
+ !nativeDurabilityBytesEqual(sha256(checkpointBytes), digest)
1189
+ ) {
1190
+ throw new NativeDurabilityStorageCorruptionError(
1191
+ `Checkpoint generation ${generation} is missing or corrupt`,
1192
+ );
1193
+ }
1194
+ return {
1195
+ version: this.version,
1196
+ generation,
1197
+ checkpointLsn,
1198
+ txSequenceHighwater,
1199
+ bytes: checkpointBytes,
1200
+ digest,
1201
+ originFence,
1202
+ manifestSlot,
1203
+ stagingCoverage,
1204
+ retainedTransactions,
1205
+ };
1206
+ }
1207
+
1208
+ private readJournalBaseCheckpoint(): Promise<
1209
+ NativeDurabilityCheckpoint | undefined
1210
+ > {
1211
+ return this.readCheckpointManifest(
1212
+ requireNodePath().join(this.checkpointDirectory, JOURNAL_BASE_MANIFEST),
1213
+ "a",
1214
+ "journal base manifest",
1215
+ );
1216
+ }
1217
+
1218
+ private async readValidCheckpointsInternal(): Promise<
1219
+ NativeDurabilityCheckpoint[]
1220
+ > {
1221
+ const checkpoints = (
1222
+ await Promise.all([
1223
+ this.readCheckpointSlot("a"),
1224
+ this.readCheckpointSlot("b"),
1225
+ ])
1226
+ ).filter(
1227
+ (checkpoint): checkpoint is NativeDurabilityCheckpoint =>
1228
+ checkpoint != null,
1229
+ );
1230
+ if (
1231
+ checkpoints.length === 2 &&
1232
+ checkpoints[0].generation === checkpoints[1].generation
1233
+ ) {
1234
+ throw new NativeDurabilityStorageCorruptionError(
1235
+ "Checkpoint manifest slots select the same generation",
1236
+ );
1237
+ }
1238
+ return checkpoints.sort((left, right) =>
1239
+ left.generation < right.generation
1240
+ ? 1
1241
+ : left.generation > right.generation
1242
+ ? -1
1243
+ : 0,
1244
+ );
1245
+ }
1246
+
1247
+ private async ensureGenesisCheckpoint(mayCreate: boolean): Promise<void> {
1248
+ const checkpoints = await this.readValidCheckpointsInternal();
1249
+ if (checkpoints.length > 0) {
1250
+ const state = await this.readGenerationState();
1251
+ if (state == null || state.generation < checkpoints[0].generation) {
1252
+ throw new NativeDurabilityStorageCorruptionError(
1253
+ "Checkpoint generation highwater is missing or regressed",
1254
+ );
1255
+ }
1256
+ const selectedPending = state.pending
1257
+ ? checkpoints.find(
1258
+ (checkpoint) => checkpoint.generation === state.pending?.generation,
1259
+ )
1260
+ : undefined;
1261
+ const expectedActiveGeneration =
1262
+ selectedPending?.generation ?? state.completed?.generation ?? 0n;
1263
+ if (checkpoints[0].generation !== expectedActiveGeneration) {
1264
+ throw new NativeDurabilityStorageCorruptionError(
1265
+ `Active checkpoint generation ${checkpoints[0].generation} does not match completed generation ${expectedActiveGeneration}`,
1266
+ );
1267
+ }
1268
+ const journalBase = await this.readJournalBaseCheckpoint();
1269
+ if (!journalBase || journalBase.generation !== 0n) {
1270
+ throw new NativeDurabilityStorageCorruptionError(
1271
+ "Journal base checkpoint is missing or unsupported",
1272
+ );
1273
+ }
1274
+ return;
1275
+ }
1276
+ if (!mayCreate) {
1277
+ throw new NativeDurabilityMigrationRequiredError(this.rootDirectory);
1278
+ }
1279
+ const journal = await readFileIfExists(this.journalPath);
1280
+ if (journal && journal.byteLength !== 0) {
1281
+ throw new NativeDurabilityStorageCorruptionError(
1282
+ "Cannot create genesis for a nonempty durability journal",
1283
+ );
1284
+ }
1285
+ const existingState = await this.readGenerationState();
1286
+ if (
1287
+ existingState != null &&
1288
+ (existingState.generation !== 0n ||
1289
+ existingState.pending != null ||
1290
+ existingState.completed != null)
1291
+ ) {
1292
+ throw new NativeDurabilityStorageCorruptionError(
1293
+ "Incomplete genesis has a nonzero checkpoint generation highwater",
1294
+ );
1295
+ }
1296
+ if (existingState == null)
1297
+ await this.writeGenerationState({ generation: 0n });
1298
+ const genesisBytes = new Uint8Array();
1299
+ const genesisPath = requireNodePath().join(
1300
+ this.checkpointDirectory,
1301
+ checkpointFileName(0n),
1302
+ );
1303
+ const existingGenesis = await readFileIfExists(genesisPath);
1304
+ if (existingGenesis) {
1305
+ if (existingGenesis.byteLength !== 0) {
1306
+ throw new NativeDurabilityStorageCorruptionError(
1307
+ "Incomplete genesis checkpoint conflicts with the canonical empty checkpoint",
1308
+ );
1309
+ }
1310
+ const handle = await requireNodeFs().open(genesisPath, "r+");
1311
+ try {
1312
+ await handle.sync();
1313
+ } finally {
1314
+ await handle.close();
1315
+ }
1316
+ await syncDirectory(this.checkpointDirectory);
1317
+ } else {
1318
+ await writeImmutableFileAtomicallyAndSync(
1319
+ genesisPath,
1320
+ genesisBytes,
1321
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1322
+ );
1323
+ }
1324
+ const manifest: DiskCheckpointManifest = {
1325
+ version: this.version,
1326
+ programId: bytesToHex(this.programId),
1327
+ generation: "0",
1328
+ checkpointLsn: "0",
1329
+ txSequenceHighwater: "0",
1330
+ file: checkpointFileName(0n),
1331
+ byteLength: 0,
1332
+ digest: sha256Hex(genesisBytes),
1333
+ originFence: diskFence(this.lease),
1334
+ stagingCoverage: [],
1335
+ retainedTransactions: [],
1336
+ };
1337
+ const journalBasePath = requireNodePath().join(
1338
+ this.checkpointDirectory,
1339
+ JOURNAL_BASE_MANIFEST,
1340
+ );
1341
+ const existingJournalBase = await this.readJournalBaseCheckpoint();
1342
+ if (existingJournalBase) {
1343
+ if (existingJournalBase.generation !== 0n) {
1344
+ throw new NativeDurabilityStorageCorruptionError(
1345
+ "Incomplete genesis has a nonzero journal base",
1346
+ );
1347
+ }
1348
+ } else {
1349
+ await replaceFileAtomicallyAndSync(
1350
+ journalBasePath,
1351
+ encodeManifest(manifest),
1352
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1353
+ );
1354
+ }
1355
+ await replaceFileAtomicallyAndSync(
1356
+ this.checkpointManifestPath("a"),
1357
+ encodeManifest(manifest),
1358
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1359
+ );
1360
+ }
1361
+
1362
+ private async reconcileCheckpointGenerationState(): Promise<void> {
1363
+ const state = await this.readGenerationState();
1364
+ if (!state) {
1365
+ throw new NativeDurabilityStorageCorruptionError(
1366
+ "Checkpoint generation state is missing",
1367
+ );
1368
+ }
1369
+ if (state.pending) {
1370
+ const checkpoints = await this.readValidCheckpointsInternal();
1371
+ const selected = checkpoints.find(
1372
+ (checkpoint) => checkpoint.generation === state.pending?.generation,
1373
+ );
1374
+ if (selected) {
1375
+ if (selected.generation !== checkpoints[0]?.generation) {
1376
+ throw new NativeDurabilityStorageCorruptionError(
1377
+ "Pending checkpoint generation is selected behind a newer manifest",
1378
+ );
1379
+ }
1380
+ await this.writeGenerationState({
1381
+ generation: state.generation,
1382
+ completed: state.pending,
1383
+ });
1384
+ } else {
1385
+ await requireNodeFs().rm(
1386
+ requireNodePath().join(
1387
+ this.checkpointDirectory,
1388
+ checkpointFileName(state.pending.generation),
1389
+ ),
1390
+ { force: true },
1391
+ );
1392
+ await syncDirectory(this.checkpointDirectory);
1393
+ await this.writeGenerationState({
1394
+ generation: state.generation,
1395
+ completed: state.completed,
1396
+ });
1397
+ }
1398
+ }
1399
+ const temporaryPrefixes = [
1400
+ CHECKPOINT_HIGHWATER_FILE_NAME,
1401
+ CHECKPOINT_MANIFEST_A,
1402
+ CHECKPOINT_MANIFEST_B,
1403
+ JOURNAL_BASE_MANIFEST,
1404
+ ].map((name) => `${name}.tmp-`);
1405
+ let removedTemporary = false;
1406
+ for (const entry of await requireNodeFs().readdir(
1407
+ this.checkpointDirectory,
1408
+ { withFileTypes: true },
1409
+ )) {
1410
+ if (
1411
+ entry.isFile() &&
1412
+ (temporaryPrefixes.some((prefix) => entry.name.startsWith(prefix)) ||
1413
+ /^checkpoint-(0|[1-9][0-9]*)\.bin\.tmp-/.test(entry.name))
1414
+ ) {
1415
+ await requireNodeFs().rm(
1416
+ requireNodePath().join(this.checkpointDirectory, entry.name),
1417
+ { force: true },
1418
+ );
1419
+ removedTemporary = true;
1420
+ }
1421
+ }
1422
+ if (removedTemporary) await syncDirectory(this.checkpointDirectory);
1423
+ }
1424
+
1425
+ private async reconcileStagingOrphans(
1426
+ records: readonly NativeDurabilityJournalRecord[],
1427
+ ): Promise<void> {
1428
+ const activeCheckpoint = (await this.readValidCheckpointsInternal())[0];
1429
+ if (!activeCheckpoint) {
1430
+ throw new NativeDurabilityStorageCorruptionError(
1431
+ "Staging reconciliation requires an active checkpoint",
1432
+ );
1433
+ }
1434
+ const journalTransactionDirectories = new Set(
1435
+ records.map((record) => transactionDirectoryName(record.transactionId)),
1436
+ );
1437
+ let removed = false;
1438
+ for (const entry of await requireNodeFs().readdir(this.stagingDirectory, {
1439
+ withFileTypes: true,
1440
+ })) {
1441
+ if (!entry.isDirectory() || !/^tx-[0-9a-f]{64}$/.test(entry.name)) {
1442
+ throw new NativeDurabilityStorageCorruptionError(
1443
+ `Unexpected private staging entry ${entry.name}`,
1444
+ );
1445
+ }
1446
+ const transactionDirectory = requireNodePath().join(
1447
+ this.stagingDirectory,
1448
+ entry.name,
1449
+ );
1450
+ if (!journalTransactionDirectories.has(entry.name)) {
1451
+ await requireNodeFs().rm(transactionDirectory, {
1452
+ recursive: true,
1453
+ force: true,
1454
+ });
1455
+ removed = true;
1456
+ continue;
1457
+ }
1458
+ const manifestBytes = await readFileIfExists(
1459
+ requireNodePath().join(
1460
+ transactionDirectory,
1461
+ STAGING_MANIFEST_FILE_NAME,
1462
+ ),
1463
+ );
1464
+ if (!manifestBytes) {
1465
+ const record = records.findLast(
1466
+ (candidate) =>
1467
+ transactionDirectoryName(candidate.transactionId) === entry.name,
1468
+ );
1469
+ if (
1470
+ record &&
1471
+ this.checkpointAuthorizesStagingDeletion(
1472
+ activeCheckpoint,
1473
+ record.transactionId,
1474
+ records,
1475
+ )
1476
+ ) {
1477
+ await requireNodeFs().rm(transactionDirectory, {
1478
+ recursive: true,
1479
+ force: true,
1480
+ });
1481
+ removed = true;
1482
+ continue;
1483
+ }
1484
+ throw new NativeDurabilityStorageCorruptionError(
1485
+ `Journal-owned staging directory ${entry.name} has no manifest`,
1486
+ );
1487
+ }
1488
+ const parsed = parseStagingManifest(manifestBytes);
1489
+ if (entry.name !== transactionDirectoryName(parsed.scope.transactionId)) {
1490
+ throw new NativeDurabilityStorageCorruptionError(
1491
+ `Staging directory ${entry.name} does not match its manifest`,
1492
+ );
1493
+ }
1494
+ await this.readStagingManifestInternal(parsed.scope.transactionId);
1495
+ if (
1496
+ this.checkpointAuthorizesStagingDeletion(
1497
+ activeCheckpoint,
1498
+ parsed.scope.transactionId,
1499
+ records,
1500
+ parsed,
1501
+ )
1502
+ ) {
1503
+ await requireNodeFs().rm(transactionDirectory, {
1504
+ recursive: true,
1505
+ force: true,
1506
+ });
1507
+ removed = true;
1508
+ continue;
1509
+ }
1510
+ for (const block of parsed.blocks) {
1511
+ const blockBytes = await readFileIfExists(
1512
+ requireNodePath().join(
1513
+ transactionDirectory,
1514
+ blockFileName(block.ordinal),
1515
+ ),
1516
+ );
1517
+ if (
1518
+ !blockBytes ||
1519
+ blockBytes.byteLength !== block.byteLength ||
1520
+ !nativeDurabilityBytesEqual(sha256(blockBytes), block.digest)
1521
+ ) {
1522
+ throw new NativeDurabilityStorageCorruptionError(
1523
+ `Journal-owned staged block ${block.ordinal} is missing or corrupt`,
1524
+ );
1525
+ }
1526
+ }
1527
+ }
1528
+ if (removed) await syncDirectory(this.stagingDirectory);
1529
+ }
1530
+
1531
+ private checkpointAuthorizesStagingDeletion(
1532
+ checkpoint: NativeDurabilityCheckpoint,
1533
+ transactionId: string,
1534
+ records: readonly NativeDurabilityJournalRecord[],
1535
+ staged?: NativeDurabilityStagingManifest,
1536
+ ): boolean {
1537
+ const coverage = checkpoint.stagingCoverage.find(
1538
+ (candidate) => candidate.transactionId === transactionId,
1539
+ );
1540
+ const retained = checkpoint.retainedTransactions.find(
1541
+ (candidate) => candidate.transactionId === transactionId,
1542
+ );
1543
+ const record = records.findLast(
1544
+ (candidate) => candidate.transactionId === transactionId,
1545
+ );
1546
+ return !!(
1547
+ coverage &&
1548
+ retained &&
1549
+ record &&
1550
+ retained.phase === NativeDurabilityPhase.Clean &&
1551
+ record.phase === NativeDurabilityPhase.Clean &&
1552
+ coverage.txSequence === retained.txSequence &&
1553
+ coverage.txSequence === record.txSequence &&
1554
+ retained.operationKind === record.operationKind &&
1555
+ nativeDurabilityBytesEqual(retained.planDigest, record.planDigest) &&
1556
+ coverage.coveredThroughLsn >= record.recordLsn &&
1557
+ coverage.coveredThroughLsn <= checkpoint.checkpointLsn &&
1558
+ (!staged ||
1559
+ (staged.scope.txSequence === coverage.txSequence &&
1560
+ coverage.coveredThroughLsn >= staged.scope.recordLsn &&
1561
+ nativeDurabilityBytesEqual(
1562
+ coverage.stagingManifestDigest,
1563
+ staged.manifestDigest,
1564
+ )))
1565
+ );
1566
+ }
1567
+
1568
+ private checkpointValidationContext(
1569
+ checkpoint: NativeDurabilityCheckpoint,
1570
+ ): NativeDurabilityJournalValidationContext {
1571
+ return {
1572
+ checkpointLsn: checkpoint.checkpointLsn,
1573
+ checkpointTxSequenceHighwater: checkpoint.txSequenceHighwater,
1574
+ expectedProgramId: copyNativeDurabilityBytes(this.programId),
1575
+ expectedWriterDomainId: this.domainId,
1576
+ checkpointWriterEpoch: checkpoint.originFence.epoch,
1577
+ checkpointWriterOwnerId: checkpoint.originFence.ownerId,
1578
+ currentWriterEpoch: this.lease.fence.epoch,
1579
+ currentWriterOwnerId: this.lease.fence.ownerId,
1580
+ retainedTransactions: checkpoint.retainedTransactions.map((retained) => ({
1581
+ ...retained,
1582
+ planDigest: copyNativeDurabilityBytes(retained.planDigest),
1583
+ })),
1584
+ };
1585
+ }
1586
+
1587
+ private transactionDirectory(transactionId: string): string {
1588
+ return requireNodePath().join(
1589
+ this.stagingDirectory,
1590
+ transactionDirectoryName(transactionId),
1591
+ );
1592
+ }
1593
+
1594
+ private async readStagingManifestInternal(
1595
+ transactionId: string,
1596
+ ): Promise<NativeDurabilityStagingManifest | undefined> {
1597
+ const bytes = await readFileIfExists(
1598
+ requireNodePath().join(
1599
+ this.transactionDirectory(transactionId),
1600
+ STAGING_MANIFEST_FILE_NAME,
1601
+ ),
1602
+ );
1603
+ if (!bytes) return undefined;
1604
+ const manifest = parseStagingManifest(bytes, transactionId);
1605
+ if (
1606
+ manifest.fence.domainId !== this.lease.fence.domainId ||
1607
+ manifest.fence.epoch > this.lease.fence.epoch ||
1608
+ (manifest.fence.epoch === this.lease.fence.epoch &&
1609
+ manifest.fence.ownerId !== this.lease.fence.ownerId)
1610
+ ) {
1611
+ throw new NativeDurabilityStorageCorruptionError(
1612
+ `Staging transaction ${transactionId} has an invalid persisted fence`,
1613
+ );
1614
+ }
1615
+ return manifest;
1616
+ }
1617
+
1618
+ async stageAndSync(
1619
+ unsafeRequest: NativeDurabilityStageRequest,
1620
+ ): Promise<NativeDurabilityStageReceipt> {
1621
+ const request = snapshotStageRequest(unsafeRequest);
1622
+ return this.enqueue(async () => {
1623
+ for (const block of request.blocks) {
1624
+ if (!nativeDurabilityBytesEqual(sha256(block.bytes), block.digest)) {
1625
+ throw new NativeDurabilityDigestMismatchError(
1626
+ `staged block ${block.ordinal}`,
1627
+ );
1628
+ }
1629
+ }
1630
+ const transactionDirectory = this.transactionDirectory(
1631
+ request.scope.transactionId,
1632
+ );
1633
+ const references: NativeDurabilityStagedBlockReference[] =
1634
+ request.blocks.map((block) => ({
1635
+ ordinal: block.ordinal,
1636
+ cid: block.cid,
1637
+ byteLength: block.bytes.byteLength,
1638
+ digest: copyNativeDurabilityBytes(block.digest),
1639
+ }));
1640
+ let started = false;
1641
+ try {
1642
+ let transactionDirectoryExists = false;
1643
+ try {
1644
+ transactionDirectoryExists = (
1645
+ await requireNodeFs().stat(transactionDirectory)
1646
+ ).isDirectory();
1647
+ } catch (error) {
1648
+ if (!isNotFound(error)) throw error;
1649
+ }
1650
+ const existing = await this.readStagingManifestInternal(
1651
+ request.scope.transactionId,
1652
+ );
1653
+ let manifest: NativeDurabilityStagingManifest;
1654
+ if (existing) {
1655
+ if (
1656
+ existing.scope.txSequence !== request.scope.txSequence ||
1657
+ existing.scope.recordLsn !== request.scope.recordLsn ||
1658
+ existing.blocks.length !== references.length ||
1659
+ existing.blocks.some(
1660
+ (block, index) =>
1661
+ block.ordinal !== references[index].ordinal ||
1662
+ block.cid !== references[index].cid ||
1663
+ block.byteLength !== references[index].byteLength ||
1664
+ !nativeDurabilityBytesEqual(
1665
+ block.digest,
1666
+ references[index].digest,
1667
+ ),
1668
+ )
1669
+ ) {
1670
+ throw new NativeDurabilityStorageCorruptionError(
1671
+ `Staging transaction ${request.scope.transactionId} conflicts with its durable manifest`,
1672
+ );
1673
+ }
1674
+ for (const block of existing.blocks) {
1675
+ const blockPath = requireNodePath().join(
1676
+ transactionDirectory,
1677
+ blockFileName(block.ordinal),
1678
+ );
1679
+ const bytes = await readFileIfExists(blockPath);
1680
+ if (
1681
+ !bytes ||
1682
+ bytes.byteLength !== block.byteLength ||
1683
+ !nativeDurabilityBytesEqual(sha256(bytes), block.digest)
1684
+ ) {
1685
+ throw new NativeDurabilityStorageCorruptionError(
1686
+ `Staged block ${block.ordinal} is missing or corrupt`,
1687
+ );
1688
+ }
1689
+ const handle = await requireNodeFs().open(blockPath, "r+");
1690
+ try {
1691
+ await handle.sync();
1692
+ } finally {
1693
+ await handle.close();
1694
+ }
1695
+ }
1696
+ await syncDirectory(transactionDirectory);
1697
+ manifest = existing;
1698
+ } else {
1699
+ started = true;
1700
+ if (!transactionDirectoryExists) {
1701
+ await ensureDirectory(transactionDirectory, this.stagingDirectory);
1702
+ } else {
1703
+ const expectedFiles = new Set(
1704
+ request.blocks.map((block) => blockFileName(block.ordinal)),
1705
+ );
1706
+ for (const entry of await requireNodeFs().readdir(
1707
+ transactionDirectory,
1708
+ {
1709
+ withFileTypes: true,
1710
+ },
1711
+ )) {
1712
+ const isManifestTemporary = entry.name.startsWith(
1713
+ `${STAGING_MANIFEST_FILE_NAME}.tmp-`,
1714
+ );
1715
+ const isBlockTemporary = [...expectedFiles].some((name) =>
1716
+ entry.name.startsWith(`${name}.tmp-`),
1717
+ );
1718
+ if (
1719
+ !entry.isFile() ||
1720
+ (!expectedFiles.has(entry.name) &&
1721
+ !isManifestTemporary &&
1722
+ !isBlockTemporary)
1723
+ ) {
1724
+ throw new NativeDurabilityStorageCorruptionError(
1725
+ `Unexpected private staging entry ${entry.name}`,
1726
+ );
1727
+ }
1728
+ if (isManifestTemporary || isBlockTemporary) {
1729
+ await requireNodeFs().rm(
1730
+ requireNodePath().join(transactionDirectory, entry.name),
1731
+ { force: true },
1732
+ );
1733
+ }
1734
+ }
1735
+ await syncDirectory(transactionDirectory);
1736
+ }
1737
+ for (const block of request.blocks) {
1738
+ const blockPath = requireNodePath().join(
1739
+ transactionDirectory,
1740
+ blockFileName(block.ordinal),
1741
+ );
1742
+ const existingBlock = await readFileIfExists(blockPath);
1743
+ if (existingBlock) {
1744
+ if (
1745
+ existingBlock.byteLength !== block.bytes.byteLength ||
1746
+ !nativeDurabilityBytesEqual(sha256(existingBlock), block.digest)
1747
+ ) {
1748
+ throw new NativeDurabilityStorageCorruptionError(
1749
+ `Orphan staged block ${block.ordinal} conflicts with retry`,
1750
+ );
1751
+ }
1752
+ const handle = await requireNodeFs().open(blockPath, "r+");
1753
+ try {
1754
+ await handle.sync();
1755
+ } finally {
1756
+ await handle.close();
1757
+ }
1758
+ } else {
1759
+ await writeImmutableFileAtomicallyAndSync(
1760
+ blockPath,
1761
+ block.bytes,
1762
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1763
+ );
1764
+ }
1765
+ }
1766
+ const payload: DiskStagingManifest = {
1767
+ version: this.version,
1768
+ scope: diskScope(request.scope),
1769
+ fence: diskFence(this.lease),
1770
+ blocks: references.map((block) => ({
1771
+ ordinal: block.ordinal,
1772
+ cid: block.cid,
1773
+ byteLength: block.byteLength,
1774
+ digest: nativeDurabilityDigestHex(block.digest),
1775
+ })),
1776
+ };
1777
+ const manifestBytes = encodeManifest(payload);
1778
+ await replaceFileAtomicallyAndSync(
1779
+ requireNodePath().join(
1780
+ transactionDirectory,
1781
+ STAGING_MANIFEST_FILE_NAME,
1782
+ ),
1783
+ manifestBytes,
1784
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
1785
+ );
1786
+ manifest = parseStagingManifest(
1787
+ manifestBytes,
1788
+ request.scope.transactionId,
1789
+ );
1790
+ }
1791
+ this.barrierOrdinal++;
1792
+ return {
1793
+ version: this.version,
1794
+ kind: "stage",
1795
+ domainId: this.domainId,
1796
+ fence: { ...this.lease.fence },
1797
+ transactionId: request.scope.transactionId,
1798
+ txSequence: request.scope.txSequence,
1799
+ firstRecordLsn: request.scope.recordLsn,
1800
+ lastRecordLsn: request.scope.recordLsn,
1801
+ scopeDigest: sha256(encodeNativeDurabilityCanonical(request)),
1802
+ barrierOrdinal: this.barrierOrdinal,
1803
+ blocks: manifest.blocks.map((block) => ({
1804
+ ...block,
1805
+ digest: copyNativeDurabilityBytes(block.digest),
1806
+ })),
1807
+ manifestDigest: copyNativeDurabilityBytes(manifest.manifestDigest),
1808
+ };
1809
+ } catch (error) {
1810
+ if (!started) throw error;
1811
+ throw new NativeDurabilityOutcomeUnknownError(
1812
+ "stage",
1813
+ request.scope.transactionId,
1814
+ request.scope.txSequence,
1815
+ error,
1816
+ );
1817
+ }
1818
+ });
1819
+ }
1820
+
1821
+ async readStagingManifest(
1822
+ transactionId: string,
1823
+ ): Promise<NativeDurabilityStagingManifest | undefined> {
1824
+ assertNativeDurabilityOperationScope({
1825
+ transactionId,
1826
+ txSequence: 0n,
1827
+ recordLsn: 0n,
1828
+ });
1829
+ return this.enqueue(async () => {
1830
+ const manifest = await this.readStagingManifestInternal(transactionId);
1831
+ return (
1832
+ manifest && {
1833
+ ...manifest,
1834
+ scope: { ...manifest.scope },
1835
+ fence: { ...manifest.fence },
1836
+ blocks: manifest.blocks.map((block) => ({
1837
+ ...block,
1838
+ digest: copyNativeDurabilityBytes(block.digest),
1839
+ })),
1840
+ manifestDigest: copyNativeDurabilityBytes(manifest.manifestDigest),
1841
+ }
1842
+ );
1843
+ });
1844
+ }
1845
+
1846
+ async readStagedBlock(
1847
+ transactionId: string,
1848
+ ordinal: number,
1849
+ ): Promise<Uint8Array | undefined> {
1850
+ assertNativeDurabilityOperationScope({
1851
+ transactionId,
1852
+ txSequence: 0n,
1853
+ recordLsn: 0n,
1854
+ });
1855
+ if (!Number.isSafeInteger(ordinal) || ordinal < 0) {
1856
+ return Promise.reject(new RangeError("Invalid staging ordinal"));
1857
+ }
1858
+ return this.enqueue(async () => {
1859
+ const manifest = await this.readStagingManifestInternal(transactionId);
1860
+ const reference = manifest?.blocks.find(
1861
+ (block) => block.ordinal === ordinal,
1862
+ );
1863
+ if (!reference) return undefined;
1864
+ const bytes = await readFileIfExists(
1865
+ requireNodePath().join(
1866
+ this.transactionDirectory(transactionId),
1867
+ blockFileName(ordinal),
1868
+ ),
1869
+ );
1870
+ if (
1871
+ !bytes ||
1872
+ bytes.byteLength !== reference.byteLength ||
1873
+ !nativeDurabilityBytesEqual(sha256(bytes), reference.digest)
1874
+ ) {
1875
+ throw new NativeDurabilityStorageCorruptionError(
1876
+ `Staged block ${ordinal} is missing or corrupt`,
1877
+ );
1878
+ }
1879
+ return bytes;
1880
+ });
1881
+ }
1882
+
1883
+ private async listStagingTransactionIdsInternal(): Promise<string[]> {
1884
+ const entries = await requireNodeFs().readdir(this.stagingDirectory, {
1885
+ withFileTypes: true,
1886
+ });
1887
+ const transactionIds: string[] = [];
1888
+ for (const entry of entries) {
1889
+ if (!entry.isDirectory() || !entry.name.startsWith("tx-")) {
1890
+ throw new NativeDurabilityStorageCorruptionError(
1891
+ `Unexpected private staging entry ${entry.name}`,
1892
+ );
1893
+ }
1894
+ const bytes = await readFileIfExists(
1895
+ requireNodePath().join(
1896
+ this.stagingDirectory,
1897
+ entry.name,
1898
+ STAGING_MANIFEST_FILE_NAME,
1899
+ ),
1900
+ );
1901
+ if (!bytes) {
1902
+ throw new NativeDurabilityStorageCorruptionError(
1903
+ `Incomplete private staging directory ${entry.name}`,
1904
+ );
1905
+ }
1906
+ const transactionId = parseStagingManifest(bytes).scope.transactionId;
1907
+ if (entry.name !== transactionDirectoryName(transactionId)) {
1908
+ throw new NativeDurabilityStorageCorruptionError(
1909
+ `Staging directory ${entry.name} does not match its transaction manifest`,
1910
+ );
1911
+ }
1912
+ await this.readStagingManifestInternal(transactionId);
1913
+ transactionIds.push(transactionId);
1914
+ }
1915
+ return transactionIds.sort();
1916
+ }
1917
+
1918
+ async listStagingTransactionIds(): Promise<string[]> {
1919
+ return this.enqueue(async () => this.listStagingTransactionIdsInternal());
1920
+ }
1921
+
1922
+ async appendJournalAndSync(
1923
+ unsafeRequest: NativeDurabilityJournalAppendRequest,
1924
+ ): Promise<NativeDurabilityJournalReceipt> {
1925
+ const request = snapshotJournalRequest(unsafeRequest);
1926
+ return this.enqueue(async () => {
1927
+ if (
1928
+ !nativeDurabilityBytesEqual(
1929
+ sha256(request.frames),
1930
+ request.framesDigest,
1931
+ )
1932
+ ) {
1933
+ throw new NativeDurabilityDigestMismatchError("journal frames");
1934
+ }
1935
+ const checkpoint = await this.readJournalBaseCheckpoint();
1936
+ if (!checkpoint) {
1937
+ throw new NativeDurabilityStorageCorruptionError(
1938
+ "Native durability journal has no valid checkpoint authority",
1939
+ );
1940
+ }
1941
+ const context = this.checkpointValidationContext(checkpoint);
1942
+ const handle = await requireNodeFs().open(this.journalPath, "r+");
1943
+ let started = false;
1944
+ try {
1945
+ const actualOffset = Number((await handle.stat()).size);
1946
+ if (!Number.isSafeInteger(actualOffset) || actualOffset < 0) {
1947
+ throw new NativeDurabilityStorageCorruptionError(
1948
+ "Native durability journal size is not a safe byte offset",
1949
+ );
1950
+ }
1951
+ const endOffset = request.expectedOffset + request.frames.byteLength;
1952
+ if (!Number.isSafeInteger(endOffset)) {
1953
+ throw new NativeDurabilityStorageCorruptionError(
1954
+ "Candidate journal end offset is not safe",
1955
+ );
1956
+ }
1957
+ const alreadyWritten = actualOffset === endOffset;
1958
+ if (actualOffset < request.expectedOffset || actualOffset > endOffset) {
1959
+ throw new NativeDurabilityJournalOffsetConflictError(
1960
+ request.expectedOffset,
1961
+ actualOffset,
1962
+ );
1963
+ }
1964
+ const observed = await readNativeDurabilityBytesFully(
1965
+ handle,
1966
+ actualOffset,
1967
+ );
1968
+ const prefix = observed.slice(0, request.expectedOffset);
1969
+ const existingRequestLength = actualOffset - request.expectedOffset;
1970
+ if (
1971
+ !nativeDurabilityBytesEqual(
1972
+ observed.slice(request.expectedOffset),
1973
+ request.frames.slice(0, existingRequestLength),
1974
+ )
1975
+ ) {
1976
+ throw new NativeDurabilityStorageCorruptionError(
1977
+ "Existing journal suffix conflicts with the exact append retry",
1978
+ );
1979
+ }
1980
+ const prefixScan = this.journalCodec.scan(prefix, context);
1981
+ if (
1982
+ prefixScan.incompleteTailOffset != null ||
1983
+ prefixScan.validLength !== prefix.byteLength
1984
+ ) {
1985
+ throw new NativeDurabilityIncompleteTailMismatchError(
1986
+ "Journal must be reconciled before appending another frame",
1987
+ );
1988
+ }
1989
+ const candidate = new Uint8Array(
1990
+ prefix.byteLength + request.frames.byteLength,
1991
+ );
1992
+ candidate.set(prefix);
1993
+ candidate.set(request.frames, prefix.byteLength);
1994
+ const candidateScan = this.journalCodec.scan(candidate, context);
1995
+ if (
1996
+ candidateScan.incompleteTailOffset != null ||
1997
+ candidateScan.validLength !== candidate.byteLength
1998
+ ) {
1999
+ throw new NativeDurabilityStorageCorruptionError(
2000
+ "Candidate journal append is not a complete canonical frame sequence",
2001
+ );
2002
+ }
2003
+ const appendedRecords = candidateScan.records.slice(
2004
+ prefixScan.records.length,
2005
+ );
2006
+ if (appendedRecords.length === 0) {
2007
+ throw new NativeDurabilityStorageCorruptionError(
2008
+ "Candidate journal append contains no records",
2009
+ );
2010
+ }
2011
+ const firstRecord = appendedRecords[0];
2012
+ const lastRecord = appendedRecords[appendedRecords.length - 1];
2013
+ if (
2014
+ firstRecord.recordLsn !== request.firstRecordLsn ||
2015
+ lastRecord.recordLsn !== request.lastRecordLsn ||
2016
+ appendedRecords.some(
2017
+ (record) =>
2018
+ record.transactionId !== request.transactionId ||
2019
+ record.txSequence !== request.txSequence ||
2020
+ record.writerDomainId !== this.lease.fence.domainId ||
2021
+ (existingRequestLength === 0 &&
2022
+ (record.writerEpoch !== this.lease.fence.epoch ||
2023
+ record.writerOwnerId !== this.lease.fence.ownerId)),
2024
+ )
2025
+ ) {
2026
+ throw new NativeDurabilityStorageCorruptionError(
2027
+ "Candidate journal metadata does not match the intended transaction or current fence",
2028
+ );
2029
+ }
2030
+ started = true;
2031
+ if (!alreadyWritten) {
2032
+ await writeNativeDurabilityBytesFully(
2033
+ handle,
2034
+ request.frames.slice(existingRequestLength),
2035
+ actualOffset,
2036
+ );
2037
+ }
2038
+ await handle.sync();
2039
+ this.barrierOrdinal++;
2040
+ return {
2041
+ version: this.version,
2042
+ kind: "journal-append",
2043
+ domainId: this.domainId,
2044
+ fence: { ...this.lease.fence },
2045
+ transactionId: firstRecord.transactionId,
2046
+ txSequence: firstRecord.txSequence,
2047
+ firstRecordLsn: firstRecord.recordLsn,
2048
+ lastRecordLsn: lastRecord.recordLsn,
2049
+ scopeDigest: sha256(encodeNativeDurabilityCanonical(request)),
2050
+ barrierOrdinal: this.barrierOrdinal,
2051
+ offset: request.expectedOffset,
2052
+ endOffset,
2053
+ framesDigest: copyNativeDurabilityBytes(request.framesDigest),
2054
+ };
2055
+ } catch (error) {
2056
+ if (!started) throw error;
2057
+ throw new NativeDurabilityOutcomeUnknownError(
2058
+ "journal-append",
2059
+ request.transactionId,
2060
+ request.txSequence,
2061
+ error,
2062
+ );
2063
+ } finally {
2064
+ try {
2065
+ await handle.close();
2066
+ } catch {
2067
+ // A completed strict barrier is authoritative. When the body failed,
2068
+ // that typed failure is authoritative instead. Close cannot alter either.
2069
+ }
2070
+ }
2071
+ });
2072
+ }
2073
+
2074
+ async readJournal(): Promise<Uint8Array> {
2075
+ return this.enqueue(
2076
+ async () =>
2077
+ new Uint8Array(await requireNodeFs().readFile(this.journalPath)),
2078
+ );
2079
+ }
2080
+
2081
+ async reconcileIncompleteJournalTailAndSync(
2082
+ unsafeRequest: NativeDurabilityIncompleteTailReconciliationRequest,
2083
+ ): Promise<NativeDurabilityJournalReconciliationReceipt> {
2084
+ const request = { ...unsafeRequest };
2085
+ try {
2086
+ assertNativeDurabilityOperationScope({
2087
+ ...request,
2088
+ recordLsn: 0n,
2089
+ });
2090
+ } catch (error) {
2091
+ return Promise.reject(error);
2092
+ }
2093
+ if (request.txSequence === 0n) {
2094
+ return Promise.reject(
2095
+ new TypeError("Invalid incomplete-tail reconciliation request"),
2096
+ );
2097
+ }
2098
+ return this.enqueue(async () => {
2099
+ const checkpoint = await this.readJournalBaseCheckpoint();
2100
+ if (!checkpoint) {
2101
+ throw new NativeDurabilityStorageCorruptionError(
2102
+ "Native durability journal has no valid checkpoint authority",
2103
+ );
2104
+ }
2105
+ const handle = await requireNodeFs().open(this.journalPath, "r+");
2106
+ let started = false;
2107
+ try {
2108
+ const observedLength = Number((await handle.stat()).size);
2109
+ if (!Number.isSafeInteger(observedLength) || observedLength < 0) {
2110
+ throw new NativeDurabilityStorageCorruptionError(
2111
+ "Native durability journal size is not a safe byte offset",
2112
+ );
2113
+ }
2114
+ const observed = await readNativeDurabilityBytesFully(
2115
+ handle,
2116
+ observedLength,
2117
+ );
2118
+ const observedDigest = sha256(observed);
2119
+ const scan = this.journalCodec.scan(
2120
+ copyNativeDurabilityBytes(observed),
2121
+ this.checkpointValidationContext(checkpoint),
2122
+ );
2123
+ const hasIncompleteTail =
2124
+ scan.incompleteTailOffset != null &&
2125
+ scan.incompleteTailReason != null &&
2126
+ scan.validLength === scan.incompleteTailOffset &&
2127
+ scan.validLength < observed.byteLength;
2128
+ const isAlreadyComplete =
2129
+ scan.incompleteTailOffset == null &&
2130
+ scan.incompleteTailReason == null &&
2131
+ scan.validLength === observed.byteLength;
2132
+ if (!hasIncompleteTail && !isAlreadyComplete) {
2133
+ throw new NativeDurabilityIncompleteTailMismatchError(
2134
+ "The exact current journal is neither complete nor a structurally incomplete tail",
2135
+ );
2136
+ }
2137
+ const rereadLength = Number((await handle.stat()).size);
2138
+ const reread =
2139
+ rereadLength === observedLength
2140
+ ? await readNativeDurabilityBytesFully(handle, rereadLength)
2141
+ : new Uint8Array();
2142
+ if (
2143
+ reread.byteLength !== observed.byteLength ||
2144
+ !nativeDurabilityBytesEqual(sha256(reread), observedDigest)
2145
+ ) {
2146
+ throw new NativeDurabilityIncompleteTailMismatchError(
2147
+ "Journal changed after incomplete-tail classification",
2148
+ );
2149
+ }
2150
+ started = true;
2151
+ if (hasIncompleteTail) await handle.truncate(scan.validLength);
2152
+ await handle.sync();
2153
+ this.barrierOrdinal++;
2154
+ return {
2155
+ version: this.version,
2156
+ kind: "journal-tail-reconciliation",
2157
+ domainId: this.domainId,
2158
+ fence: { ...this.lease.fence },
2159
+ transactionId: request.transactionId,
2160
+ txSequence: request.txSequence,
2161
+ firstRecordLsn: scan.lastRecordLsn,
2162
+ lastRecordLsn: scan.lastRecordLsn,
2163
+ scopeDigest: sha256(
2164
+ encodeNativeDurabilityCanonical({
2165
+ ...request,
2166
+ validLength: scan.validLength,
2167
+ incompleteTailReason:
2168
+ scan.incompleteTailReason ?? "already-complete",
2169
+ observedDigest,
2170
+ }),
2171
+ ),
2172
+ barrierOrdinal: this.barrierOrdinal,
2173
+ previousLength: observed.byteLength,
2174
+ validLength: scan.validLength,
2175
+ observedDigest,
2176
+ };
2177
+ } catch (error) {
2178
+ if (!started) throw error;
2179
+ throw new NativeDurabilityOutcomeUnknownError(
2180
+ "journal-tail-reconciliation",
2181
+ request.transactionId,
2182
+ request.txSequence,
2183
+ error,
2184
+ );
2185
+ } finally {
2186
+ try {
2187
+ await handle.close();
2188
+ } catch {
2189
+ // Preserve the strict barrier result or the typed operation failure.
2190
+ }
2191
+ }
2192
+ });
2193
+ }
2194
+
2195
+ private checkpointReceipt(
2196
+ request: NativeDurabilityCheckpointRequest,
2197
+ requestDigest: Uint8Array,
2198
+ checkpoint: Pick<
2199
+ NativeDurabilityCheckpoint,
2200
+ "generation" | "checkpointLsn" | "digest" | "manifestSlot"
2201
+ >,
2202
+ ): NativeDurabilityCheckpointReceipt {
2203
+ this.barrierOrdinal++;
2204
+ return {
2205
+ version: this.version,
2206
+ kind: "checkpoint",
2207
+ domainId: this.domainId,
2208
+ fence: { ...this.lease.fence },
2209
+ transactionId: request.scope.transactionId,
2210
+ txSequence: request.scope.txSequence,
2211
+ firstRecordLsn: request.scope.recordLsn,
2212
+ lastRecordLsn: request.scope.recordLsn,
2213
+ scopeDigest: copyNativeDurabilityBytes(requestDigest),
2214
+ barrierOrdinal: this.barrierOrdinal,
2215
+ generation: checkpoint.generation,
2216
+ checkpointLsn: checkpoint.checkpointLsn,
2217
+ checkpointDigest: copyNativeDurabilityBytes(checkpoint.digest),
2218
+ manifestSlot: checkpoint.manifestSlot,
2219
+ stagingCoverageDigest: sha256(
2220
+ encodeNativeDurabilityCanonical(request.stagingCoverage),
2221
+ ),
2222
+ };
2223
+ }
2224
+
2225
+ async writeCheckpointAndSync(
2226
+ unsafeRequest: NativeDurabilityCheckpointRequest,
2227
+ ): Promise<NativeDurabilityCheckpointReceipt> {
2228
+ const request = snapshotCheckpointRequest(unsafeRequest);
2229
+ return this.enqueue(async () => {
2230
+ if (!nativeDurabilityBytesEqual(sha256(request.bytes), request.digest)) {
2231
+ throw new NativeDurabilityDigestMismatchError("checkpoint");
2232
+ }
2233
+ const journalBase = await this.readJournalBaseCheckpoint();
2234
+ if (!journalBase) {
2235
+ throw new NativeDurabilityStorageCorruptionError(
2236
+ "Checkpoint write has no journal base authority",
2237
+ );
2238
+ }
2239
+ const journalBytes = new Uint8Array(
2240
+ await requireNodeFs().readFile(this.journalPath),
2241
+ );
2242
+ const journalScan = this.journalCodec.scan(
2243
+ journalBytes,
2244
+ this.checkpointValidationContext(journalBase),
2245
+ );
2246
+ if (
2247
+ journalScan.incompleteTailOffset != null ||
2248
+ journalScan.validLength !== journalBytes.byteLength ||
2249
+ request.checkpointLsn > journalScan.lastRecordLsn
2250
+ ) {
2251
+ throw new NativeDurabilityStorageCorruptionError(
2252
+ "Checkpoint cannot cover an incomplete or shorter journal",
2253
+ );
2254
+ }
2255
+ const checkpointScopeRecord = journalScan.records.find(
2256
+ (record) => record.recordLsn === request.scope.recordLsn,
2257
+ );
2258
+ if (
2259
+ !checkpointScopeRecord ||
2260
+ checkpointScopeRecord.transactionId !== request.scope.transactionId ||
2261
+ checkpointScopeRecord.txSequence !== request.scope.txSequence ||
2262
+ checkpointScopeRecord.phase !== NativeDurabilityPhase.Clean
2263
+ ) {
2264
+ throw new NativeDurabilityStorageCorruptionError(
2265
+ "Checkpoint operation scope does not match the journal authority",
2266
+ );
2267
+ }
2268
+ const latestRecords = new Map<string, NativeDurabilityJournalRecord>();
2269
+ let coveredTxSequenceHighwater = journalBase.txSequenceHighwater;
2270
+ for (const retained of journalBase.retainedTransactions) {
2271
+ latestRecords.set(retained.transactionId, {
2272
+ recordLsn: journalBase.checkpointLsn,
2273
+ txSequence: retained.txSequence,
2274
+ writerEpoch: journalBase.originFence.epoch,
2275
+ writerOwnerId: journalBase.originFence.ownerId,
2276
+ writerDomainId: journalBase.originFence.domainId,
2277
+ phase: retained.phase,
2278
+ operationKind: retained.operationKind,
2279
+ programId: copyNativeDurabilityBytes(this.programId),
2280
+ transactionId: retained.transactionId,
2281
+ planDigest: copyNativeDurabilityBytes(retained.planDigest),
2282
+ payload: new Uint8Array(),
2283
+ });
2284
+ }
2285
+ for (const record of journalScan.records) {
2286
+ if (record.recordLsn <= request.checkpointLsn) {
2287
+ latestRecords.set(record.transactionId, record);
2288
+ if (record.txSequence > coveredTxSequenceHighwater) {
2289
+ coveredTxSequenceHighwater = record.txSequence;
2290
+ }
2291
+ }
2292
+ }
2293
+ if (request.txSequenceHighwater < coveredTxSequenceHighwater) {
2294
+ throw new NativeDurabilityStorageCorruptionError(
2295
+ "Checkpoint transaction highwater regresses below covered journal records",
2296
+ );
2297
+ }
2298
+ for (const retained of request.retainedTransactions) {
2299
+ const record = latestRecords.get(retained.transactionId);
2300
+ if (
2301
+ !record ||
2302
+ record.txSequence !== retained.txSequence ||
2303
+ record.phase !== retained.phase ||
2304
+ record.operationKind !== retained.operationKind ||
2305
+ !nativeDurabilityBytesEqual(record.planDigest, retained.planDigest)
2306
+ ) {
2307
+ throw new NativeDurabilityStorageCorruptionError(
2308
+ `Checkpoint retained transaction ${retained.transactionId} does not match the journal authority`,
2309
+ );
2310
+ }
2311
+ }
2312
+ const requestDigest = sha256(encodeNativeDurabilityCanonical(request));
2313
+ let state = await this.readGenerationState();
2314
+ if (!state) {
2315
+ throw new NativeDurabilityStorageCorruptionError(
2316
+ "Checkpoint generation state is missing",
2317
+ );
2318
+ }
2319
+ const currentCheckpoints = await this.readValidCheckpointsInternal();
2320
+ const activeCheckpoint = currentCheckpoints[0];
2321
+ if (
2322
+ !activeCheckpoint ||
2323
+ request.checkpointLsn < activeCheckpoint.checkpointLsn ||
2324
+ request.txSequenceHighwater < activeCheckpoint.txSequenceHighwater
2325
+ ) {
2326
+ throw new NativeDurabilityStorageCorruptionError(
2327
+ "Checkpoint generation cannot regress active checkpoint watermarks",
2328
+ );
2329
+ }
2330
+ if (
2331
+ !state.pending &&
2332
+ state.completed &&
2333
+ state.completed.transactionId === request.scope.transactionId &&
2334
+ state.completed.txSequence === request.scope.txSequence
2335
+ ) {
2336
+ if (
2337
+ !generationIdentityMatches(state.completed, request, requestDigest)
2338
+ ) {
2339
+ throw new NativeDurabilityStorageCorruptionError(
2340
+ "Completed checkpoint scope was reused with different checkpoint content",
2341
+ );
2342
+ }
2343
+ if (
2344
+ activeCheckpoint.generation !== state.completed.generation ||
2345
+ activeCheckpoint.checkpointLsn !== request.checkpointLsn ||
2346
+ activeCheckpoint.txSequenceHighwater !==
2347
+ request.txSequenceHighwater ||
2348
+ !nativeDurabilityBytesEqual(activeCheckpoint.bytes, request.bytes) ||
2349
+ !nativeDurabilityBytesEqual(
2350
+ activeCheckpoint.digest,
2351
+ request.digest,
2352
+ ) ||
2353
+ activeCheckpoint.stagingCoverage.length !==
2354
+ request.stagingCoverage.length ||
2355
+ activeCheckpoint.stagingCoverage.some((coverage, index) => {
2356
+ const expected = request.stagingCoverage[index];
2357
+ return (
2358
+ !expected ||
2359
+ coverage.transactionId !== expected.transactionId ||
2360
+ coverage.txSequence !== expected.txSequence ||
2361
+ coverage.coveredThroughLsn !== expected.coveredThroughLsn ||
2362
+ !nativeDurabilityBytesEqual(
2363
+ coverage.stagingManifestDigest,
2364
+ expected.stagingManifestDigest,
2365
+ )
2366
+ );
2367
+ }) ||
2368
+ activeCheckpoint.retainedTransactions.length !==
2369
+ request.retainedTransactions.length ||
2370
+ activeCheckpoint.retainedTransactions.some((retained, index) => {
2371
+ const expected = request.retainedTransactions[index];
2372
+ return (
2373
+ !expected ||
2374
+ retained.transactionId !== expected.transactionId ||
2375
+ retained.txSequence !== expected.txSequence ||
2376
+ retained.phase !== expected.phase ||
2377
+ retained.operationKind !== expected.operationKind ||
2378
+ !nativeDurabilityBytesEqual(
2379
+ retained.planDigest,
2380
+ expected.planDigest,
2381
+ )
2382
+ );
2383
+ })
2384
+ ) {
2385
+ throw new NativeDurabilityStorageCorruptionError(
2386
+ "Completed checkpoint identity does not match the active generation",
2387
+ );
2388
+ }
2389
+ await syncDirectory(this.checkpointDirectory);
2390
+ return this.checkpointReceipt(request, requestDigest, activeCheckpoint);
2391
+ }
2392
+ for (const coverage of request.stagingCoverage) {
2393
+ const staged = await this.readStagingManifestInternal(
2394
+ coverage.transactionId,
2395
+ );
2396
+ const record = latestRecords.get(coverage.transactionId);
2397
+ if (
2398
+ !staged ||
2399
+ !record ||
2400
+ record.phase !== NativeDurabilityPhase.Clean ||
2401
+ record.txSequence !== coverage.txSequence ||
2402
+ staged.scope.txSequence !== coverage.txSequence ||
2403
+ coverage.coveredThroughLsn < record.recordLsn ||
2404
+ coverage.coveredThroughLsn < staged.scope.recordLsn ||
2405
+ !nativeDurabilityBytesEqual(
2406
+ staged.manifestDigest,
2407
+ coverage.stagingManifestDigest,
2408
+ )
2409
+ ) {
2410
+ throw new NativeDurabilityStorageCorruptionError(
2411
+ `Checkpoint coverage does not prove CLEAN staging transaction ${coverage.transactionId}`,
2412
+ );
2413
+ }
2414
+ }
2415
+
2416
+ let generation: bigint;
2417
+ let started = false;
2418
+ try {
2419
+ if (state.pending) {
2420
+ if (
2421
+ state.pending.transactionId !== request.scope.transactionId ||
2422
+ state.pending.txSequence !== request.scope.txSequence ||
2423
+ !nativeDurabilityBytesEqual(
2424
+ state.pending.requestDigest,
2425
+ requestDigest,
2426
+ )
2427
+ ) {
2428
+ throw new NativeDurabilityStorageCorruptionError(
2429
+ `Checkpoint generation ${state.pending.generation} has an unresolved different owner`,
2430
+ );
2431
+ }
2432
+ generation = state.pending.generation;
2433
+ } else {
2434
+ if (state.generation === NATIVE_DURABILITY_MAX_U64) {
2435
+ throw new NativeDurabilityStorageCorruptionError(
2436
+ "Checkpoint generation highwater is exhausted",
2437
+ );
2438
+ }
2439
+ generation = state.generation + 1n;
2440
+ const targetSlot = generation % 2n === 0n ? "a" : "b";
2441
+ if (activeCheckpoint.manifestSlot === targetSlot) {
2442
+ if (generation === NATIVE_DURABILITY_MAX_U64) {
2443
+ throw new NativeDurabilityStorageCorruptionError(
2444
+ "Checkpoint generation highwater is exhausted before a safe manifest slot",
2445
+ );
2446
+ }
2447
+ generation++;
2448
+ }
2449
+ state = {
2450
+ generation,
2451
+ completed: state.completed,
2452
+ pending: {
2453
+ generation,
2454
+ requestDigest,
2455
+ transactionId: request.scope.transactionId,
2456
+ txSequence: request.scope.txSequence,
2457
+ },
2458
+ };
2459
+ started = true;
2460
+ await this.writeGenerationState(state);
2461
+ }
2462
+ const generationPath = requireNodePath().join(
2463
+ this.checkpointDirectory,
2464
+ checkpointFileName(generation),
2465
+ );
2466
+ const existingGeneration = await readFileIfExists(generationPath);
2467
+ let writeGeneration = existingGeneration == null;
2468
+ if (existingGeneration) {
2469
+ if (
2470
+ existingGeneration.byteLength !== request.bytes.byteLength ||
2471
+ !nativeDurabilityBytesEqual(
2472
+ sha256(existingGeneration),
2473
+ request.digest,
2474
+ )
2475
+ ) {
2476
+ started = true;
2477
+ await requireNodeFs().rm(generationPath, { force: true });
2478
+ await syncDirectory(this.checkpointDirectory);
2479
+ writeGeneration = true;
2480
+ } else {
2481
+ const handle = await requireNodeFs().open(generationPath, "r+");
2482
+ try {
2483
+ await handle.sync();
2484
+ } finally {
2485
+ await handle.close();
2486
+ }
2487
+ await syncDirectory(this.checkpointDirectory);
2488
+ }
2489
+ }
2490
+ if (writeGeneration) {
2491
+ started = true;
2492
+ await writeImmutableFileAtomicallyAndSync(
2493
+ generationPath,
2494
+ request.bytes,
2495
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
2496
+ );
2497
+ }
2498
+ const manifestSlot = generation % 2n === 0n ? "a" : "b";
2499
+ const manifest: DiskCheckpointManifest = {
2500
+ version: this.version,
2501
+ programId: bytesToHex(this.programId),
2502
+ generation: generation.toString(),
2503
+ checkpointLsn: request.checkpointLsn.toString(),
2504
+ txSequenceHighwater: request.txSequenceHighwater.toString(),
2505
+ file: checkpointFileName(generation),
2506
+ byteLength: request.bytes.byteLength,
2507
+ digest: nativeDurabilityDigestHex(request.digest),
2508
+ originFence: diskFence(this.lease),
2509
+ stagingCoverage: request.stagingCoverage.map((coverage) => ({
2510
+ transactionId: coverage.transactionId,
2511
+ txSequence: coverage.txSequence.toString(),
2512
+ coveredThroughLsn: coverage.coveredThroughLsn.toString(),
2513
+ stagingManifestDigest: nativeDurabilityDigestHex(
2514
+ coverage.stagingManifestDigest,
2515
+ ),
2516
+ })),
2517
+ retainedTransactions: request.retainedTransactions.map(
2518
+ (retained) => ({
2519
+ txSequence: retained.txSequence.toString(),
2520
+ transactionId: retained.transactionId,
2521
+ phase: retained.phase,
2522
+ operationKind: retained.operationKind,
2523
+ planDigest: nativeDurabilityDigestHex(retained.planDigest),
2524
+ }),
2525
+ ),
2526
+ };
2527
+ started = true;
2528
+ await replaceFileAtomicallyAndSync(
2529
+ this.checkpointManifestPath(manifestSlot),
2530
+ encodeManifest(manifest),
2531
+ `${this.lease.fence.epoch}-${this.lease.fence.ownerId}`,
2532
+ );
2533
+ const completed: GenerationIdentity = {
2534
+ generation,
2535
+ requestDigest: copyNativeDurabilityBytes(requestDigest),
2536
+ transactionId: request.scope.transactionId,
2537
+ txSequence: request.scope.txSequence,
2538
+ };
2539
+ await this.writeGenerationState({ generation, completed });
2540
+ return this.checkpointReceipt(request, requestDigest, {
2541
+ generation,
2542
+ checkpointLsn: request.checkpointLsn,
2543
+ digest: request.digest,
2544
+ manifestSlot,
2545
+ });
2546
+ } catch (error) {
2547
+ if (!started) throw error;
2548
+ throw new NativeDurabilityOutcomeUnknownError(
2549
+ "checkpoint",
2550
+ request.scope.transactionId,
2551
+ request.scope.txSequence,
2552
+ error,
2553
+ );
2554
+ }
2555
+ });
2556
+ }
2557
+
2558
+ async readLatestCheckpoint(): Promise<
2559
+ NativeDurabilityCheckpoint | undefined
2560
+ > {
2561
+ return this.enqueue(async () => {
2562
+ const checkpoint = (await this.readValidCheckpointsInternal())[0];
2563
+ return checkpoint && cloneCheckpoint(checkpoint);
2564
+ });
2565
+ }
2566
+
2567
+ async deleteAndSync(
2568
+ unsafeRequest: NativeDurabilityDeleteRequest,
2569
+ ): Promise<NativeDurabilityDeleteReceipt> {
2570
+ const request = snapshotDeleteRequest(unsafeRequest);
2571
+ return this.enqueue(async () => {
2572
+ const checkpoints = await this.readValidCheckpointsInternal();
2573
+ const active = checkpoints[0];
2574
+ const previous = checkpoints[1];
2575
+ const journalBase = await this.readJournalBaseCheckpoint();
2576
+ const generationState = await this.readGenerationState();
2577
+ if (!active || !journalBase || !generationState) {
2578
+ throw new NativeDurabilityStorageCorruptionError(
2579
+ "Strict deletion requires valid checkpoint authorities",
2580
+ );
2581
+ }
2582
+ const journalBytes = new Uint8Array(
2583
+ await requireNodeFs().readFile(this.journalPath),
2584
+ );
2585
+ const journalScan = this.journalCodec.scan(
2586
+ journalBytes,
2587
+ this.checkpointValidationContext(journalBase),
2588
+ );
2589
+ const scopeRecord = journalScan.records.find(
2590
+ (record) => record.recordLsn === request.scope.recordLsn,
2591
+ );
2592
+ if (
2593
+ journalScan.incompleteTailOffset != null ||
2594
+ journalScan.validLength !== journalBytes.byteLength ||
2595
+ !scopeRecord ||
2596
+ scopeRecord.transactionId !== request.scope.transactionId ||
2597
+ scopeRecord.txSequence !== request.scope.txSequence ||
2598
+ (scopeRecord.phase !== NativeDurabilityPhase.CleanupPending &&
2599
+ scopeRecord.phase !== NativeDurabilityPhase.Clean)
2600
+ ) {
2601
+ throw new NativeDurabilityStorageCorruptionError(
2602
+ "Delete operation scope does not match a complete journal authority",
2603
+ );
2604
+ }
2605
+ if (generationState.pending) {
2606
+ throw new NativeDurabilityStorageCorruptionError(
2607
+ `Cannot delete while checkpoint generation ${generationState.pending.generation} is unresolved`,
2608
+ );
2609
+ }
2610
+ const stagingToDelete: string[] = [];
2611
+ const checkpointsToDelete: bigint[] = [];
2612
+ for (const target of request.targets) {
2613
+ if (target.kind === "staging") {
2614
+ const transactionDirectory = this.transactionDirectory(
2615
+ target.transactionId,
2616
+ );
2617
+ let directoryExists = false;
2618
+ try {
2619
+ directoryExists = (
2620
+ await requireNodeFs().stat(transactionDirectory)
2621
+ ).isDirectory();
2622
+ } catch (error) {
2623
+ if (!isNotFound(error)) throw error;
2624
+ }
2625
+ const staged = await this.readStagingManifestInternal(
2626
+ target.transactionId,
2627
+ );
2628
+ if (
2629
+ directoryExists &&
2630
+ !staged &&
2631
+ !this.checkpointAuthorizesStagingDeletion(
2632
+ active,
2633
+ target.transactionId,
2634
+ journalScan.records,
2635
+ )
2636
+ ) {
2637
+ throw new NativeDurabilityStorageCorruptionError(
2638
+ `Cannot checkpoint-delete incomplete staging transaction ${target.transactionId}`,
2639
+ );
2640
+ }
2641
+ if (
2642
+ staged &&
2643
+ !this.checkpointAuthorizesStagingDeletion(
2644
+ active,
2645
+ target.transactionId,
2646
+ journalScan.records,
2647
+ staged,
2648
+ )
2649
+ ) {
2650
+ throw new NativeDurabilityStorageCorruptionError(
2651
+ `Active checkpoint does not exactly cover CLEAN staging transaction ${target.transactionId}`,
2652
+ );
2653
+ }
2654
+ stagingToDelete.push(transactionDirectory);
2655
+ } else {
2656
+ if (
2657
+ target.generation === active.generation ||
2658
+ target.generation === previous?.generation ||
2659
+ target.generation === journalBase.generation
2660
+ ) {
2661
+ throw new NativeDurabilityStorageCorruptionError(
2662
+ `Cannot delete active, previous, or journal-base checkpoint generation ${target.generation}`,
2663
+ );
2664
+ }
2665
+ if (target.generation > generationState.generation) {
2666
+ throw new NativeDurabilityStorageCorruptionError(
2667
+ `Checkpoint generation ${target.generation} exceeds the permanent highwater`,
2668
+ );
2669
+ }
2670
+ checkpointsToDelete.push(target.generation);
2671
+ }
2672
+ }
2673
+ let started = false;
2674
+ try {
2675
+ for (const directory of stagingToDelete) {
2676
+ started = true;
2677
+ await requireNodeFs().rm(directory, {
2678
+ recursive: true,
2679
+ force: true,
2680
+ });
2681
+ await syncDirectory(this.stagingDirectory);
2682
+ }
2683
+ for (const generation of checkpointsToDelete) {
2684
+ started = true;
2685
+ await requireNodeFs().rm(
2686
+ requireNodePath().join(
2687
+ this.checkpointDirectory,
2688
+ checkpointFileName(generation),
2689
+ ),
2690
+ { force: true },
2691
+ );
2692
+ await syncDirectory(this.checkpointDirectory);
2693
+ }
2694
+ this.strictDeleteCount++;
2695
+ this.barrierOrdinal++;
2696
+ return {
2697
+ version: this.version,
2698
+ kind: "delete",
2699
+ domainId: this.domainId,
2700
+ fence: { ...this.lease.fence },
2701
+ transactionId: request.scope.transactionId,
2702
+ txSequence: request.scope.txSequence,
2703
+ firstRecordLsn: request.scope.recordLsn,
2704
+ lastRecordLsn: request.scope.recordLsn,
2705
+ scopeDigest: sha256(encodeNativeDurabilityCanonical(request)),
2706
+ barrierOrdinal: this.barrierOrdinal,
2707
+ targets: request.targets.map((target) => ({ ...target })),
2708
+ };
2709
+ } catch (error) {
2710
+ if (!started) throw error;
2711
+ throw new NativeDurabilityOutcomeUnknownError(
2712
+ "delete",
2713
+ request.scope.transactionId,
2714
+ request.scope.txSequence,
2715
+ error,
2716
+ );
2717
+ }
2718
+ });
2719
+ }
2720
+
2721
+ async stats(): Promise<NativeDurabilityStorageStats> {
2722
+ return this.enqueue(async () => {
2723
+ const transactionIds = await this.listStagingTransactionIdsInternal();
2724
+ let stagedBlocks = 0;
2725
+ let stagedBytes = 0;
2726
+ for (const transactionId of transactionIds) {
2727
+ const manifest = await this.readStagingManifestInternal(transactionId);
2728
+ if (!manifest) continue;
2729
+ stagedBlocks += manifest.blocks.length;
2730
+ for (const block of manifest.blocks) stagedBytes += block.byteLength;
2731
+ }
2732
+ let checkpointGenerations = 0;
2733
+ let checkpointBytes = 0;
2734
+ for (const entry of await requireNodeFs().readdir(
2735
+ this.checkpointDirectory,
2736
+ { withFileTypes: true },
2737
+ )) {
2738
+ if (
2739
+ entry.isFile() &&
2740
+ /^checkpoint-(0|[1-9][0-9]*)\.bin$/.test(entry.name)
2741
+ ) {
2742
+ checkpointGenerations++;
2743
+ checkpointBytes += Number(
2744
+ (
2745
+ await requireNodeFs().stat(
2746
+ requireNodePath().join(this.checkpointDirectory, entry.name),
2747
+ )
2748
+ ).size,
2749
+ );
2750
+ }
2751
+ }
2752
+ const journalBytes = Number(
2753
+ (await requireNodeFs().stat(this.journalPath)).size,
2754
+ );
2755
+ return {
2756
+ kind: this.kind,
2757
+ domainId: this.domainId,
2758
+ strictBarrierCount: this.barrierOrdinal,
2759
+ strictDeleteCount: this.strictDeleteCount,
2760
+ journalBytes,
2761
+ stagingTransactions: transactionIds.length,
2762
+ stagedBlocks,
2763
+ stagedBytes,
2764
+ checkpointGenerations,
2765
+ checkpointBytes,
2766
+ };
2767
+ });
2768
+ }
2769
+
2770
+ /** Drains admitted operations before releasing the owned OS lease. */
2771
+ close(): Promise<void> {
2772
+ if (this.closePromise) return this.closePromise;
2773
+ this.closing = true;
2774
+ this.closePromise = (async () => {
2775
+ let operationError: unknown;
2776
+ try {
2777
+ await this.operationTail;
2778
+ } catch (error) {
2779
+ operationError = error;
2780
+ }
2781
+ let leaseError: unknown;
2782
+ try {
2783
+ await this.lease.close();
2784
+ } catch (error) {
2785
+ leaseError = error;
2786
+ }
2787
+ this.closed = true;
2788
+ if (operationError) throw operationError;
2789
+ if (leaseError) throw leaseError;
2790
+ })();
2791
+ return this.closePromise;
2792
+ }
2793
+ }
2794
+
2795
+ export const createNodeNativeDurabilityStorage = async (
2796
+ options: NativeDurabilityNodeStorageOptions,
2797
+ ): Promise<NodeNativeDurabilityStorage> =>
2798
+ NodeNativeDurabilityStorage.create(options);