@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,683 @@
1
+ import type {
2
+ NativeDurabilityJournalClassification,
3
+ NativeDurabilityJournalClassifier,
4
+ } from "./storage.js";
5
+
6
+ export const NATIVE_DURABILITY_JOURNAL_FORMAT_VERSION = 1 as const;
7
+ export const NATIVE_DURABILITY_JOURNAL_MAX_U64 = (1n << 64n) - 1n;
8
+ export const NATIVE_DURABILITY_JOURNAL_MAX_BODY_LENGTH = 64 * 1024 * 1024;
9
+ export const NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH = 4096;
10
+ export const NATIVE_DURABILITY_JOURNAL_MAX_TRANSACTION_ID_LENGTH = 1024;
11
+ export const NATIVE_DURABILITY_JOURNAL_MAX_WRITER_OWNER_ID_LENGTH = 1024;
12
+ export const NATIVE_DURABILITY_JOURNAL_MAX_WRITER_DOMAIN_ID_LENGTH = 1024;
13
+ export const NATIVE_DURABILITY_JOURNAL_CODEC_BRAND = Symbol.for(
14
+ "@peerbit/native-backbone/native-durability-journal-codec/v1",
15
+ );
16
+
17
+ export enum NativeDurabilityPhase {
18
+ DurablePrepared = 1,
19
+ NativeApplied = 2,
20
+ Published = 3,
21
+ Committed = 4,
22
+ CleanupPending = 5,
23
+ Clean = 6,
24
+ }
25
+
26
+ export enum NativeDurabilityOperationKind {
27
+ Append = 1,
28
+ Batch = 2,
29
+ Document = 3,
30
+ Receive = 4,
31
+ Repair = 5,
32
+ }
33
+
34
+ export type NativeDurabilityJournalRecord = {
35
+ recordLsn: bigint;
36
+ txSequence: bigint;
37
+ writerEpoch: bigint;
38
+ writerOwnerId: string;
39
+ writerDomainId: string;
40
+ phase: NativeDurabilityPhase;
41
+ operationKind: NativeDurabilityOperationKind;
42
+ programId: Uint8Array;
43
+ transactionId: string;
44
+ planDigest: Uint8Array;
45
+ payload: Uint8Array;
46
+ };
47
+
48
+ export type NativeDurabilityCheckpointTransactionState = Pick<
49
+ NativeDurabilityJournalRecord,
50
+ "txSequence" | "transactionId" | "phase" | "operationKind" | "planDigest"
51
+ >;
52
+
53
+ export type NativeDurabilityJournalValidationContext = {
54
+ checkpointLsn: bigint;
55
+ checkpointTxSequenceHighwater: bigint;
56
+ expectedProgramId: Uint8Array;
57
+ expectedWriterDomainId: string;
58
+ checkpointWriterEpoch: bigint;
59
+ checkpointWriterOwnerId?: string;
60
+ currentWriterEpoch: bigint;
61
+ currentWriterOwnerId: string;
62
+ retainedTransactions?: readonly NativeDurabilityCheckpointTransactionState[];
63
+ };
64
+
65
+ export type NativeDurabilityIncompleteTailReason =
66
+ | "short-header"
67
+ | "short-body"
68
+ | "short-trailer";
69
+
70
+ export type NativeDurabilityJournalScan = {
71
+ records: NativeDurabilityJournalRecord[];
72
+ validLength: number;
73
+ incompleteTailOffset?: number;
74
+ incompleteTailReason?: NativeDurabilityIncompleteTailReason;
75
+ lastRecordLsn: bigint;
76
+ };
77
+
78
+ export type NativeDurabilityJournalErrorCode =
79
+ | "ERR_NATIVE_DURABILITY_JOURNAL_INPUT"
80
+ | `ERR_NATIVE_DURABILITY_JOURNAL_${string}`;
81
+
82
+ export class NativeDurabilityJournalCodecError extends Error {
83
+ constructor(
84
+ readonly code: NativeDurabilityJournalErrorCode,
85
+ message: string,
86
+ readonly byteOffset?: number,
87
+ readonly cause?: unknown,
88
+ ) {
89
+ super(message);
90
+ this.name = "NativeDurabilityJournalCodecError";
91
+ }
92
+ }
93
+
94
+ export class NativeDurabilityJournalInputError extends NativeDurabilityJournalCodecError {
95
+ constructor(readonly field: string, message: string) {
96
+ super("ERR_NATIVE_DURABILITY_JOURNAL_INPUT", message);
97
+ this.name = "NativeDurabilityJournalInputError";
98
+ }
99
+ }
100
+
101
+ export class NativeDurabilityJournalCorruptionError extends NativeDurabilityJournalCodecError {
102
+ constructor(
103
+ code: NativeDurabilityJournalErrorCode,
104
+ message: string,
105
+ byteOffset?: number,
106
+ cause?: unknown,
107
+ ) {
108
+ super(code, message, byteOffset, cause);
109
+ this.name = "NativeDurabilityJournalCorruptionError";
110
+ }
111
+ }
112
+
113
+ type NativeDurabilityJournalCodecHandle = {
114
+ encodeFrame(
115
+ recordLsn: string,
116
+ txSequence: string,
117
+ writerEpoch: string,
118
+ writerOwnerId: string,
119
+ writerDomainId: string,
120
+ phase: number,
121
+ operationKind: number,
122
+ programId: Uint8Array,
123
+ transactionId: string,
124
+ planDigest: Uint8Array,
125
+ payload: Uint8Array,
126
+ ): Uint8Array;
127
+ scan(
128
+ bytes: Uint8Array,
129
+ checkpointLsn: string,
130
+ checkpointTxSequenceHighwater: string,
131
+ expectedProgramId: Uint8Array,
132
+ expectedWriterDomainId: string,
133
+ checkpointWriterEpoch: string,
134
+ checkpointWriterOwnerId: string | undefined,
135
+ currentWriterEpoch: string,
136
+ currentWriterOwnerId: string,
137
+ retainedTransactions: unknown[],
138
+ ): unknown[];
139
+ };
140
+
141
+ type NativeBackboneWasmModule = {
142
+ default(input?: unknown): Promise<unknown>;
143
+ initSync(input?: unknown): unknown;
144
+ NativeDurabilityJournalCodec: new () => NativeDurabilityJournalCodecHandle;
145
+ };
146
+
147
+ let wasmModulePromise: Promise<NativeBackboneWasmModule> | undefined;
148
+ let wasmInitialization: Promise<NativeBackboneWasmModule> | undefined;
149
+ const nativeDurabilityJournalCodecConstructionToken = Symbol();
150
+ const trustedNativeDurabilityJournalCodecs = new WeakSet<object>();
151
+
152
+ const loadWasm = async (): Promise<NativeBackboneWasmModule> => {
153
+ const wasmModulePath = "../../wasm/native_backbone.js";
154
+ wasmModulePromise ??= import(
155
+ /* @vite-ignore */ wasmModulePath
156
+ ) as Promise<NativeBackboneWasmModule>;
157
+ wasmInitialization ??= wasmModulePromise.then(async (wasm) => {
158
+ const processLike = globalThis as {
159
+ process?: { versions?: { node?: string } };
160
+ };
161
+ if (processLike.process?.versions?.node) {
162
+ const fsPromises = "fs/promises";
163
+ const { readFile } = (await import(
164
+ /* @vite-ignore */ fsPromises
165
+ )) as typeof import("fs/promises");
166
+ const bytes = await readFile(
167
+ new URL("../../wasm/native_backbone_bg.wasm", import.meta.url),
168
+ );
169
+ wasm.initSync({ module: bytes });
170
+ } else {
171
+ await wasm.default({
172
+ module_or_path: new URL(
173
+ "../../wasm/native_backbone_bg.wasm",
174
+ import.meta.url,
175
+ ),
176
+ });
177
+ }
178
+ return wasm;
179
+ });
180
+ return wasmInitialization;
181
+ };
182
+
183
+ let textEncoder: TextEncoder | undefined;
184
+
185
+ const utf8ByteLength = (value: string): number => {
186
+ textEncoder ??= new TextEncoder();
187
+ return textEncoder.encode(value).byteLength;
188
+ };
189
+
190
+ const assertU64 = (
191
+ value: bigint,
192
+ field: string,
193
+ options: { nonzero?: boolean } = {},
194
+ ): void => {
195
+ if (
196
+ typeof value !== "bigint" ||
197
+ value < 0n ||
198
+ value > NATIVE_DURABILITY_JOURNAL_MAX_U64 ||
199
+ (options.nonzero === true && value === 0n)
200
+ ) {
201
+ throw new NativeDurabilityJournalInputError(
202
+ field,
203
+ `${field} must be ${options.nonzero ? "a non-zero" : "an"} unsigned 64-bit bigint`,
204
+ );
205
+ }
206
+ };
207
+
208
+ const assertBytes = (
209
+ value: Uint8Array,
210
+ field: string,
211
+ options: { exact?: number; min?: number; max?: number } = {},
212
+ ): void => {
213
+ if (!(value instanceof Uint8Array)) {
214
+ throw new NativeDurabilityJournalInputError(field, `${field} must be Uint8Array`);
215
+ }
216
+ if (options.exact != null && value.byteLength !== options.exact) {
217
+ throw new NativeDurabilityJournalInputError(
218
+ field,
219
+ `${field} must contain exactly ${options.exact} bytes`,
220
+ );
221
+ }
222
+ if (options.min != null && value.byteLength < options.min) {
223
+ throw new NativeDurabilityJournalInputError(
224
+ field,
225
+ `${field} must contain at least ${options.min} bytes`,
226
+ );
227
+ }
228
+ if (options.max != null && value.byteLength > options.max) {
229
+ throw new NativeDurabilityJournalInputError(
230
+ field,
231
+ `${field} exceeds ${options.max} bytes`,
232
+ );
233
+ }
234
+ };
235
+
236
+ const assertBoundedString = (
237
+ value: string,
238
+ field: string,
239
+ maxBytes: number,
240
+ ): void => {
241
+ if (typeof value !== "string" || value.length === 0) {
242
+ throw new NativeDurabilityJournalInputError(field, `${field} must not be empty`);
243
+ }
244
+ const byteLength = utf8ByteLength(value);
245
+ if (byteLength > maxBytes) {
246
+ throw new NativeDurabilityJournalInputError(
247
+ field,
248
+ `${field} exceeds ${maxBytes} UTF-8 bytes`,
249
+ );
250
+ }
251
+ };
252
+
253
+ const assertPhase = (value: NativeDurabilityPhase, field: string): void => {
254
+ if (!Number.isInteger(value) || value < 1 || value > 6) {
255
+ throw new NativeDurabilityJournalInputError(field, `${field} is invalid`);
256
+ }
257
+ };
258
+
259
+ const assertOperationKind = (
260
+ value: NativeDurabilityOperationKind,
261
+ field: string,
262
+ ): void => {
263
+ if (!Number.isInteger(value) || value < 1 || value > 5) {
264
+ throw new NativeDurabilityJournalInputError(field, `${field} is invalid`);
265
+ }
266
+ };
267
+
268
+ const assertRecord = (record: NativeDurabilityJournalRecord): void => {
269
+ assertU64(record.recordLsn, "recordLsn", { nonzero: true });
270
+ assertU64(record.txSequence, "txSequence", { nonzero: true });
271
+ assertU64(record.writerEpoch, "writerEpoch", { nonzero: true });
272
+ assertBoundedString(
273
+ record.writerOwnerId,
274
+ "writerOwnerId",
275
+ NATIVE_DURABILITY_JOURNAL_MAX_WRITER_OWNER_ID_LENGTH,
276
+ );
277
+ assertBoundedString(
278
+ record.writerDomainId,
279
+ "writerDomainId",
280
+ NATIVE_DURABILITY_JOURNAL_MAX_WRITER_DOMAIN_ID_LENGTH,
281
+ );
282
+ assertPhase(record.phase, "phase");
283
+ assertOperationKind(record.operationKind, "operationKind");
284
+ assertBytes(record.programId, "programId", {
285
+ min: 1,
286
+ max: NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH,
287
+ });
288
+ assertBoundedString(
289
+ record.transactionId,
290
+ "transactionId",
291
+ NATIVE_DURABILITY_JOURNAL_MAX_TRANSACTION_ID_LENGTH,
292
+ );
293
+ assertBytes(record.planDigest, "planDigest", { exact: 32 });
294
+ assertBytes(record.payload, "payload", {
295
+ max: NATIVE_DURABILITY_JOURNAL_MAX_BODY_LENGTH,
296
+ });
297
+ };
298
+
299
+ const assertContext = (context: NativeDurabilityJournalValidationContext): void => {
300
+ assertU64(context.checkpointLsn, "checkpointLsn");
301
+ assertU64(
302
+ context.checkpointTxSequenceHighwater,
303
+ "checkpointTxSequenceHighwater",
304
+ );
305
+ assertBytes(context.expectedProgramId, "expectedProgramId", {
306
+ min: 1,
307
+ max: NATIVE_DURABILITY_JOURNAL_MAX_PROGRAM_ID_LENGTH,
308
+ });
309
+ assertBoundedString(
310
+ context.expectedWriterDomainId,
311
+ "expectedWriterDomainId",
312
+ NATIVE_DURABILITY_JOURNAL_MAX_WRITER_DOMAIN_ID_LENGTH,
313
+ );
314
+ assertU64(context.checkpointWriterEpoch, "checkpointWriterEpoch");
315
+ assertU64(context.currentWriterEpoch, "currentWriterEpoch", { nonzero: true });
316
+ if (context.checkpointWriterEpoch > context.currentWriterEpoch) {
317
+ throw new NativeDurabilityJournalInputError(
318
+ "checkpointWriterEpoch",
319
+ "checkpointWriterEpoch must not exceed currentWriterEpoch",
320
+ );
321
+ }
322
+ if (context.checkpointWriterEpoch > 0n && context.checkpointWriterOwnerId == null) {
323
+ throw new NativeDurabilityJournalInputError(
324
+ "checkpointWriterOwnerId",
325
+ "checkpointWriterOwnerId is required for a non-genesis checkpoint epoch",
326
+ );
327
+ }
328
+ if (context.checkpointWriterOwnerId != null) {
329
+ assertBoundedString(
330
+ context.checkpointWriterOwnerId,
331
+ "checkpointWriterOwnerId",
332
+ NATIVE_DURABILITY_JOURNAL_MAX_WRITER_OWNER_ID_LENGTH,
333
+ );
334
+ }
335
+ assertBoundedString(
336
+ context.currentWriterOwnerId,
337
+ "currentWriterOwnerId",
338
+ NATIVE_DURABILITY_JOURNAL_MAX_WRITER_OWNER_ID_LENGTH,
339
+ );
340
+ for (const [index, retained] of (context.retainedTransactions ?? []).entries()) {
341
+ assertU64(retained.txSequence, `retainedTransactions[${index}].txSequence`, {
342
+ nonzero: true,
343
+ });
344
+ if (retained.txSequence > context.checkpointTxSequenceHighwater) {
345
+ throw new NativeDurabilityJournalInputError(
346
+ `retainedTransactions[${index}].txSequence`,
347
+ "retained transaction sequence exceeds checkpoint highwater",
348
+ );
349
+ }
350
+ assertBoundedString(
351
+ retained.transactionId,
352
+ `retainedTransactions[${index}].transactionId`,
353
+ NATIVE_DURABILITY_JOURNAL_MAX_TRANSACTION_ID_LENGTH,
354
+ );
355
+ assertPhase(retained.phase, `retainedTransactions[${index}].phase`);
356
+ assertOperationKind(
357
+ retained.operationKind,
358
+ `retainedTransactions[${index}].operationKind`,
359
+ );
360
+ assertBytes(retained.planDigest, `retainedTransactions[${index}].planDigest`, {
361
+ exact: 32,
362
+ });
363
+ }
364
+ };
365
+
366
+ const copyContext = (
367
+ context: NativeDurabilityJournalValidationContext,
368
+ ): NativeDurabilityJournalValidationContext => ({
369
+ ...context,
370
+ expectedProgramId: new Uint8Array(context.expectedProgramId),
371
+ retainedTransactions: (context.retainedTransactions ?? []).map((retained) => ({
372
+ ...retained,
373
+ planDigest: new Uint8Array(retained.planDigest),
374
+ })),
375
+ });
376
+
377
+ const parseDecimalU64 = (value: unknown, field: string): bigint => {
378
+ if (typeof value !== "string" || !/^(0|[1-9][0-9]*)$/.test(value)) {
379
+ throw new NativeDurabilityJournalCorruptionError(
380
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_DECIMAL_U64",
381
+ `${field} is not a canonical decimal u64`,
382
+ );
383
+ }
384
+ const parsed = BigInt(value);
385
+ if (parsed > NATIVE_DURABILITY_JOURNAL_MAX_U64) {
386
+ throw new NativeDurabilityJournalCorruptionError(
387
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_DECIMAL_U64",
388
+ `${field} exceeds u64`,
389
+ );
390
+ }
391
+ return parsed;
392
+ };
393
+
394
+ const parseSafeOffset = (
395
+ value: unknown,
396
+ field: string,
397
+ maximum: number,
398
+ ): number => {
399
+ if (
400
+ typeof value !== "number" ||
401
+ !Number.isSafeInteger(value) ||
402
+ value < 0 ||
403
+ value > maximum
404
+ ) {
405
+ throw new NativeDurabilityJournalCorruptionError(
406
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
407
+ `${field} is not a safe byte offset`,
408
+ );
409
+ }
410
+ return value;
411
+ };
412
+
413
+ const bytesFromRow = (value: unknown, field: string): Uint8Array => {
414
+ if (!(value instanceof Uint8Array)) {
415
+ throw new NativeDurabilityJournalCorruptionError(
416
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
417
+ `${field} is not bytes`,
418
+ );
419
+ }
420
+ return new Uint8Array(value);
421
+ };
422
+
423
+ const stringFromRow = (value: unknown, field: string): string => {
424
+ if (typeof value !== "string") {
425
+ throw new NativeDurabilityJournalCorruptionError(
426
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
427
+ `${field} is not a string`,
428
+ );
429
+ }
430
+ return value;
431
+ };
432
+
433
+ const enumFromRow = <T extends number>(
434
+ value: unknown,
435
+ field: string,
436
+ min: number,
437
+ max: number,
438
+ ): T => {
439
+ if (typeof value !== "number" || !Number.isInteger(value) || value < min || value > max) {
440
+ throw new NativeDurabilityJournalCorruptionError(
441
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
442
+ `${field} is invalid`,
443
+ );
444
+ }
445
+ return value as T;
446
+ };
447
+
448
+ const errorFromWasm = (
449
+ error: unknown,
450
+ kind: "input" | "corruption",
451
+ ): NativeDurabilityJournalCodecError => {
452
+ if (Array.isArray(error) && typeof error[0] === "string" && typeof error[1] === "string") {
453
+ const offset =
454
+ typeof error[2] === "number" && Number.isSafeInteger(error[2])
455
+ ? error[2]
456
+ : undefined;
457
+ if (kind === "corruption") {
458
+ return new NativeDurabilityJournalCorruptionError(
459
+ error[0] as NativeDurabilityJournalErrorCode,
460
+ error[1],
461
+ offset,
462
+ error,
463
+ );
464
+ }
465
+ return new NativeDurabilityJournalCodecError(
466
+ error[0] as NativeDurabilityJournalErrorCode,
467
+ error[1],
468
+ offset,
469
+ error,
470
+ );
471
+ }
472
+ return kind === "corruption"
473
+ ? new NativeDurabilityJournalCorruptionError(
474
+ "ERR_NATIVE_DURABILITY_JOURNAL_WASM",
475
+ "Native durability journal codec failed",
476
+ undefined,
477
+ error,
478
+ )
479
+ : new NativeDurabilityJournalCodecError(
480
+ "ERR_NATIVE_DURABILITY_JOURNAL_WASM",
481
+ "Native durability journal codec failed",
482
+ undefined,
483
+ error,
484
+ );
485
+ };
486
+
487
+ const parseScan = (raw: unknown, byteLength: number): NativeDurabilityJournalScan => {
488
+ if (!Array.isArray(raw) || raw.length !== 5 || !Array.isArray(raw[4])) {
489
+ throw new NativeDurabilityJournalCorruptionError(
490
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
491
+ "Native durability journal scan result is malformed",
492
+ );
493
+ }
494
+ const validLength = parseSafeOffset(raw[0], "validLength", byteLength);
495
+ const incompleteTailOffset =
496
+ raw[1] == null
497
+ ? undefined
498
+ : parseSafeOffset(raw[1], "incompleteTailOffset", byteLength);
499
+ const reasonCode = enumFromRow<number>(raw[2], "incompleteTailReason", 0, 3);
500
+ const incompleteTailReason =
501
+ reasonCode === 0
502
+ ? undefined
503
+ : reasonCode === 1
504
+ ? "short-header"
505
+ : reasonCode === 2
506
+ ? "short-body"
507
+ : "short-trailer";
508
+ if ((incompleteTailOffset == null) !== (incompleteTailReason == null)) {
509
+ throw new NativeDurabilityJournalCorruptionError(
510
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
511
+ "Incomplete-tail offset and reason disagree",
512
+ );
513
+ }
514
+ const records = (raw[4] as unknown[]).map((unknownRow, index) => {
515
+ if (!Array.isArray(unknownRow) || unknownRow.length !== 11) {
516
+ throw new NativeDurabilityJournalCorruptionError(
517
+ "ERR_NATIVE_DURABILITY_JOURNAL_INVALID_SCAN_ROW",
518
+ `Journal record row ${index} is malformed`,
519
+ );
520
+ }
521
+ const row = unknownRow;
522
+ return {
523
+ recordLsn: parseDecimalU64(row[0], `records[${index}].recordLsn`),
524
+ txSequence: parseDecimalU64(row[1], `records[${index}].txSequence`),
525
+ writerEpoch: parseDecimalU64(row[2], `records[${index}].writerEpoch`),
526
+ writerOwnerId: stringFromRow(row[3], `records[${index}].writerOwnerId`),
527
+ writerDomainId: stringFromRow(row[4], `records[${index}].writerDomainId`),
528
+ phase: enumFromRow<NativeDurabilityPhase>(
529
+ row[5],
530
+ `records[${index}].phase`,
531
+ 1,
532
+ 6,
533
+ ),
534
+ operationKind: enumFromRow<NativeDurabilityOperationKind>(
535
+ row[6],
536
+ `records[${index}].operationKind`,
537
+ 1,
538
+ 5,
539
+ ),
540
+ programId: bytesFromRow(row[7], `records[${index}].programId`),
541
+ transactionId: stringFromRow(row[8], `records[${index}].transactionId`),
542
+ planDigest: bytesFromRow(row[9], `records[${index}].planDigest`),
543
+ payload: bytesFromRow(row[10], `records[${index}].payload`),
544
+ } satisfies NativeDurabilityJournalRecord;
545
+ });
546
+ return {
547
+ records,
548
+ validLength,
549
+ incompleteTailOffset,
550
+ incompleteTailReason,
551
+ lastRecordLsn: parseDecimalU64(raw[3], "lastRecordLsn"),
552
+ };
553
+ };
554
+
555
+ export class NativeDurabilityJournalCodec implements NativeDurabilityJournalClassifier {
556
+ readonly formatVersion = NATIVE_DURABILITY_JOURNAL_FORMAT_VERSION;
557
+ readonly [NATIVE_DURABILITY_JOURNAL_CODEC_BRAND] = true as const;
558
+ private readonly validationContext: NativeDurabilityJournalValidationContext;
559
+
560
+ private constructor(
561
+ constructionToken: typeof nativeDurabilityJournalCodecConstructionToken,
562
+ private readonly native: NativeDurabilityJournalCodecHandle,
563
+ context: NativeDurabilityJournalValidationContext,
564
+ ) {
565
+ if (constructionToken !== nativeDurabilityJournalCodecConstructionToken) {
566
+ throw new TypeError(
567
+ "NativeDurabilityJournalCodec must be created by createNativeDurabilityJournalCodec",
568
+ );
569
+ }
570
+ assertContext(context);
571
+ this.validationContext = copyContext(context);
572
+ }
573
+
574
+ static async create(
575
+ context: NativeDurabilityJournalValidationContext,
576
+ ): Promise<NativeDurabilityJournalCodec> {
577
+ assertContext(context);
578
+ // Snapshot before the first await so caller mutation cannot change the
579
+ // validation authority while the Wasm module is loading.
580
+ const validationContext = copyContext(context);
581
+ const wasm = await loadWasm();
582
+ const codec = new NativeDurabilityJournalCodec(
583
+ nativeDurabilityJournalCodecConstructionToken,
584
+ new wasm.NativeDurabilityJournalCodec(),
585
+ validationContext,
586
+ );
587
+ trustedNativeDurabilityJournalCodecs.add(codec);
588
+ return codec;
589
+ }
590
+
591
+ get context(): NativeDurabilityJournalValidationContext {
592
+ return copyContext(this.validationContext);
593
+ }
594
+
595
+ encode(record: NativeDurabilityJournalRecord): Uint8Array {
596
+ assertRecord(record);
597
+ try {
598
+ return new Uint8Array(
599
+ this.native.encodeFrame(
600
+ record.recordLsn.toString(),
601
+ record.txSequence.toString(),
602
+ record.writerEpoch.toString(),
603
+ record.writerOwnerId,
604
+ record.writerDomainId,
605
+ record.phase,
606
+ record.operationKind,
607
+ record.programId,
608
+ record.transactionId,
609
+ record.planDigest,
610
+ record.payload,
611
+ ),
612
+ );
613
+ } catch (error) {
614
+ throw errorFromWasm(error, "input");
615
+ }
616
+ }
617
+
618
+ scan(
619
+ bytes: Uint8Array,
620
+ context: NativeDurabilityJournalValidationContext = this.validationContext,
621
+ ): NativeDurabilityJournalScan {
622
+ assertBytes(bytes, "journalBytes");
623
+ assertContext(context);
624
+ const retainedTransactions = (context.retainedTransactions ?? []).map(
625
+ (retained) => [
626
+ retained.txSequence.toString(),
627
+ retained.transactionId,
628
+ retained.phase,
629
+ retained.operationKind,
630
+ retained.planDigest,
631
+ ],
632
+ );
633
+ try {
634
+ return parseScan(
635
+ this.native.scan(
636
+ bytes,
637
+ context.checkpointLsn.toString(),
638
+ context.checkpointTxSequenceHighwater.toString(),
639
+ context.expectedProgramId,
640
+ context.expectedWriterDomainId,
641
+ context.checkpointWriterEpoch.toString(),
642
+ context.checkpointWriterOwnerId,
643
+ context.currentWriterEpoch.toString(),
644
+ context.currentWriterOwnerId,
645
+ retainedTransactions,
646
+ ),
647
+ bytes.byteLength,
648
+ );
649
+ } catch (error) {
650
+ if (error instanceof NativeDurabilityJournalCodecError) throw error;
651
+ throw errorFromWasm(error, "corruption");
652
+ }
653
+ }
654
+
655
+ classify(bytes: Uint8Array): NativeDurabilityJournalClassification {
656
+ const scan = this.scan(bytes);
657
+ if (scan.incompleteTailOffset != null && scan.incompleteTailReason != null) {
658
+ return {
659
+ kind: "incomplete-tail",
660
+ validLength: scan.validLength,
661
+ lastRecordLsn: scan.lastRecordLsn,
662
+ reason: scan.incompleteTailReason,
663
+ };
664
+ }
665
+ return {
666
+ kind: "complete",
667
+ validLength: scan.validLength,
668
+ lastRecordLsn: scan.lastRecordLsn,
669
+ };
670
+ }
671
+ }
672
+
673
+ export const isNativeDurabilityJournalCodec = (
674
+ value: unknown,
675
+ ): value is NativeDurabilityJournalCodec =>
676
+ (typeof value === "object" || typeof value === "function") &&
677
+ value !== null &&
678
+ trustedNativeDurabilityJournalCodecs.has(value);
679
+
680
+ export const createNativeDurabilityJournalCodec = async (
681
+ context: NativeDurabilityJournalValidationContext,
682
+ ): Promise<NativeDurabilityJournalCodec> =>
683
+ NativeDurabilityJournalCodec.create(context);