chore: initialize from frontend template 4dc033c

This commit is contained in:
DongHyeonka
2026-08-13 18:23:26 +09:00
commit 40107eec84
897 changed files with 234824 additions and 0 deletions
@@ -0,0 +1,391 @@
import { createFailure } from "../../contracts/errors.ts";
import {
getStorageDefinition,
isStorageValueAllowed,
type StorageDefinition,
} from "../../contracts/storage-keys.ts";
import type { DiagnosticsPort } from "../../application/ports/diagnostics-port.ts";
import type {
StorageMutationResult,
StoragePort,
} from "../../application/ports/storage-port.ts";
import {
assertValidBrowserStorageByteLimit,
decodeBrowserStorageEnvelope,
DEFAULT_BROWSER_STORAGE_MAX_SERIALIZED_BYTES,
encodeBrowserStorageEnvelope,
type BrowserStorageCodecFailure,
} from "./browser-storage-codec.ts";
export type BrowserStorageDependencies = Readonly<{
localStorage?: Storage;
sessionStorage?: Storage;
now?: () => number;
diagnostics?: DiagnosticsPort;
maxSerializedBytes?: number;
resolveDefinition?: (logicalName: string) => StorageDefinition;
}>;
type StorageFailureCause =
| "QUOTA_EXCEEDED"
| "SIZE_LIMIT_EXCEEDED"
| "UNAVAILABLE"
| "VALUE_REJECTED";
export function createBrowserStorageAdapter(
dependencies: BrowserStorageDependencies = {},
): StoragePort {
const memoryOverlay = new Map<string, string>();
const suppressedPersistentValues = new Set<string>();
const now = dependencies.now ?? Date.now;
const maxSerializedBytes =
dependencies.maxSerializedBytes ??
DEFAULT_BROWSER_STORAGE_MAX_SERIALIZED_BYTES;
const resolveDefinition =
dependencies.resolveDefinition ?? getStorageDefinition;
assertValidBrowserStorageByteLimit(maxSerializedBytes);
function backendFor(name: string): Storage | undefined {
if (name === "localStorage") return dependencies.localStorage;
if (name === "sessionStorage") return dependencies.sessionStorage;
return undefined;
}
function definitionFor(
logicalName: string,
phase: string,
):
| Readonly<{ ok: true; value: StorageDefinition }>
| Extract<StorageMutationResult, { ok: false }> {
try {
return { ok: true, value: resolveDefinition(logicalName) };
} catch {
return unavailable(phase, logicalName, dependencies.diagnostics);
}
}
function currentTime(
phase: string,
logicalName: string,
):
| Readonly<{ ok: true; value: number }>
| Extract<StorageMutationResult, { ok: false }> {
try {
const value = now();
if (!Number.isSafeInteger(value) || value < 0) {
throw new TypeError("Invalid storage clock.");
}
return { ok: true, value };
} catch {
return unavailable(phase, logicalName, dependencies.diagnostics);
}
}
function discardRecord(
definition: StorageDefinition,
backend: Storage | undefined,
): void {
memoryOverlay.delete(definition.physicalKey);
suppressedPersistentValues.add(definition.physicalKey);
if (!backend) {
suppressedPersistentValues.delete(definition.physicalKey);
return;
}
try {
backend.removeItem(definition.physicalKey);
suppressedPersistentValues.delete(definition.physicalKey);
} catch {
// Keep the in-memory tombstone so the rejected value is not parsed again.
}
}
function readEnvelope(
raw: string,
definition: StorageDefinition,
backend: Storage | undefined,
logicalName: string,
) {
const decoded = decodeBrowserStorageEnvelope(raw, maxSerializedBytes);
if (!decoded.ok) {
recordStorageFailure(
dependencies.diagnostics,
"discard",
logicalName,
codecFailureCause(decoded.reason),
);
discardRecord(definition, backend);
return { ok: true as const, value: undefined };
}
const envelope = decoded.value;
const expectsExpiry = typeof definition.ttl === "number";
if (
envelope.schemaVersion !== definition.schemaVersion ||
expectsExpiry !== (envelope.expiresAt !== null) ||
!isStorageValueAllowed(definition, envelope.value)
) {
recordStorageFailure(
dependencies.diagnostics,
"discard",
logicalName,
"VALUE_REJECTED",
);
discardRecord(definition, backend);
return { ok: true as const, value: undefined };
}
if (envelope.expiresAt !== null) {
const timestamp = currentTime("read", logicalName);
if (!timestamp.ok) return timestamp;
if (envelope.expiresAt <= timestamp.value) {
discardRecord(definition, backend);
return { ok: true as const, value: undefined };
}
}
return { ok: true as const, value: envelope.value };
}
return Object.freeze({
read(logicalName) {
const selected = definitionFor(logicalName, "read");
if (!selected.ok) return selected;
const definition = selected.value;
const backend = backendFor(definition.backend);
const overlay = memoryOverlay.get(definition.physicalKey);
if (overlay !== undefined) {
return readEnvelope(
overlay,
definition,
backend,
logicalName,
);
}
if (suppressedPersistentValues.has(definition.physicalKey)) {
return { ok: true, value: undefined };
}
try {
const raw = backend?.getItem(definition.physicalKey);
if (raw === null || raw === undefined) {
return { ok: true, value: undefined };
}
return readEnvelope(raw, definition, backend, logicalName);
} catch {
return unavailable("read", logicalName, dependencies.diagnostics);
}
},
write(logicalName, value) {
const selected = definitionFor(logicalName, "write");
if (!selected.ok) return selected;
const definition = selected.value;
if (!isStorageValueAllowed(definition, value)) {
recordStorageFailure(
dependencies.diagnostics,
"write",
logicalName,
"VALUE_REJECTED",
);
return {
ok: false,
error: storageFailure(
"VALUE_REJECTED",
"write",
logicalName,
),
};
}
let expiresAt: number | null = null;
if (typeof definition.ttl === "number") {
const timestamp = currentTime("write", logicalName);
if (!timestamp.ok) return timestamp;
const expiration = timestamp.value + definition.ttl;
if (!Number.isSafeInteger(expiration)) {
return unavailable(
"write",
logicalName,
dependencies.diagnostics,
);
}
expiresAt = expiration;
}
const encoded = encodeBrowserStorageEnvelope(
{
schemaVersion: definition.schemaVersion,
expiresAt,
value,
},
maxSerializedBytes,
);
if (!encoded.ok) {
const cause = codecFailureCause(encoded.reason);
recordStorageFailure(
dependencies.diagnostics,
"write",
logicalName,
cause,
);
return {
ok: false,
error: storageFailure(cause, "write", logicalName),
};
}
if (definition.backend === "memory") {
memoryOverlay.set(definition.physicalKey, encoded.value);
suppressedPersistentValues.delete(definition.physicalKey);
return { ok: true };
}
const backend = backendFor(definition.backend);
try {
if (!backend) {
throw new DOMException("Storage unavailable", "SecurityError");
}
backend.setItem(definition.physicalKey, encoded.value);
memoryOverlay.delete(definition.physicalKey);
suppressedPersistentValues.delete(definition.physicalKey);
return { ok: true };
} catch (error) {
const cause: StorageFailureCause = isQuotaError(error)
? "QUOTA_EXCEEDED"
: "UNAVAILABLE";
if (definition.quotaFallback === "memory") {
memoryOverlay.set(definition.physicalKey, encoded.value);
suppressedPersistentValues.delete(definition.physicalKey);
recordStorageFailure(
dependencies.diagnostics,
"write",
logicalName,
cause,
);
return {
ok: false,
error: storageFailure(cause, "write", logicalName),
fallback: "memory",
};
}
recordStorageFailure(
dependencies.diagnostics,
"write",
logicalName,
cause,
);
return {
ok: false,
error: storageFailure(cause, "write", logicalName),
fallback: definition.quotaFallback,
};
}
},
remove(logicalName) {
const selected = definitionFor(logicalName, "remove");
if (!selected.ok) return selected;
const definition = selected.value;
const backend = backendFor(definition.backend);
memoryOverlay.delete(definition.physicalKey);
suppressedPersistentValues.add(definition.physicalKey);
try {
backend?.removeItem(definition.physicalKey);
suppressedPersistentValues.delete(definition.physicalKey);
return { ok: true };
} catch {
recordStorageFailure(
dependencies.diagnostics,
"remove",
logicalName,
"UNAVAILABLE",
);
return {
ok: false,
error: storageFailure("UNAVAILABLE", "remove", logicalName),
};
}
},
});
}
function codecFailureCause(
reason: BrowserStorageCodecFailure,
): StorageFailureCause {
return reason === "OVERSIZE"
? "SIZE_LIMIT_EXCEEDED"
: "VALUE_REJECTED";
}
function isQuotaError(error: unknown): boolean {
try {
if (!error || typeof error !== "object") return false;
const name = (error as Readonly<{ name?: unknown }>).name;
return (
typeof name === "string" &&
["QuotaExceededError", "NS_ERROR_DOM_QUOTA_REACHED"].includes(name)
);
} catch {
return false;
}
}
function storageFailure(
cause: StorageFailureCause,
phase: string,
logicalName: string,
) {
const quota = cause === "QUOTA_EXCEEDED";
return createFailure(
quota ? "STORAGE_QUOTA_EXCEEDED" : "STORAGE_UNAVAILABLE",
"STORAGE",
0,
{
code: `${safeLogicalName(logicalName)}_${phase.toUpperCase()}_${cause}`,
},
);
}
function unavailable(
phase: string,
logicalName: string,
diagnostics: DiagnosticsPort | undefined,
): Extract<StorageMutationResult, { ok: false }> {
recordStorageFailure(
diagnostics,
phase,
logicalName,
"UNAVAILABLE",
);
return {
ok: false,
error: storageFailure("UNAVAILABLE", phase, logicalName),
};
}
function recordStorageFailure(
diagnostics: DiagnosticsPort | undefined,
phase: string,
logicalName: string,
cause: StorageFailureCause,
): void {
try {
diagnostics?.record({
level: "warn",
eventId: "storage.operation.failed",
context: {
operation: `${phase}:${safeLogicalName(logicalName)}`,
error_kind:
cause === "QUOTA_EXCEEDED"
? "STORAGE_QUOTA_EXCEEDED"
: "STORAGE_UNAVAILABLE",
},
});
} catch {
// Storage behavior remains independent from diagnostics.
}
}
function safeLogicalName(logicalName: string): string {
return /^[A-Z][A-Z0-9_]{0,63}$/u.test(logicalName)
? logicalName
: "UNKNOWN_KEY";
}