chore: initialize from frontend template 4dc033c
This commit is contained in:
@@ -0,0 +1,741 @@
|
||||
import type {
|
||||
CapabilityResult,
|
||||
Cleanup,
|
||||
TransferProgress,
|
||||
} from "./contracts.ts";
|
||||
|
||||
/**
|
||||
* Deep opt-in contracts for browser file and origin-storage capabilities.
|
||||
*
|
||||
* These contracts intentionally expose no File, Blob, FileSystemHandle,
|
||||
* IDBDatabase, IDBTransaction, Cache, Request or Response. A selected project
|
||||
* copies and narrows only the ports that its product actually owns.
|
||||
*/
|
||||
|
||||
declare const byteCountBrand: unique symbol;
|
||||
declare const localFileRefBrand: unique symbol;
|
||||
declare const fileVerificationReceiptBrand: unique symbol;
|
||||
declare const filePolicyKeyBrand: unique symbol;
|
||||
declare const filePolicyIntentionBrand: unique symbol;
|
||||
declare const browserManagedCapabilityReceiptBrand: unique symbol;
|
||||
declare const authorizedDownloadCapabilityBrand: unique symbol;
|
||||
declare const authorizedDownloadCapabilityReceiptBrand: unique symbol;
|
||||
declare const objectIdBrand: unique symbol;
|
||||
declare const objectGenerationBrand: unique symbol;
|
||||
|
||||
export type ByteCount = number & {
|
||||
readonly [byteCountBrand]: "ByteCount";
|
||||
};
|
||||
|
||||
export type LocalFileRef = string & {
|
||||
readonly [localFileRefBrand]: "LocalFileRef";
|
||||
};
|
||||
|
||||
export type FileVerificationReceipt = string & {
|
||||
readonly [fileVerificationReceiptBrand]: "FileVerificationReceipt";
|
||||
};
|
||||
|
||||
export type FilePolicyKey = string & {
|
||||
readonly [filePolicyKeyBrand]: "FilePolicyKey";
|
||||
};
|
||||
|
||||
export type FilePolicyIntention = string & {
|
||||
readonly [filePolicyIntentionBrand]: "FilePolicyIntention";
|
||||
};
|
||||
|
||||
/**
|
||||
* Composition-issued capability selector. Implementations must resolve the
|
||||
* exact registered object identity, not merely an equal string pair.
|
||||
*/
|
||||
export type FilePolicyReference = Readonly<{
|
||||
policyKey: FilePolicyKey;
|
||||
intention: FilePolicyIntention;
|
||||
}>;
|
||||
|
||||
export type BrowserManagedDownloadCapabilityReceipt = string & {
|
||||
readonly [browserManagedCapabilityReceiptBrand]:
|
||||
"BrowserManagedDownloadCapabilityReceipt";
|
||||
};
|
||||
|
||||
export type AuthorizedDownloadCapabilityReceipt = string & {
|
||||
readonly [authorizedDownloadCapabilityReceiptBrand]:
|
||||
"AuthorizedDownloadCapabilityReceipt";
|
||||
};
|
||||
|
||||
/**
|
||||
* GET-only provider-issued handle. The corresponding URL, signed query and
|
||||
* headers stay inside the selected transfer adapter's in-memory identity
|
||||
* vault; a feature cannot replace them or supply its own digest.
|
||||
*/
|
||||
export type AuthorizedDownloadCapability = Readonly<{
|
||||
capabilityReceipt: AuthorizedDownloadCapabilityReceipt;
|
||||
method: "GET";
|
||||
binding: Readonly<{
|
||||
kind: "DOWNLOAD";
|
||||
resourceId: string;
|
||||
}>;
|
||||
mediaType: string;
|
||||
byteLength: ByteCount;
|
||||
maxBytes: ByteCount;
|
||||
expectedSha256: string;
|
||||
expiresAtEpochMs: number;
|
||||
readonly [authorizedDownloadCapabilityBrand]:
|
||||
"AuthorizedDownloadCapability";
|
||||
}>;
|
||||
|
||||
export type DurableObjectId = string & {
|
||||
readonly [objectIdBrand]: "DurableObjectId";
|
||||
};
|
||||
|
||||
export type ObjectGeneration = number & {
|
||||
readonly [objectGenerationBrand]: "ObjectGeneration";
|
||||
};
|
||||
|
||||
export type PersistableDataClass =
|
||||
| "PUBLIC"
|
||||
| "INTERNAL"
|
||||
| "PERSONAL"
|
||||
| "CONFIDENTIAL";
|
||||
|
||||
export type FileSelectionSource =
|
||||
| "NATIVE_INPUT"
|
||||
| "SYSTEM_PICKER"
|
||||
| "DROP";
|
||||
|
||||
export type FileAcceptRule = Readonly<{
|
||||
mediaType: string;
|
||||
extensions: ReadonlyArray<string>;
|
||||
}>;
|
||||
|
||||
export type FileSelectionPolicy = Readonly<{
|
||||
policyId: string;
|
||||
purpose: string;
|
||||
classification: PersistableDataClass;
|
||||
multiple: boolean;
|
||||
maxCount: number;
|
||||
maxFileBytes: ByteCount;
|
||||
maxTotalBytes: ByteCount;
|
||||
allowEmpty: boolean;
|
||||
accept: ReadonlyArray<FileAcceptRule>;
|
||||
}>;
|
||||
|
||||
export type FileSelectionLimitReduction = Readonly<{
|
||||
maxCount?: number;
|
||||
maxFileBytes?: ByteCount;
|
||||
maxTotalBytes?: ByteCount;
|
||||
}>;
|
||||
|
||||
export type FileCandidate = Readonly<{
|
||||
ref: LocalFileRef;
|
||||
displayName: string;
|
||||
sizeBytes: ByteCount;
|
||||
reportedMediaType: string | null;
|
||||
lastModifiedEpochMs: number | null;
|
||||
source: FileSelectionSource;
|
||||
}>;
|
||||
|
||||
export type FileSelectionOutcome =
|
||||
| Readonly<{
|
||||
kind: "SELECTED";
|
||||
files: ReadonlyArray<FileCandidate>;
|
||||
}>
|
||||
| Readonly<{ kind: "DISMISSED" }>;
|
||||
|
||||
export type FilePickerSupport = Readonly<{
|
||||
nativeInput: true;
|
||||
systemOpenPicker: boolean;
|
||||
systemSavePicker: boolean;
|
||||
}>;
|
||||
|
||||
export interface FilePickerPort {
|
||||
readonly support: FilePickerSupport;
|
||||
|
||||
select(input: {
|
||||
policy: FilePolicyReference;
|
||||
limits?: FileSelectionLimitReduction;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<FileSelectionOutcome>>;
|
||||
|
||||
release(ref: LocalFileRef): void;
|
||||
}
|
||||
|
||||
export type FileSignatureResult =
|
||||
| "MATCHED"
|
||||
| "MISMATCHED"
|
||||
| "UNKNOWN";
|
||||
|
||||
export type FileBytePattern = Readonly<{
|
||||
offset: ByteCount;
|
||||
bytes: ReadonlyArray<number>;
|
||||
mask?: ReadonlyArray<number>;
|
||||
}>;
|
||||
|
||||
export type FileSignatureRule = Readonly<{
|
||||
mediaType: string;
|
||||
extensions: ReadonlyArray<string>;
|
||||
patterns: ReadonlyArray<FileBytePattern>;
|
||||
}>;
|
||||
|
||||
export type FileInspectionPolicy = Readonly<{
|
||||
policyId: string;
|
||||
maxInspectionBytes: ByteCount;
|
||||
acceptedSignatures: ReadonlyArray<FileSignatureRule>;
|
||||
}>;
|
||||
|
||||
export type FileInspection = Readonly<{
|
||||
byteLength: ByteCount;
|
||||
reportedMediaType: string | null;
|
||||
detectedMediaType: string | null;
|
||||
normalizedExtension: string | null;
|
||||
signature: FileSignatureResult;
|
||||
verificationReceipt: FileVerificationReceipt | null;
|
||||
}>;
|
||||
|
||||
export type FileByteSource = Readonly<{
|
||||
byteLength: ByteCount | null;
|
||||
stream(
|
||||
signal: AbortSignal,
|
||||
): AsyncIterable<CapabilityResult<Uint8Array>>;
|
||||
}>;
|
||||
|
||||
export interface FileContentPort {
|
||||
inspect(input: {
|
||||
ref: LocalFileRef;
|
||||
policy: FilePolicyReference;
|
||||
maxInspectionBytes?: ByteCount;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<FileInspection>>;
|
||||
|
||||
readRange(input: {
|
||||
ref: LocalFileRef;
|
||||
offset: ByteCount;
|
||||
length: ByteCount;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<Uint8Array>>;
|
||||
|
||||
openSource(input: {
|
||||
ref: LocalFileRef;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<FileByteSource>>;
|
||||
|
||||
release(ref: LocalFileRef): void;
|
||||
}
|
||||
|
||||
export type PreviewLease = Readonly<{
|
||||
url: string;
|
||||
mediaType: string;
|
||||
release: Cleanup;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Presentation-local capability. The implementation owns object URL creation
|
||||
* and must revoke each lease on replacement, load failure and unmount.
|
||||
*/
|
||||
export interface TransientPreviewPort {
|
||||
create(input: {
|
||||
ref: LocalFileRef;
|
||||
verificationReceipt: FileVerificationReceipt;
|
||||
policy: FilePolicyReference;
|
||||
maxPreviewBytes?: ByteCount;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<PreviewLease>>;
|
||||
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature/backend workflow example, not a browser capability dependency.
|
||||
* Copy it only when the backend owns authorization, resumable-session expiry,
|
||||
* integrity verification, content inspection and quarantine promotion.
|
||||
*/
|
||||
export type ExampleUploadSession = Readonly<{
|
||||
sessionId: string;
|
||||
partSizeBytes: ByteCount;
|
||||
maxConcurrency: number;
|
||||
expiresAt: string;
|
||||
checksumAlgorithm: "SHA-256";
|
||||
}>;
|
||||
|
||||
export type ExampleUploadPartReceipt = Readonly<{
|
||||
partNumber: number;
|
||||
acceptedBytes: ByteCount;
|
||||
checksumSha256: string;
|
||||
}>;
|
||||
|
||||
export type ExampleQuarantinedUpload = Readonly<{
|
||||
resourceId: string;
|
||||
state: "QUARANTINED";
|
||||
}>;
|
||||
|
||||
export interface ExampleQuarantinedUploadPort {
|
||||
create(input: {
|
||||
purpose: string;
|
||||
byteLength: ByteCount;
|
||||
detectedMediaType: string;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<ExampleUploadSession>>;
|
||||
|
||||
uploadPart(input: {
|
||||
sessionId: string;
|
||||
partNumber: number;
|
||||
offset: ByteCount;
|
||||
bytes: Uint8Array;
|
||||
checksumSha256: string;
|
||||
idempotencyKey: string;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<ExampleUploadPartReceipt>>;
|
||||
|
||||
complete(input: {
|
||||
sessionId: string;
|
||||
parts: ReadonlyArray<ExampleUploadPartReceipt>;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<ExampleQuarantinedUpload>>;
|
||||
|
||||
abort(
|
||||
sessionId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CapabilityResult<void>>;
|
||||
}
|
||||
|
||||
export type DownloadSource =
|
||||
| Readonly<{
|
||||
kind: "BROWSER_MANAGED_RESOURCE";
|
||||
resourceId: string;
|
||||
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "AUTHORIZED_STREAM_RESOURCE";
|
||||
resourceId: string;
|
||||
capability: AuthorizedDownloadCapability;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "GENERATED";
|
||||
bytes: FileByteSource;
|
||||
expectedSha256?: string;
|
||||
}>;
|
||||
|
||||
export type DownloadStrategy =
|
||||
| "BROWSER_MANAGED"
|
||||
| "PROMPT_AND_STREAM"
|
||||
| "BOUNDED_OBJECT_URL";
|
||||
|
||||
export type DownloadOutcome =
|
||||
| Readonly<{
|
||||
kind: "BROWSER_HANDOFF";
|
||||
transferId: string;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "SAVED";
|
||||
transferId: string;
|
||||
bytesWritten: ByteCount;
|
||||
integrity: "VERIFIED" | "NOT_PROVIDED";
|
||||
}>
|
||||
| Readonly<{ kind: "DISMISSED" }>;
|
||||
|
||||
export interface DownloadDeliveryPort {
|
||||
deliver(input: {
|
||||
policy: FilePolicyReference;
|
||||
source: DownloadSource;
|
||||
suggestedFileName: string;
|
||||
maxTransferBytes?: ByteCount;
|
||||
maxBufferedBytes?: ByteCount;
|
||||
signal: AbortSignal;
|
||||
onProgress(progress: TransferProgress): void;
|
||||
}): Promise<CapabilityResult<DownloadOutcome>>;
|
||||
}
|
||||
|
||||
export type BrowserManagedDownloadCapability = Readonly<{
|
||||
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
|
||||
href: string;
|
||||
resourceId: string;
|
||||
mediaType: string;
|
||||
safeExtension: string;
|
||||
maxBytes: ByteCount;
|
||||
expectedSha256?: string;
|
||||
expiresAtEpochMs: number;
|
||||
}>;
|
||||
|
||||
export interface BrowserManagedDownloadCapabilityResolver {
|
||||
resolve(input: Readonly<{
|
||||
resourceId: string;
|
||||
capabilityReceipt: BrowserManagedDownloadCapabilityReceipt;
|
||||
}>): CapabilityResult<BrowserManagedDownloadCapability>;
|
||||
}
|
||||
|
||||
export type RegisteredPreviewPolicy = Readonly<{
|
||||
allowedMediaTypes: ReadonlyArray<string>;
|
||||
maxPreviewBytes: ByteCount;
|
||||
}>;
|
||||
|
||||
export type RegisteredDownloadPolicy = Readonly<{
|
||||
strategy: DownloadStrategy;
|
||||
mediaType: string;
|
||||
safeExtension: string;
|
||||
maxTransferBytes: ByteCount;
|
||||
maxBufferedBytes: ByteCount;
|
||||
integrity: "OPTIONAL" | "REQUIRED";
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Dataset policy belongs at composition. Feature and presentation callers
|
||||
* receive only the reference plus optional reductions.
|
||||
*/
|
||||
export type BrowserFilePolicyProfile = Readonly<{
|
||||
reference: FilePolicyReference;
|
||||
selection?: FileSelectionPolicy;
|
||||
inspection?: FileInspectionPolicy;
|
||||
preview?: RegisteredPreviewPolicy;
|
||||
download?: RegisteredDownloadPolicy;
|
||||
}>;
|
||||
|
||||
export type BrowserPersistencePolicy = Readonly<{
|
||||
owner: string;
|
||||
namespace: string;
|
||||
classification: PersistableDataClass;
|
||||
authority: "SERVER" | "LOCAL_FIRST" | "RECONSTRUCTABLE";
|
||||
accountScope: "ORIGIN_SHARED" | "OPAQUE_PARTITION";
|
||||
retention:
|
||||
| Readonly<{ kind: "SESSION" }>
|
||||
| Readonly<{ kind: "TTL"; maxAgeMs: number }>
|
||||
| Readonly<{ kind: "UNTIL_SYNCED" }>
|
||||
| Readonly<{ kind: "EXPLICIT_DELETE" }>;
|
||||
softBudgetBytes: ByteCount;
|
||||
hardBudgetBytes: ByteCount;
|
||||
evictionPriority: "RECONSTRUCTABLE" | "SYNCED_COPY" | "USER_AUTHORED";
|
||||
logoutAction:
|
||||
| "KEEP_ORIGIN_SHARED"
|
||||
| "PURGE_PARTITION"
|
||||
| "EXPORT_THEN_PURGE";
|
||||
accountDeletionAction: "KEEP_ORIGIN_SHARED" | "PURGE_PARTITION";
|
||||
pressureAction: "EVICT_RECONSTRUCTABLE" | "RETAIN";
|
||||
unavailableFallback: "ONLINE_ONLY" | "READ_ONLY" | "EXPORT_REQUIRED";
|
||||
}>;
|
||||
|
||||
export type OfflineStoreStatus =
|
||||
| Readonly<{
|
||||
kind: "READY";
|
||||
persistence: "BEST_EFFORT" | "PERSISTENT";
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "READ_ONLY";
|
||||
reason: "FUTURE_SCHEMA" | "QUOTA" | "RECOVERY";
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "ONLINE_ONLY";
|
||||
reason: "UNAVAILABLE" | "MIGRATION_FAILED";
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "UPGRADE_BLOCKED";
|
||||
targetVersion: number;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "CLOSED";
|
||||
reason: "VERSION_CHANGE" | "FORCED" | "DISPOSED";
|
||||
}>;
|
||||
|
||||
export type OfflineStoreLifecycleEvent =
|
||||
| Readonly<{ kind: "STATUS_CHANGED"; status: OfflineStoreStatus }>
|
||||
| Readonly<{
|
||||
kind: "MIGRATION_PROGRESS";
|
||||
migrationId: string;
|
||||
processedCount: number;
|
||||
remainingEstimate: number | null;
|
||||
}>;
|
||||
|
||||
export type StoredRecord<T> = Readonly<{
|
||||
id: string;
|
||||
revision: number;
|
||||
payloadVersion: number;
|
||||
createdAtEpochMs: number;
|
||||
updatedAtEpochMs: number;
|
||||
expiresAtEpochMs: number | null;
|
||||
value: T;
|
||||
}>;
|
||||
|
||||
export type RevisionGuard =
|
||||
| Readonly<{ kind: "ANY" }>
|
||||
| Readonly<{ kind: "MUST_NOT_EXIST" }>
|
||||
| Readonly<{ kind: "MATCH"; revision: number }>;
|
||||
|
||||
export type StructuredOfflineMutation<T> =
|
||||
| Readonly<{
|
||||
kind: "PUT";
|
||||
id: string;
|
||||
value: T;
|
||||
payloadVersion: number;
|
||||
expiresAtEpochMs: number | null;
|
||||
revision: RevisionGuard;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "DELETE";
|
||||
id: string;
|
||||
revision: RevisionGuard;
|
||||
}>;
|
||||
|
||||
export type OfflineCommitReceipt = Readonly<{
|
||||
commitId: string;
|
||||
revisions: Readonly<Record<string, number | null>>;
|
||||
replayed: boolean;
|
||||
}>;
|
||||
|
||||
export type OfflinePage<T> = Readonly<{
|
||||
records: ReadonlyArray<StoredRecord<T>>;
|
||||
nextCursor: string | null;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Copy this as a feature-specific repository. Do not expose a generic
|
||||
* transaction callback, object-store name, index name or schema version.
|
||||
*/
|
||||
export interface StructuredOfflineStore<T> {
|
||||
open(input: {
|
||||
partitionKey: string;
|
||||
signal?: AbortSignal;
|
||||
onLifecycle(event: OfflineStoreLifecycleEvent): void;
|
||||
}): Promise<CapabilityResult<OfflineStoreStatus>>;
|
||||
|
||||
read(
|
||||
id: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CapabilityResult<StoredRecord<T> | null>>;
|
||||
|
||||
page(input: {
|
||||
cursor?: string;
|
||||
limit: number;
|
||||
includeExpired?: boolean;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<OfflinePage<T>>>;
|
||||
|
||||
commit(input: {
|
||||
idempotencyKey: string;
|
||||
mutations: ReadonlyArray<StructuredOfflineMutation<T>>;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<OfflineCommitReceipt>>;
|
||||
|
||||
clearPartition(input: {
|
||||
reason: "LOGOUT" | "ACCOUNT_DELETION" | "USER_REQUEST";
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<void>>;
|
||||
|
||||
status(): OfflineStoreStatus;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
export type MigrationProgress = Readonly<{
|
||||
migrationId: string;
|
||||
state: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED";
|
||||
processedCount: number;
|
||||
remainingEstimate: number | null;
|
||||
}>;
|
||||
|
||||
export interface OfflineStoreMaintenancePort {
|
||||
resumeDataMigration(input: {
|
||||
migrationId: string;
|
||||
maxRecords: number;
|
||||
timeBudgetMs: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<MigrationProgress>>;
|
||||
|
||||
purgeExpired(input: {
|
||||
maxRecords: number;
|
||||
nowEpochMs: number;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<Readonly<{ purgedCount: number }>>>;
|
||||
}
|
||||
|
||||
export type StorageEstimate = Readonly<{
|
||||
usageBytes: number | null;
|
||||
quotaBytes: number | null;
|
||||
persisted: boolean | null;
|
||||
pressure: "UNKNOWN" | "NORMAL" | "PRESSURE" | "CRITICAL";
|
||||
}>;
|
||||
|
||||
export interface StorageDurabilityPort {
|
||||
inspect(signal?: AbortSignal): Promise<CapabilityResult<StorageEstimate>>;
|
||||
|
||||
requestPersistence(input: {
|
||||
reason: "PROTECT_UNSYNCED_USER_DATA";
|
||||
userInitiated: true;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<"GRANTED" | "DENIED">>;
|
||||
}
|
||||
|
||||
export type DurableObjectDataClass =
|
||||
| "RECONSTRUCTABLE"
|
||||
| "USER_CREATED_PRIVATE";
|
||||
|
||||
export type DurableObjectIntegrity = Readonly<{
|
||||
algorithm: "SHA-256-TREE-V1";
|
||||
rootDigest: string;
|
||||
chunkSizeBytes: ByteCount;
|
||||
}>;
|
||||
|
||||
export type DurableObjectDescriptor = Readonly<{
|
||||
id: DurableObjectId;
|
||||
generation: ObjectGeneration;
|
||||
byteLength: ByteCount;
|
||||
mediaType: string | null;
|
||||
integrity: DurableObjectIntegrity;
|
||||
dataClass: DurableObjectDataClass;
|
||||
retention:
|
||||
| Readonly<{ kind: "EXPLICIT_DELETE" }>
|
||||
| Readonly<{ kind: "EXPIRES"; expiresAt: string }>;
|
||||
}>;
|
||||
|
||||
export type DurableObjectRead = Readonly<{
|
||||
descriptor: DurableObjectDescriptor;
|
||||
chunks: AsyncIterable<CapabilityResult<Uint8Array>>;
|
||||
}>;
|
||||
|
||||
export interface DurableObjectStorePort {
|
||||
capabilities(signal?: AbortSignal): Promise<
|
||||
CapabilityResult<
|
||||
Readonly<{
|
||||
backend: "OPFS" | "INDEXEDDB_BLOB" | "NONE";
|
||||
persistence: "BEST_EFFORT" | "PERSISTENT";
|
||||
maxObjectBytes: ByteCount;
|
||||
}>
|
||||
>
|
||||
>;
|
||||
|
||||
put(input: {
|
||||
id: DurableObjectId;
|
||||
expectedGeneration: ObjectGeneration | null;
|
||||
source: AsyncIterable<CapabilityResult<Uint8Array>>;
|
||||
declaredByteLength: ByteCount;
|
||||
mediaType: string | null;
|
||||
dataClass: DurableObjectDataClass;
|
||||
retention: DurableObjectDescriptor["retention"];
|
||||
signal: AbortSignal;
|
||||
onProgress(progress: TransferProgress): void;
|
||||
}): Promise<CapabilityResult<DurableObjectDescriptor>>;
|
||||
|
||||
open(
|
||||
id: DurableObjectId,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CapabilityResult<DurableObjectRead>>;
|
||||
|
||||
remove(input: {
|
||||
id: DurableObjectId;
|
||||
expectedGeneration: ObjectGeneration;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<void>>;
|
||||
}
|
||||
|
||||
export type ObjectStoreRecoverySummary = Readonly<{
|
||||
resumedCount: number;
|
||||
purgedCount: number;
|
||||
quarantinedCount: number;
|
||||
nextCursor: string | null;
|
||||
}>;
|
||||
|
||||
export interface DurableObjectMaintenancePort {
|
||||
reconcile(input: {
|
||||
timeBudgetMs: number;
|
||||
maxEntries: number;
|
||||
cursor?: string;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<ObjectStoreRecoverySummary>>;
|
||||
}
|
||||
|
||||
export type PublicAssetEntry = Readonly<{
|
||||
url: string;
|
||||
byteLength: ByteCount;
|
||||
mediaType: string;
|
||||
integritySha256: string;
|
||||
requestCredentials: "OMIT";
|
||||
dataClass: "PUBLIC";
|
||||
}>;
|
||||
|
||||
export type PublicCacheLookup =
|
||||
| Readonly<{
|
||||
kind: "HIT";
|
||||
releaseId: string;
|
||||
entry: PublicAssetEntry;
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: "MISS";
|
||||
reason:
|
||||
| "NOT_FOUND"
|
||||
| "EXPIRED"
|
||||
| "NO_ACTIVE_RELEASE"
|
||||
| "POLICY_REJECTED";
|
||||
}>;
|
||||
|
||||
export type PublicCacheInspection = Readonly<{
|
||||
activeReleaseId: string | null;
|
||||
previousReleaseId: string | null;
|
||||
candidateReleaseIds: ReadonlyArray<string>;
|
||||
ownedBytes: number | null;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Platform-local Cache Storage policy facade. Raw Request/Response/Cache
|
||||
* objects remain inside the adapter, and no application repository imports it.
|
||||
*/
|
||||
export interface PublicResponseCacheAdmin {
|
||||
stageRelease(input: {
|
||||
releaseId: string;
|
||||
manifestDigest: string;
|
||||
entries: ReadonlyArray<PublicAssetEntry>;
|
||||
signal: AbortSignal;
|
||||
}): Promise<CapabilityResult<void>>;
|
||||
|
||||
activateRelease(input: {
|
||||
releaseId: string;
|
||||
manifestDigest: string;
|
||||
expectedPreviousReleaseId: string | null;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<void>>;
|
||||
|
||||
lookup(input: {
|
||||
url: string;
|
||||
requestCredentials: "OMIT";
|
||||
hasAuthorization: false;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<PublicCacheLookup>>;
|
||||
|
||||
inspect(signal?: AbortSignal): Promise<
|
||||
CapabilityResult<PublicCacheInspection>
|
||||
>;
|
||||
|
||||
rollback(signal?: AbortSignal): Promise<CapabilityResult<void>>;
|
||||
|
||||
deleteOwned(input: {
|
||||
roles: ReadonlyArray<"CANDIDATE" | "PREVIOUS" | "RUNTIME_PUBLIC">;
|
||||
signal?: AbortSignal;
|
||||
}): Promise<CapabilityResult<Readonly<{ deletedCount: number }>>>;
|
||||
}
|
||||
|
||||
export type BrowserFileComposition = Readonly<{
|
||||
picker: FilePickerPort;
|
||||
content: FileContentPort;
|
||||
previews: TransientPreviewPort;
|
||||
downloads: DownloadDeliveryPort;
|
||||
}>;
|
||||
|
||||
export type StructuredOfflineStorageComposition<T> = Readonly<{
|
||||
store: StructuredOfflineStore<T>;
|
||||
maintenance: OfflineStoreMaintenancePort;
|
||||
}>;
|
||||
|
||||
export type StorageDurabilityComposition = Readonly<{
|
||||
durability: StorageDurabilityPort;
|
||||
}>;
|
||||
|
||||
export type DurableObjectStorageComposition = Readonly<{
|
||||
store: DurableObjectStorePort;
|
||||
maintenance: DurableObjectMaintenancePort;
|
||||
}>;
|
||||
|
||||
export type PublicResponseCacheComposition = Readonly<{
|
||||
cache: PublicResponseCacheAdmin;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Explicitly separate from BrowserFileComposition: installing file selection,
|
||||
* preview or download must never imply a resumable backend upload protocol.
|
||||
*/
|
||||
export type ExampleBackendUploadComposition = Readonly<{
|
||||
upload: ExampleQuarantinedUploadPort;
|
||||
}>;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* 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;
|
||||
}>;
|
||||
@@ -0,0 +1,578 @@
|
||||
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.ts";
|
||||
|
||||
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.");
|
||||
}
|
||||
let transferredBytes = 0;
|
||||
for await (const chunk of input.file.content.chunks) {
|
||||
const duringTransfer = aborted(input.signal);
|
||||
if (duringTransfer) return duringTransfer;
|
||||
transferredBytes += chunk.byteLength;
|
||||
if (transferredBytes > input.file.size) {
|
||||
return failure(
|
||||
"INTEGRITY_FAILED",
|
||||
false,
|
||||
"Uploaded bytes exceeded the declared file size.",
|
||||
);
|
||||
}
|
||||
input.onProgress({
|
||||
phase: "TRANSFERRING",
|
||||
transferredBytes,
|
||||
totalBytes: input.file.size,
|
||||
});
|
||||
}
|
||||
if (
|
||||
transferredBytes !== input.file.size ||
|
||||
input.file.content.byteLength !== input.file.size
|
||||
) {
|
||||
return failure(
|
||||
"INTEGRITY_FAILED",
|
||||
false,
|
||||
"Uploaded bytes did not match the declared file size.",
|
||||
);
|
||||
}
|
||||
return success({ resourceId: "fake:opaque-resource" });
|
||||
}
|
||||
|
||||
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);
|
||||
const chunks = async function* () {
|
||||
if (input.signal.aborted) return;
|
||||
input.onProgress({
|
||||
phase: "TRANSFERRING" as const,
|
||||
transferredBytes: bytes.byteLength,
|
||||
totalBytes: bytes.byteLength,
|
||||
});
|
||||
yield bytes.slice();
|
||||
};
|
||||
return success({
|
||||
fileName: "download.bin",
|
||||
mediaType: "application/octet-stream",
|
||||
content: {
|
||||
byteLength: bytes.byteLength,
|
||||
chunks: chunks(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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() {},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from "./browser-file-storage-contracts.ts";
|
||||
export * from "./browser-file-storage-fakes.ts";
|
||||
export * from "./contracts.ts";
|
||||
export * from "./fake-adapters.ts";
|
||||
Reference in New Issue
Block a user