diff --git a/src/apps/cli/services/NodeKeyValueDBService.ts b/src/apps/cli/services/NodeKeyValueDBService.ts index 421fa0f5..c39372da 100644 --- a/src/apps/cli/services/NodeKeyValueDBService.ts +++ b/src/apps/cli/services/NodeKeyValueDBService.ts @@ -25,6 +25,10 @@ type SerializableContainer = | { [NODE_KV_TYPED_KEY]: "ArrayBuffer"; [NODE_KV_VALUES_KEY]: number[]; + } + | { + [NODE_KV_TYPED_KEY]: "BigInt"; + [NODE_KV_VALUES_KEY]: string; }; function isRecord(value: unknown): value is Record { @@ -32,6 +36,12 @@ function isRecord(value: unknown): value is Record { } function serializeForNodeKV(value: unknown): unknown { + if (typeof value === "bigint") { + return { + [NODE_KV_TYPED_KEY]: "BigInt", + [NODE_KV_VALUES_KEY]: value.toString(10), + } satisfies SerializableContainer; + } if (value instanceof Set) { return { [NODE_KV_TYPED_KEY]: "Set", @@ -78,6 +88,9 @@ function deserializeFromNodeKV(value: unknown): unknown { if (taggedType === "ArrayBuffer" && Array.isArray(taggedValues)) { return Uint8Array.from(taggedValues).buffer; } + if (taggedType === "BigInt" && typeof taggedValues === "string" && /^-?(?:0|[1-9]\d*)$/u.test(taggedValues)) { + return BigInt(taggedValues); + } return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFromNodeKV(v)])); } diff --git a/src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts b/src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts index a8d3d1d4..f362b781 100644 --- a/src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts +++ b/src/apps/cli/services/NodeKeyValueDBService.unit.spec.ts @@ -1,8 +1,39 @@ import { describe, expect, it, vi } from "vitest"; import { createServiceContext } from "@vrtmrz/livesync-commonlib/compat/services/base/ServiceBase"; +import { fsPromises as fs, os, path } from "@vrtmrz/livesync-commonlib/node"; import type { NodeKeyValueDBDependencies } from "./NodeKeyValueDBService"; import { NodeKeyValueDBService } from "./NodeKeyValueDBService"; +function createInitialisableDependencies(): { + dependencies: NodeKeyValueDBDependencies; + initialise: () => Promise; +} { + let initialise: (() => Promise) | undefined; + const dependencies = { + appLifecycle: { + onSettingLoaded: { + addHandler: vi.fn((handler: () => Promise) => { + initialise = handler; + }), + }, + }, + databaseEvents: { + onResetDatabase: { addHandler: vi.fn() }, + onDatabaseInitialisation: { addHandler: vi.fn() }, + onUnloadDatabase: { addHandler: vi.fn() }, + onCloseDatabase: { addHandler: vi.fn() }, + }, + vault: {}, + } as unknown as NodeKeyValueDBDependencies; + return { + dependencies, + initialise: async () => { + if (!initialise) throw new Error("Initialisation handler was not registered"); + return await initialise(); + }, + }; +} + describe("NodeKeyValueDBService.openSimpleStore", () => { it("creates a namespaced store handle before the backing database is initialised", () => { const dependencies = { @@ -44,4 +75,28 @@ describe("NodeKeyValueDBService.openSimpleStore", () => { await expect(store.get("key")).rejects.toThrow("KeyValueDB is not initialized yet"); }); + + it("preserves bigint values used by Adaptive Journal writer state", async () => { + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "livesync-node-kv-bigint-")); + const filePath = path.join(tempDir, "keyvalue-db.json"); + const writerState = { + writerEpoch: 9007199254740993n, + nested: [0n, { sequence: 18446744073709551615n }], + }; + + try { + const firstLifecycle = createInitialisableDependencies(); + const first = new NodeKeyValueDBService(createServiceContext(), firstLifecycle.dependencies, filePath); + await expect(firstLifecycle.initialise()).resolves.toBe(true); + await first.openSimpleStore("adaptive").set("writer-state", writerState); + + const secondLifecycle = createInitialisableDependencies(); + const second = new NodeKeyValueDBService(createServiceContext(), secondLifecycle.dependencies, filePath); + await expect(secondLifecycle.initialise()).resolves.toBe(true); + + await expect(second.openSimpleStore("adaptive").get("writer-state")).resolves.toEqual(writerState); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } + }); });