refactor: 프론트엔드 리펙토링
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import type {
|
||||
IndexedDbRuntimeDependencies,
|
||||
} from "../../src/adapters/storage/indexeddb/indexeddb-types.ts";
|
||||
import type { LocalDraft } from "../../src/features/local-draft-feature/domain/local-draft.ts";
|
||||
import {
|
||||
expect,
|
||||
test,
|
||||
} from "../support/browser/strict-browser-test.ts";
|
||||
|
||||
test("composes the local-draft feature over the real browser IndexedDB runtime", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.route("**/favicon.ico", (route) =>
|
||||
route.fulfill({ status: 204 }),
|
||||
);
|
||||
await page.goto("/config.json");
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const indexedDbModulePath =
|
||||
"/src/adapters/storage/indexeddb/index.ts";
|
||||
const contributionModulePath =
|
||||
"/src/features/feature-adapter-contribution.ts";
|
||||
const localDraftModulePath =
|
||||
"/src/features/local-draft-feature/adapters/create-local-draft-feature-input.ts";
|
||||
|
||||
const {
|
||||
createIndexedDbRuntime,
|
||||
indexedDbPhysicalDatabaseName,
|
||||
} = (await import(
|
||||
/* @vite-ignore */ indexedDbModulePath
|
||||
)) as typeof import("../../src/adapters/storage/indexeddb/index.ts");
|
||||
const {
|
||||
composeFeatureAdapterInputs,
|
||||
createIndexedDbRepositoryProvider,
|
||||
} = (await import(
|
||||
/* @vite-ignore */ contributionModulePath
|
||||
)) as typeof import("../../src/features/feature-adapter-contribution.ts");
|
||||
const {
|
||||
LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION,
|
||||
} = (await import(
|
||||
/* @vite-ignore */ localDraftModulePath
|
||||
)) as typeof import("../../src/features/local-draft-feature/adapters/create-local-draft-feature-input.ts");
|
||||
|
||||
const suffix = crypto.randomUUID().replaceAll("-", "");
|
||||
const scope = Object.freeze({
|
||||
authorityToken: `a${suffix.slice(0, 31)}`,
|
||||
namespaceToken: `n${suffix.slice(1, 32)}`,
|
||||
partitionToken: `p${[...suffix].reverse().join("").slice(0, 31)}`,
|
||||
accountScope: "ORIGIN_SHARED" as const,
|
||||
});
|
||||
const storagePolicy = Object.freeze({
|
||||
owner: "local-draft-feature",
|
||||
namespace: "local-draft",
|
||||
classification: "INTERNAL" as const,
|
||||
authority: "LOCAL_FIRST" as const,
|
||||
accountScope: "ORIGIN_SHARED" as const,
|
||||
retention: Object.freeze({ kind: "EXPLICIT_DELETE" as const }),
|
||||
softBudgetBytes: 1_000_000,
|
||||
hardBudgetBytes: 2_000_000,
|
||||
evictionPriority: "USER_AUTHORED" as const,
|
||||
logoutAction: "KEEP_ORIGIN_SHARED" as const,
|
||||
accountDeletionAction: "KEEP_ORIGIN_SHARED" as const,
|
||||
pressureAction: "RETAIN" as const,
|
||||
unavailableFallback: "READ_ONLY" as const,
|
||||
});
|
||||
|
||||
const config: IndexedDbRuntimeDependencies<
|
||||
LocalDraft,
|
||||
LocalDraft,
|
||||
never
|
||||
> = {
|
||||
scope,
|
||||
storagePolicy,
|
||||
schemaVersion: 1,
|
||||
recordStore: "records",
|
||||
governanceStore: "governance",
|
||||
retentionStore: "retention",
|
||||
retentionEligibilityIndex: "by-eligibility",
|
||||
lifecycleMetadataStores: [],
|
||||
idempotencyStore: "receipts",
|
||||
idempotencyExpiryIndex: "by-expiry",
|
||||
receiptRetentionMs: 60_000,
|
||||
maxIdempotencyReceipts: 100,
|
||||
migrations: [
|
||||
{
|
||||
id: "local-draft-schema-v1",
|
||||
fromVersion: 0,
|
||||
toVersion: 1,
|
||||
operations: [
|
||||
{
|
||||
kind: "CREATE_STORE",
|
||||
name: "governance",
|
||||
keyPath: "bindingKey",
|
||||
},
|
||||
{
|
||||
kind: "CREATE_STORE",
|
||||
name: "retention",
|
||||
keyPath: "recordKey",
|
||||
indexes: [
|
||||
{
|
||||
name: "by-eligibility",
|
||||
keyPath: "eligibleAtEpochMs",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
kind: "CREATE_STORE",
|
||||
name: "records",
|
||||
keyPath: "key",
|
||||
},
|
||||
{
|
||||
kind: "CREATE_STORE",
|
||||
name: "receipts",
|
||||
keyPath: "idempotencyKey",
|
||||
indexes: [
|
||||
{
|
||||
name: "by-expiry",
|
||||
keyPath: "expiresAtEpochMs",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
codec: {
|
||||
currentVersion: 1,
|
||||
encode: (value) => ({ ok: true, value }),
|
||||
measureStoredBytes: (value) =>
|
||||
new TextEncoder().encode(JSON.stringify(value)).byteLength,
|
||||
decode: (version, value) => {
|
||||
if (
|
||||
version !== 1 ||
|
||||
value === null ||
|
||||
typeof value !== "object"
|
||||
) {
|
||||
return { ok: false };
|
||||
}
|
||||
const candidate = value as Partial<LocalDraft>;
|
||||
return typeof candidate.draftId === "string" &&
|
||||
typeof candidate.title === "string" &&
|
||||
typeof candidate.body === "string"
|
||||
? { ok: true, value: candidate as LocalDraft }
|
||||
: { ok: false };
|
||||
},
|
||||
fingerprint: async (value) => {
|
||||
const bytes = new TextEncoder().encode(
|
||||
JSON.stringify([value.draftId, value.title, value.body]),
|
||||
);
|
||||
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
||||
return Array.from(
|
||||
new Uint8Array(digest),
|
||||
(byte) => byte.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
},
|
||||
},
|
||||
queryPolicy: {
|
||||
plan: () => ({ limit: 1 }),
|
||||
},
|
||||
authorizeLifecycle: () => ({
|
||||
authorized: true,
|
||||
proofToken: "localdraftauthorityproof_001",
|
||||
}),
|
||||
};
|
||||
|
||||
const runtime = createIndexedDbRuntime(config);
|
||||
const databaseName = indexedDbPhysicalDatabaseName(scope);
|
||||
const opened = await runtime.open();
|
||||
|
||||
try {
|
||||
const indexedDb = createIndexedDbRepositoryProvider(
|
||||
Object.freeze({ "local-draft": runtime }),
|
||||
);
|
||||
const composed = composeFeatureAdapterInputs(
|
||||
Object.freeze([LOCAL_DRAFT_FEATURE_ADAPTER_CONTRIBUTION] as const),
|
||||
Object.freeze(["local-draft"]),
|
||||
Object.freeze({ indexedDb }),
|
||||
);
|
||||
const localDraft = composed["local-draft"];
|
||||
if (!localDraft) {
|
||||
throw new Error("local-draft feature input was not composed");
|
||||
}
|
||||
|
||||
const draft = Object.freeze({
|
||||
draftId: "draft-1",
|
||||
title: "Architecture notes",
|
||||
body: "Stored through the reusable IndexedDB capability.",
|
||||
});
|
||||
|
||||
const saved = await localDraft.saveDraft({
|
||||
draft,
|
||||
expectedRevision: null,
|
||||
idempotencyKey: "local-draft-create-1",
|
||||
});
|
||||
const found = await localDraft.findDraft(draft.draftId);
|
||||
const removed = await localDraft.removeDraft({
|
||||
draftId: draft.draftId,
|
||||
expectedRevision: saved.ok ? saved.value.revision : 1,
|
||||
idempotencyKey: "local-draft-remove-1",
|
||||
});
|
||||
const afterRemove = await localDraft.findDraft(draft.draftId);
|
||||
|
||||
return {
|
||||
opened,
|
||||
saved,
|
||||
found,
|
||||
removed,
|
||||
afterRemove,
|
||||
composedFeatureIds: Object.keys(composed),
|
||||
runtimeStatus: runtime.getStatus(),
|
||||
};
|
||||
} finally {
|
||||
runtime.close();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase(databaseName);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () =>
|
||||
reject(request.error ?? new Error("IndexedDB cleanup failed"));
|
||||
request.onblocked = () =>
|
||||
reject(new Error("IndexedDB cleanup was blocked"));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
expect(result.opened).toEqual({ ok: true, value: undefined });
|
||||
expect(result.composedFeatureIds).toEqual(["local-draft"]);
|
||||
expect(result.saved).toEqual({ ok: true, value: { revision: 1 } });
|
||||
expect(result.found).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
draft: {
|
||||
draftId: "draft-1",
|
||||
title: "Architecture notes",
|
||||
body: "Stored through the reusable IndexedDB capability.",
|
||||
},
|
||||
revision: 1,
|
||||
},
|
||||
});
|
||||
expect(result.removed).toEqual({ ok: true, value: undefined });
|
||||
expect(result.afterRemove).toEqual({ ok: true, value: null });
|
||||
expect(result.runtimeStatus).toMatchObject({
|
||||
kind: "READY",
|
||||
schemaVersion: 1,
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user