@civitai/blocks-react 0.43.0 → 0.44.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.
@@ -111,46 +111,57 @@ export interface WatchWorkflowOptions {
111
111
  * reproduces the bug one layer down: a caller who logs `message` expecting the
112
112
  * server's words gets a constant and is exactly as stuck as before.
113
113
  *
114
- * - {@link WorkflowEstimateError.snapshot}`.error` — the server's own words,
115
- * verbatim. **Server-authored and UNSANITISED**: civitai's `errorHandling.ts`
116
- * documents that raw upstream text — Prisma/`pg` column and constraint names
117
- * among it — can reach this field. Log it, or show it in a developer-facing
118
- * error surface; do NOT render it as trusted copy.
119
- * - {@link WorkflowEstimateError.message} — a CONSTANT template with only
120
- * `code` interpolated. It contains no server text and nothing to sanitise, so
121
- * it is safe to print. That is deliberate: `message` is what an uncaught
114
+ * 🔴 THREE FIELDS, THREE AUDIENCES — AND **NONE OF THEM IS VIEWER-FACING COPY**.
115
+ * The viewer-facing string is one the APP owns; this error carries nothing you
116
+ * may render as-is. Two apps migrating to `0.43.0` each had a `catch` that piped
117
+ * `err.message` straight into rendered UI, and each would have shipped
118
+ * `estimate did not return a usable price (failed) — reason on .snapshot.error`
119
+ * to end users; both were caught only by a test asserting that exact string.
120
+ *
121
+ * - {@link WorkflowEstimateError.snapshot}`.error` — **the DIAGNOSTIC read.**
122
+ * The server's own words, verbatim. **Server-authored and UNSANITISED**:
123
+ * civitai's `errorHandling.ts` documents that raw upstream text —
124
+ * Prisma/`pg` column and constraint names among it — can reach this field.
125
+ * Log it, or show it in a developer-facing error surface. **NEVER render it
126
+ * verbatim into markup**, and think before shipping it to a third-party
127
+ * error tracker.
128
+ * - {@link WorkflowEstimateError.message} — **DEVELOPER-FACING.** A CONSTANT
129
+ * template with only `code` interpolated. It contains no server text and
130
+ * nothing to sanitise, so it is safe to LOG and safe to let a stack trace
131
+ * print — that is deliberate, because `message` is what an uncaught
122
132
  * rejection prints and what a third-party block's error reporter ships
123
133
  * upstream by default, and this is a package consumed by third-party code.
124
- * Its exact wording is NOT a contract.
125
- * - {@link WorkflowEstimateError.code} — the only stable branch target.
134
+ * **It is NOT intended for display to viewers**: it names an internal field
135
+ * path, it is not localised, and **its exact wording is NOT a contract** —
136
+ * it can change in any release, so a UI built on it silently rots.
137
+ * - {@link WorkflowEstimateError.code} — **the BRANCH TARGET**, and the only
138
+ * stable one. Switch on it to pick a viewer-facing string YOUR APP owns
139
+ * (see the mapper in the `@example` on {@link useBuzzWorkflow}).
126
140
  *
127
141
  * 🔴 DO NOT "SIMPLIFY" THIS BY PUTTING `snapshot.error` BACK ON `message`. The
128
142
  * two fields are split on purpose and the split is what keeps database internals
129
143
  * off a third party's default-printed surface. `test/useBuzzWorkflow.test.tsx`
130
144
  * pins it with a realistic `Unique constraint failed: Key (email)=(…)` fixture.
131
145
  *
132
- * 🔴 NOT APPLIED TO `submit`, and the reason is narrower than it looks. On
133
- * `WORKFLOW_SUBMITTED` there are likewise TWO producers, indistinguishable by
134
- * `status`: budget/cap REJECTIONS, which are a documented outcome the block
135
- * recovers from (open a top-up flow) and which carry a `cost`; and caught server
136
- * exceptions posted via the SAME `failureSnapshot(err)` used above, which do not.
137
- * The `#4159` defect is therefore live on `submit` too, discriminated by `cost`
138
- * presence rather than by `status`. Fixing it there is a separate change with its
139
- * own blast radius on the recovery path (a fix MUST keep the budget-rejection arm
140
- * RESOLVING, or the top-up flow breaks) and is deliberately NOT attempted here —
141
- * so `submit` is left resolving BOTH, and the accompanying test pins only the
142
- * budget-rejection producer, not a claim that submit is correct.
143
- *
144
- * 🔴 TRACKED, NOT FORGOTTEN: civitai/civitai-app-starters#251. Read it before
145
- * touching `submit`'s failure handling.
146
+ * 🔴 `submit` HAS ITS OWN CLASS — {@link WorkflowSubmitError} — BECAUSE ITS
147
+ * DISCRIMINATOR IS DIFFERENT. On `WORKFLOW_SUBMITTED` both producers report
148
+ * `status:'failed'`, so `status` separates nothing there and `cost` presence is
149
+ * what does. Do not merge the two classes or reuse this one for submit: the
150
+ * question each answers is different ("was a usable price returned?" vs "was a
151
+ * workflow actually queued?") and so is the arm that must keep resolving.
152
+ * (civitai/civitai-app-starters#251, the `submit` half of civitai/civitai#4159.)
146
153
  */
147
154
  export declare class WorkflowEstimateError extends Error {
148
155
  /**
149
156
  * WHICH PRODUCER this was, structurally — so a caller branches on an enum
150
157
  * rather than on prose. It is the ONLY stable branch target here:
151
158
  * `snapshot.error` is server-authored and can change without notice, and
152
- * {@link WorkflowEstimateError.message} is a generic summary whose exact
153
- * wording is not a contract either.
159
+ * {@link WorkflowEstimateError.message} is a generic developer-facing summary
160
+ * whose exact wording is not a contract either.
161
+ *
162
+ * 🔴 THIS IS WHAT A VIEWER-FACING MESSAGE MUST BE DERIVED FROM. Branch on
163
+ * `code` and return a string your app owns and localises; never render
164
+ * `message` or `snapshot.error` to a viewer.
154
165
  *
155
166
  * - `'failed'` — the reply's `status` is `'failed'`. USUALLY that means
156
167
  * `blocks.estimateWorkflow` threw server-side and the host posted its
@@ -175,12 +186,176 @@ export declare class WorkflowEstimateError extends Error {
175
186
  * will not price — the whole point of civitai/civitai#4159), but civitai's
176
187
  * `errorHandling.ts` documents that raw upstream text — Prisma/`pg` column and
177
188
  * constraint names among it — can reach that field. Log it, show it in a
178
- * developer-facing error surface, do NOT render it as trusted copy and do not
179
- * ship it to a third-party error tracker without thinking about it.
189
+ * developer-facing error surface, **NEVER render it verbatim into markup**,
190
+ * and do not ship it to a third-party error tracker without thinking about it.
180
191
  */
181
192
  readonly snapshot: BlockWorkflowSnapshot;
182
193
  constructor(snapshot: BlockWorkflowSnapshot, code: 'failed' | 'no-cost');
183
194
  }
195
+ /**
196
+ * Why {@link UseBuzzWorkflowReturn.submit} rejected. **The two differ on whether
197
+ * money may already have moved** — see {@link WorkflowSubmitError.code}.
198
+ */
199
+ export type WorkflowSubmitErrorCode = 'exception' | 'workflow-failed';
200
+ /**
201
+ * Thrown by {@link UseBuzzWorkflowReturn.submit} when the host's reply carries no
202
+ * usable workflow outcome — either the submit ERRORED before anything was queued,
203
+ * or a workflow-shaped reply came back already failed with no price.
204
+ *
205
+ * 🔴 READ {@link WorkflowSubmitError.code} BEFORE SAYING ANYTHING ABOUT MONEY.
206
+ * The two codes differ on exactly that, and getting it wrong is expensive in both
207
+ * directions — see the per-code notes below.
208
+ *
209
+ * 🔴 WHY THIS EXISTS — civitai/civitai-app-starters#251, the `submit` half of
210
+ * civitai/civitai#4159. A server-side `blocks.submitWorkflow` throw cannot reach
211
+ * the block as a rejection; the reply crosses `postMessage`. The host instead
212
+ * posts a well-formed `WORKFLOW_SUBMITTED` carrying its `failureSnapshot(err)` —
213
+ * `{ workflowId: 'failed', status: 'failed', error: '<server message>' }`, with
214
+ * **no `cost`**. That snapshot is perfectly legal, so `submit()` resolved it and
215
+ * moved the hook to `'done'`, handing the caller a "workflow" that does not
216
+ * exist.
217
+ *
218
+ * 🔴 TWO PRODUCERS, ONE `status` — AND HERE `status` DISCRIMINATES NOTHING. This
219
+ * is what makes submit's version of the defect different from estimate's, and
220
+ * why {@link WorkflowEstimateError}'s guard could not simply be copied:
221
+ *
222
+ * - **budget / spend-cap REJECTION** — a legitimate OUTCOME. The server quotes
223
+ * the price it refused to charge, so the snapshot carries a numeric
224
+ * `cost.total`. Blocks recover from it (open a top-up flow), so it MUST keep
225
+ * RESOLVING. Every such exit on the server attaches a cost: the per-call
226
+ * `buzzBudget` gate, the per-user daily Buzz cap, the per-app aggregate and
227
+ * velocity caps, and the dev-tunnel session cap — on all three body kinds.
228
+ * - **caught server EXCEPTION** — `failureSnapshot(err)`, no `cost`, and the
229
+ * `'failed'` sentinel id. The host had no workflow to report — usually
230
+ * nothing was queued, but a lost response or an in-progress idempotency
231
+ * conflict lands here too. Rejects as `'exception'`.
232
+ * - **a reply that came back failed and unpriced with a NON-sentinel id** —
233
+ * normally a genuine orchestrator id, no `cost` (the server's
234
+ * `snapshotFromWorkflow` omits the key whenever `workflow.cost?.total` is not
235
+ * numeric). Rejects as `'workflow-failed'`, and **money may already be
236
+ * committed** — see that code's note.
237
+ *
238
+ * So the rule is `cost` presence, and ONLY among failure-shaped replies: an
239
+ * ordinary in-flight submit (`{ workflowId:'wf_…', status:'pending' }`) is
240
+ * cost-less too and must keep resolving, which is why the guard is not keyed on
241
+ * `cost` alone either. Both clauses are load-bearing; each has its own control
242
+ * in `test/useBuzzWorkflow.test.tsx`.
243
+ *
244
+ * 🔴 THE GUARD IS DELIBERATELY NOT WIDENED TO ALL TERMINAL STATUSES. A cost-less
245
+ * `succeeded` / `canceled` / `expired` reply is a legitimate outcome and must
246
+ * RESOLVE; only `'failed'` is unusable. Swapping the clause for
247
+ * `TERMINAL_STATUSES.has(...)` is a silent WIDENING that a delete-only mutation
248
+ * sweep cannot see — `test/useBuzzWorkflow.test.tsx` pins each of those three
249
+ * statuses explicitly for exactly that reason.
250
+ *
251
+ * 🔴 THREE FIELDS, THREE AUDIENCES — AND **NONE OF THEM IS VIEWER-FACING COPY**,
252
+ * exactly as on {@link WorkflowEstimateError}. Two apps migrating to `0.43.0`
253
+ * piped that class's `err.message` straight into rendered UI
254
+ * (civitai/civitai-app-starters#253); the same split applies here so the same
255
+ * mistake is not re-enabled on the submit path.
256
+ *
257
+ * - {@link WorkflowSubmitError.snapshot}`.error` — **the DIAGNOSTIC read.** The
258
+ * server's own words, verbatim. **Server-authored and UNSANITISED**:
259
+ * civitai's `errorHandling.ts` documents that raw upstream text —
260
+ * Prisma/`pg` column and constraint names among it — can reach this field.
261
+ * Log it, or show it in a developer-facing error surface. **NEVER render it
262
+ * verbatim into markup**, and think before shipping it to a third-party
263
+ * error tracker.
264
+ * - {@link WorkflowSubmitError.message} — **DEVELOPER-FACING.** A CONSTANT
265
+ * template with only `code` interpolated. It contains no server text, so it
266
+ * is safe to LOG and safe to let a stack trace print — deliberate, because
267
+ * `message` is what an uncaught rejection prints and what a third-party
268
+ * block's error reporter ships upstream by default. **It is NOT intended for
269
+ * display to viewers** and **its exact wording is NOT a contract.**
270
+ * - {@link WorkflowSubmitError.code} — **the BRANCH TARGET**, and the only
271
+ * stable one.
272
+ *
273
+ * 🔴 DO NOT "SIMPLIFY" THIS BY PUTTING `snapshot.error` BACK ON `message`, and do
274
+ * not widen the guard to reject a budget rejection. The first re-opens #253; the
275
+ * second breaks the top-up recovery flow, which is the one thing #251 says must
276
+ * not break.
277
+ */
278
+ export declare class WorkflowSubmitError extends Error {
279
+ /**
280
+ * WHICH PRODUCER this was, structurally — so a caller branches on an enum
281
+ * rather than on prose. It is the ONLY stable branch target here:
282
+ * `snapshot.error` is server-authored and can change without notice, and
283
+ * {@link WorkflowSubmitError.message} is a generic developer-facing summary
284
+ * whose exact wording is not a contract either.
285
+ *
286
+ * 🔴 THE TWO CODES DIFFER ON WHETHER MONEY MOVED. Do not collapse them, and do
287
+ * not write recovery copy that ignores the distinction.
288
+ *
289
+ * - `'exception'` — the host built this reply itself
290
+ * (`failureSnapshot(err)`, id {@link HOST_SYNTHESISED_WORKFLOW_ID}), from a
291
+ * `catch` OR from a non-catch short-circuit such as the moderator-review
292
+ * nack. **It means the host had no workflow to report — NOT that nothing
293
+ * happened.**
294
+ *
295
+ * 🔴 USUALLY nothing was queued and nothing was charged, and a retry is the
296
+ * sensible recovery. But the same shape is reachable when a workflow MAY have
297
+ * been created and charged, because the failure happened after the request
298
+ * left the block:
299
+ * - a **lost response** — the server's own catch concedes that "the
300
+ * orchestrator externalId dedupe still protects a retry that DID create a
301
+ * workflow server-side despite a lost response". What that path refunds
302
+ * is its own reservation and cap counters; the Buzz debit happens inside
303
+ * the orchestrator's submit.
304
+ * - an **in-progress idempotency CONFLICT** — a concurrent first attempt is
305
+ * still in flight and may be committing right now. Retrying blind is the
306
+ * worst option there.
307
+ * - a **transient transport failure** (5xx / 408 / 429 / 401), which the
308
+ * `dev:live` host collapses into this same shape.
309
+ *
310
+ * So prefer **reusing the same {@link SubmitWorkflowOptions.idempotencyKey}**
311
+ * on retry rather than letting a fresh one be minted, and do not render
312
+ * "nothing was charged" to a viewer as a certainty. There is no workflow id
313
+ * on this arm to poll.
314
+ *
315
+ * - `'workflow-failed'` — the id was NOT the host's sentinel, so the reply came
316
+ * from the server's `snapshotFromWorkflow`: normally a real orchestrator id,
317
+ * already `'failed'` and with no price.
318
+ *
319
+ * 🔴 **DO NOT TELL THE VIEWER NOTHING WAS CHARGED, AND DO NOT BLIND-RETRY.**
320
+ * Server-side, `blocks.submitWorkflow` treats *any resolved* submit as
321
+ * money-COMMITTED: its own comment is "A resolved submit is money-COMMITTED
322
+ * (the reservation is kept regardless of snapshot status)… we do NOT refund
323
+ * on a non-throwing failed snapshot", and `finalizeGenIdempotency` runs on
324
+ * that path. So Buzz may already be spent for this call.
325
+ *
326
+ * A retry is a SECOND reservation unless you reuse the same
327
+ * {@link SubmitWorkflowOptions.idempotencyKey} — `submit()` mints a fresh key
328
+ * per call by default, so an automatic retry double-reserves.
329
+ *
330
+ * 🔴 `err.snapshot.workflowId` IS USUALLY POLLABLE, BUT CHECK IT FIRST — this
331
+ * arm is defined by what the id is NOT. The server emits `workflow.id ??
332
+ * 'whatif'`, and it treats **both** `'failed'` and `'whatif'` as non-workflow
333
+ * sentinels (it skips its own persistence and settle steps on either). So
334
+ * `'whatif'` lands here — correctly, because the cautious money reading still
335
+ * applies — but there is nothing to poll. Guard with
336
+ * `err.snapshot.workflowId !== 'whatif'` before calling
337
+ * {@link UseBuzzWorkflowReturn.watch} / {@link UseBuzzWorkflowReturn.poll} to
338
+ * learn the workflow's actual fate before spending again.
339
+ *
340
+ * A union rather than a boolean so a future producer gets its own code without
341
+ * breaking a caller's `switch`.
342
+ *
343
+ * 🔴 A BUDGET REJECTION NEVER ARRIVES HERE. It resolves, and is read off the
344
+ * returned snapshot as `status === 'failed'` with a numeric `cost.total`.
345
+ */
346
+ readonly code: WorkflowSubmitErrorCode;
347
+ /**
348
+ * The snapshot the host replied with, VERBATIM — including `snapshot.error`,
349
+ * the server's own words, when there are any.
350
+ *
351
+ * 🔴 `snapshot.error` IS SERVER-AUTHORED AND UNSANITISED. It is where the
352
+ * reason lives, but raw upstream text — Prisma/`pg` column and constraint
353
+ * names among it — can reach that field. Log it, show it in a developer-facing
354
+ * error surface, **NEVER render it verbatim into markup**.
355
+ */
356
+ readonly snapshot: BlockWorkflowSnapshot;
357
+ constructor(snapshot: BlockWorkflowSnapshot, code: WorkflowSubmitErrorCode);
358
+ }
184
359
  /** Optional per-submit controls. */
185
360
  export interface SubmitWorkflowOptions {
186
361
  /**
@@ -189,6 +364,11 @@ export interface SubmitWorkflowOptions {
189
364
  * host+orchestrator collapse it to ONE Buzz charge instead of double-charging.
190
365
  * Omit → the hook generates a fresh key per `submit()` call (each call is a new
191
366
  * logical submit); pass a stable id (e.g. a grid-cell id) to make a retry safe.
367
+ *
368
+ * 🔴 THIS IS THE FIELD THAT MAKES A RETRY AFTER A `'workflow-failed'` REJECTION
369
+ * SAFE. That code means a workflow probably exists and its spend may already
370
+ * be committed server-side; retrying WITHOUT reusing the key mints a fresh one
371
+ * and therefore a SECOND reservation. See {@link WorkflowSubmitError.code}.
192
372
  */
193
373
  idempotencyKey?: string;
194
374
  }
@@ -216,6 +396,43 @@ interface UseBuzzWorkflowReturn {
216
396
  * estimate's price sitting in `result` for a Confirm gate to read.
217
397
  */
218
398
  estimate: (body: WorkflowBody) => Promise<BlockWorkflowSnapshot>;
399
+ /**
400
+ * Queue a workflow. Resolves ONLY with a reply that represents a real workflow
401
+ * OUTCOME — one that was queued, or one the server priced and then refused.
402
+ *
403
+ * 🔴 REJECTS with {@link WorkflowSubmitError} when the reply is failure-shaped
404
+ * and carries no price. Wrap every call in `try/catch`; see that class for why
405
+ * resolving such a reply was the `submit` half of civitai/civitai#4159.
406
+ * **Check `err.code` before saying anything about money** — and note that
407
+ * NEITHER code guarantees nothing was spent. `'exception'` usually means
408
+ * nothing was queued or charged, but a lost response or an in-progress
409
+ * idempotency conflict reaches it too; `'workflow-failed'` means a workflow
410
+ * PROBABLY exists (the `'whatif'` sentinel lands here too and has nothing to
411
+ * poll — guard before polling) and its spend may already be committed. Do not
412
+ * tell the viewer it was free, and on either code prefer reusing the same
413
+ * {@link SubmitWorkflowOptions.idempotencyKey}.
414
+ *
415
+ * 🔴 A BUDGET / SPEND-CAP REJECTION STILL RESOLVES, and that is deliberate. It
416
+ * is a documented outcome, not an error: the server quotes what it refused to
417
+ * charge, so the resolved snapshot has `status === 'failed'` AND a numeric
418
+ * `cost.total`. THAT is the shape to branch on when offering a top-up —
419
+ * `useBuzzPurchase().openPurchaseModal()` — not a `catch`.
420
+ *
421
+ * 🔴 BUT A RESOLVED `'failed'` IS NOT ALWAYS AN AFFORDABILITY PROBLEM, so do not
422
+ * wire every one of them to a purchase modal. The per-app **velocity** limit,
423
+ * the per-app **aggregate daily** cap, a fail-closed "temporarily unavailable"
424
+ * deny and a **missing price quote** are all priced, resolving outcomes that
425
+ * buying Buzz cannot fix.
426
+ *
427
+ * 🔴 THIS ALSO INCLUDES MODERATOR REVIEW PREVIEW. While an app is under review
428
+ * the host short-circuits every workflow request with
429
+ * `failureSnapshot('not available in review preview')`, so `submit()` rejects
430
+ * there where it used to resolve — the correct reading (no workflow was
431
+ * queued), and why the catch is not optional.
432
+ *
433
+ * `result` is updated to the returned snapshot BEFORE any rejection, so a
434
+ * failed submit can never leave a previous submit's workflow in `result`.
435
+ */
219
436
  submit: (body: WorkflowBody, options?: SubmitWorkflowOptions) => Promise<BlockWorkflowSnapshot>;
220
437
  /**
221
438
  * ONE host round-trip. The low-level pull primitive — you almost certainly
@@ -264,10 +481,20 @@ interface UseBuzzWorkflowReturn {
264
481
  * Orchestrates the estimate → confirm → submit → poll dance through the
265
482
  * host-mediated `postMessage` path.
266
483
  *
267
- * The host enforces budget rules (`cost_estimate <= token.buzzBudget`)
268
- * before forwarding to the orchestrator; submit() will reject if the host
269
- * refuses. Block apps should call `useBuzzPurchase().openPurchaseModal()`
270
- * when that happens.
484
+ * The host enforces budget rules (`cost_estimate <= token.buzzBudget`) before
485
+ * forwarding to the orchestrator.
486
+ *
487
+ * 🔴 A BUDGET REFUSAL DOES **NOT** REJECT — IT RESOLVES. It comes back as a
488
+ * snapshot with `status: 'failed'`, an `error` string and the `cost` the server
489
+ * declined to charge, and THAT resolved shape is the cue to call
490
+ * `useBuzzPurchase().openPurchaseModal()`. What DOES reject is a submit with no
491
+ * usable outcome — see {@link WorkflowSubmitError}. Routing a rejection into a
492
+ * top-up sells Buzz for a failure Buzz cannot fix.
493
+ *
494
+ * 🔴 NOR IS EVERY RESOLVED `'failed'` AN AFFORDABILITY PROBLEM. The per-app
495
+ * velocity limit, the per-app aggregate daily cap, a fail-closed "temporarily
496
+ * unavailable" deny and a missing price quote are all priced, resolving outcomes
497
+ * too. Branch on the message/your own policy before offering to sell anything.
271
498
  *
272
499
  * AFTER `submit` FLIPS `status` TO `'polling'`, USE `watch(workflowId)`. It owns
273
500
  * the loop, resolves on the terminal snapshot, and pushes every intermediate
@@ -310,21 +537,58 @@ interface UseBuzzWorkflowReturn {
310
537
  * modelVersionId,
311
538
  * params: { prompt: 'a cat' },
312
539
  * };
540
+ * // A viewer-facing string YOUR APP owns, chosen by `code`. Neither
541
+ * // `err.message` (developer-facing, wording not a contract) nor
542
+ * // `err.snapshot.error` (server-authored, unsanitised) may be rendered.
543
+ * const estimateFailureMessage = (err: WorkflowEstimateError) =>
544
+ * err.code === 'no-cost'
545
+ * ? 'We could not get a price for this configuration. Try adjusting it.'
546
+ * : 'Pricing is unavailable right now. Please try again shortly.';
547
+ *
313
548
  * // 🔴 estimate() REJECTS when the reply carries no usable price — an errored
314
549
  * // estimate, or one that came back with no numeric cost. ALWAYS catch it.
315
550
  * try {
316
551
  * await estimate(body); // status 'estimating' → 'confirming' (cost in result.cost.total)
317
552
  * } catch (err) {
318
553
  * // status is now 'error'; `result` holds the unusable snapshot, never a
319
- * // STALE priced one. Branch on err.code ('failed' | 'no-cost'); the server's
320
- * // reason is on err.snapshot.error (unsanitised — log it, don't render it as
321
- * // trusted copy). See WorkflowEstimateError.
554
+ * // STALE priced one. See WorkflowEstimateError for the three-audience split.
322
555
  * if (!(err instanceof WorkflowEstimateError)) throw err;
323
- * logForDebugging(err.snapshot.error);
324
- * showError('This configuration cannot be priced right now.');
556
+ * logForDebugging(err.message, err.snapshot.error); // developer-facing: LOG only
557
+ * showError(estimateFailureMessage(err)); // viewer-facing: app-owned
558
+ * }
559
+ * // 🔴 submit() REJECTS when the reply carries no usable workflow outcome, so it
560
+ * // needs a catch too — and the two codes differ on whether MONEY MOVED.
561
+ * try {
562
+ * const snap = await submit(body); // status 'submitting' → 'polling'
563
+ * if (snap.status === 'failed') {
564
+ * // RESOLVED + priced = a server outcome (affordability, a cap, a velocity
565
+ * // limit, a transient deny). Only some of those are fixable by buying Buzz.
566
+ * showError(submitOutcomeMessage(snap));
567
+ * } else {
568
+ * const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
569
+ * if (done.status === 'succeeded') setImages(done.imageUrls ?? []);
570
+ * }
571
+ * } catch (err) {
572
+ * if (!(err instanceof WorkflowSubmitError)) throw err;
573
+ * logForDebugging(err.message, err.snapshot.error); // developer-facing: LOG only
574
+ * // 🔴 TWO SEPARATE QUESTIONS: `code` decides what you may say about MONEY,
575
+ * // the id decides only whether there is anything to POLL. Conjoining them
576
+ * // sends a 'whatif' id into the reassuring arm.
577
+ * if (err.code === 'workflow-failed') {
578
+ * // Spend may already be committed. Do NOT retry blindly (that mints a new
579
+ * // idempotency key = a second reservation) and do NOT claim nothing was
580
+ * // charged.
581
+ * showError('The generation may have started but did not complete.');
582
+ * // 'whatif' is a non-workflow sentinel — nothing behind it to poll.
583
+ * if (err.snapshot.workflowId !== 'whatif') {
584
+ * await watch(err.snapshot.workflowId, { onUpdate: render });
585
+ * }
586
+ * } else {
587
+ * // 'exception' — usually nothing was queued. Retry, but reuse the SAME
588
+ * // idempotencyKey: a lost response can also land here.
589
+ * showError('Could not start the generation. Please try again.');
590
+ * }
325
591
  * }
326
- * const snap = await submit(body); // status 'submitting' → 'polling'; returns a workflowId
327
- * const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
328
592
  */
329
593
  export declare function useBuzzWorkflow(): UseBuzzWorkflowReturn;
330
594
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"useBuzzWorkflow.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AA8BnG;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,KAAK,CAAC;AAW7C,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,qBAAqB,KAAK,IAAI,CAAC;IACrD;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;;;;OAQG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAyBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwEG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;IAC9C;;;;;;;;;;;;;;;;;;;OAmBG;IACH,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,SAAS,CAAC;IAEpC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,CAAC;gBAE7B,QAAQ,EAAE,qBAAqB,EAAE,IAAI,EAAE,QAAQ,GAAG,SAAS;CAexE;AAED,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,QAAQ,EAAE,CAAC,IAAI,EAAE,YAAY,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjE,MAAM,EAAE,CACN,IAAI,EAAE,YAAY,EAClB,OAAO,CAAC,EAAE,qBAAqB,KAC5B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;OAGG;IACH,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7D;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,KAAK,EAAE,CACL,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,oBAAoB,KAC3B,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACpC;;;;;;OAMG;IACH,MAAM,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC/D,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACrC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAgB,eAAe,IAAI,qBAAqB,CAkPvD"}
1
+ {"version":3,"file":"useBuzzWorkflow.d.ts","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"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"}