refactor: 프론트엔드 리펙토링

This commit is contained in:
donghyeon-ka
2026-09-18 22:05:42 +09:00
parent 5cc41467ae
commit ec7f20e2ee
100 changed files with 6005 additions and 2867 deletions
@@ -18,13 +18,13 @@ function databaseName(_prefix: string): string {
return `ca-idb-v1:a${token.slice(0, 31)}.n${token.slice(1)}.p${token.split("").reverse().join("").slice(0, 31)}`;
}
async function installLifecycleRuntime(
async function startLifecycleRuntime(
page: Page,
name: string,
targetVersion: 1 | 2,
blockedTimeoutMs = 1_000,
) {
return await page.evaluate(
): Promise<void> {
await page.evaluate(
async ({
currentDatabaseName,
currentTargetVersion,
@@ -173,8 +173,12 @@ async function installLifecycleRuntime(
__indexedDbLifecycleRuntime?: unknown;
}
).__indexedDbLifecycleRuntime = runtime;
const opened = await runtime.open();
return { opened, status: runtime.getStatus() };
const opening = runtime.open();
(
globalThis as unknown as {
__indexedDbLifecycleOpening?: Promise<unknown>;
}
).__indexedDbLifecycleOpening = opening;
},
{
currentDatabaseName: name,
@@ -184,6 +188,35 @@ async function installLifecycleRuntime(
);
}
async function settleLifecycleRuntime(page: Page): Promise<unknown> {
return await page.evaluate(async () => {
const state = globalThis as unknown as {
__indexedDbLifecycleRuntime?: {
getStatus(): unknown;
};
__indexedDbLifecycleOpening?: Promise<unknown>;
};
if (!state.__indexedDbLifecycleRuntime || !state.__indexedDbLifecycleOpening) {
throw new Error("IndexedDB lifecycle runtime is not installed.");
}
const opened = await state.__indexedDbLifecycleOpening;
return {
opened,
status: state.__indexedDbLifecycleRuntime.getStatus(),
};
});
}
async function installLifecycleRuntime(
page: Page,
name: string,
targetVersion: 1 | 2,
blockedTimeoutMs = 1_000,
): Promise<unknown> {
await startLifecycleRuntime(page, name, targetVersion, blockedTimeoutMs);
return await settleLifecycleRuntime(page);
}
async function lifecycleStatus(page: Page): Promise<unknown> {
return await page.evaluate(() => {
const runtime = (
@@ -977,12 +1010,16 @@ test("fails a blocked v2 upgrade closed, then closes its late connection after t
);
expect(blocker).toBe("OPEN");
const blocked = await installLifecycleRuntime(
page,
name,
2,
25,
);
await startLifecycleRuntime(page, name, 2, 500);
await expect
.poll(async () => await lifecycleStatus(page))
.toEqual({
kind: "BLOCKED",
currentVersion: 1,
targetVersion: 2,
});
const blocked = await settleLifecycleRuntime(page);
expect(blocked).toMatchObject({
opened: {
ok: false,
@@ -992,9 +1029,8 @@ test("fails a blocked v2 upgrade closed, then closes its late connection after t
},
},
status: {
kind: "BLOCKED",
currentVersion: 1,
targetVersion: 2,
kind: "CLOSED",
reason: "NOT_OPENED",
},
});
@@ -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,
});
});
@@ -1,29 +1,48 @@
import { expect, test } from "../support/browser/strict-browser-test.ts";
test("executes the storage durability adapter against the real browser StorageManager", async ({
test("executes the storage durability adapter against the real browser StorageManager capability", async ({
page,
}) => {
await page.goto("/");
const result = await page.evaluate(async () => {
const observation = await page.evaluate(async () => {
const modulePath =
"/src/adapters/browser-file-storage/storage-manager-adapter.ts";
const { createStorageDurabilityAdapter } = await import(
/* @vite-ignore */ modulePath
);
const hasStorageManager = navigator.storage !== undefined;
const adapter = createStorageDurabilityAdapter(navigator.storage);
return adapter.inspect();
return {
hasStorageManager,
result: await adapter.inspect(),
};
});
expect(result.ok).toBe(true);
if (result.ok) {
if (!observation.hasStorageManager) {
expect(observation.result).toEqual({
ok: false,
error: {
code: "UNSUPPORTED",
operation: "STORAGE_ESTIMATE",
retryable: false,
recovery: "ONLINE_ONLY",
},
});
return;
}
expect(observation.result.ok).toBe(true);
if (observation.result.ok) {
expect(["UNKNOWN", "NORMAL", "PRESSURE", "CRITICAL"]).toContain(
result.value.pressure,
observation.result.value.pressure,
);
expect(
result.value.usageBytes === null || result.value.usageBytes >= 0,
observation.result.value.usageBytes === null ||
observation.result.value.usageBytes >= 0,
).toBe(true);
expect(
result.value.quotaBytes === null || result.value.quotaBytes >= 0,
observation.result.value.quotaBytes === null ||
observation.result.value.quotaBytes >= 0,
).toBe(true);
}
});