162 lines
4.4 KiB
TypeScript
162 lines
4.4 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import type {
|
|
BrowserDataResult,
|
|
IndexedDbRepositoryPort,
|
|
} from "../../../src/application/ports/browser-file-storage/index.ts";
|
|
import {
|
|
createLocalDraftStore,
|
|
type LocalDraft,
|
|
} from "./fixtures/local-draft-feature.ts";
|
|
|
|
function success<Value>(value: Value): BrowserDataResult<Value> {
|
|
return Object.freeze({ ok: true as const, value });
|
|
}
|
|
|
|
function repositoryFixture(): IndexedDbRepositoryPort<LocalDraft, never> {
|
|
let stored: Readonly<{ value: LocalDraft; revision: number }> | null = null;
|
|
|
|
const repository: IndexedDbRepositoryPort<LocalDraft, never> = {
|
|
async open() {
|
|
return success(undefined);
|
|
},
|
|
async read(key) {
|
|
if (stored === null || stored.value.draftId !== key) {
|
|
return success(null);
|
|
}
|
|
return success(stored);
|
|
},
|
|
async query() {
|
|
return success(Object.freeze({ items: [], nextCursor: null }));
|
|
},
|
|
async compareAndSwap(input) {
|
|
const currentRevision = stored?.revision ?? null;
|
|
if (currentRevision !== input.expectedRevision) {
|
|
return Object.freeze({
|
|
ok: false as const,
|
|
error: Object.freeze({
|
|
code: "CONFLICT" as const,
|
|
operation: "INDEXEDDB_WRITE" as const,
|
|
retryable: false,
|
|
recovery: "RECONCILE" as const,
|
|
}),
|
|
});
|
|
}
|
|
const revision = (currentRevision ?? 0) + 1;
|
|
stored = Object.freeze({ value: input.value, revision });
|
|
return success(
|
|
Object.freeze({
|
|
key: input.key,
|
|
revision,
|
|
replayed: false,
|
|
}),
|
|
);
|
|
},
|
|
async remove(input) {
|
|
if (stored === null || stored.revision !== input.expectedRevision) {
|
|
return Object.freeze({
|
|
ok: false as const,
|
|
error: Object.freeze({
|
|
code: "CONFLICT" as const,
|
|
operation: "INDEXEDDB_WRITE" as const,
|
|
retryable: false,
|
|
recovery: "RECONCILE" as const,
|
|
}),
|
|
});
|
|
}
|
|
stored = null;
|
|
return success(
|
|
Object.freeze({
|
|
key: input.key,
|
|
revision: input.expectedRevision + 1,
|
|
replayed: false,
|
|
}),
|
|
);
|
|
},
|
|
async enforceLifecycleBatch() {
|
|
stored = null;
|
|
return success(
|
|
Object.freeze({
|
|
state: "COMPLETE" as const,
|
|
scannedRows: 0,
|
|
deletedRows: 0,
|
|
budgetExhausted: false,
|
|
}),
|
|
);
|
|
},
|
|
getStatus() {
|
|
return Object.freeze({ kind: "READY" as const, schemaVersion: 1 });
|
|
},
|
|
subscribeStatus() {
|
|
return () => {};
|
|
},
|
|
close() {},
|
|
};
|
|
return Object.freeze(repository);
|
|
}
|
|
|
|
describe("IndexedDB local-draft consumer experience", () => {
|
|
it("implements save/find/remove through the public application port", async () => {
|
|
const store = createLocalDraftStore(repositoryFixture());
|
|
const draft = Object.freeze({
|
|
draftId: "draft-1",
|
|
title: "Architecture notes",
|
|
body: "Feature code owns the draft model.",
|
|
});
|
|
|
|
await expect(
|
|
store.save({
|
|
draft,
|
|
expectedRevision: null,
|
|
idempotencyKey: "draft-save-0001",
|
|
}),
|
|
).resolves.toEqual({ ok: true, value: { revision: 1 } });
|
|
|
|
await expect(store.find("draft-1")).resolves.toEqual({
|
|
ok: true,
|
|
value: { draft, revision: 1 },
|
|
});
|
|
|
|
await expect(
|
|
store.remove({
|
|
draftId: "draft-1",
|
|
expectedRevision: 1,
|
|
idempotencyKey: "draft-remove-0001",
|
|
}),
|
|
).resolves.toEqual({ ok: true, value: undefined });
|
|
|
|
await expect(store.find("draft-1")).resolves.toEqual({
|
|
ok: true,
|
|
value: null,
|
|
});
|
|
});
|
|
|
|
it("keeps native IndexedDB and runtime internals out of feature-owned code", async () => {
|
|
const source = await readFile(
|
|
new URL("./fixtures/local-draft-feature.ts", import.meta.url),
|
|
"utf8",
|
|
);
|
|
|
|
const importLines = source
|
|
.split("\n")
|
|
.filter((line) => line.startsWith("import "));
|
|
expect(importLines).toHaveLength(1);
|
|
expect(source).toContain(
|
|
"src/application/ports/browser-file-storage/index.ts",
|
|
);
|
|
|
|
for (const forbidden of [
|
|
"src/adapters/storage/indexeddb",
|
|
"globalThis.indexedDB",
|
|
"IDBFactory",
|
|
"IDBDatabase",
|
|
"IDBTransaction",
|
|
"IDBObjectStore",
|
|
]) {
|
|
expect(source).not.toContain(forbidden);
|
|
}
|
|
});
|
|
});
|