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
+186
View File
@@ -0,0 +1,186 @@
/**
* §20.4. The single owner of window lifecycle listeners.
*
* No capability adds its own `visibilitychange`, `online`, `offline`, `focus`,
* `pagehide` or `pageshow` listener. They subscribe here instead, so the
* listener count stays constant and leak inspection (§23.14) is meaningful.
*
* §20.6: nothing in this module is a correctness boundary. `beforeunload` is a
* user prompt, never a place to complete a command, write a checkpoint or
* guarantee a lease release.
*/
export type BrowserLifecycleSnapshot = Readonly<{
visibility: "VISIBLE" | "HIDDEN";
connectivityHint: "ONLINE" | "OFFLINE";
pageState: "ACTIVE" | "PAGEHIDE" | "BFCACHE_RESTORED";
generation: number;
}>;
export type BrowserLifecycleEvent =
| Readonly<{ kind: "VISIBILITY_CHANGED"; snapshot: BrowserLifecycleSnapshot }>
| Readonly<{ kind: "ONLINE"; snapshot: BrowserLifecycleSnapshot }>
| Readonly<{ kind: "OFFLINE"; snapshot: BrowserLifecycleSnapshot }>
| Readonly<{ kind: "FOCUS"; snapshot: BrowserLifecycleSnapshot }>
| Readonly<{
kind: "PAGEHIDE";
persisted: boolean;
snapshot: BrowserLifecycleSnapshot;
}>
| Readonly<{
kind: "PAGESHOW";
persisted: boolean;
snapshot: BrowserLifecycleSnapshot;
}>;
export type BrowserLifecycleRuntime = Readonly<{
getSnapshot(): BrowserLifecycleSnapshot;
subscribe(listener: (event: BrowserLifecycleEvent) => void): () => void;
/**
* Registers a dirty-state source. `beforeunload` is attached only while at
* least one source reports dirty, and it uses the browser's standard prompt.
*/
registerDirtySource(isDirty: () => boolean): () => void;
dispose(): void;
}>;
type LifecycleHost = Readonly<{
addEventListener: Window["addEventListener"];
removeEventListener: Window["removeEventListener"];
document?: Pick<Document, "visibilityState"> & {
addEventListener: Document["addEventListener"];
removeEventListener: Document["removeEventListener"];
};
navigator?: Pick<Navigator, "onLine">;
}>;
export function createBrowserLifecycleRuntime(
host: LifecycleHost = globalThis as unknown as LifecycleHost,
): BrowserLifecycleRuntime {
const listeners = new Set<(event: BrowserLifecycleEvent) => void>();
const dirtySources = new Set<() => boolean>();
const document = host.document;
let generation = 1;
let visibility: BrowserLifecycleSnapshot["visibility"] =
document?.visibilityState === "hidden" ? "HIDDEN" : "VISIBLE";
let connectivityHint: BrowserLifecycleSnapshot["connectivityHint"] =
host.navigator?.onLine === false ? "OFFLINE" : "ONLINE";
let pageState: BrowserLifecycleSnapshot["pageState"] = "ACTIVE";
let disposed = false;
let beforeUnloadAttached = false;
function snapshot(): BrowserLifecycleSnapshot {
return Object.freeze({
visibility,
connectivityHint,
pageState,
generation,
});
}
function publish(event: BrowserLifecycleEvent): void {
for (const listener of listeners) {
try {
listener(event);
} catch {
// One subscriber defect cannot suppress the signal for the others.
}
}
}
const onVisibility = () => {
visibility = document?.visibilityState === "hidden" ? "HIDDEN" : "VISIBLE";
publish({ kind: "VISIBILITY_CHANGED", snapshot: snapshot() });
};
const onOnline = () => {
connectivityHint = "ONLINE";
publish({ kind: "ONLINE", snapshot: snapshot() });
};
const onOffline = () => {
connectivityHint = "OFFLINE";
publish({ kind: "OFFLINE", snapshot: snapshot() });
};
const onFocus = () => {
publish({ kind: "FOCUS", snapshot: snapshot() });
};
const onPageHide = (event: Event) => {
const persisted = (event as PageTransitionEvent).persisted === true;
pageState = "PAGEHIDE";
publish({ kind: "PAGEHIDE", persisted, snapshot: snapshot() });
};
const onPageShow = (event: Event) => {
const persisted = (event as PageTransitionEvent).persisted === true;
// §20.5. A bfcache restore is a new lifecycle generation, not a fresh boot.
if (persisted) generation += 1;
pageState = persisted ? "BFCACHE_RESTORED" : "ACTIVE";
publish({ kind: "PAGESHOW", persisted, snapshot: snapshot() });
};
const onBeforeUnload = (event: Event) => {
if (!hasDirtyState()) return;
event.preventDefault();
};
function hasDirtyState(): boolean {
for (const isDirty of dirtySources) {
try {
if (isDirty()) return true;
} catch {
// A defective reporter is treated as clean rather than trapping the user.
}
}
return false;
}
function syncBeforeUnload(): void {
const shouldAttach = dirtySources.size > 0;
if (shouldAttach && !beforeUnloadAttached) {
host.addEventListener("beforeunload", onBeforeUnload);
beforeUnloadAttached = true;
} else if (!shouldAttach && beforeUnloadAttached) {
host.removeEventListener("beforeunload", onBeforeUnload);
beforeUnloadAttached = false;
}
}
document?.addEventListener("visibilitychange", onVisibility);
host.addEventListener("online", onOnline);
host.addEventListener("offline", onOffline);
host.addEventListener("focus", onFocus);
host.addEventListener("pagehide", onPageHide);
host.addEventListener("pageshow", onPageShow);
return Object.freeze({
getSnapshot: snapshot,
subscribe(listener) {
if (disposed) return () => {};
listeners.add(listener);
return () => listeners.delete(listener);
},
registerDirtySource(isDirty) {
if (disposed) return () => {};
dirtySources.add(isDirty);
syncBeforeUnload();
return () => {
dirtySources.delete(isDirty);
syncBeforeUnload();
};
},
dispose() {
if (disposed) return;
disposed = true;
document?.removeEventListener("visibilitychange", onVisibility);
host.removeEventListener("online", onOnline);
host.removeEventListener("offline", onOffline);
host.removeEventListener("focus", onFocus);
host.removeEventListener("pagehide", onPageHide);
host.removeEventListener("pageshow", onPageShow);
if (beforeUnloadAttached) {
host.removeEventListener("beforeunload", onBeforeUnload);
beforeUnloadAttached = false;
}
listeners.clear();
dirtySources.clear();
},
});
}
@@ -0,0 +1,50 @@
import type { MutationIntentFactory } from "../../application/ports/mutation-intent-factory.ts";
import { defineMutationIntent } from "../../contracts/mutation-intent.ts";
export type BrowserMutationIntentFactoryDependencies = Readonly<{
randomUUID?: () => string;
monotonicNow?: () => number;
}>;
export function createBrowserMutationIntentFactory(
dependencies: BrowserMutationIntentFactoryDependencies = {},
): MutationIntentFactory {
const randomUUID =
dependencies.randomUUID ??
(() => {
if (
typeof globalThis.crypto === "undefined" ||
typeof globalThis.crypto.randomUUID !== "function"
) {
throw new TypeError("Secure mutation identity generation is unavailable.");
}
return globalThis.crypto.randomUUID();
});
const monotonicNow =
dependencies.monotonicNow ??
(() => {
if (
typeof globalThis.performance === "undefined" ||
typeof globalThis.performance.now !== "function"
) {
throw new TypeError("Monotonic time is unavailable.");
}
return globalThis.performance.now();
});
return Object.freeze({
create(input) {
const intentId = randomUUID();
const idempotencyKey = input.requiresIdempotencyKey
? randomUUID()
: undefined;
return defineMutationIntent({
intentId,
operationId: input.operationId,
canonicalInputIdentity: input.canonicalInputIdentity,
...(idempotencyKey === undefined ? {} : { idempotencyKey }),
createdAtMonotonicMs: monotonicNow(),
});
},
});
}
+24
View File
@@ -0,0 +1,24 @@
import type { ClockPort } from "../../application/ports/clock-port.ts";
export const systemClock: ClockPort = Object.freeze({
now: () => Date.now(),
sleep(milliseconds, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, milliseconds);
const onAbort = () => {
clearTimeout(timer);
signal?.removeEventListener("abort", onAbort);
reject(signal?.reason);
};
signal?.addEventListener("abort", onAbort, { once: true });
});
},
});