@voltro/client 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -384,6 +384,14 @@ export declare interface ClientTraceEvent {
384
384
 
385
385
  declare type ClientTraceListener = (event: ClientTraceEvent) => void;
386
386
 
387
+ /** A compensation that itself failed. Surfaced rather than swallowed: the app
388
+ * now has an orphan it may need to reconcile, and hiding that is how orphans
389
+ * become permanent. */
390
+ export declare interface CompensationFailure {
391
+ readonly step: string;
392
+ readonly error: unknown;
393
+ }
394
+
387
395
  /** Pure virtualization math: scroll position → the row window to fetch +
388
396
  * the spacer heights. `total` (when known) caps the window + sizes the bottom
389
397
  * spacer; without it, infinite-scroll (bottomSpacer 0). */
@@ -1115,6 +1123,31 @@ export declare class RpcErrorBus {
1115
1123
 
1116
1124
  export declare type RpcErrorListener = (event: RpcError) => void;
1117
1125
 
1126
+ /**
1127
+ * Run a sequence. Resolves with a discriminated result rather than rejecting —
1128
+ * the same "handled" semantics `useMutation`/`useAction` use, so the call site
1129
+ * loses its try/catch without losing the failure.
1130
+ */
1131
+ export declare const runSequence: <Ctx>(builder: SequenceBuilder<Ctx>, options?: RunSequenceOptions) => Promise<SequenceResult<Ctx>>;
1132
+
1133
+ export declare interface RunSequenceOptions {
1134
+ /** Called once, with the original failure. The whole point of the primitive:
1135
+ * one handler for the sequence instead of a try/catch per step. */
1136
+ readonly onError?: (error: unknown, detail: {
1137
+ readonly failedStep: string;
1138
+ readonly compensated: ReadonlyArray<string>;
1139
+ readonly compensationFailures: ReadonlyArray<CompensationFailure>;
1140
+ readonly compensationCovered: ReadonlyArray<string>;
1141
+ readonly compensationUncertain: ReadonlyArray<{
1142
+ readonly step: string;
1143
+ readonly coveredBy: string;
1144
+ }>;
1145
+ }) => void;
1146
+ /** Skip compensation entirely. For a sequence whose steps are all idempotent
1147
+ * upserts, undoing is churn rather than cleanup. */
1148
+ readonly compensate?: boolean;
1149
+ }
1150
+
1118
1151
  /**
1119
1152
  * Derive table COLUMNS from a query's output `Schema` — an
1120
1153
  * `Schema.Array(Schema.Struct({...}))`. Drills into the array's element and maps
@@ -1137,6 +1170,52 @@ export declare interface SeqElement<E> {
1137
1170
  readonly event: E;
1138
1171
  }
1139
1172
 
1173
+ /** Start a sequence. Pure and React-free, so it is testable on its own. */
1174
+ export declare const sequence: () => SequenceBuilder<Record<never, never>>;
1175
+
1176
+ /**
1177
+ * A sequence under construction. Each `.step()` widens the context type, so a
1178
+ * later step reads earlier results by name with real types — `linkThread` can
1179
+ * see both `ctx.ticket` and `ctx.thread` without threading them by hand.
1180
+ */
1181
+ export declare interface SequenceBuilder<Ctx> {
1182
+ /** A CONDITIONAL step. `Result | undefined` in the context, because it may not
1183
+ * have run — later steps have to say what they do about that. */
1184
+ step<Name extends string, Result>(name: Name, run: (ctx: Ctx) => Result | Promise<Result>, options: StepOptions<Ctx, Result> & {
1185
+ readonly when: (ctx: Ctx) => boolean;
1186
+ }): SequenceBuilder<Ctx & {
1187
+ readonly [K in Name]: Result | undefined;
1188
+ }>;
1189
+ step<Name extends string, Result>(name: Name, run: (ctx: Ctx) => Result | Promise<Result>, options?: StepOptions<Ctx, Result>): SequenceBuilder<Ctx & {
1190
+ readonly [K in Name]: Result;
1191
+ }>;
1192
+ /* Excluded from this release type: steps */
1193
+ }
1194
+
1195
+ export declare type SequenceResult<Ctx> = {
1196
+ readonly ok: true;
1197
+ readonly data: Ctx;
1198
+ } | {
1199
+ readonly ok: false;
1200
+ /** The step whose `run` threw. Its `undo` was NOT called — see the header. */
1201
+ readonly failedStep: string;
1202
+ /** The original failure. Never replaced by a compensation error. */
1203
+ readonly error: unknown;
1204
+ /** Names of the steps whose `undo` ran, newest first. */
1205
+ readonly compensated: ReadonlyArray<string>;
1206
+ /** Undos that threw. Non-empty means something is left over upstream. */
1207
+ readonly compensationFailures: ReadonlyArray<CompensationFailure>;
1208
+ /** Steps whose `undo` was skipped because a later step's `covers` names
1209
+ * them and that undo SUCCEEDED. They are reversed; nothing to do. */
1210
+ readonly compensationCovered: ReadonlyArray<string>;
1211
+ /** Steps whose covering undo FAILED. Neither reversed nor known to be —
1212
+ * the honest third answer, and the one worth reconciling. */
1213
+ readonly compensationUncertain: ReadonlyArray<{
1214
+ readonly step: string;
1215
+ readonly coveredBy: string;
1216
+ }>;
1217
+ };
1218
+
1140
1219
  /** Register (or clear) the app-wide notifier that `notify:` routes to. Call once
1141
1220
  * at boot, next to your toast provider. */
1142
1221
  export declare const setMutationNotifier: (notifier: MutationNotifier | undefined) => void;
@@ -1191,6 +1270,58 @@ export declare const stableKey: (key: ReadonlyArray<unknown>) => string;
1191
1270
  */
1192
1271
  export declare const startMutation: (args: Pick<MutationEvent, "id" | "apiName" | "tag" | "input">) => void;
1193
1272
 
1273
+ declare interface Step {
1274
+ readonly name: string;
1275
+ readonly run: (ctx: Record<string, unknown>) => unknown | Promise<unknown>;
1276
+ readonly undo: StepUndo<never> | undefined;
1277
+ readonly covers: ReadonlyArray<string>;
1278
+ readonly when: ((ctx: Record<string, unknown>) => boolean) | undefined;
1279
+ }
1280
+
1281
+ export declare interface StepOptions<Ctx, Result> {
1282
+ /** How to reverse this step if a LATER step fails. Omit when the step has no
1283
+ * externally visible effect worth reversing.
1284
+ *
1285
+ * **Your undo may be called while an earlier step's undo also runs.** They
1286
+ * are independent by default — see `covers` when one already reverses the
1287
+ * other, and the module header for why the runner cannot infer it. */
1288
+ readonly undo?: StepUndo<Result>;
1289
+ /**
1290
+ * Earlier steps this step's `undo` ALREADY reverses. Their undos are skipped.
1291
+ *
1292
+ * The case that produced this: `deleteJiraDraftTicket` deletes the issue AND
1293
+ * discards the draft, so the earlier `discardDraft` undo ran a second time.
1294
+ * That was harmless only because discarding is idempotent — for a refund or a
1295
+ * cancellation email the double-run is a real defect, and the runner has no
1296
+ * way to know which kind it is looking at.
1297
+ *
1298
+ * .step('draft', createDraft, { undo: discardDraft })
1299
+ * .step('jira', createTicket, { undo: deleteJiraDraftTicket, covers: ['draft'] })
1300
+ *
1301
+ * If the covering undo FAILS, the covered steps are neither run nor claimed:
1302
+ * whether the cascade got that far is genuinely unknown, and guessing either
1303
+ * way is wrong. They come back in `compensationUncertain` so the app can
1304
+ * reconcile instead of finding out later.
1305
+ */
1306
+ readonly covers?: ReadonlyArray<keyof Ctx & string>;
1307
+ /**
1308
+ * Run this step only when the predicate holds. A skipped step contributes
1309
+ * `undefined` to the context (the type says so) and gets no `undo`.
1310
+ *
1311
+ * Narrow on purpose: this is for a sequence that is linear EXCEPT for one
1312
+ * optional step — "save, then schedule the summary only if the set changed",
1313
+ * `if (assigneeKey) assign else unassign`. Loops and real branches still keep
1314
+ * their `try/catch`; an optional step is a different shape from either, and
1315
+ * without this the whole flow fell back even though the rest was a clean
1316
+ * pipeline.
1317
+ */
1318
+ readonly when?: (ctx: Ctx) => boolean;
1319
+ }
1320
+
1321
+ /** One step's compensation. Receives the step's OWN result, which is the point:
1322
+ * `undo: (created) => remove.run({ id: created.id })`. */
1323
+ export declare type StepUndo<Result> = (result: Result) => unknown | Promise<unknown>;
1324
+
1194
1325
  /** Subscribe to client errors. Returns an unsubscribe fn. */
1195
1326
  export declare const subscribeClientErrors: (listener: ClientErrorListener) => (() => void);
1196
1327
 
@@ -1350,6 +1481,34 @@ export declare interface SubscriptionCacheOptions {
1350
1481
  readonly errorBus?: RpcErrorBus;
1351
1482
  }
1352
1483
 
1484
+ /**
1485
+ * Deliberately NOT subscribed — `skip: true`.
1486
+ *
1487
+ * This state exists because `loading` was answering two different questions
1488
+ * with one boolean: "waiting for the first snapshot" and "not asking at all".
1489
+ * A skipped subscription used to report `loading: true` forever, so the blessed
1490
+ * pattern
1491
+ *
1492
+ * if (loading) return <Skeleton/>
1493
+ *
1494
+ * rendered a permanent skeleton for a query the app had consciously switched
1495
+ * off — and `skip: !currentUser?.id` / `skip: !open` are among the most common
1496
+ * things anyone writes. Call sites only survived it by carrying a redundant
1497
+ * `if (!currentUser) return null` in front, which is the very check `loading`
1498
+ * was supposed to replace.
1499
+ *
1500
+ * `loading` is `false` here: nothing is in flight. `data` is `undefined`,
1501
+ * which is why this state is only handed to callers that actually pass a
1502
+ * dynamic `skip` — see the overloads. Everyone else keeps the two-state union
1503
+ * where `!loading` still proves `data` is present.
1504
+ */
1505
+ export declare interface SubscriptionIdle extends SubscriptionMeta {
1506
+ readonly data: undefined;
1507
+ readonly loading: false;
1508
+ readonly isEmpty: false;
1509
+ readonly idle: true;
1510
+ }
1511
+
1353
1512
  /** The fields that mean the same thing in every subscription state. */
1354
1513
  export declare interface SubscriptionMeta {
1355
1514
  /** Server revision counter; -1 before first snapshot. */
@@ -1407,6 +1566,9 @@ export declare interface SubscriptionOptions<T = unknown> {
1407
1566
  export declare interface SubscriptionPending extends SubscriptionMeta {
1408
1567
  readonly data: undefined;
1409
1568
  readonly loading: true;
1569
+ /** Always `false` — this subscription IS asking, it just has no answer yet.
1570
+ * See {@link SubscriptionIdle} for the deliberately-not-asking case. */
1571
+ readonly idle: false;
1410
1572
  /** Always `false` while pending. "Empty" is a statement about data that
1411
1573
  * ARRIVED; before then it would be a guess, and rendering an empty state on
1412
1574
  * that guess is the flash-of-empty-state bug. */
@@ -1417,6 +1579,8 @@ export declare interface SubscriptionPending extends SubscriptionMeta {
1417
1579
  export declare interface SubscriptionSettled<T> extends SubscriptionMeta {
1418
1580
  readonly data: T;
1419
1581
  readonly loading: false;
1582
+ /** Always `false` — data arrived. */
1583
+ readonly idle: false;
1420
1584
  /** The data that arrived is empty — an empty array, or a null value. */
1421
1585
  readonly isEmpty: boolean;
1422
1586
  }
@@ -1451,6 +1615,10 @@ export declare type SubscriptionState<T> = SubscriptionPending | SubscriptionSet
1451
1615
  export declare interface SubscriptionStateWithFallback<T> extends SubscriptionMeta {
1452
1616
  readonly data: T;
1453
1617
  readonly loading: boolean;
1618
+ /** True when `skip` is on. `data` is the fallback, and no request is in
1619
+ * flight — distinguishes "showing the fallback because we chose not to ask"
1620
+ * from "…because the answer has not arrived". */
1621
+ readonly idle: boolean;
1454
1622
  readonly isEmpty: boolean;
1455
1623
  }
1456
1624
 
@@ -1854,11 +2022,54 @@ export declare interface UseResourceCanState {
1854
2022
 
1855
2023
  export declare const useResumableAgentStream: <E = unknown>(apiName: string, rpcTag: string, options?: ResumableAgentStreamOptions<E>) => ResumableAgentStreamControls<E>;
1856
2024
 
2025
+ /**
2026
+ * @param options Sequence-wide defaults — most usefully `onError`, which fires
2027
+ * once per failed sequence instead of once per step.
2028
+ *
2029
+ * ```tsx
2030
+ * const seq = useSequence({ onError: (e) => notify.error(String(e)) })
2031
+ *
2032
+ * const attach = async (file: File) => {
2033
+ * const result = await seq.run(
2034
+ * sequence()
2035
+ * .step('upload', () => upload.run({ file }), {
2036
+ * undo: (r) => removeObject.run({ id: r.id }), // the created id, right here
2037
+ * })
2038
+ * .step('attach', (c) => createAttachment.run({ refId: c.upload.id })),
2039
+ * )
2040
+ * if (result.ok) setAttachmentId(result.data.attach.id)
2041
+ * }
2042
+ * ```
2043
+ *
2044
+ * Compensation is best-effort and tab-bound — see `sequence.ts`. When the
2045
+ * rollback has to survive a closed tab, move the sequence into a workflow.
2046
+ */
2047
+ export declare const useSequence: (options?: RunSequenceOptions) => UseSequenceResult;
2048
+
2049
+ export declare interface UseSequenceResult {
2050
+ /** Run a built sequence. Resolves with the discriminated result; it does not
2051
+ * reject, which is what removes the try/catch. */
2052
+ readonly run: <Ctx>(builder: SequenceBuilder<Ctx>, options?: RunSequenceOptions) => Promise<SequenceResult<Ctx>>;
2053
+ /** True while a sequence is in flight. */
2054
+ readonly pending: boolean;
2055
+ /** The last sequence's original failure, or `undefined` after a success. */
2056
+ readonly error: unknown | undefined;
2057
+ /** The step that failed last time, for a targeted message ("couldn't create
2058
+ * the ticket") rather than a generic one. */
2059
+ readonly failedStep: string | undefined;
2060
+ }
2061
+
1857
2062
  export declare function useSubscription<T = unknown>(apiName: string, rpcTag: string, input: Readonly<Record<string, unknown>>, options: SubscriptionOptions<T> & {
1858
2063
  readonly fallback: T;
1859
2064
  }): SubscriptionStateWithFallback<T>;
1860
2065
 
1861
- export declare function useSubscription<T = unknown>(apiName: string, rpcTag: string, input?: Readonly<Record<string, unknown>>, options?: SubscriptionOptions<T>): SubscriptionState<T>;
2066
+ export declare function useSubscription<T = unknown>(apiName: string, rpcTag: string, input?: Readonly<Record<string, unknown>>, options?: SubscriptionOptions<T> & {
2067
+ readonly skip?: false;
2068
+ }): SubscriptionState<T>;
2069
+
2070
+ export declare function useSubscription<T = unknown>(apiName: string, rpcTag: string, input: Readonly<Record<string, unknown>>, options: SubscriptionOptions<T> & {
2071
+ readonly skip?: boolean;
2072
+ }): SubscriptionState<T> | SubscriptionIdle;
1862
2073
 
1863
2074
  /** The column shape a `<DataTable query=…>` will render — from the query's
1864
2075
  * output Schema. */
@@ -2012,11 +2223,11 @@ export declare interface WorkflowClientMessages {
2012
2223
  }>>;
2013
2224
  }
2014
2225
 
2015
- export declare type WorkflowDomainEventsState = SubscriptionState<ReadonlyArray<WorkflowDomainEventRow>> & {
2226
+ export declare type WorkflowDomainEventsState = (SubscriptionState<ReadonlyArray<WorkflowDomainEventRow>> | SubscriptionIdle) & {
2016
2227
  readonly events: ReadonlyArray<WorkflowDomainEventRow>;
2017
2228
  };
2018
2229
 
2019
- export declare type WorkflowEventDeliveriesState = SubscriptionState<ReadonlyArray<WorkflowEventDeliveryRow>> & {
2230
+ export declare type WorkflowEventDeliveriesState = (SubscriptionState<ReadonlyArray<WorkflowEventDeliveryRow>> | SubscriptionIdle) & {
2020
2231
  readonly deliveries: ReadonlyArray<WorkflowEventDeliveryRow>;
2021
2232
  };
2022
2233
 
@@ -2029,7 +2240,7 @@ export declare interface WorkflowRunError {
2029
2240
 
2030
2241
  export { WorkflowRunEventRow }
2031
2242
 
2032
- export declare type WorkflowRunEventsState = SubscriptionState<ReadonlyArray<WorkflowRunEventRow>> & {
2243
+ export declare type WorkflowRunEventsState = (SubscriptionState<ReadonlyArray<WorkflowRunEventRow>> | SubscriptionIdle) & {
2033
2244
  readonly events: ReadonlyArray<WorkflowRunEventRow>;
2034
2245
  };
2035
2246
 
@@ -2041,11 +2252,16 @@ export declare interface WorkflowRunsFilter {
2041
2252
  readonly limit?: number;
2042
2253
  }
2043
2254
 
2044
- export declare type WorkflowRunsState = SubscriptionState<ReadonlyArray<WorkflowRunRow>> & {
2255
+ export declare type WorkflowRunsState = (SubscriptionState<ReadonlyArray<WorkflowRunRow>> | SubscriptionIdle) & {
2045
2256
  readonly runs: ReadonlyArray<WorkflowRunRow>;
2046
2257
  };
2047
2258
 
2048
- export declare type WorkflowRunState = SubscriptionState<ReadonlyArray<WorkflowRunRow>> & {
2259
+ /** `SubscriptionIdle` is in the union because `id === undefined` skips: asking
2260
+ * for "the run with no id" is not a pending request, it is no request. Before
2261
+ * the idle state existed this reported `loading: true` forever, so a caller
2262
+ * following the blessed `if (loading) return <Skeleton/>` rendered a permanent
2263
+ * skeleton whenever nothing was selected. */
2264
+ export declare type WorkflowRunState = (SubscriptionState<ReadonlyArray<WorkflowRunRow>> | SubscriptionIdle) & {
2049
2265
  readonly run: WorkflowRunRow | undefined;
2050
2266
  };
2051
2267
 
@@ -2075,7 +2291,7 @@ export { WorkflowRunStatus }
2075
2291
 
2076
2292
  export { WorkflowRunStepRow }
2077
2293
 
2078
- export declare type WorkflowRunStepsState = SubscriptionState<ReadonlyArray<WorkflowRunStepRow>> & {
2294
+ export declare type WorkflowRunStepsState = (SubscriptionState<ReadonlyArray<WorkflowRunStepRow>> | SubscriptionIdle) & {
2079
2295
  readonly steps: ReadonlyArray<WorkflowRunStepRow>;
2080
2296
  };
2081
2297