@civitai/blocks-react 0.38.0 → 0.40.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/README.md +33 -5
- package/dist/hooks/useBlockContext.d.ts +6 -0
- package/dist/hooks/useBlockContext.d.ts.map +1 -1
- package/dist/hooks/useBlockContext.js +6 -0
- package/dist/hooks/useBlockContext.js.map +1 -1
- package/dist/hooks/useBlockTheme.d.ts +39 -0
- package/dist/hooks/useBlockTheme.d.ts.map +1 -0
- package/dist/hooks/useBlockTheme.js +41 -0
- package/dist/hooks/useBlockTheme.js.map +1 -0
- package/dist/hooks/useBuzzWorkflow.d.ts +121 -12
- package/dist/hooks/useBuzzWorkflow.d.ts.map +1 -1
- package/dist/hooks/useBuzzWorkflow.js +173 -15
- package/dist/hooks/useBuzzWorkflow.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/iframeTransport.d.ts +54 -0
- package/dist/internal/iframeTransport.d.ts.map +1 -1
- package/dist/internal/iframeTransport.js +160 -1
- package/dist/internal/iframeTransport.js.map +1 -1
- package/dist/internal/liveHost.d.ts +8 -2
- package/dist/internal/liveHost.d.ts.map +1 -1
- package/dist/internal/liveHost.js +73 -6
- package/dist/internal/liveHost.js.map +1 -1
- package/dist/internal/mockHost.d.ts +20 -5
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +128 -10
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/transport.d.ts +58 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +62 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts +47 -3
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +99 -3
- package/dist/internal/validate.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ your block app and the SDK share a single React tree.
|
|
|
27
27
|
import { useRef } from 'react';
|
|
28
28
|
import { useBlockContext, useBlockResize, useBuzzWorkflow } from '@civitai/blocks-react';
|
|
29
29
|
import { Button } from '@civitai/blocks-react/ui';
|
|
30
|
-
import
|
|
30
|
+
import { isModelSlotContext } from '@civitai/app-sdk/blocks';
|
|
31
31
|
|
|
32
32
|
export function App() {
|
|
33
33
|
const { ready, context, viewer, theme } = useBlockContext();
|
|
@@ -36,21 +36,22 @@ export function App() {
|
|
|
36
36
|
useBlockResize(rootRef); // host fits the iframe to content
|
|
37
37
|
|
|
38
38
|
if (!ready) return <div ref={rootRef}>Loading…</div>;
|
|
39
|
-
|
|
39
|
+
// `context` is a union keyed on slotId — narrow with the guard, not a cast.
|
|
40
|
+
if (!isModelSlotContext(context)) return <div ref={rootRef}>Wrong slot.</div>;
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
// GOTCHA #60: set data-theme on YOUR OWN root — the host can't reach into
|
|
43
44
|
// the iframe to set it. Without this any [data-theme="dark"] CSS is dormant.
|
|
44
45
|
<div ref={rootRef} data-theme={theme}>
|
|
45
|
-
<p>Block for model {
|
|
46
|
+
<p>Block for model {context.modelName} ({viewer ? 'signed in' : 'anon'})</p>
|
|
46
47
|
{/* `/ui` Button — themed by the data-theme above; `loading` disables + shows a spinner */}
|
|
47
48
|
<Button
|
|
48
49
|
loading={status === 'submitting' || status === 'polling'}
|
|
49
50
|
onClick={() =>
|
|
50
51
|
submit({
|
|
51
52
|
kind: 'textToImage',
|
|
52
|
-
modelId:
|
|
53
|
-
modelVersionId:
|
|
53
|
+
modelId: context.modelId,
|
|
54
|
+
modelVersionId: context.modelVersionId,
|
|
54
55
|
params: { prompt: 'a cat' },
|
|
55
56
|
})
|
|
56
57
|
}
|
|
@@ -112,8 +113,35 @@ const { ready, context, viewer, theme, settings, blockId, blockInstanceId, appId
|
|
|
112
113
|
model-page slots.
|
|
113
114
|
- `viewer` — `ViewerInfo | null` (`null` = anonymous).
|
|
114
115
|
- `theme` — `'light' | 'dark'`. **Set `data-theme={theme}` on your root** (gotcha #60).
|
|
116
|
+
LIVE: it starts at the `BLOCK_INIT` value and then tracks the host's
|
|
117
|
+
`THEME_CHANGE` push when the viewer toggles dark mode mid-session — see
|
|
118
|
+
[`useBlockTheme()`](#useblocktheme).
|
|
115
119
|
- `settings` — `{ publisherSettings, userSettings }`.
|
|
116
120
|
|
|
121
|
+
### `useBlockTheme()`
|
|
122
|
+
|
|
123
|
+
The host's CURRENT site theme, and nothing else. Same value as
|
|
124
|
+
`useBlockContext().theme` — reach for this when theme is all you need.
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
function ThemedRoot() {
|
|
128
|
+
const theme = useBlockTheme(); // 'light' | 'dark'
|
|
129
|
+
return <div data-theme={theme}>…</div>;
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The viewer can toggle light/dark **while your block is mounted**. The host pushes
|
|
134
|
+
a `THEME_CHANGE` message and this hook re-renders. You get that for free as long
|
|
135
|
+
as you *read* the theme on every render — a block that copies it into state once
|
|
136
|
+
at mount, or writes `data-theme` imperatively in a mount-only effect, will stay
|
|
137
|
+
stuck on the old theme.
|
|
138
|
+
|
|
139
|
+
Against a host that predates `THEME_CHANGE` the value simply never moves (the
|
|
140
|
+
old behaviour). Nothing awaits the message, so there is no hang either way.
|
|
141
|
+
|
|
142
|
+
Exercise it locally: `createMockHost(...).setTheme('light')` (and the same on the
|
|
143
|
+
`dev:live` host) pushes the real message.
|
|
144
|
+
|
|
117
145
|
### `useBlockResize(ref)`
|
|
118
146
|
|
|
119
147
|
Attach to your root element. Observes its height and posts `RESIZE_IFRAME` so the
|
|
@@ -18,6 +18,12 @@ declare function useTransportSnapshot(): BlockSnapshot;
|
|
|
18
18
|
* set `data-theme={theme}` on your root, gotcha #60), `blockId`,
|
|
19
19
|
* `blockInstanceId`, and `appId`.
|
|
20
20
|
*
|
|
21
|
+
* `theme` is LIVE: it starts at the `BLOCK_INIT` (or URL-fragment) value and
|
|
22
|
+
* then tracks the host's `THEME_CHANGE` push when the viewer toggles light/dark
|
|
23
|
+
* mid-session. Reading it here is enough — {@link useBlockTheme} is the same
|
|
24
|
+
* value, narrower. Against a host that never pushes it, the value simply never
|
|
25
|
+
* moves (today's behaviour).
|
|
26
|
+
*
|
|
21
27
|
* @example
|
|
22
28
|
* const { ready, context, viewer, theme, settings } = useBlockContext();
|
|
23
29
|
* if (!ready) return <div>Loading…</div>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBlockContext.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,iBAAS,oBAAoB,IAAI,aAAa,CAS7C;AAED
|
|
1
|
+
{"version":3,"file":"useBlockContext.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,iBAAS,oBAAoB,IAAI,aAAa,CAS7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,eAAe,IAAI,IAAI,CACrC,aAAa,EACX,OAAO,GACP,YAAY,GACZ,SAAS,GACT,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,GACP,SAAS,GACT,iBAAiB,GACjB,OAAO,CACV,CAcA;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -25,6 +25,12 @@ function useTransportSnapshot() {
|
|
|
25
25
|
* set `data-theme={theme}` on your root, gotcha #60), `blockId`,
|
|
26
26
|
* `blockInstanceId`, and `appId`.
|
|
27
27
|
*
|
|
28
|
+
* `theme` is LIVE: it starts at the `BLOCK_INIT` (or URL-fragment) value and
|
|
29
|
+
* then tracks the host's `THEME_CHANGE` push when the viewer toggles light/dark
|
|
30
|
+
* mid-session. Reading it here is enough — {@link useBlockTheme} is the same
|
|
31
|
+
* value, narrower. Against a host that never pushes it, the value simply never
|
|
32
|
+
* moves (today's behaviour).
|
|
33
|
+
*
|
|
28
34
|
* @example
|
|
29
35
|
* const { ready, context, viewer, theme, settings } = useBlockContext();
|
|
30
36
|
* if (!ready) return <div>Loading…</div>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBlockContext.js","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;GAGG;AACH,SAAS,oBAAoB;IAC3B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,oBAAoB,CACzB,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAC/B,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE;IAC7B,iEAAiE;IACjE,mEAAmE;IACnE,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAC9B,CAAC;AACJ,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"useBlockContext.js","sourceRoot":"","sources":["../../src/hooks/useBlockContext.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAC;AAE7C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD;;;GAGG;AACH,SAAS,oBAAoB;IAC3B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,OAAO,oBAAoB,CACzB,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,SAAS,CAAC,EAAE,CAAC,EAC/B,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE;IAC7B,iEAAiE;IACjE,mEAAmE;IACnE,GAAG,EAAE,CAAC,SAAS,CAAC,WAAW,EAAE,CAC9B,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,eAAe;IAa7B,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,OAAO;QACL,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,QAAQ,EAAE,IAAI,CAAC,QAAQ;QACvB,MAAM,EAAE,IAAI,CAAC,MAAM;QACnB,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,eAAe,EAAE,IAAI,CAAC,eAAe;QACrC,KAAK,EAAE,IAAI,CAAC,KAAK;KAClB,CAAC;AACJ,CAAC;AAED,6EAA6E;AAC7E,OAAO,EAAE,oBAAoB,EAAE,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Theme } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
|
|
4
|
+
* life of the block ON THE IFRAME TRANSPORT.
|
|
5
|
+
*
|
|
6
|
+
* Reads the SAME singleton transport snapshot {@link useBlockContext} does, so
|
|
7
|
+
* it re-renders when the value changes. Three things can set it, in increasing
|
|
8
|
+
* order of authority:
|
|
9
|
+
*
|
|
10
|
+
* 1. the iframe URL fragment fast path (`#civitai-block=v1&theme=…`), read
|
|
11
|
+
* synchronously at construction, BEFORE any message — this is why a block
|
|
12
|
+
* can paint its first frame in the right theme;
|
|
13
|
+
* 2. `BLOCK_INIT` (authoritative — replaces the whole snapshot);
|
|
14
|
+
* 3. `THEME_CHANGE`, the host's push when the viewer toggles light/dark WHILE
|
|
15
|
+
* the block is mounted. Without it a mounted block kept its mount-time
|
|
16
|
+
* theme until reloaded: `BLOCK_INIT` is deduped by the transport and the
|
|
17
|
+
* URL fragment is frozen at mount, so neither can carry a later value.
|
|
18
|
+
*
|
|
19
|
+
* BEFORE `BLOCK_INIT` (and with no fragment) this returns the snapshot's
|
|
20
|
+
* `'light'` sentinel, exactly like `useBlockContext().theme`. Gate first paint
|
|
21
|
+
* on `useBlockContext().ready` if that matters to you.
|
|
22
|
+
*
|
|
23
|
+
* 🔴 OLD HOST: a host that never sends `THEME_CHANGE` simply never moves the
|
|
24
|
+
* value — the hook degrades to today's mount-time-constant behaviour. Nothing
|
|
25
|
+
* here awaits a message, so there is no hang and no timeout.
|
|
26
|
+
*
|
|
27
|
+
* 🔴 INLINE TRANSPORT: the value is FROZEN at the bootstrap theme. v1 inline
|
|
28
|
+
* mode receives no host pushes at all (`InlineTransport.onMessage` is a stub and
|
|
29
|
+
* `subscribe` is a no-op, so nothing can emit), exactly the way
|
|
30
|
+
* {@link useBlockResize} is a no-op there. Same degradation as an old host —
|
|
31
|
+
* correct first paint, no live toggle — and it lifts when v2 inline mode lands.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // The host cannot reach into your iframe's DOM — put the theme on YOUR root.
|
|
35
|
+
* const theme = useBlockTheme();
|
|
36
|
+
* return <div data-theme={theme}>…</div>;
|
|
37
|
+
*/
|
|
38
|
+
export declare function useBlockTheme(): Theme;
|
|
39
|
+
//# sourceMappingURL=useBlockTheme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBlockTheme.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockTheme.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,yBAAyB,CAAC;AAIrD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,aAAa,IAAI,KAAK,CAErC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useTransportSnapshot } from './useBlockContext.js';
|
|
2
|
+
/**
|
|
3
|
+
* The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
|
|
4
|
+
* life of the block ON THE IFRAME TRANSPORT.
|
|
5
|
+
*
|
|
6
|
+
* Reads the SAME singleton transport snapshot {@link useBlockContext} does, so
|
|
7
|
+
* it re-renders when the value changes. Three things can set it, in increasing
|
|
8
|
+
* order of authority:
|
|
9
|
+
*
|
|
10
|
+
* 1. the iframe URL fragment fast path (`#civitai-block=v1&theme=…`), read
|
|
11
|
+
* synchronously at construction, BEFORE any message — this is why a block
|
|
12
|
+
* can paint its first frame in the right theme;
|
|
13
|
+
* 2. `BLOCK_INIT` (authoritative — replaces the whole snapshot);
|
|
14
|
+
* 3. `THEME_CHANGE`, the host's push when the viewer toggles light/dark WHILE
|
|
15
|
+
* the block is mounted. Without it a mounted block kept its mount-time
|
|
16
|
+
* theme until reloaded: `BLOCK_INIT` is deduped by the transport and the
|
|
17
|
+
* URL fragment is frozen at mount, so neither can carry a later value.
|
|
18
|
+
*
|
|
19
|
+
* BEFORE `BLOCK_INIT` (and with no fragment) this returns the snapshot's
|
|
20
|
+
* `'light'` sentinel, exactly like `useBlockContext().theme`. Gate first paint
|
|
21
|
+
* on `useBlockContext().ready` if that matters to you.
|
|
22
|
+
*
|
|
23
|
+
* 🔴 OLD HOST: a host that never sends `THEME_CHANGE` simply never moves the
|
|
24
|
+
* value — the hook degrades to today's mount-time-constant behaviour. Nothing
|
|
25
|
+
* here awaits a message, so there is no hang and no timeout.
|
|
26
|
+
*
|
|
27
|
+
* 🔴 INLINE TRANSPORT: the value is FROZEN at the bootstrap theme. v1 inline
|
|
28
|
+
* mode receives no host pushes at all (`InlineTransport.onMessage` is a stub and
|
|
29
|
+
* `subscribe` is a no-op, so nothing can emit), exactly the way
|
|
30
|
+
* {@link useBlockResize} is a no-op there. Same degradation as an old host —
|
|
31
|
+
* correct first paint, no live toggle — and it lifts when v2 inline mode lands.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* // The host cannot reach into your iframe's DOM — put the theme on YOUR root.
|
|
35
|
+
* const theme = useBlockTheme();
|
|
36
|
+
* return <div data-theme={theme}>…</div>;
|
|
37
|
+
*/
|
|
38
|
+
export function useBlockTheme() {
|
|
39
|
+
return useTransportSnapshot().theme;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=useBlockTheme.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useBlockTheme.js","sourceRoot":"","sources":["../../src/hooks/useBlockTheme.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,oBAAoB,EAAE,CAAC,KAAK,CAAC;AACtC,CAAC"}
|
|
@@ -1,4 +1,76 @@
|
|
|
1
1
|
import type { BlockWorkflowSnapshot, WorkflowBody, WorkflowStatus } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* Default orchestrator-side hold per {@link UseBuzzWorkflowReturn.watch} poll,
|
|
4
|
+
* in SECONDS.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 THE UNIT IS SECONDS, matching the orchestrator's `?wait=` parameter — not
|
|
7
|
+
* the milliseconds every other timing option in this file uses. Named
|
|
8
|
+
* `waitSeconds` everywhere for exactly that reason.
|
|
9
|
+
*
|
|
10
|
+
* 15 rather than something larger for two reasons, both of them ceilings this
|
|
11
|
+
* value must stay UNDER, not preferences:
|
|
12
|
+
* - civitai's shared `getWorkflow` helper aborts a single orchestrator read at
|
|
13
|
+
* 20s. A hold at or above that is cancelled by our own backstop before the
|
|
14
|
+
* orchestrator can answer.
|
|
15
|
+
* - the host's poll additionally runs an inline output-moderation scan with
|
|
16
|
+
* its own ~12s ceiling, and the two are SEQUENTIAL — so the round trip's
|
|
17
|
+
* worst case is 15 + 12 = 27s, which must stay inside the host's own
|
|
18
|
+
* end-to-end response budget.
|
|
19
|
+
*/
|
|
20
|
+
export declare const DEFAULT_WATCH_WAIT_SECONDS = 15;
|
|
21
|
+
/** Optional controls for {@link UseBuzzWorkflowReturn.watch}. */
|
|
22
|
+
export interface WatchWorkflowOptions {
|
|
23
|
+
/**
|
|
24
|
+
* Called with EVERY snapshot the host returns, intermediate ones included, in
|
|
25
|
+
* order. This is the push side of the API: render from here instead of
|
|
26
|
+
* re-reading `result` on a timer.
|
|
27
|
+
*
|
|
28
|
+
* A throw from this callback is not caught — it rejects the `watch` promise.
|
|
29
|
+
*/
|
|
30
|
+
onUpdate?: (snapshot: BlockWorkflowSnapshot) => void;
|
|
31
|
+
/**
|
|
32
|
+
* Stop watching. The promise RESOLVES with the last snapshot seen rather than
|
|
33
|
+
* rejecting: an abort is the caller's own decision, not a failure, and the
|
|
34
|
+
* common case (a component unmounting) has nobody left to catch a rejection.
|
|
35
|
+
*
|
|
36
|
+
* 🔴 This does NOT cancel the workflow — it stops watching it. Buzz is already
|
|
37
|
+
* spent and the orchestrator keeps running. To actually stop the work, call
|
|
38
|
+
* {@link UseBuzzWorkflowReturn.cancel}.
|
|
39
|
+
*/
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
/**
|
|
42
|
+
* Orchestrator-side hold per poll, in **seconds**. Default
|
|
43
|
+
* {@link DEFAULT_WATCH_WAIT_SECONDS}. `0` disables long polling and falls back
|
|
44
|
+
* to a plain read per `intervalMs`.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 CURRENTLY ADVISORY. It travels on the `POLL_WORKFLOW` message and a host
|
|
47
|
+
* that does not yet read the field simply answers immediately, exactly as
|
|
48
|
+
* today — so `watch` is correct either way, it just polls more often. See the
|
|
49
|
+
* field's note on `BlockToParentMessage`.
|
|
50
|
+
*/
|
|
51
|
+
waitSeconds?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Delay between polls, in ms. Default 1500.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 NOT REDUNDANT WITH `waitSeconds`. When the host long-polls, the hold
|
|
56
|
+
* dominates and this is a few percent of overhead. When it does NOT — an
|
|
57
|
+
* older host, or `waitSeconds: 0` — this is the only thing standing between
|
|
58
|
+
* this loop and a request storm.
|
|
59
|
+
*/
|
|
60
|
+
intervalMs?: number;
|
|
61
|
+
/** Give up and resolve with the last snapshot after this long. Default 10min. */
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Consecutive transport failures to absorb before rejecting. Default 3.
|
|
65
|
+
*
|
|
66
|
+
* A poll can fail for reasons that have nothing to do with the workflow (a
|
|
67
|
+
* pod rolling, a network blip). Because `watch` OWNS the loop, a single blip
|
|
68
|
+
* would otherwise end a generation the caller's own retry loop used to
|
|
69
|
+
* survive. The counter RESETS on any successful poll, so this bounds a burst,
|
|
70
|
+
* not a lifetime.
|
|
71
|
+
*/
|
|
72
|
+
maxRetries?: number;
|
|
73
|
+
}
|
|
2
74
|
/** Optional per-submit controls. */
|
|
3
75
|
export interface SubmitWorkflowOptions {
|
|
4
76
|
/**
|
|
@@ -13,7 +85,37 @@ export interface SubmitWorkflowOptions {
|
|
|
13
85
|
interface UseBuzzWorkflowReturn {
|
|
14
86
|
estimate: (body: WorkflowBody) => Promise<BlockWorkflowSnapshot>;
|
|
15
87
|
submit: (body: WorkflowBody, options?: SubmitWorkflowOptions) => Promise<BlockWorkflowSnapshot>;
|
|
88
|
+
/**
|
|
89
|
+
* ONE host round-trip. The low-level pull primitive — you almost certainly
|
|
90
|
+
* want {@link UseBuzzWorkflowReturn.watch} instead, which owns the loop.
|
|
91
|
+
*/
|
|
16
92
|
poll: (workflowId: string) => Promise<BlockWorkflowSnapshot>;
|
|
93
|
+
/**
|
|
94
|
+
* Watch a workflow to completion. Resolves with the TERMINAL snapshot; calls
|
|
95
|
+
* `onUpdate` with every intermediate snapshot along the way.
|
|
96
|
+
*
|
|
97
|
+
* This is the replacement for the `useEffect` + `setTimeout` backoff every
|
|
98
|
+
* block used to hand-write around {@link UseBuzzWorkflowReturn.poll}. The app
|
|
99
|
+
* consumes a promise and/or a callback; the loop lives here.
|
|
100
|
+
*
|
|
101
|
+
* 🔴 THE LOOP IS SEQUENTIAL AND NON-OVERLAPPING BY CONSTRUCTION — each poll is
|
|
102
|
+
* awaited before the next is scheduled, so exactly one request per watched
|
|
103
|
+
* workflow is ever in flight. That is not tidiness: it is the property that
|
|
104
|
+
* makes a long hold SAFE. A caller-written `setInterval(poll, 2000)` against
|
|
105
|
+
* a host holding 15s would stack ~7 concurrent requests per workflow, and
|
|
106
|
+
* that is precisely why long polling is opt-in on the wire rather than
|
|
107
|
+
* switched on for every deployed block.
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* const { submit, watch, cancel } = useBuzzWorkflow();
|
|
111
|
+
* const submitted = await submit(body);
|
|
112
|
+
* const done = await watch(submitted.workflowId, {
|
|
113
|
+
* onUpdate: (snap) => setProgress(snap.status),
|
|
114
|
+
* signal: abortRef.current.signal,
|
|
115
|
+
* });
|
|
116
|
+
* if (done.status === 'succeeded') setImages(done.imageUrls ?? []);
|
|
117
|
+
*/
|
|
118
|
+
watch: (workflowId: string, options?: WatchWorkflowOptions) => Promise<BlockWorkflowSnapshot>;
|
|
17
119
|
/**
|
|
18
120
|
* Cancel a running workflow on the orchestrator (a real server-side stop,
|
|
19
121
|
* not just client-side untracking). The host re-derives ownership from the
|
|
@@ -35,22 +137,29 @@ interface UseBuzzWorkflowReturn {
|
|
|
35
137
|
* refuses. Block apps should call `useBuzzPurchase().openPurchaseModal()`
|
|
36
138
|
* when that happens.
|
|
37
139
|
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
41
|
-
*
|
|
140
|
+
* AFTER `submit` FLIPS `status` TO `'polling'`, USE `watch(workflowId)`. It owns
|
|
141
|
+
* the loop, resolves on the terminal snapshot, and pushes every intermediate
|
|
142
|
+
* one to an `onUpdate` callback — so a block consumes a promise/callback rather
|
|
143
|
+
* than running its own timer. `poll(workflowId)` remains the single-round-trip
|
|
144
|
+
* primitive for callers that genuinely want to drive their own cadence; the
|
|
145
|
+
* hand-written `useEffect` + backoff around it that this docstring used to
|
|
146
|
+
* prescribe is no longer the recommended shape. `status === 'confirming'` is
|
|
147
|
+
* IDLE (estimate landed, user reviewing cost) — keep the Generate button
|
|
148
|
+
* enabled.
|
|
42
149
|
*
|
|
43
150
|
* `estimate`/`submit` take a full {@link WorkflowBody} — the discriminated
|
|
44
|
-
* union keyed by `kind`,
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
151
|
+
* union keyed by `kind`, with THREE members as of `@civitai/app-sdk@0.30.0`:
|
|
152
|
+
* a `textToImage` body (`{ kind, modelId, modelVersionId, params }`), a
|
|
153
|
+
* `customComfy` recipe body (`{ kind, recipe, params }`), or a `step` body
|
|
154
|
+
* (`{ kind: 'step', step, params }` — a server-registered orchestrator step
|
|
155
|
+
* such as `'chat-completion'`), never a bare `{ prompt }`. The hook forwards
|
|
156
|
+
* the body to the host verbatim and never reads variant-specific fields, so
|
|
157
|
+
* every member flows through unchanged, including any member added later.
|
|
49
158
|
*
|
|
50
|
-
* @returns `{ estimate, submit, poll, cancel, status, result, error }`.
|
|
159
|
+
* @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
|
|
51
160
|
*
|
|
52
161
|
* @example
|
|
53
|
-
* const { estimate, submit,
|
|
162
|
+
* const { estimate, submit, watch, status, result } = useBuzzWorkflow();
|
|
54
163
|
* const body = {
|
|
55
164
|
* kind: 'textToImage' as const,
|
|
56
165
|
* modelId,
|
|
@@ -59,7 +168,7 @@ interface UseBuzzWorkflowReturn {
|
|
|
59
168
|
* };
|
|
60
169
|
* await estimate(body); // status 'estimating' → 'confirming' (cost in result.cost.total)
|
|
61
170
|
* const snap = await submit(body); // status 'submitting' → 'polling'; returns a workflowId
|
|
62
|
-
* await
|
|
171
|
+
* const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
|
|
63
172
|
*/
|
|
64
173
|
export declare function useBuzzWorkflow(): UseBuzzWorkflowReturn;
|
|
65
174
|
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,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,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,IAAI,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC7D;;;;;;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
|
|
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,oCAAoC;AACpC,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,eAAe,IAAI,qBAAqB,CA+MvD"}
|
|
@@ -24,6 +24,54 @@ const TERMINAL_STATUSES = new Set([
|
|
|
24
24
|
* a spurious `request "SUBMIT_WORKFLOW" timed out` rejection.
|
|
25
25
|
*/
|
|
26
26
|
const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
|
|
27
|
+
/**
|
|
28
|
+
* Default orchestrator-side hold per {@link UseBuzzWorkflowReturn.watch} poll,
|
|
29
|
+
* in SECONDS.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 THE UNIT IS SECONDS, matching the orchestrator's `?wait=` parameter — not
|
|
32
|
+
* the milliseconds every other timing option in this file uses. Named
|
|
33
|
+
* `waitSeconds` everywhere for exactly that reason.
|
|
34
|
+
*
|
|
35
|
+
* 15 rather than something larger for two reasons, both of them ceilings this
|
|
36
|
+
* value must stay UNDER, not preferences:
|
|
37
|
+
* - civitai's shared `getWorkflow` helper aborts a single orchestrator read at
|
|
38
|
+
* 20s. A hold at or above that is cancelled by our own backstop before the
|
|
39
|
+
* orchestrator can answer.
|
|
40
|
+
* - the host's poll additionally runs an inline output-moderation scan with
|
|
41
|
+
* its own ~12s ceiling, and the two are SEQUENTIAL — so the round trip's
|
|
42
|
+
* worst case is 15 + 12 = 27s, which must stay inside the host's own
|
|
43
|
+
* end-to-end response budget.
|
|
44
|
+
*/
|
|
45
|
+
export const DEFAULT_WATCH_WAIT_SECONDS = 15;
|
|
46
|
+
/** Gap between `watch` polls, in ms. See {@link WatchWorkflowOptions.intervalMs}. */
|
|
47
|
+
const DEFAULT_WATCH_INTERVAL_MS = 1_500;
|
|
48
|
+
/** Total `watch` budget, in ms. Generation can legitimately take minutes. */
|
|
49
|
+
const DEFAULT_WATCH_TIMEOUT_MS = 10 * 60_000;
|
|
50
|
+
/** How many CONSECUTIVE transport failures `watch` absorbs before rejecting. */
|
|
51
|
+
const DEFAULT_WATCH_MAX_RETRIES = 3;
|
|
52
|
+
/**
|
|
53
|
+
* Sleep, waking EARLY if `signal` aborts.
|
|
54
|
+
*
|
|
55
|
+
* 🔴 A PLAIN `setTimeout` WOULD MAKE ABORT LATENCY EQUAL THE POLL INTERVAL. A
|
|
56
|
+
* component unmounting mid-gap would keep a pending timer alive for up to
|
|
57
|
+
* `intervalMs` and only then notice, which is exactly the "unmounted component
|
|
58
|
+
* kept working" shape an abort signal exists to prevent. The listener is always
|
|
59
|
+
* removed, so an abort long after the sleep resolved cannot fire into a
|
|
60
|
+
* finished loop.
|
|
61
|
+
*/
|
|
62
|
+
function sleep(ms, signal) {
|
|
63
|
+
if (signal?.aborted)
|
|
64
|
+
return Promise.resolve();
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const done = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
signal?.removeEventListener('abort', done);
|
|
69
|
+
resolve();
|
|
70
|
+
};
|
|
71
|
+
const timer = setTimeout(done, ms);
|
|
72
|
+
signal?.addEventListener('abort', done);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
27
75
|
/**
|
|
28
76
|
* Orchestrates the estimate → confirm → submit → poll dance through the
|
|
29
77
|
* host-mediated `postMessage` path.
|
|
@@ -33,22 +81,29 @@ const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
|
|
|
33
81
|
* refuses. Block apps should call `useBuzzPurchase().openPurchaseModal()`
|
|
34
82
|
* when that happens.
|
|
35
83
|
*
|
|
36
|
-
*
|
|
37
|
-
* the
|
|
38
|
-
*
|
|
39
|
-
*
|
|
84
|
+
* AFTER `submit` FLIPS `status` TO `'polling'`, USE `watch(workflowId)`. It owns
|
|
85
|
+
* the loop, resolves on the terminal snapshot, and pushes every intermediate
|
|
86
|
+
* one to an `onUpdate` callback — so a block consumes a promise/callback rather
|
|
87
|
+
* than running its own timer. `poll(workflowId)` remains the single-round-trip
|
|
88
|
+
* primitive for callers that genuinely want to drive their own cadence; the
|
|
89
|
+
* hand-written `useEffect` + backoff around it that this docstring used to
|
|
90
|
+
* prescribe is no longer the recommended shape. `status === 'confirming'` is
|
|
91
|
+
* IDLE (estimate landed, user reviewing cost) — keep the Generate button
|
|
92
|
+
* enabled.
|
|
40
93
|
*
|
|
41
94
|
* `estimate`/`submit` take a full {@link WorkflowBody} — the discriminated
|
|
42
|
-
* union keyed by `kind`,
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
95
|
+
* union keyed by `kind`, with THREE members as of `@civitai/app-sdk@0.30.0`:
|
|
96
|
+
* a `textToImage` body (`{ kind, modelId, modelVersionId, params }`), a
|
|
97
|
+
* `customComfy` recipe body (`{ kind, recipe, params }`), or a `step` body
|
|
98
|
+
* (`{ kind: 'step', step, params }` — a server-registered orchestrator step
|
|
99
|
+
* such as `'chat-completion'`), never a bare `{ prompt }`. The hook forwards
|
|
100
|
+
* the body to the host verbatim and never reads variant-specific fields, so
|
|
101
|
+
* every member flows through unchanged, including any member added later.
|
|
47
102
|
*
|
|
48
|
-
* @returns `{ estimate, submit, poll, cancel, status, result, error }`.
|
|
103
|
+
* @returns `{ estimate, submit, poll, watch, cancel, status, result, error }`.
|
|
49
104
|
*
|
|
50
105
|
* @example
|
|
51
|
-
* const { estimate, submit,
|
|
106
|
+
* const { estimate, submit, watch, status, result } = useBuzzWorkflow();
|
|
52
107
|
* const body = {
|
|
53
108
|
* kind: 'textToImage' as const,
|
|
54
109
|
* modelId,
|
|
@@ -57,7 +112,7 @@ const WORKFLOW_REQUEST_TIMEOUT_MS = 120_000;
|
|
|
57
112
|
* };
|
|
58
113
|
* await estimate(body); // status 'estimating' → 'confirming' (cost in result.cost.total)
|
|
59
114
|
* const snap = await submit(body); // status 'submitting' → 'polling'; returns a workflowId
|
|
60
|
-
* await
|
|
115
|
+
* const done = await watch(snap.workflowId, { onUpdate: render }); // → terminal
|
|
61
116
|
*/
|
|
62
117
|
export function useBuzzWorkflow() {
|
|
63
118
|
const [status, setStatus] = useState('idle');
|
|
@@ -96,10 +151,45 @@ export function useBuzzWorkflow() {
|
|
|
96
151
|
throw err;
|
|
97
152
|
}
|
|
98
153
|
}, []);
|
|
154
|
+
/**
|
|
155
|
+
* ONE poll round-trip, with an optional long-poll hint. The single place that
|
|
156
|
+
* builds a `POLL_WORKFLOW` message, so `poll` and `watch` cannot drift on the
|
|
157
|
+
* message shape or the request timeout.
|
|
158
|
+
*/
|
|
159
|
+
const pollOnce = useCallback(async (workflowId, waitSeconds) => {
|
|
160
|
+
const { snapshot } = await sendTypedRequest(getTransport(), {
|
|
161
|
+
type: 'POLL_WORKFLOW',
|
|
162
|
+
payload: {
|
|
163
|
+
workflowId,
|
|
164
|
+
// Omitted rather than sent as 0 when long polling is off, so the
|
|
165
|
+
// message stays byte-identical to what pre-`watch` blocks send.
|
|
166
|
+
...(waitSeconds !== undefined && waitSeconds > 0 ? { waitSeconds } : {}),
|
|
167
|
+
},
|
|
168
|
+
}, 'WORKFLOW_STATUS', { timeoutMs: WORKFLOW_REQUEST_TIMEOUT_MS });
|
|
169
|
+
// A reply without a usable snapshot is a failure, not a result: returning
|
|
170
|
+
// `undefined` would make `poll` resolve with a non-snapshot, and would
|
|
171
|
+
// make `watch` — whose stop condition is
|
|
172
|
+
// `TERMINAL_STATUSES.has(snapshot.status)` — loop to its timeout against a
|
|
173
|
+
// host that is answering with nothing. Throwing routes it into `watch`'s
|
|
174
|
+
// bounded retry instead.
|
|
175
|
+
//
|
|
176
|
+
// 🔴 NOT CLAIMED TO BE REACHABLE THROUGH `IframeTransport`, WHICH ALREADY
|
|
177
|
+
// FAIL-CLOSES THIS. Its `payloadValidatorFor('WORKFLOW_STATUS')`
|
|
178
|
+
// (internal/validate.ts) drops a reply whose `snapshot.status` is absent
|
|
179
|
+
// or outside the known set, so the request never resolves at all and
|
|
180
|
+
// times out instead. This guard covers the OTHER transports
|
|
181
|
+
// `sendTypedRequest` accepts (mock/test hosts, `dev:live`), and is kept as
|
|
182
|
+
// cheap defence — do not cite it as validated input handling on the
|
|
183
|
+
// iframe path.
|
|
184
|
+
if (!snapshot || typeof snapshot.status !== 'string') {
|
|
185
|
+
throw new Error(`POLL_WORKFLOW: malformed response (no snapshot) for ${workflowId}`);
|
|
186
|
+
}
|
|
187
|
+
return snapshot;
|
|
188
|
+
}, []);
|
|
99
189
|
const poll = useCallback(async (workflowId) => {
|
|
100
190
|
setStatus('polling');
|
|
101
191
|
try {
|
|
102
|
-
const
|
|
192
|
+
const snapshot = await pollOnce(workflowId);
|
|
103
193
|
setResult(snapshot);
|
|
104
194
|
if (TERMINAL_STATUSES.has(snapshot.status)) {
|
|
105
195
|
setStatus('done');
|
|
@@ -111,7 +201,75 @@ export function useBuzzWorkflow() {
|
|
|
111
201
|
setStatus('error');
|
|
112
202
|
throw err;
|
|
113
203
|
}
|
|
114
|
-
}, []);
|
|
204
|
+
}, [pollOnce]);
|
|
205
|
+
const watch = useCallback(async (workflowId, options) => {
|
|
206
|
+
const interval = options?.intervalMs ?? DEFAULT_WATCH_INTERVAL_MS;
|
|
207
|
+
const waitSeconds = options?.waitSeconds ?? DEFAULT_WATCH_WAIT_SECONDS;
|
|
208
|
+
const maxRetries = options?.maxRetries ?? DEFAULT_WATCH_MAX_RETRIES;
|
|
209
|
+
const deadline = Date.now() + (options?.timeoutMs ?? DEFAULT_WATCH_TIMEOUT_MS);
|
|
210
|
+
setError(null);
|
|
211
|
+
setStatus('polling');
|
|
212
|
+
let last = null;
|
|
213
|
+
let consecutiveFailures = 0;
|
|
214
|
+
// 🔴 SEQUENTIAL BY CONSTRUCTION. Every iteration AWAITS its poll before
|
|
215
|
+
// scheduling the next, so at most one request per watched workflow is
|
|
216
|
+
// ever in flight no matter how long the host holds it. A timer-driven
|
|
217
|
+
// caller cannot make that guarantee, which is why long polling is
|
|
218
|
+
// requested here and not enabled globally.
|
|
219
|
+
for (;;) {
|
|
220
|
+
if (options?.signal?.aborted)
|
|
221
|
+
break;
|
|
222
|
+
let snapshot;
|
|
223
|
+
try {
|
|
224
|
+
snapshot = await pollOnce(workflowId, waitSeconds);
|
|
225
|
+
consecutiveFailures = 0;
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
// A blip is not the end of a generation. `watch` owns the loop, so a
|
|
229
|
+
// single transport failure would otherwise kill a run that a
|
|
230
|
+
// caller-written retry loop used to survive.
|
|
231
|
+
consecutiveFailures += 1;
|
|
232
|
+
if (consecutiveFailures > maxRetries || Date.now() >= deadline) {
|
|
233
|
+
setError(err);
|
|
234
|
+
setStatus('error');
|
|
235
|
+
throw err;
|
|
236
|
+
}
|
|
237
|
+
await sleep(interval, options?.signal);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
last = snapshot;
|
|
241
|
+
setResult(snapshot);
|
|
242
|
+
// 🔴 `onUpdate` RECEIVES THE SNAPSHOT AS AN ARGUMENT AND MUST USE IT.
|
|
243
|
+
// `setResult` above is a React state update, so it is NOT visible to a
|
|
244
|
+
// callback reading the hook's `result` in this same tick — that reads
|
|
245
|
+
// the PREVIOUS render's value. The argument is the current one; the
|
|
246
|
+
// hook state catches up on the next render.
|
|
247
|
+
//
|
|
248
|
+
// A throw here propagates deliberately — swallowing a consumer's error
|
|
249
|
+
// would leave the loop spinning against a broken consumer.
|
|
250
|
+
options?.onUpdate?.(snapshot);
|
|
251
|
+
if (TERMINAL_STATUSES.has(snapshot.status)) {
|
|
252
|
+
setStatus('done');
|
|
253
|
+
return snapshot;
|
|
254
|
+
}
|
|
255
|
+
// Budget exhausted: resolve with what we have rather than throwing. A
|
|
256
|
+
// non-terminal result is a legitimate answer ("still running"), and the
|
|
257
|
+
// caller can tell the difference by reading `.status`.
|
|
258
|
+
if (Date.now() + interval >= deadline)
|
|
259
|
+
break;
|
|
260
|
+
await sleep(interval, options?.signal);
|
|
261
|
+
}
|
|
262
|
+
// Aborted or timed out with a snapshot in hand — resolve with it.
|
|
263
|
+
if (last)
|
|
264
|
+
return last;
|
|
265
|
+
// 🔴 NO SNAPSHOT AT ALL. Only reachable when the signal was ALREADY
|
|
266
|
+
// aborted on entry. Rejecting is the only honest option: there is nothing
|
|
267
|
+
// to resolve with, and issuing a poll here would make a request the
|
|
268
|
+
// caller explicitly asked us not to make.
|
|
269
|
+
const aborted = new Error(`watch(${workflowId}) aborted before any snapshot`);
|
|
270
|
+
aborted.name = 'AbortError';
|
|
271
|
+
throw aborted;
|
|
272
|
+
}, [pollOnce]);
|
|
115
273
|
const cancel = useCallback(async (workflowId) => {
|
|
116
274
|
try {
|
|
117
275
|
const { snapshot } = await sendTypedRequest(getTransport(), { type: 'CANCEL_WORKFLOW', payload: { workflowId } }, 'WORKFLOW_CANCELED', { timeoutMs: WORKFLOW_REQUEST_TIMEOUT_MS });
|
|
@@ -127,6 +285,6 @@ export function useBuzzWorkflow() {
|
|
|
127
285
|
throw err;
|
|
128
286
|
}
|
|
129
287
|
}, []);
|
|
130
|
-
return { estimate, submit, poll, cancel, status, result, error };
|
|
288
|
+
return { estimate, submit, poll, watch, cancel, status, result, error };
|
|
131
289
|
}
|
|
132
290
|
//# sourceMappingURL=useBuzzWorkflow.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBuzzWorkflow.js","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAI9C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAEpF;;;;GAIG;AACH,MAAM,iBAAiB,GAAiD,IAAI,GAAG,CAAC;IAC9E,WAAW;IACX,QAAQ;IACR,UAAU;IACV,SAAS;CACV,CAAC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,2BAA2B,GAAG,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useBuzzWorkflow.js","sourceRoot":"","sources":["../../src/hooks/useBuzzWorkflow.ts"],"names":[],"mappings":"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"}
|