238 lines
6.3 KiB
TypeScript
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;
|
|
}>;
|