543 lines
16 KiB
TypeScript
543 lines
16 KiB
TypeScript
import type {
|
|
AnalyticsErrorSink,
|
|
BrowserCapability,
|
|
BrowserPermissionPort,
|
|
CapabilityFailure,
|
|
CapabilityResult,
|
|
ClientWorkflowPort,
|
|
FeatureFlagPort,
|
|
FileTransferPort,
|
|
GeneratedApiFacade,
|
|
LargeDataUiFacade,
|
|
MultiTabEvent,
|
|
MultiTabPort,
|
|
OptionalCapabilityPorts,
|
|
PermissionDecision,
|
|
RealtimeEvent,
|
|
RealtimePort,
|
|
RealtimeSubscription,
|
|
SafeAnalyticsValue,
|
|
ServiceWorkerUpdatePort,
|
|
VersionedOfflineRepository,
|
|
WorkerTaskPort,
|
|
} from "./contracts.js";
|
|
|
|
export function success<T>(value: T): CapabilityResult<T> {
|
|
return Object.freeze({ ok: true, value });
|
|
}
|
|
|
|
export function failure(
|
|
code: CapabilityFailure["code"],
|
|
retryable = false,
|
|
safeMessage = "Optional capability is unavailable.",
|
|
): CapabilityResult<never> {
|
|
return Object.freeze({
|
|
ok: false,
|
|
failure: Object.freeze({ code, retryable, safeMessage }),
|
|
});
|
|
}
|
|
|
|
function aborted(signal?: AbortSignal): CapabilityResult<never> | null {
|
|
return signal?.aborted
|
|
? failure("ABORTED", false, "The operation was cancelled.")
|
|
: null;
|
|
}
|
|
|
|
export class FakeRealtimeAdapter<T> implements RealtimePort<T> {
|
|
readonly #subscriptions = new Map<
|
|
string,
|
|
{
|
|
lastSequence: number;
|
|
onEvent(event: CapabilityResult<RealtimeEvent<T>>): void;
|
|
}
|
|
>();
|
|
|
|
async subscribe(input: {
|
|
channel: string;
|
|
resumeToken?: string;
|
|
signal?: AbortSignal;
|
|
onEvent(event: CapabilityResult<RealtimeEvent<T>>): void;
|
|
}): Promise<CapabilityResult<RealtimeSubscription>> {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
const key = `${input.channel}:${this.#subscriptions.size + 1}`;
|
|
this.#subscriptions.set(key, { lastSequence: -1, onEvent: input.onEvent });
|
|
const unsubscribe = () => this.#subscriptions.delete(key);
|
|
input.signal?.addEventListener("abort", unsubscribe, { once: true });
|
|
return success(
|
|
Object.freeze({
|
|
resumeToken: input.resumeToken ?? null,
|
|
unsubscribe,
|
|
}),
|
|
);
|
|
}
|
|
|
|
async heartbeat(signal?: AbortSignal): Promise<CapabilityResult<void>> {
|
|
return aborted(signal) ?? success(undefined);
|
|
}
|
|
|
|
emit(channel: string, event: RealtimeEvent<T>): void {
|
|
for (const [key, subscription] of this.#subscriptions) {
|
|
if (!key.startsWith(`${channel}:`)) continue;
|
|
if (event.sequence <= subscription.lastSequence) {
|
|
subscription.onEvent(
|
|
failure(
|
|
"STALE_RESULT",
|
|
false,
|
|
"A duplicate or out-of-order event was ignored.",
|
|
),
|
|
);
|
|
continue;
|
|
}
|
|
subscription.lastSequence = event.sequence;
|
|
subscription.onEvent(success(event));
|
|
}
|
|
}
|
|
|
|
get activeSubscriptionCount(): number {
|
|
return this.#subscriptions.size;
|
|
}
|
|
}
|
|
|
|
export class MemoryOfflineRepository<T extends { id: string }>
|
|
implements VersionedOfflineRepository<T>
|
|
{
|
|
readonly #records = new Map<string, T>();
|
|
#openVersion: number | null = null;
|
|
|
|
async open(input: {
|
|
schemaVersion: number;
|
|
signal?: AbortSignal;
|
|
}): Promise<CapabilityResult<void>> {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
if (!Number.isInteger(input.schemaVersion) || input.schemaVersion < 1) {
|
|
return failure("CORRUPT_DATA", false, "Invalid offline schema version.");
|
|
}
|
|
this.#openVersion = input.schemaVersion;
|
|
return success(undefined);
|
|
}
|
|
|
|
async get(id: string, signal?: AbortSignal): Promise<CapabilityResult<T | null>> {
|
|
const cancelled = aborted(signal);
|
|
if (cancelled) return cancelled;
|
|
if (this.#openVersion === null) {
|
|
return failure("PROVIDER_UNAVAILABLE", false, "Repository is closed.");
|
|
}
|
|
return success(this.#records.get(id) ?? null);
|
|
}
|
|
|
|
async put(value: T, signal?: AbortSignal): Promise<CapabilityResult<void>> {
|
|
const cancelled = aborted(signal);
|
|
if (cancelled) return cancelled;
|
|
if (this.#openVersion === null) {
|
|
return failure("PROVIDER_UNAVAILABLE", false, "Repository is closed.");
|
|
}
|
|
this.#records.set(value.id, structuredClone(value));
|
|
return success(undefined);
|
|
}
|
|
|
|
async migrate(input: {
|
|
from: number;
|
|
to: number;
|
|
signal?: AbortSignal;
|
|
}): Promise<CapabilityResult<void>> {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
if (this.#openVersion !== input.from || input.to <= input.from) {
|
|
return failure("MIGRATION_FAILED", false, "Offline migration was rejected.");
|
|
}
|
|
this.#openVersion = input.to;
|
|
return success(undefined);
|
|
}
|
|
|
|
close(): void {
|
|
this.#openVersion = null;
|
|
}
|
|
}
|
|
|
|
export class FakeServiceWorkerUpdateAdapter implements ServiceWorkerUpdatePort {
|
|
#activeVersion: string | null;
|
|
#candidateVersion: string | null;
|
|
|
|
constructor(activeVersion: string | null, candidateVersion: string | null) {
|
|
this.#activeVersion = activeVersion;
|
|
this.#candidateVersion = candidateVersion;
|
|
}
|
|
|
|
async inspect(signal?: AbortSignal) {
|
|
return (
|
|
aborted(signal) ??
|
|
success({
|
|
updateAvailable: this.#candidateVersion !== null,
|
|
version: this.#candidateVersion,
|
|
})
|
|
);
|
|
}
|
|
|
|
async activate(version: string, signal?: AbortSignal) {
|
|
const cancelled = aborted(signal);
|
|
if (cancelled) return cancelled;
|
|
if (version !== this.#candidateVersion) {
|
|
return failure("STALE_RESULT", false, "Worker update is no longer current.");
|
|
}
|
|
this.#activeVersion = version;
|
|
this.#candidateVersion = null;
|
|
return success(undefined);
|
|
}
|
|
|
|
async rollback(signal?: AbortSignal) {
|
|
const cancelled = aborted(signal);
|
|
if (cancelled) return cancelled;
|
|
if (!this.#activeVersion) {
|
|
return failure("NOT_FOUND", false, "No active worker can be rolled back.");
|
|
}
|
|
this.#activeVersion = null;
|
|
return success(undefined);
|
|
}
|
|
|
|
async unregister() {
|
|
this.#activeVersion = null;
|
|
this.#candidateVersion = null;
|
|
return success(undefined);
|
|
}
|
|
}
|
|
|
|
export class FakeFileTransferAdapter implements FileTransferPort {
|
|
constructor(
|
|
private readonly maxBytes = 5_000_000,
|
|
private readonly acceptedTypes: ReadonlySet<string> = new Set([
|
|
"application/pdf",
|
|
"image/png",
|
|
]),
|
|
) {}
|
|
|
|
async upload(input: Parameters<FileTransferPort["upload"]>[0]) {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
if (
|
|
input.file.size > this.maxBytes ||
|
|
!this.acceptedTypes.has(input.file.type)
|
|
) {
|
|
return failure("LIMIT_EXCEEDED", false, "File size or type is not allowed.");
|
|
}
|
|
input.onProgress({
|
|
transferredBytes: input.file.size,
|
|
totalBytes: input.file.size,
|
|
});
|
|
return success({ resourceId: `fake:${input.file.name}` });
|
|
}
|
|
|
|
async download(input: Parameters<FileTransferPort["download"]>[0]) {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
if (input.resourceId.startsWith("expired:")) {
|
|
return failure("EXPIRED_RESOURCE", true, "The download link expired.");
|
|
}
|
|
const bytes = new TextEncoder().encode(input.resourceId);
|
|
input.onProgress({
|
|
transferredBytes: bytes.byteLength,
|
|
totalBytes: bytes.byteLength,
|
|
});
|
|
return success(bytes);
|
|
}
|
|
}
|
|
|
|
export class FakeGeneratedApiAdapter implements GeneratedApiFacade {
|
|
constructor(
|
|
private readonly contractVersion: string,
|
|
private readonly handlers: Readonly<
|
|
Record<string, (body: unknown) => unknown | Promise<unknown>>
|
|
>,
|
|
) {}
|
|
|
|
async execute<TOutput>(
|
|
input: Parameters<GeneratedApiFacade["execute"]>[0],
|
|
): Promise<CapabilityResult<TOutput>> {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
if (input.contractVersion !== this.contractVersion) {
|
|
return failure("CONTRACT_DRIFT", false, "API contract version is unsupported.");
|
|
}
|
|
const handler = this.handlers[input.operationId];
|
|
if (!handler) {
|
|
return failure("UNSUPPORTED", false, "API operation is unsupported.");
|
|
}
|
|
return success((await handler(input.body)) as TOutput);
|
|
}
|
|
}
|
|
|
|
export class FakeFeatureFlagAdapter<
|
|
TFlags extends Record<string, boolean | string | number>,
|
|
> implements FeatureFlagPort<TFlags>
|
|
{
|
|
constructor(
|
|
private readonly values: Readonly<Partial<TFlags>>,
|
|
private readonly available = true,
|
|
) {}
|
|
|
|
async evaluate<TKey extends keyof TFlags>(input: {
|
|
key: TKey;
|
|
fallback: TFlags[TKey];
|
|
maxAgeMs: number;
|
|
}): Promise<CapabilityResult<TFlags[TKey]>> {
|
|
if (!this.available) {
|
|
return failure("PROVIDER_UNAVAILABLE", true, "Flag provider is unavailable.");
|
|
}
|
|
const value = this.values[input.key];
|
|
return success((value ?? input.fallback) as TFlags[TKey]);
|
|
}
|
|
}
|
|
|
|
export class FakeWorkerTaskAdapter<TInput, TOutput>
|
|
implements WorkerTaskPort<TInput, TOutput>
|
|
{
|
|
readonly #cancelled = new Set<string>();
|
|
|
|
constructor(
|
|
private readonly handler: (input: TInput) => TOutput | Promise<TOutput>,
|
|
) {}
|
|
|
|
async run(input: {
|
|
taskId: string;
|
|
generation: number;
|
|
payload: TInput;
|
|
signal: AbortSignal;
|
|
}): Promise<CapabilityResult<TOutput>> {
|
|
if (input.signal.aborted || this.#cancelled.has(input.taskId)) {
|
|
return failure("ABORTED", false, "Worker task was cancelled.");
|
|
}
|
|
const output = await this.handler(input.payload);
|
|
if (input.signal.aborted || this.#cancelled.has(input.taskId)) {
|
|
return failure("STALE_RESULT", false, "Stale worker result was discarded.");
|
|
}
|
|
return success(output);
|
|
}
|
|
|
|
cancel(taskId: string): void {
|
|
this.#cancelled.add(taskId);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.#cancelled.clear();
|
|
}
|
|
}
|
|
|
|
export class FakeMultiTabAdapter<T> implements MultiTabPort<T> {
|
|
readonly #seen = new Set<string>();
|
|
readonly #listeners = new Set<{
|
|
sourceId: string;
|
|
onEvent(event: CapabilityResult<MultiTabEvent<T>>): void;
|
|
}>();
|
|
|
|
publish(event: MultiTabEvent<T>): CapabilityResult<void> {
|
|
if (this.#seen.has(event.eventId)) {
|
|
return failure("CONFLICT", false, "Duplicate multi-tab event was ignored.");
|
|
}
|
|
this.#seen.add(event.eventId);
|
|
for (const listener of this.#listeners) {
|
|
if (listener.sourceId !== event.sourceId) {
|
|
listener.onEvent(success(event));
|
|
}
|
|
}
|
|
return success(undefined);
|
|
}
|
|
|
|
subscribe(input: {
|
|
sourceId: string;
|
|
onEvent(event: CapabilityResult<MultiTabEvent<T>>): void;
|
|
}) {
|
|
this.#listeners.add(input);
|
|
return () => this.#listeners.delete(input);
|
|
}
|
|
|
|
close(): void {
|
|
this.#listeners.clear();
|
|
this.#seen.clear();
|
|
}
|
|
}
|
|
|
|
export class FakeBrowserPermissionAdapter implements BrowserPermissionPort {
|
|
constructor(
|
|
private readonly decisions: Readonly<
|
|
Partial<Record<BrowserCapability, PermissionDecision>>
|
|
>,
|
|
) {}
|
|
|
|
async request(input: {
|
|
capability: BrowserCapability;
|
|
signal?: AbortSignal;
|
|
}): Promise<CapabilityResult<PermissionDecision>> {
|
|
const cancelled = aborted(input.signal);
|
|
if (cancelled) return cancelled;
|
|
const decision = this.decisions[input.capability];
|
|
return decision
|
|
? success(decision)
|
|
: failure("UNSUPPORTED", false, "Browser capability is unsupported.");
|
|
}
|
|
}
|
|
|
|
export class FakeClientWorkflowAdapter<TState, TEvent>
|
|
implements ClientWorkflowPort<TState, TEvent>
|
|
{
|
|
readonly #initial: TState;
|
|
readonly #listeners = new Set<(state: Readonly<TState>) => void>();
|
|
#state: TState;
|
|
|
|
constructor(
|
|
initial: TState,
|
|
private readonly transition: (state: TState, event: TEvent) => TState,
|
|
) {
|
|
this.#initial = structuredClone(initial);
|
|
this.#state = structuredClone(initial);
|
|
}
|
|
|
|
snapshot(): Readonly<TState> {
|
|
return structuredClone(this.#state);
|
|
}
|
|
|
|
dispatch(event: TEvent): CapabilityResult<Readonly<TState>> {
|
|
this.#state = this.transition(this.#state, event);
|
|
const snapshot = this.snapshot();
|
|
this.#listeners.forEach((listener) => listener(snapshot));
|
|
return success(snapshot);
|
|
}
|
|
|
|
reset(): void {
|
|
this.#state = structuredClone(this.#initial);
|
|
const snapshot = this.snapshot();
|
|
this.#listeners.forEach((listener) => listener(snapshot));
|
|
}
|
|
|
|
subscribe(listener: (state: Readonly<TState>) => void) {
|
|
this.#listeners.add(listener);
|
|
return () => this.#listeners.delete(listener);
|
|
}
|
|
}
|
|
|
|
export class FakeLargeDataUiAdapter<TRow extends { id: string }>
|
|
implements LargeDataUiFacade<TRow>
|
|
{
|
|
#rows: ReadonlyArray<TRow> = [];
|
|
#generation = 0;
|
|
|
|
window(input: { offset: number; limit: number; generation: number }) {
|
|
if (input.generation !== this.#generation) {
|
|
return failure("STALE_RESULT", false, "Stale row window was discarded.");
|
|
}
|
|
if (input.offset < 0 || input.limit < 1) {
|
|
return failure("INVALID_INPUT", false, "Invalid row window.");
|
|
}
|
|
return success(this.#rows.slice(input.offset, input.offset + input.limit));
|
|
}
|
|
|
|
focus(rowId: string) {
|
|
return this.#rows.some((row) => row.id === rowId)
|
|
? success(undefined)
|
|
: failure("NOT_FOUND", false, "Row is no longer available.");
|
|
}
|
|
|
|
replace(rows: ReadonlyArray<TRow>, generation: number): void {
|
|
this.#rows = rows;
|
|
this.#generation = generation;
|
|
}
|
|
}
|
|
|
|
const sensitiveAttribute = /credential|authorization|cookie|password|secret|token/i;
|
|
|
|
export class RecordingAnalyticsAdapter implements AnalyticsErrorSink {
|
|
readonly records: Array<
|
|
Readonly<{
|
|
kind: "analytics" | "error";
|
|
eventId: string;
|
|
attributes: Readonly<Record<string, SafeAnalyticsValue>>;
|
|
}>
|
|
> = [];
|
|
|
|
constructor(private readonly capacity = 100) {}
|
|
|
|
record(input: Parameters<AnalyticsErrorSink["record"]>[0]) {
|
|
if (input.kind === "analytics" && input.consent !== "granted") {
|
|
return failure("CONSENT_DENIED", false, "Analytics consent was not granted.");
|
|
}
|
|
if (this.records.length >= this.capacity) {
|
|
return failure("LIMIT_EXCEEDED", true, "Analytics queue is full.");
|
|
}
|
|
const attributes = Object.fromEntries(
|
|
Object.entries(input.attributes).filter(([key]) => !sensitiveAttribute.test(key)),
|
|
);
|
|
this.records.push(
|
|
Object.freeze({ kind: input.kind, eventId: input.eventId, attributes }),
|
|
);
|
|
return success(undefined);
|
|
}
|
|
|
|
async flush(signal?: AbortSignal) {
|
|
return aborted(signal) ?? success(undefined);
|
|
}
|
|
|
|
dispose(): void {
|
|
this.records.length = 0;
|
|
}
|
|
}
|
|
|
|
const unavailableAsync = async () =>
|
|
failure("PROVIDER_UNAVAILABLE", true, "Capability was not installed.");
|
|
const unavailableSync = () =>
|
|
failure("PROVIDER_UNAVAILABLE", true, "Capability was not installed.");
|
|
|
|
export function createUnavailableAdapters(): OptionalCapabilityPorts {
|
|
return Object.freeze({
|
|
realtime: {
|
|
subscribe: unavailableAsync,
|
|
heartbeat: unavailableAsync,
|
|
},
|
|
offline: {
|
|
open: unavailableAsync,
|
|
get: unavailableAsync,
|
|
put: unavailableAsync,
|
|
migrate: unavailableAsync,
|
|
close() {},
|
|
},
|
|
serviceWorker: {
|
|
inspect: unavailableAsync,
|
|
activate: unavailableAsync,
|
|
rollback: unavailableAsync,
|
|
unregister: unavailableAsync,
|
|
},
|
|
fileTransfer: {
|
|
upload: unavailableAsync,
|
|
download: unavailableAsync,
|
|
},
|
|
generatedApi: { execute: unavailableAsync },
|
|
featureFlag: { evaluate: unavailableAsync },
|
|
worker: {
|
|
run: unavailableAsync,
|
|
cancel() {},
|
|
dispose() {},
|
|
},
|
|
multiTab: {
|
|
publish: unavailableSync,
|
|
subscribe: () => () => {},
|
|
close() {},
|
|
},
|
|
browserPermission: { request: unavailableAsync },
|
|
clientWorkflow: {
|
|
snapshot: () => Object.freeze({ unavailable: true }),
|
|
dispatch: unavailableSync,
|
|
reset() {},
|
|
subscribe: () => () => {},
|
|
},
|
|
largeDataUi: {
|
|
window: unavailableSync,
|
|
focus: unavailableSync,
|
|
replace() {},
|
|
},
|
|
analytics: {
|
|
record: unavailableSync,
|
|
flush: unavailableAsync,
|
|
dispose() {},
|
|
},
|
|
});
|
|
}
|