feat: add local-first EventDB storage package
This commit is contained in:
71
tests/event-db.test.ts
Normal file
71
tests/event-db.test.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
CanonicalJsonEventCodec,
|
||||
EventDB,
|
||||
EventHashMismatchError,
|
||||
MemoryBlobStorage,
|
||||
composeStorageTransforms,
|
||||
createAesGcmTransform,
|
||||
createEventIdentity,
|
||||
createMsgPackTransform,
|
||||
type BaseEventData,
|
||||
type BlobStorageAddedEvent,
|
||||
} from "../src/index.js";
|
||||
|
||||
interface TestEvent extends BaseEventData {
|
||||
readonly type: "test";
|
||||
readonly payload: unknown;
|
||||
}
|
||||
|
||||
const codec = new CanonicalJsonEventCodec<TestEvent>();
|
||||
const identify = createEventIdentity<TestEvent>((event) => codec.encodeCanonical(event));
|
||||
|
||||
describe("EventDB", () => {
|
||||
it("loads historical events, deduplicates observations, and sorts deterministically", async () => {
|
||||
const storage = new ObservableMemoryStorage<TestEvent>();
|
||||
const first: TestEvent = { version: 1, timestamp: 2, nonce: "a", type: "test", payload: 1 };
|
||||
const firstId = await identify(first);
|
||||
await storage.set(firstId, first);
|
||||
const database = await EventDB.open(storage, { identify });
|
||||
const observed: string[] = [];
|
||||
database.on("event", (event) => observed.push(event.id));
|
||||
|
||||
expect((await database.append(first)).id).toBe(firstId);
|
||||
storage.notify({ id: firstId, data: first, source: "remote" });
|
||||
const earlier = await database.append({ ...first, timestamp: 1, nonce: "b" });
|
||||
const tied = await database.append({ ...first, nonce: "c" });
|
||||
|
||||
const events = await database.events();
|
||||
expect(events[0]?.id).toBe(earlier.id);
|
||||
expect(events.slice(1).map((event) => event.id)).toEqual([firstId, tied.id].sort());
|
||||
expect(observed).toEqual([earlier.id, tied.id]);
|
||||
});
|
||||
|
||||
it("rejects a logical value stored under the wrong ID", async () => {
|
||||
const storage = new MemoryBlobStorage<TestEvent>();
|
||||
await storage.set("sha256:wrong", {
|
||||
version: 1, timestamp: 1, nonce: "a", type: "test", payload: null,
|
||||
});
|
||||
await expect(EventDB.open(storage, { identify })).rejects.toBeInstanceOf(EventHashMismatchError);
|
||||
});
|
||||
|
||||
it("keeps event identity stable when storage encryption changes physical bytes", async () => {
|
||||
const key = await crypto.subtle.generateKey({ name: "AES-GCM", length: 256 }, true, ["encrypt", "decrypt"]);
|
||||
const transform = composeStorageTransforms(
|
||||
createMsgPackTransform<TestEvent>(),
|
||||
createAesGcmTransform(key),
|
||||
);
|
||||
const plainStorage = new MemoryBlobStorage<TestEvent>();
|
||||
const encryptedStorage = new MemoryBlobStorage<TestEvent>({ transform });
|
||||
const plain = await EventDB.open(plainStorage, { identify });
|
||||
const encrypted = await EventDB.open(encryptedStorage, { identify });
|
||||
const event: TestEvent = { version: 1, timestamp: 1, nonce: "same", type: "test", payload: "value" };
|
||||
expect((await plain.append(event)).id).toBe((await encrypted.append(event)).id);
|
||||
});
|
||||
});
|
||||
|
||||
class ObservableMemoryStorage<T> extends MemoryBlobStorage<T> {
|
||||
notify(event: BlobStorageAddedEvent<T>): void {
|
||||
this.emitAdded(event);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user