@peerbit/native-backbone 0.1.4 → 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 (44) 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 +131 -115
  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 +119 -115
  35. package/package.json +4 -3
  36. package/src/durability/codec.ts +683 -0
  37. package/src/durability/lease.ts +87 -0
  38. package/src/durability/memory-storage.ts +593 -0
  39. package/src/durability/node-lease.ts +293 -0
  40. package/src/durability/node-storage.ts +2798 -0
  41. package/src/durability/storage.ts +682 -0
  42. package/src/durability.rs +1872 -0
  43. package/src/index.ts +1392 -735
  44. package/src/lib.rs +1 -0
@@ -0,0 +1,682 @@
1
+ import type { NativeDurabilityCheckpointTransactionState } from "./codec.js";
2
+ import {
3
+ NATIVE_DURABILITY_MAX_U64,
4
+ NATIVE_DURABILITY_MAX_WRITER_ID_BYTES,
5
+ type NativeDurabilityLease,
6
+ } from "./lease.js";
7
+
8
+ export const NATIVE_DURABILITY_STORAGE_VERSION = 1 as const;
9
+ export const NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES = 1024;
10
+
11
+ export type NativeDurabilityStorageKind = "memory" | "node-fsync";
12
+
13
+ export type NativeDurabilityOperationScope = {
14
+ transactionId: string;
15
+ txSequence: bigint;
16
+ recordLsn: bigint;
17
+ };
18
+
19
+ export type NativeDurabilityStagedBlock = {
20
+ ordinal: number;
21
+ cid: string;
22
+ bytes: Uint8Array;
23
+ digest: Uint8Array;
24
+ };
25
+
26
+ export type NativeDurabilityStagedBlockReference = Omit<
27
+ NativeDurabilityStagedBlock,
28
+ "bytes"
29
+ > & {
30
+ byteLength: number;
31
+ };
32
+
33
+ export type NativeDurabilityStagingManifest = {
34
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
35
+ scope: NativeDurabilityOperationScope;
36
+ fence: NativeDurabilityLease["fence"];
37
+ blocks: NativeDurabilityStagedBlockReference[];
38
+ manifestDigest: Uint8Array;
39
+ };
40
+
41
+ export type NativeDurabilityStageRequest = {
42
+ scope: NativeDurabilityOperationScope;
43
+ blocks: readonly NativeDurabilityStagedBlock[];
44
+ };
45
+
46
+ export type NativeDurabilityJournalAppendRequest = {
47
+ transactionId: string;
48
+ txSequence: bigint;
49
+ firstRecordLsn: bigint;
50
+ lastRecordLsn: bigint;
51
+ expectedOffset: number;
52
+ frames: Uint8Array;
53
+ framesDigest: Uint8Array;
54
+ };
55
+
56
+ /**
57
+ * The storage adapter asks an injected journal codec to produce this result
58
+ * from the exact bytes it is about to truncate. Complete-frame corruption must
59
+ * throw from the classifier and can never be represented as `incomplete-tail`.
60
+ */
61
+ export type NativeDurabilityJournalClassification =
62
+ | {
63
+ kind: "complete";
64
+ validLength: number;
65
+ lastRecordLsn: bigint;
66
+ }
67
+ | {
68
+ kind: "incomplete-tail";
69
+ validLength: number;
70
+ lastRecordLsn: bigint;
71
+ reason: "short-header" | "short-body" | "short-trailer";
72
+ };
73
+
74
+ export interface NativeDurabilityJournalClassifier {
75
+ classify(
76
+ bytes: Uint8Array,
77
+ ):
78
+ | NativeDurabilityJournalClassification
79
+ | Promise<NativeDurabilityJournalClassification>;
80
+ }
81
+
82
+ export type NativeDurabilityIncompleteTailReconciliationRequest = {
83
+ transactionId: string;
84
+ txSequence: bigint;
85
+ };
86
+
87
+ export type NativeDurabilityStagingCoverage = {
88
+ transactionId: string;
89
+ txSequence: bigint;
90
+ coveredThroughLsn: bigint;
91
+ stagingManifestDigest: Uint8Array;
92
+ };
93
+
94
+ export type NativeDurabilityCheckpointRequest = {
95
+ scope: NativeDurabilityOperationScope;
96
+ checkpointLsn: bigint;
97
+ txSequenceHighwater: bigint;
98
+ bytes: Uint8Array;
99
+ digest: Uint8Array;
100
+ stagingCoverage: readonly NativeDurabilityStagingCoverage[];
101
+ retainedTransactions: readonly NativeDurabilityCheckpointTransactionState[];
102
+ };
103
+
104
+ export type NativeDurabilityCheckpoint = {
105
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
106
+ generation: bigint;
107
+ checkpointLsn: bigint;
108
+ txSequenceHighwater: bigint;
109
+ bytes: Uint8Array;
110
+ digest: Uint8Array;
111
+ originFence: NativeDurabilityLease["fence"];
112
+ manifestSlot: "a" | "b";
113
+ stagingCoverage: NativeDurabilityStagingCoverage[];
114
+ retainedTransactions: NativeDurabilityCheckpointTransactionState[];
115
+ };
116
+
117
+ export type NativeDurabilityDeleteTarget =
118
+ | { kind: "staging"; transactionId: string }
119
+ | { kind: "checkpoint"; generation: bigint };
120
+
121
+ export type NativeDurabilityDeleteRequest = {
122
+ scope: NativeDurabilityOperationScope;
123
+ targets: readonly NativeDurabilityDeleteTarget[];
124
+ };
125
+
126
+ type NativeDurabilityReceiptBase<TKind extends string> = {
127
+ version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
128
+ kind: TKind;
129
+ domainId: string;
130
+ fence: NativeDurabilityLease["fence"];
131
+ transactionId: string;
132
+ txSequence: bigint;
133
+ firstRecordLsn: bigint;
134
+ lastRecordLsn: bigint;
135
+ scopeDigest: Uint8Array;
136
+ barrierOrdinal: bigint;
137
+ };
138
+
139
+ export type NativeDurabilityStageReceipt =
140
+ NativeDurabilityReceiptBase<"stage"> & {
141
+ blocks: NativeDurabilityStagedBlockReference[];
142
+ manifestDigest: Uint8Array;
143
+ };
144
+
145
+ export type NativeDurabilityJournalReceipt =
146
+ NativeDurabilityReceiptBase<"journal-append"> & {
147
+ offset: number;
148
+ endOffset: number;
149
+ framesDigest: Uint8Array;
150
+ };
151
+
152
+ export type NativeDurabilityJournalReconciliationReceipt =
153
+ NativeDurabilityReceiptBase<"journal-tail-reconciliation"> & {
154
+ previousLength: number;
155
+ validLength: number;
156
+ observedDigest: Uint8Array;
157
+ };
158
+
159
+ export type NativeDurabilityCheckpointReceipt =
160
+ NativeDurabilityReceiptBase<"checkpoint"> & {
161
+ generation: bigint;
162
+ checkpointLsn: bigint;
163
+ checkpointDigest: Uint8Array;
164
+ manifestSlot: "a" | "b";
165
+ stagingCoverageDigest: Uint8Array;
166
+ };
167
+
168
+ export type NativeDurabilityDeleteReceipt =
169
+ NativeDurabilityReceiptBase<"delete"> & {
170
+ targets: NativeDurabilityDeleteTarget[];
171
+ };
172
+
173
+ export type NativeDurabilityStorageStats = {
174
+ kind: NativeDurabilityStorageKind;
175
+ domainId: string;
176
+ strictBarrierCount: bigint;
177
+ strictDeleteCount: bigint;
178
+ journalBytes: number;
179
+ stagingTransactions: number;
180
+ stagedBlocks: number;
181
+ stagedBytes: number;
182
+ checkpointGenerations: number;
183
+ checkpointBytes: number;
184
+ };
185
+
186
+ export interface NativeDurabilityStorage {
187
+ readonly version: typeof NATIVE_DURABILITY_STORAGE_VERSION;
188
+ readonly kind: NativeDurabilityStorageKind;
189
+ readonly domainId: string;
190
+ readonly fence: NativeDurabilityLease["fence"];
191
+ readonly crashSafe: boolean;
192
+
193
+ stageAndSync(
194
+ request: NativeDurabilityStageRequest,
195
+ ): Promise<NativeDurabilityStageReceipt>;
196
+ readStagingManifest(
197
+ transactionId: string,
198
+ ): Promise<NativeDurabilityStagingManifest | undefined>;
199
+ readStagedBlock(
200
+ transactionId: string,
201
+ ordinal: number,
202
+ ): Promise<Uint8Array | undefined>;
203
+ listStagingTransactionIds(): Promise<string[]>;
204
+
205
+ appendJournalAndSync(
206
+ request: NativeDurabilityJournalAppendRequest,
207
+ ): Promise<NativeDurabilityJournalReceipt>;
208
+ readJournal(): Promise<Uint8Array>;
209
+ reconcileIncompleteJournalTailAndSync(
210
+ request: NativeDurabilityIncompleteTailReconciliationRequest,
211
+ ): Promise<NativeDurabilityJournalReconciliationReceipt>;
212
+
213
+ writeCheckpointAndSync(
214
+ request: NativeDurabilityCheckpointRequest,
215
+ ): Promise<NativeDurabilityCheckpointReceipt>;
216
+ readLatestCheckpoint(): Promise<NativeDurabilityCheckpoint | undefined>;
217
+
218
+ deleteAndSync(
219
+ request: NativeDurabilityDeleteRequest,
220
+ ): Promise<NativeDurabilityDeleteReceipt>;
221
+ stats(): Promise<NativeDurabilityStorageStats>;
222
+ close(): Promise<void>;
223
+ }
224
+
225
+ export class NativeDurabilityStorageUnsupportedError extends Error {
226
+ readonly code = "ERR_NATIVE_DURABILITY_STORAGE_UNSUPPORTED";
227
+
228
+ constructor(message: string) {
229
+ super(message);
230
+ this.name = "NativeDurabilityStorageUnsupportedError";
231
+ }
232
+ }
233
+
234
+ export class NativeDurabilityMigrationRequiredError extends Error {
235
+ readonly code = "ERR_NATIVE_DURABILITY_MIGRATION_REQUIRED";
236
+
237
+ constructor(readonly directory: string) {
238
+ super(
239
+ `Native crash-safe durability requires an explicit migration for nonempty program directory: ${directory}`,
240
+ );
241
+ this.name = "NativeDurabilityMigrationRequiredError";
242
+ }
243
+ }
244
+
245
+ export class NativeDurabilityStorageClosedError extends Error {
246
+ readonly code = "ERR_NATIVE_DURABILITY_STORAGE_CLOSED";
247
+
248
+ constructor() {
249
+ super("Native durability storage is closed");
250
+ this.name = "NativeDurabilityStorageClosedError";
251
+ }
252
+ }
253
+
254
+ export class NativeDurabilityStorageCorruptionError extends Error {
255
+ readonly code = "ERR_NATIVE_DURABILITY_STORAGE_CORRUPTION";
256
+
257
+ constructor(
258
+ message: string,
259
+ readonly cause?: unknown,
260
+ ) {
261
+ super(message);
262
+ this.name = "NativeDurabilityStorageCorruptionError";
263
+ }
264
+ }
265
+
266
+ export class NativeDurabilityDigestMismatchError extends Error {
267
+ readonly code = "ERR_NATIVE_DURABILITY_DIGEST_MISMATCH";
268
+
269
+ constructor(readonly subject: string) {
270
+ super(`Native durability digest mismatch for ${subject}`);
271
+ this.name = "NativeDurabilityDigestMismatchError";
272
+ }
273
+ }
274
+
275
+ export class NativeDurabilityJournalOffsetConflictError extends Error {
276
+ readonly code = "ERR_NATIVE_DURABILITY_JOURNAL_OFFSET_CONFLICT";
277
+
278
+ constructor(
279
+ readonly expectedOffset: number,
280
+ readonly actualOffset: number,
281
+ ) {
282
+ super(
283
+ `Native durability journal offset conflict: expected ${expectedOffset}, found ${actualOffset}`,
284
+ );
285
+ this.name = "NativeDurabilityJournalOffsetConflictError";
286
+ }
287
+ }
288
+
289
+ export class NativeDurabilityOutcomeUnknownError extends Error {
290
+ readonly code = "ERR_NATIVE_DURABILITY_OUTCOME_UNKNOWN";
291
+ readonly outcome = "unknown" as const;
292
+
293
+ constructor(
294
+ readonly operation:
295
+ | "stage"
296
+ | "journal-append"
297
+ | "journal-tail-reconciliation"
298
+ | "checkpoint"
299
+ | "delete",
300
+ readonly transactionId: string,
301
+ readonly txSequence: bigint,
302
+ readonly cause: unknown,
303
+ ) {
304
+ super(
305
+ `Native durability ${operation} outcome is unknown for transaction ${transactionId}`,
306
+ );
307
+ this.name = "NativeDurabilityOutcomeUnknownError";
308
+ }
309
+ }
310
+
311
+ export class NativeDurabilityIncompleteTailMismatchError extends Error {
312
+ readonly code = "ERR_NATIVE_DURABILITY_INCOMPLETE_TAIL_MISMATCH";
313
+
314
+ constructor(message: string) {
315
+ super(message);
316
+ this.name = "NativeDurabilityIncompleteTailMismatchError";
317
+ }
318
+ }
319
+
320
+ export const copyNativeDurabilityBytes = (bytes: Uint8Array): Uint8Array =>
321
+ new Uint8Array(bytes);
322
+
323
+ export const nativeDurabilityBytesEqual = (
324
+ left: Uint8Array,
325
+ right: Uint8Array,
326
+ ): boolean => {
327
+ if (left.byteLength !== right.byteLength) return false;
328
+ for (let i = 0; i < left.byteLength; i++) {
329
+ if (left[i] !== right[i]) return false;
330
+ }
331
+ return true;
332
+ };
333
+
334
+ export const nativeDurabilityDigestHex = (digest: Uint8Array): string =>
335
+ Array.from(digest, (byte) => byte.toString(16).padStart(2, "0")).join("");
336
+
337
+ export const nativeDurabilityDigestFromHex = (value: string): Uint8Array => {
338
+ if (!/^[0-9a-f]{64}$/.test(value)) {
339
+ throw new NativeDurabilityStorageCorruptionError(
340
+ "Native durability digest must be 32 lowercase hexadecimal bytes",
341
+ );
342
+ }
343
+ const bytes = new Uint8Array(32);
344
+ for (let i = 0; i < bytes.byteLength; i++) {
345
+ bytes[i] = Number.parseInt(value.slice(i * 2, i * 2 + 2), 16);
346
+ }
347
+ return bytes;
348
+ };
349
+
350
+ const canonicalValue = (value: unknown): unknown => {
351
+ if (typeof value === "bigint") return { bigint: value.toString() };
352
+ if (value instanceof Uint8Array) {
353
+ return { bytes: nativeDurabilityDigestHex(value) };
354
+ }
355
+ if (Array.isArray(value)) return value.map(canonicalValue);
356
+ if (value && typeof value === "object") {
357
+ return Object.fromEntries(
358
+ Object.entries(value as Record<string, unknown>)
359
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
360
+ .map(([key, entry]) => [key, canonicalValue(entry)]),
361
+ );
362
+ }
363
+ return value;
364
+ };
365
+
366
+ export const encodeNativeDurabilityCanonical = (value: unknown): Uint8Array =>
367
+ new TextEncoder().encode(JSON.stringify(canonicalValue(value)));
368
+
369
+ export const sha256NativeDurability = async (
370
+ bytes: Uint8Array,
371
+ ): Promise<Uint8Array> => {
372
+ if (!globalThis.crypto?.subtle) {
373
+ throw new NativeDurabilityStorageUnsupportedError(
374
+ "SHA-256 requires Web Crypto in the memory durability adapter",
375
+ );
376
+ }
377
+ const input = new Uint8Array(bytes.byteLength);
378
+ input.set(bytes);
379
+ return new Uint8Array(
380
+ await globalThis.crypto.subtle.digest("SHA-256", input.buffer),
381
+ );
382
+ };
383
+
384
+ export const assertNativeDurabilityDigest = async (
385
+ subject: string,
386
+ bytes: Uint8Array,
387
+ expected: Uint8Array,
388
+ ): Promise<void> => {
389
+ if (expected.byteLength !== 32) {
390
+ throw new NativeDurabilityDigestMismatchError(subject);
391
+ }
392
+ const actual = await sha256NativeDurability(bytes);
393
+ if (!nativeDurabilityBytesEqual(actual, expected)) {
394
+ throw new NativeDurabilityDigestMismatchError(subject);
395
+ }
396
+ };
397
+
398
+ export const nativeDurabilityScopeDigest = async (
399
+ value: unknown,
400
+ ): Promise<Uint8Array> =>
401
+ sha256NativeDurability(encodeNativeDurabilityCanonical(value));
402
+
403
+ const assertU64 = (name: string, value: bigint): void => {
404
+ if (
405
+ typeof value !== "bigint" ||
406
+ value < 0n ||
407
+ value > NATIVE_DURABILITY_MAX_U64
408
+ ) {
409
+ throw new TypeError(`${name} must be an unsigned 64-bit bigint`);
410
+ }
411
+ };
412
+
413
+ export const assertNativeDurabilityOperationScope = (
414
+ scope: NativeDurabilityOperationScope,
415
+ ): void => {
416
+ if (
417
+ !scope ||
418
+ typeof scope.transactionId !== "string" ||
419
+ !scope.transactionId
420
+ ) {
421
+ throw new TypeError("transactionId must be a non-empty string");
422
+ }
423
+ if (
424
+ new TextEncoder().encode(scope.transactionId).byteLength >
425
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES
426
+ ) {
427
+ throw new TypeError("transactionId exceeds the journal format limit");
428
+ }
429
+ assertU64("txSequence", scope.txSequence);
430
+ assertU64("recordLsn", scope.recordLsn);
431
+ };
432
+
433
+ export const assertNativeDurabilityFence = (
434
+ fence: NativeDurabilityLease["fence"],
435
+ ): void => {
436
+ if (
437
+ !fence ||
438
+ typeof fence.ownerId !== "string" ||
439
+ !fence.ownerId ||
440
+ typeof fence.domainId !== "string" ||
441
+ !fence.domainId
442
+ ) {
443
+ throw new TypeError("Durability fence ownerId/domainId must be non-empty");
444
+ }
445
+ if (
446
+ new TextEncoder().encode(fence.ownerId).byteLength >
447
+ NATIVE_DURABILITY_MAX_WRITER_ID_BYTES ||
448
+ new TextEncoder().encode(fence.domainId).byteLength >
449
+ NATIVE_DURABILITY_MAX_WRITER_ID_BYTES
450
+ ) {
451
+ throw new TypeError(
452
+ "Durability fence ownerId/domainId exceeds format limit",
453
+ );
454
+ }
455
+ assertU64("fence.epoch", fence.epoch);
456
+ if (fence.epoch === 0n) {
457
+ throw new TypeError("Durability fence epoch must be non-zero");
458
+ }
459
+ };
460
+
461
+ export const assertNativeDurabilityStageRequest = (
462
+ request: NativeDurabilityStageRequest,
463
+ ): void => {
464
+ assertNativeDurabilityOperationScope(request?.scope);
465
+ if (request.scope.txSequence === 0n || request.scope.recordLsn === 0n) {
466
+ throw new RangeError("Staging txSequence and recordLsn must be non-zero");
467
+ }
468
+ if (!Array.isArray(request.blocks)) {
469
+ throw new TypeError("Staging blocks must be an array");
470
+ }
471
+ const sorted = [...request.blocks].sort(
472
+ (left, right) => left.ordinal - right.ordinal,
473
+ );
474
+ for (let index = 0; index < sorted.length; index++) {
475
+ const block = sorted[index];
476
+ if (block.ordinal !== index) {
477
+ throw new RangeError("Staging ordinals must be contiguous from zero");
478
+ }
479
+ if (typeof block.cid !== "string" || !block.cid) {
480
+ throw new TypeError(`Staging block ${index} has an invalid CID`);
481
+ }
482
+ if (!(block.bytes instanceof Uint8Array)) {
483
+ throw new TypeError(`Staging block ${index} bytes must be Uint8Array`);
484
+ }
485
+ if (
486
+ !(block.digest instanceof Uint8Array) ||
487
+ block.digest.byteLength !== 32
488
+ ) {
489
+ throw new TypeError(`Staging block ${index} digest must be 32 bytes`);
490
+ }
491
+ }
492
+ };
493
+
494
+ export const assertNativeDurabilityJournalAppendRequest = (
495
+ request: NativeDurabilityJournalAppendRequest,
496
+ ): void => {
497
+ if (
498
+ !request ||
499
+ typeof request.transactionId !== "string" ||
500
+ !request.transactionId
501
+ ) {
502
+ throw new TypeError("transactionId must be a non-empty string");
503
+ }
504
+ if (
505
+ new TextEncoder().encode(request.transactionId).byteLength >
506
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES
507
+ ) {
508
+ throw new TypeError("transactionId exceeds the journal format limit");
509
+ }
510
+ assertU64("txSequence", request.txSequence);
511
+ assertU64("firstRecordLsn", request.firstRecordLsn);
512
+ assertU64("lastRecordLsn", request.lastRecordLsn);
513
+ if (
514
+ request.txSequence === 0n ||
515
+ request.firstRecordLsn === 0n ||
516
+ request.lastRecordLsn === 0n
517
+ ) {
518
+ throw new RangeError("Journal sequence and LSN values must be non-zero");
519
+ }
520
+ if (request.lastRecordLsn < request.firstRecordLsn) {
521
+ throw new RangeError("lastRecordLsn must not precede firstRecordLsn");
522
+ }
523
+ if (
524
+ !Number.isSafeInteger(request.expectedOffset) ||
525
+ request.expectedOffset < 0
526
+ ) {
527
+ throw new RangeError("expectedOffset must be a non-negative safe integer");
528
+ }
529
+ if (
530
+ !(request.frames instanceof Uint8Array) ||
531
+ request.frames.byteLength === 0
532
+ ) {
533
+ throw new TypeError("Journal frames must be a non-empty Uint8Array");
534
+ }
535
+ if (
536
+ !(request.framesDigest instanceof Uint8Array) ||
537
+ request.framesDigest.byteLength !== 32
538
+ ) {
539
+ throw new TypeError("framesDigest must be 32 bytes");
540
+ }
541
+ };
542
+
543
+ export const assertNativeDurabilityCheckpointRequest = (
544
+ request: NativeDurabilityCheckpointRequest,
545
+ ): void => {
546
+ assertNativeDurabilityOperationScope(request?.scope);
547
+ assertU64("checkpointLsn", request.checkpointLsn);
548
+ assertU64("txSequenceHighwater", request.txSequenceHighwater);
549
+ if (
550
+ request.scope.txSequence === 0n ||
551
+ request.scope.recordLsn === 0n ||
552
+ request.scope.recordLsn > request.checkpointLsn ||
553
+ request.scope.txSequence > request.txSequenceHighwater
554
+ ) {
555
+ throw new RangeError(
556
+ "Checkpoint scope cannot exceed its LSN or transaction highwater",
557
+ );
558
+ }
559
+ if (!(request.bytes instanceof Uint8Array)) {
560
+ throw new TypeError("Checkpoint bytes must be Uint8Array");
561
+ }
562
+ if (
563
+ !(request.digest instanceof Uint8Array) ||
564
+ request.digest.byteLength !== 32
565
+ ) {
566
+ throw new TypeError("Checkpoint digest must be 32 bytes");
567
+ }
568
+ if (!Array.isArray(request.stagingCoverage)) {
569
+ throw new TypeError("Checkpoint stagingCoverage must be an array");
570
+ }
571
+ const transactionIds = new Set<string>();
572
+ for (const coverage of request.stagingCoverage) {
573
+ if (
574
+ typeof coverage.transactionId !== "string" ||
575
+ !coverage.transactionId ||
576
+ transactionIds.has(coverage.transactionId)
577
+ ) {
578
+ throw new TypeError(
579
+ "Checkpoint staging coverage transaction IDs must be unique",
580
+ );
581
+ }
582
+ transactionIds.add(coverage.transactionId);
583
+ assertU64("coverage.txSequence", coverage.txSequence);
584
+ assertU64("coverage.coveredThroughLsn", coverage.coveredThroughLsn);
585
+ if (coverage.coveredThroughLsn > request.checkpointLsn) {
586
+ throw new RangeError(
587
+ "Staging coverage cannot extend beyond checkpointLsn",
588
+ );
589
+ }
590
+ if (
591
+ new TextEncoder().encode(coverage.transactionId).byteLength >
592
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES ||
593
+ !(coverage.stagingManifestDigest instanceof Uint8Array) ||
594
+ coverage.stagingManifestDigest.byteLength !== 32
595
+ ) {
596
+ throw new TypeError(
597
+ "Checkpoint staging coverage fields exceed their format limits",
598
+ );
599
+ }
600
+ }
601
+ if (!Array.isArray(request.retainedTransactions)) {
602
+ throw new TypeError("Checkpoint retainedTransactions must be an array");
603
+ }
604
+ const retainedIds = new Set<string>();
605
+ const retainedSequences = new Set<bigint>();
606
+ for (const retained of request.retainedTransactions) {
607
+ assertU64("retained transaction sequence", retained.txSequence);
608
+ if (
609
+ retained.txSequence === 0n ||
610
+ retained.txSequence > request.txSequenceHighwater ||
611
+ typeof retained.transactionId !== "string" ||
612
+ !retained.transactionId ||
613
+ new TextEncoder().encode(retained.transactionId).byteLength >
614
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES ||
615
+ retainedIds.has(retained.transactionId) ||
616
+ retainedSequences.has(retained.txSequence) ||
617
+ !Number.isInteger(retained.phase) ||
618
+ retained.phase < 1 ||
619
+ retained.phase > 6 ||
620
+ !Number.isInteger(retained.operationKind) ||
621
+ retained.operationKind < 1 ||
622
+ retained.operationKind > 5 ||
623
+ !(retained.planDigest instanceof Uint8Array) ||
624
+ retained.planDigest.byteLength !== 32
625
+ ) {
626
+ throw new TypeError("Checkpoint retained transaction is invalid");
627
+ }
628
+ retainedIds.add(retained.transactionId);
629
+ retainedSequences.add(retained.txSequence);
630
+ }
631
+ for (const coverage of request.stagingCoverage) {
632
+ const retained = request.retainedTransactions.find(
633
+ (candidate) => candidate.transactionId === coverage.transactionId,
634
+ );
635
+ if (
636
+ !retained ||
637
+ retained.txSequence !== coverage.txSequence ||
638
+ retained.phase !== 6
639
+ ) {
640
+ throw new TypeError(
641
+ "Checkpoint staging coverage requires the exact retained CLEAN transaction",
642
+ );
643
+ }
644
+ }
645
+ };
646
+
647
+ export const assertNativeDurabilityDeleteRequest = (
648
+ request: NativeDurabilityDeleteRequest,
649
+ ): void => {
650
+ assertNativeDurabilityOperationScope(request?.scope);
651
+ if (!Array.isArray(request.targets) || request.targets.length === 0) {
652
+ throw new TypeError("Delete targets must be a non-empty array");
653
+ }
654
+ if (request.scope.txSequence === 0n || request.scope.recordLsn === 0n) {
655
+ throw new RangeError("Delete scope sequence and LSN must be non-zero");
656
+ }
657
+ const targetKeys = new Set<string>();
658
+ for (const target of request.targets) {
659
+ if (target.kind === "staging") {
660
+ if (
661
+ typeof target.transactionId !== "string" ||
662
+ !target.transactionId ||
663
+ new TextEncoder().encode(target.transactionId).byteLength >
664
+ NATIVE_DURABILITY_MAX_TRANSACTION_ID_BYTES
665
+ ) {
666
+ throw new TypeError("Staging delete transactionId must be non-empty");
667
+ }
668
+ } else if (target.kind === "checkpoint") {
669
+ assertU64("checkpoint generation", target.generation);
670
+ } else {
671
+ throw new TypeError("Unknown native durability delete target");
672
+ }
673
+ const key =
674
+ target.kind === "staging"
675
+ ? `staging:${target.transactionId}`
676
+ : `checkpoint:${target.generation}`;
677
+ if (targetKeys.has(key)) {
678
+ throw new TypeError("Delete targets must be unique");
679
+ }
680
+ targetKeys.add(key);
681
+ }
682
+ };