import type { ReadonlySignal } from "../../signals/types/index.js"; import type { TMapError } from "./api.js"; import type { IQueryCacheEntry, TCacheEntryAddedContext, TQueryStartedContext } from "./cache.js"; import type { Args } from "./common.js"; import type { IResource, TPackedResource } from "./resource.js"; import type { TCommandAgentState } from "./state.js"; export interface TLinkConfig { resource: IResource; forwardArgs: (commandArgs: TArgs) => TResArgs | undefined; invalidate?: boolean; optimisticUpdate?: (draft: TResData, commandArgs: TArgs) => void; update?: (draft: TResData, commandArgs: TArgs, result: TData) => void; } export type TLinksInput = (link: (config: TLinkConfig) => void) => void; export interface ICommand { /** * Imperatively execute the mutation. * * Returns the raw mutation promise: resolves with the result, rejects with * the mapError-normalized error (`TError`). Never throws synchronously. */ execute(args: Args, key?: string): Promise; /** * @deprecated Renamed to {@link execute} (identical contract). Will be * removed in a future release. */ trigger(args: Args, key?: string): Promise; getEntry(key: string): IQueryCacheEntry | null; getEntry$(key: string): IQueryCacheEntry | null; createAgent(key?: string): ICommandAgent; pack(args: Args, key?: string): TPackedCommand; } /** * Inert descriptor binding a command to a set of arguments (and an optional * cache key). Produced by {@link ICommand.pack} — lets a consumer hand "what to * run, with which args" back to the library without executing anything. * Discriminated by `kind`. */ export interface TPackedCommand { kind: "command"; command: ICommand; args: Args; key?: string; } /** * Discriminated union of every packed descriptor. Narrow on `kind` to recover * the concrete resource/command shape. */ export type TPacked = TPackedResource | TPackedCommand; /** * Settled outcome of a mutation, discriminated by `status`. * * The optional `undefined` counterparts let consumers narrow both ways: * `result.status === "error"` and `if (result.error)` work equally well. */ export type TTriggerResult = { status: "success"; data: TData; error?: undefined; } | { status: "error"; data?: undefined; error: TError; }; /** * Promise returned by agent/hook-level `trigger`. * * Never rejects — the outcome is delivered as a {@link TTriggerResult} * envelope, so a bare `await trigger(...)` needs no try/catch. Call * {@link unwrap} when throwing semantics are wanted instead. */ export interface TTriggerPromise extends Promise> { /** * The raw result: resolves with the mutation data, rejects with the * original error — the same contract as `Command.execute`. */ unwrap(): Promise; } export interface ICommandAgent { state$: ReadonlySignal>; /** * Execute the mutation and track its cache entry via {@link state$}. * * Returns a {@link TTriggerPromise}: it never rejects — the outcome arrives * as a {@link TTriggerResult} envelope, so a fire-and-forget call site * (`onClick={() => trigger(args)}`) can never surface an unhandled * rejection. `unwrap()` hands back the raw throwing promise * (`Command.execute`'s contract) when that is wanted instead. */ trigger(args: Args, key?: string): TTriggerPromise; setKey(key: string): void; /** Re-execute the tracked mutation after it failed. No-op unless in the `error` state. */ retry(): void; } export interface TCommandOptions { /** * Executes the mutation. The second argument is the request id — a stable * idempotency token that is minted once per cache entry and reused across * retries, so a failed-then-retried mutation carries the same token to the * backend. Forward it as e.g. an `Idempotency-Key` header. */ queryFn: (args: TArgs, requestId: string) => Promise; key?: string; links?: TLinksInput; retentionTime?: number | false; /** * Derives the request id passed to {@link queryFn}. Called once per cache * entry (its result is reused across retries). Defaults to `crypto.randomUUID()`. */ generateRequestId?: (args: TArgs) => string | Promise; onCacheEntryAdded?: (args: TArgs, ctx: TCacheEntryAddedContext) => void; onQueryStarted?: (args: TArgs, ctx: TQueryStartedContext) => void | Promise; } /** * Configuration object for creating a {@link Command}. * * @template TArgs - The argument type accepted by the mutation function. * @template TData - The data type returned by the mutation function. */ export interface ICommandConfig { /** Function that executes the mutation. Receives the per-entry request id as the second argument. */ queryFn: (args: TArgs, requestId: string) => Promise; /** Derives the request id; called once per cache entry. Defaults to `crypto.randomUUID()`. */ generateRequestId?: (args: TArgs) => string | Promise; /** Optional prefix for cache keys and devtools display. */ key?: string; /** * Normalizes raw mutation errors before they enter the machine. The Api * always supplies one (identity when the consumer configured no `mapError`); * defaults to identity if constructed directly. See {@link TMapError}. */ mapError?: TMapError; /** Link descriptors that bind this command to related resources. */ links: TLinkConfig[]; /** Time (ms) to keep a cache entry after subscribers drop off. `false` disables auto-removal. */ retentionTime: number | false; /** Called when a new cache entry is created. See lifecycle hooks documentation. */ onCacheEntryAdded?: (args: TArgs, ctx: TCacheEntryAddedContext) => void; /** Called every time `queryFn` starts. See lifecycle hooks documentation. */ onQueryStarted?: (args: TArgs, ctx: TQueryStartedContext) => void | Promise; }