Files
clean-architecture-frontend…/src/application/ports/browser-file-storage/indexeddb-port.ts
T

201 lines
5.7 KiB
TypeScript

import type {
BrowserAccountScope,
BrowserDataResult,
BrowserStoragePolicy,
} from "./shared.ts";
/**
* Registry-issued, non-semantic dataset identity. Tokens must be random and
* must never contain a tenant, account, user, email, domain object ID, or the
* human-readable policy namespace.
*/
export type IndexedDbDatasetScope = Readonly<{
authorityToken: string;
namespaceToken: string;
partitionToken: string;
accountScope: BrowserAccountScope;
}>;
export type IndexedDbSynchronizationState =
| "PENDING"
| "CONFIRMED";
export type IndexedDbConnectionStatus =
| Readonly<{
kind: "CLOSED";
/**
* NOT_OPENED means the current open attempt has settled without a live
* connection. A blocked upgrade may therefore be observed as BLOCKED
* while it is waiting, then transition to CLOSED/NOT_OPENED when its
* blocked deadline fails closed.
*/
reason: "NOT_OPENED" | "VERSION_CHANGE" | "FORCED";
}>
| Readonly<{ kind: "OPENING"; targetVersion: number }>
| Readonly<{
/**
* A live open/upgrade attempt is currently waiting for another
* IndexedDB context. BLOCKED is transient and is not the settled state
* after the blocked deadline has expired.
*/
kind: "BLOCKED";
currentVersion: number;
targetVersion: number;
}>
| Readonly<{ kind: "READY"; schemaVersion: number }>
| Readonly<{ kind: "DISPOSED" }>;
export type IndexedDbCursorKey =
| string
| number
| Date
| ArrayBuffer
| readonly IndexedDbCursorKey[];
/**
* Opaque continuation state owned by an adapter query policy. Feature ports
* should wrap this value if a cursor crosses a presentation or URL boundary.
*/
export type IndexedDbCursor = Readonly<{
indexKey: IndexedDbCursorKey;
primaryKey: IndexedDbCursorKey;
}>;
export type IndexedDbPage<Value> = Readonly<{
items: readonly Value[];
nextCursor: IndexedDbCursor | null;
}>;
export type IndexedDbWriteReceipt = Readonly<{
key: string;
revision: number;
replayed: boolean;
}>;
export type IndexedDbCompareAndSwapInput<Value> = Readonly<{
key: string;
value: Value;
expectedRevision: number | null;
idempotencyKey: string;
/**
* Required only for UNTIL_SYNCED datasets. The adapter never infers server
* acknowledgement from a successful local write.
*/
synchronization?: IndexedDbSynchronizationState;
signal?: AbortSignal;
}>;
export type IndexedDbDeleteInput = Readonly<{
key: string;
expectedRevision: number;
idempotencyKey: string;
signal?: AbortSignal;
}>;
export type IndexedDbMaintenanceBatchInput = Readonly<{
/**
* Hard row-count ceiling for one invocation. The adapter also observes the
* cooperative duration budget between asynchronous storage operations.
*/
maxRows: number;
maxDurationMs: number;
signal?: AbortSignal;
}>;
export type IndexedDbMaintenanceBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
checkpointedRows: number;
migratedRows: number;
concurrentlyChangedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbReceiptPruneBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
deletedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbLifecycleAction =
| "SESSION_END"
| "LOGOUT"
| "ACCOUNT_DELETION"
| "RETENTION_SWEEP";
export type IndexedDbLifecycleBatchInput = Readonly<{
action: IndexedDbLifecycleAction;
maxRows: number;
maxDurationMs: number;
signal?: AbortSignal;
}>;
export type IndexedDbLifecycleBatchReceipt = Readonly<{
state: "MORE" | "COMPLETE";
scannedRows: number;
deletedRows: number;
budgetExhausted: boolean;
}>;
export type IndexedDbLifecycleAuthorityRequest = Readonly<{
action: IndexedDbLifecycleAction;
scope: IndexedDbDatasetScope;
storagePolicy: BrowserStoragePolicy;
signal?: AbortSignal;
}>;
export type IndexedDbLifecycleAuthorityDecision =
| Readonly<{ authorized: false }>
| Readonly<{
authorized: true;
/** Opaque, short-lived proof. It is validated and discarded, never stored. */
proofToken: string;
}>;
/**
* Domain-neutral asynchronous repository boundary. Native IndexedDB objects,
* object-store names, indexes and transaction callbacks remain adapter-local.
*/
export interface IndexedDbRepositoryPort<Value, Query> {
open(signal?: AbortSignal): Promise<BrowserDataResult<void>>;
read(
key: string,
signal?: AbortSignal,
): Promise<BrowserDataResult<Readonly<{ value: Value; revision: number }> | null>>;
query(
query: Query,
cursor?: IndexedDbCursor | null,
signal?: AbortSignal,
): Promise<BrowserDataResult<IndexedDbPage<Value>>>;
compareAndSwap(
input: IndexedDbCompareAndSwapInput<Value>,
): Promise<BrowserDataResult<IndexedDbWriteReceipt>>;
remove(
input: IndexedDbDeleteInput,
): Promise<BrowserDataResult<IndexedDbWriteReceipt>>;
/**
* Bounded destructive lifecycle work. Every deleting invocation is gated by
* the composition-root authority callback; callers cannot provide proof.
*/
enforceLifecycleBatch(
input: IndexedDbLifecycleBatchInput,
): Promise<BrowserDataResult<IndexedDbLifecycleBatchReceipt>>;
getStatus(): IndexedDbConnectionStatus;
subscribeStatus(listener: (status: IndexedDbConnectionStatus) => void): () => void;
close(): void;
}
/**
* Bounded, restart-safe maintenance boundary. Checkpoint keys and raw stored
* records stay private to the adapter; callers receive aggregate progress only.
*/
export interface IndexedDbMaintenancePort {
migrateCodecBatch(
input: IndexedDbMaintenanceBatchInput,
): Promise<BrowserDataResult<IndexedDbMaintenanceBatchReceipt>>;
pruneExpiredReceipts(
input: IndexedDbMaintenanceBatchInput,
): Promise<BrowserDataResult<IndexedDbReceiptPruneBatchReceipt>>;
}