| 1 | import type { MutationClient } from "./client.ts"; |
| 2 | |
| 3 | /** |
| 4 | * Plain JSON value. The default serializable leaf set, still exported for |
| 5 | * consumers that want the exact JSON shape. |
| 6 | */ |
| 7 | export type Json = |
| 8 | | string |
| 9 | | number |
| 10 | | boolean |
| 11 | | null |
| 12 | | Json[] |
| 13 | | { [key: string]: Json }; |
| 14 | |
| 15 | /** |
| 16 | * Registry of serializable leaf types. Empty by default, which reduces |
| 17 | * {@link SerializableValue} to plain JSON. Downstream consumers whose transport |
| 18 | * handles a superset of JSON augment this to register those types: |
| 19 | * |
| 20 | * ```ts |
| 21 | * declare module "@clo/react-mutation" { |
| 22 | * interface SerializableLeaves { file: File | Blob } |
| 23 | * } |
| 24 | * ``` |
| 25 | */ |
| 26 | export interface SerializableLeaves {} |
| 27 | |
| 28 | type Leaf = string | number | boolean | null | undefined | SerializableLeaves[keyof SerializableLeaves]; |
| 29 | |
| 30 | /** |
| 31 | * Validates that `T` is serializable: every leaf is a registered {@link Leaf} |
| 32 | * and nested objects/arrays recurse. Enforced at `define` for `auth: true` |
| 33 | * arguments; unlike a concrete value type it accepts `interface` arguments, |
| 34 | * because the object case is a mapped type over `keyof T` and so needs no index |
| 35 | * signature the way `{ [k: string]: ... }` would. Any non-serializable member |
| 36 | * (a function/method, an unregistered class) collapses to `never`. |
| 37 | */ |
| 38 | export type Serializable<T> = [T] extends [Leaf] ? T |
| 39 | : T extends (...args: never[]) => unknown ? never |
| 40 | : T extends readonly unknown[] ? { [I in keyof T]: Serializable<T[I]> } |
| 41 | : T extends object ? { [K in keyof T]: Serializable<T[K]> } |
| 42 | : never; |
| 43 | |
| 44 | /** |
| 45 | * A concrete serializable value, driven by the {@link SerializableLeaves} |
| 46 | * registry. Stored in a {@link MutationAction} for replay after sign-in. |
| 47 | */ |
| 48 | export type SerializableValue = |
| 49 | | Leaf |
| 50 | | SerializableValue[] |
| 51 | | { [key: string]: SerializableValue }; |
| 52 | |
| 53 | /** |
| 54 | * A serializable record of a mutation call. Produced when an authenticated |
| 55 | * mutation is attempted without a user; pass it to `MutationClient.run` |
| 56 | * after sign-in to re-run the call. |
| 57 | */ |
| 58 | export interface MutationAction { |
| 59 | id: string; |
| 60 | description: string; |
| 61 | args: SerializableValue[]; |
| 62 | } |
| 63 | |
| 64 | export interface Mutation<Args extends unknown[], Result> { |
| 65 | /** Unique identifier passed to `define`. */ |
| 66 | readonly id: string; |
| 67 | /** Calling the mutation. Errors are turned into UI toasts. */ |
| 68 | run(...args: Args): void; |
| 69 | /** Calls the mutation with custom handlers that can suppress global handlers. */ |
| 70 | runWithOptions(...args: [...args: Args, options: RunOptions<Result>]): void; |
| 71 | /** |
| 72 | * Using this in any situation is likely incorrect, even internally. |
| 73 | * Use {@linkcode runWithOptions} to handle success and error. |
| 74 | * |
| 75 | * By using this, you must handle the success and error conditions of the |
| 76 | * promise, or else the user will never see the result on screen. If the |
| 77 | * mutation has snapshots, be aware that cancelled mutations are implemented |
| 78 | * with promises that never resolve. |
| 79 | */ |
| 80 | runAsHeadlessPromise(...array: [...Args, RunOptions<Result>]): Promise<Result>; |
| 81 | |
| 82 | /** Returns the concurrency key used for a given set of arguments */ |
| 83 | key(args: Args): string; |
| 84 | /** Subscribe to status changes using the key from `key()` */ |
| 85 | subscribe( |
| 86 | key: string, |
| 87 | cb: (update: MutationEvent<Result>) => void, |
| 88 | ): () => void; |
| 89 | describe(...args: Args): string; |
| 90 | describeResult: ((args: Args, result: Result) => string | undefined) | null; |
| 91 | /** |
| 92 | * `true` when an `auth: true` mutation has no user available. Scoped |
| 93 | * mutations are excluded; they disable instead of routing to the sign-in flow. |
| 94 | */ |
| 95 | isUnauthenticated(): boolean; |
| 96 | isAllowed(): boolean; |
| 97 | subscribeAllowed(cb: () => void): () => void; |
| 98 | client: MutationClient<object, object, object>; |
| 99 | } |
| 100 | |
| 101 | export interface RunOptions<Result> { |
| 102 | /** Called to show the success UI. Passing this suppresses the global success handler. */ |
| 103 | onSuccessUi?: (result: Result) => void; |
| 104 | /** |
| 105 | * Called with result data, but unlike `onSuccessUi`, this indicates the |
| 106 | * caller is not concerned with the UI flow of the success. Passing this |
| 107 | * does NOT suppress the global success handler. |
| 108 | */ |
| 109 | onSuccessData?: (result: Result) => void; |
| 110 | /** Called on error, suppresses the global error handler */ |
| 111 | onError?: (error: unknown) => void; |
| 112 | /** Called on settled (doesn't suppress global handlers) */ |
| 113 | onSettled?: ( |
| 114 | status: |
| 115 | | { status: "success"; result: Result } |
| 116 | | { status: "error"; error: unknown }, |
| 117 | ) => void; |
| 118 | /** Called when optimistic state is being restored/rolled back */ |
| 119 | onRestore?: () => void; |
| 120 | /** |
| 121 | * Called instead of running an authenticated mutation when no user is |
| 122 | * available. The action is JSON serializable and can be passed to |
| 123 | * `MutationClient.run` after sign-in. |
| 124 | */ |
| 125 | onUnauthenticated?: (action: MutationAction, mutation: Mutation<SerializableValue[], unknown>) => void; |
| 126 | } |
| 127 | |
| 128 | export interface MutationEvent<Result> { |
| 129 | status: "idle" | "waiting" | "mutating" | "refetching" | "skipped"; |
| 130 | result: Result | null; |
| 131 | error: unknown; |
| 132 | debounced: boolean; |
| 133 | } |