Files
clean-architecture-frontend…/recipes/frontend-capabilities/contracts.ts
T

238 lines
6.3 KiB
TypeScript

/**
* Opt-in capability contracts.
*
* This directory is a copyable recipe source, not a production entry. A project
* moves only the selected contract into its application-owned boundary and puts
* a concrete implementation behind that port.
*/
export const OPTIONAL_RECIPE_RUNTIME_SENTINEL =
"frontend-optional-recipe-must-not-reach-production";
export type CapabilityFailureCode =
| "ABORTED"
| "AUTH_EXPIRED"
| "BLOCKED"
| "CONFLICT"
| "CONSENT_DENIED"
| "CONTRACT_DRIFT"
| "CORRUPT_DATA"
| "DISCONNECTED"
| "EXPIRED_RESOURCE"
| "INTEGRITY_FAILED"
| "INVALID_INPUT"
| "LIMIT_EXCEEDED"
| "MIGRATION_FAILED"
| "NOT_FOUND"
| "NOT_READABLE"
| "PERMISSION_DENIED"
| "POLICY_REJECTED"
| "PROVIDER_UNAVAILABLE"
| "QUOTA_EXCEEDED"
| "STALE_RESULT"
| "STORAGE_EVICTED"
| "UNSUPPORTED";
export type CapabilityFailure = Readonly<{
code: CapabilityFailureCode;
retryable: boolean;
safeMessage: string;
}>;
export type CapabilityResult<T> =
| Readonly<{ ok: true; value: T }>
| Readonly<{ ok: false; failure: CapabilityFailure }>;
export type Cleanup = () => void;
export type RealtimeEvent<T> = Readonly<{
id: string;
sequence: number;
occurredAt: string;
payload: T;
}>;
export interface RealtimeSubscription {
readonly resumeToken: string | null;
unsubscribe(): void;
}
export interface RealtimePort<T> {
subscribe(input: {
channel: string;
resumeToken?: string;
signal?: AbortSignal;
onEvent(event: CapabilityResult<RealtimeEvent<T>>): void;
}): Promise<CapabilityResult<RealtimeSubscription>>;
heartbeat(signal?: AbortSignal): Promise<CapabilityResult<void>>;
}
export interface VersionedOfflineRepository<T extends { id: string }> {
open(input: {
schemaVersion: number;
signal?: AbortSignal;
}): Promise<CapabilityResult<void>>;
get(id: string, signal?: AbortSignal): Promise<CapabilityResult<T | null>>;
put(value: T, signal?: AbortSignal): Promise<CapabilityResult<void>>;
migrate(input: {
from: number;
to: number;
signal?: AbortSignal;
}): Promise<CapabilityResult<void>>;
close(): void;
}
export interface ServiceWorkerUpdatePort {
inspect(signal?: AbortSignal): Promise<
CapabilityResult<Readonly<{ updateAvailable: boolean; version: string | null }>>
>;
activate(version: string, signal?: AbortSignal): Promise<CapabilityResult<void>>;
rollback(signal?: AbortSignal): Promise<CapabilityResult<void>>;
unregister(): Promise<CapabilityResult<void>>;
}
export type TransferProgress = Readonly<{
phase?:
| "VALIDATING"
| "PREPARING"
| "TRANSFERRING"
| "VERIFYING"
| "FINALIZING";
transferredBytes: number;
totalBytes: number | null;
}>;
export type TransferByteSource = Readonly<{
byteLength: number | null;
chunks: AsyncIterable<Uint8Array>;
}>;
export interface FileTransferPort {
upload(input: {
file: Readonly<{
name: string;
size: number;
type: string;
content: TransferByteSource;
}>;
signal: AbortSignal;
onProgress(progress: TransferProgress): void;
}): Promise<CapabilityResult<Readonly<{ resourceId: string }>>>;
download(input: {
resourceId: string;
signal: AbortSignal;
onProgress(progress: TransferProgress): void;
}): Promise<
CapabilityResult<
Readonly<{
fileName: string;
mediaType: string;
content: TransferByteSource;
}>
>
>;
}
export interface GeneratedApiFacade {
execute<TOutput>(input: {
operationId: string;
contractVersion: string;
body?: unknown;
signal?: AbortSignal;
}): Promise<CapabilityResult<TOutput>>;
}
export interface FeatureFlagPort<TFlags extends Record<string, boolean | string | number>> {
evaluate<TKey extends keyof TFlags>(input: {
key: TKey;
fallback: TFlags[TKey];
maxAgeMs: number;
}): Promise<CapabilityResult<TFlags[TKey]>>;
}
export interface WorkerTaskPort<TInput, TOutput> {
run(input: {
taskId: string;
generation: number;
payload: TInput;
signal: AbortSignal;
}): Promise<CapabilityResult<TOutput>>;
cancel(taskId: string): void;
dispose(): void;
}
export type MultiTabEvent<T> = Readonly<{
eventId: string;
sourceId: string;
version: number;
payload: T;
}>;
export interface MultiTabPort<T> {
publish(event: MultiTabEvent<T>): CapabilityResult<void>;
subscribe(input: {
sourceId: string;
onEvent(event: CapabilityResult<MultiTabEvent<T>>): void;
}): Cleanup;
close(): void;
}
export type BrowserCapability =
| "clipboard-read"
| "clipboard-write"
| "media"
| "notification";
export type PermissionDecision = "granted" | "denied" | "dismissed";
export interface BrowserPermissionPort {
request(input: {
capability: BrowserCapability;
signal?: AbortSignal;
}): Promise<CapabilityResult<PermissionDecision>>;
}
export interface ClientWorkflowPort<TState, TEvent> {
snapshot(): Readonly<TState>;
dispatch(event: TEvent): CapabilityResult<Readonly<TState>>;
reset(): void;
subscribe(listener: (state: Readonly<TState>) => void): Cleanup;
}
export interface LargeDataUiFacade<TRow extends { id: string }> {
window(input: {
offset: number;
limit: number;
generation: number;
}): CapabilityResult<ReadonlyArray<TRow>>;
focus(rowId: string): CapabilityResult<void>;
replace(rows: ReadonlyArray<TRow>, generation: number): void;
}
export type SafeAnalyticsValue = boolean | number | string | null;
export interface AnalyticsErrorSink {
record(input: {
kind: "analytics" | "error";
eventId: string;
consent: "granted" | "denied" | "not-required";
attributes: Readonly<Record<string, SafeAnalyticsValue>>;
}): CapabilityResult<void>;
flush(signal?: AbortSignal): Promise<CapabilityResult<void>>;
dispose(): void;
}
export type OptionalCapabilityPorts = Readonly<{
realtime: RealtimePort<unknown>;
offline: VersionedOfflineRepository<{ id: string }>;
serviceWorker: ServiceWorkerUpdatePort;
fileTransfer: FileTransferPort;
generatedApi: GeneratedApiFacade;
featureFlag: FeatureFlagPort<Record<string, boolean | string | number>>;
worker: WorkerTaskPort<unknown, unknown>;
multiTab: MultiTabPort<unknown>;
browserPermission: BrowserPermissionPort;
clientWorkflow: ClientWorkflowPort<unknown, unknown>;
largeDataUi: LargeDataUiFacade<{ id: string }>;
analytics: AnalyticsErrorSink;
}>;