@civitai/blocks-react 0.67.1 → 0.69.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 +520 -8
- package/dist/hooks/useBatchEstimate.d.ts +191 -0
- package/dist/hooks/useBatchEstimate.js +196 -0
- package/dist/hooks/useBlockAnalytics.d.ts +46 -4
- package/dist/hooks/useBlockAnalytics.js +41 -4
- package/dist/hooks/useBuzzWorkflow.d.ts +4 -1
- package/dist/hooks/useBuzzWorkflow.js +4 -1
- package/dist/hooks/useResourcePicker.d.ts +98 -37
- package/dist/hooks/useResourcePicker.js +77 -7
- package/dist/hooks/useSharedStorage.d.ts +24 -0
- package/dist/hooks/useSharedStorage.js +55 -0
- package/dist/hooks/useWorkflowBatch.d.ts +196 -0
- package/dist/hooks/useWorkflowBatch.js +352 -0
- package/dist/index.d.ts +7 -3
- package/dist/index.js +4 -2
- package/dist/internal/liveHost.d.ts +27 -2
- package/dist/internal/liveHost.js +133 -15
- package/dist/internal/mockHost.d.ts +104 -2
- package/dist/internal/mockHost.js +369 -84
- package/dist/internal/pickerOverlay.d.ts +32 -0
- package/dist/internal/pickerOverlay.js +99 -2
- package/dist/internal/trackEventCheck.d.ts +34 -0
- package/dist/internal/trackEventCheck.js +194 -0
- package/dist/transport/requestTimeouts.js +3 -0
- package/dist/transport/validate.d.ts +49 -6
- package/dist/transport/validate.js +138 -28
- package/package.json +2 -2
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { BlockEstimateBatchAggregate, BlockWorkflowSnapshot, WorkflowBody } from '@civitai/app-sdk/blocks';
|
|
2
|
+
import { WorkflowEstimateError } from './useBuzzWorkflow.js';
|
|
3
|
+
export type { BlockEstimateBatchAggregate };
|
|
4
|
+
/**
|
|
5
|
+
* The most bodies one {@link UseBatchEstimate.estimateBatch} call may carry.
|
|
6
|
+
*
|
|
7
|
+
* The host's own cap (`BLOCK_ESTIMATE_BATCH_MAX_CELLS` in civitai/civitai). The
|
|
8
|
+
* hook refuses a longer list before sending it, with the code the host would
|
|
9
|
+
* have answered with a round trip later. A larger grid is priced in several
|
|
10
|
+
* calls; each call is still one request against the estimate allowance.
|
|
11
|
+
*/
|
|
12
|
+
export declare const BATCH_ESTIMATE_MAX_CELLS = 16;
|
|
13
|
+
/**
|
|
14
|
+
* The host's `snapshot.error` for a batch cell its time budget ran out on before
|
|
15
|
+
* the cell priced. The copy is the host's (`BLOCK_ESTIMATE_BATCH_TIMEOUT_ERROR`
|
|
16
|
+
* in civitai/civitai `src/server/routers/blocks.router.ts`). This is the ONE
|
|
17
|
+
* place the SDK spells it: the hook maps it to {@link BatchEstimateCell}'s
|
|
18
|
+
* `timedOut`, and the mock host answers with it. Not exported from the package:
|
|
19
|
+
* apps branch on `timedOut`, never on this string.
|
|
20
|
+
*/
|
|
21
|
+
export declare const BATCH_ESTIMATE_CELL_TIMEOUT_ERROR = "estimate timed out";
|
|
22
|
+
/**
|
|
23
|
+
* Why a WHOLE batch estimate was refused. The stable branch target on
|
|
24
|
+
* {@link BatchEstimateError}.
|
|
25
|
+
*
|
|
26
|
+
* - `'unsupported'` — the host answered `unsupported on this host`: it knows the
|
|
27
|
+
* message and has no handler for it on this surface. Nothing was priced. **Fall
|
|
28
|
+
* back to one `useBuzzWorkflow().estimate()` per cell.**
|
|
29
|
+
* - `'timeout'` — no reply arrived in time. A host that supports the message
|
|
30
|
+
* bounds its own work and replies well inside the default wait, marking any
|
|
31
|
+
* cell it could not price in time as an `ok: false` cell with `timedOut:
|
|
32
|
+
* true`, so this code usually means the host or its upstream is SLOW, not
|
|
33
|
+
* that it is old. 🔴 Do NOT fall back to per-cell estimates here:
|
|
34
|
+
* that sends up to 16 more estimates at an upstream that is already struggling.
|
|
35
|
+
* Show the estimate as unavailable and let the viewer retry later. A host that
|
|
36
|
+
* predates the message also ends here (it never replies); an app that must
|
|
37
|
+
* tell the two apart can probe once with a one-body batch and a short explicit
|
|
38
|
+
* `timeoutMs`, and treat only THAT probe's `'timeout'` as "no batch support".
|
|
39
|
+
* - `'invalid-request'` — the list is empty, is not an array, or holds more than
|
|
40
|
+
* {@link BATCH_ESTIMATE_MAX_CELLS} bodies. Refused by the hook before sending,
|
|
41
|
+
* or by the host. A per-cell fallback is pointless for an empty list and is the
|
|
42
|
+
* wrong fix for an over-long one: split it.
|
|
43
|
+
* - `'failed'` — the host or the server refused the call for another reason (the
|
|
44
|
+
* scope, the rate limit, review preview, no credential) or returned something
|
|
45
|
+
* that is not a batch result. The reason is on
|
|
46
|
+
* {@link BatchEstimateError.hostError}.
|
|
47
|
+
*/
|
|
48
|
+
export type BatchEstimateErrorCode = 'unsupported' | 'timeout' | 'invalid-request' | 'failed';
|
|
49
|
+
/**
|
|
50
|
+
* Thrown by {@link UseBatchEstimate.estimateBatch} when the WHOLE call failed.
|
|
51
|
+
*
|
|
52
|
+
* One cell failing is NOT this: a cell that could not be priced comes back as an
|
|
53
|
+
* `ok: false` entry in {@link BatchEstimateResult.cells} and the call resolves.
|
|
54
|
+
*
|
|
55
|
+
* Same three-audience split as `WorkflowEstimateError`:
|
|
56
|
+
* - {@link BatchEstimateError.code} is what you branch on.
|
|
57
|
+
* - {@link BatchEstimateError.hostError} is the host's or server's own text. It
|
|
58
|
+
* is server-authored and unsanitised: log it, never render it.
|
|
59
|
+
* - `message` is a generic developer-facing summary whose wording is not a
|
|
60
|
+
* contract.
|
|
61
|
+
*/
|
|
62
|
+
export declare class BatchEstimateError extends Error {
|
|
63
|
+
readonly code: BatchEstimateErrorCode;
|
|
64
|
+
/** The host's or server's reason, verbatim, when it gave one. */
|
|
65
|
+
readonly hostError?: string;
|
|
66
|
+
constructor(code: BatchEstimateErrorCode, hostError?: string);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One cell of a batch estimate — the answer to the body at the same index.
|
|
70
|
+
*
|
|
71
|
+
* - `ok: true` — the cell priced. `cost` is the cost object a single
|
|
72
|
+
* `estimate()` returns for that body, passed through untouched (`cost.total`
|
|
73
|
+
* is the price shown before `submit()`).
|
|
74
|
+
* - `ok: false` — the cell did not price. `error` is the SAME
|
|
75
|
+
* `WorkflowEstimateError` a single `estimate()` of that body would have
|
|
76
|
+
* rejected with (`code: 'failed' | 'no-cost'`, the reason on
|
|
77
|
+
* `error.snapshot.error`), so one function can turn either into viewer copy.
|
|
78
|
+
* It is a value here, not a throw: the other cells are still usable.
|
|
79
|
+
* - `timedOut` (on `ok: false` only) — `true` when the host's time budget for
|
|
80
|
+
* the call ran out before this cell priced (its `error.code` is `'failed'`).
|
|
81
|
+
* Show it as unavailable and retry later rather than re-estimating it at once.
|
|
82
|
+
* Branch on `timedOut`, never on `snapshot.error`: the host's wording is not a
|
|
83
|
+
* contract, and the hook maps it here so no app has to match it.
|
|
84
|
+
*
|
|
85
|
+
* `snapshot` is the host's snapshot for the cell, verbatim, on both arms.
|
|
86
|
+
*/
|
|
87
|
+
export type BatchEstimateCell = {
|
|
88
|
+
ok: true;
|
|
89
|
+
cost: NonNullable<BlockWorkflowSnapshot['cost']>;
|
|
90
|
+
snapshot: BlockWorkflowSnapshot;
|
|
91
|
+
} | {
|
|
92
|
+
ok: false;
|
|
93
|
+
error: WorkflowEstimateError;
|
|
94
|
+
/** `true` when the host's time budget for the call ran out before this cell priced. */
|
|
95
|
+
timedOut: boolean;
|
|
96
|
+
snapshot: BlockWorkflowSnapshot;
|
|
97
|
+
};
|
|
98
|
+
/** What {@link UseBatchEstimate.estimateBatch} resolves with. */
|
|
99
|
+
export interface BatchEstimateResult {
|
|
100
|
+
/** One entry per body, in the order the bodies were given. */
|
|
101
|
+
cells: BatchEstimateCell[];
|
|
102
|
+
/**
|
|
103
|
+
* The host's run total over the cells that priced. It covers the whole list
|
|
104
|
+
* only when `aggregate.pricedCells === aggregate.cellCount`.
|
|
105
|
+
*/
|
|
106
|
+
aggregate: BlockEstimateBatchAggregate;
|
|
107
|
+
}
|
|
108
|
+
/** Options for one {@link UseBatchEstimate.estimateBatch} call. */
|
|
109
|
+
export interface EstimateBatchOptions {
|
|
110
|
+
/**
|
|
111
|
+
* How long to wait for the host's reply, in milliseconds. Default
|
|
112
|
+
* `WORKFLOW_REQUEST_TIMEOUT_MS` (120,000), the single estimate's bound.
|
|
113
|
+
*
|
|
114
|
+
* A host that supports the batch replies within its own time budget, well
|
|
115
|
+
* inside this default, so leave it unset for ordinary pricing. Pass a short
|
|
116
|
+
* value only for a deliberate old-host probe (a one-body batch, sent once):
|
|
117
|
+
* a host that predates the message never replies, and a short wait finds that
|
|
118
|
+
* out sooner. A short wait on a full grid gives up on slow but healthy batches.
|
|
119
|
+
*/
|
|
120
|
+
timeoutMs?: number;
|
|
121
|
+
}
|
|
122
|
+
/** What {@link useBatchEstimate} returns. */
|
|
123
|
+
export interface UseBatchEstimate {
|
|
124
|
+
/**
|
|
125
|
+
* Price several workflow bodies in one request. Resolves with one cell per
|
|
126
|
+
* body, in order, plus a run total; rejects with {@link BatchEstimateError}
|
|
127
|
+
* only when the whole call failed.
|
|
128
|
+
*
|
|
129
|
+
* 🔴 ESTIMATE ONLY. Nothing is submitted and no Buzz is held or spent. Submit
|
|
130
|
+
* each cell with `useBuzzWorkflow().submit()`, which asks the viewer and
|
|
131
|
+
* checks the budget per cell.
|
|
132
|
+
*/
|
|
133
|
+
estimateBatch: (bodies: WorkflowBody[], options?: EstimateBatchOptions) => Promise<BatchEstimateResult>;
|
|
134
|
+
/** `true` while the most recent call is in flight. */
|
|
135
|
+
pending: boolean;
|
|
136
|
+
/**
|
|
137
|
+
* The most recent call's result, or `null` before the first one and after a
|
|
138
|
+
* call that failed — a failed call never leaves an earlier total on display.
|
|
139
|
+
*/
|
|
140
|
+
result: BatchEstimateResult | null;
|
|
141
|
+
/** The most recent call's error, or `null`. */
|
|
142
|
+
error: BatchEstimateError | null;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Price a grid of generations in ONE request: a list of workflow bodies in, one
|
|
146
|
+
* result per body (a cost, or that cell's error) plus a run total out. Backed by
|
|
147
|
+
* the `ESTIMATE_WORKFLOW_BATCH` → `ESTIMATE_BATCH_RESULT` bridge.
|
|
148
|
+
*
|
|
149
|
+
* Use it where an app would otherwise call `useBuzzWorkflow().estimate()` once
|
|
150
|
+
* per cell. Every rule a single estimate enforces still applies to each cell;
|
|
151
|
+
* what changes is the accounting and the round trips:
|
|
152
|
+
*
|
|
153
|
+
* - **One request against the estimate allowance**, whatever the number of
|
|
154
|
+
* cells. On a page app that allowance is shared by every viewer of the app and
|
|
155
|
+
* by the host's other reads for it, so a 16-cell grid used to take 16 of it per
|
|
156
|
+
* pricing pass.
|
|
157
|
+
* - **Cells are metered separately.** The host also counts the cells of every
|
|
158
|
+
* batch against an allowance of their own, shared like the estimate allowance
|
|
159
|
+
* (on a page app, by every viewer of the app), so a batch is not a way around
|
|
160
|
+
* it. A call over either is refused whole (`code: 'failed'`, the host's
|
|
161
|
+
* rate-limit text on `hostError`); retry in a few seconds.
|
|
162
|
+
* - **At most {@link BATCH_ESTIMATE_MAX_CELLS} bodies per call.**
|
|
163
|
+
* - **`kind: 'training'` bodies are refused per cell.** Estimate a training run
|
|
164
|
+
* with `useBuzzWorkflow().estimate()`.
|
|
165
|
+
*
|
|
166
|
+
* 🔴 ESTIMATE ONLY. There is no batch submit and no server-side id for the
|
|
167
|
+
* list. To watch and cancel the submitted cells together, use
|
|
168
|
+
* `useWorkflowBatch()`. The total is a quote: nothing reserves it and each cell
|
|
169
|
+
* is priced again at `submit()`.
|
|
170
|
+
*
|
|
171
|
+
* Like `estimate()`, this does NOT prompt for consent. It needs the
|
|
172
|
+
* `ai:write:budgeted` scope; without it the call rejects with `code: 'failed'`.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* const { estimateBatch } = useBatchEstimate();
|
|
176
|
+
* try {
|
|
177
|
+
* const { cells, aggregate } = await estimateBatch(bodies);
|
|
178
|
+
* // cells[i] answers bodies[i]; a cell with `!c.ok && c.timedOut` was not
|
|
179
|
+
* // priced in time — show it as unavailable and retry later
|
|
180
|
+
* } catch (err) {
|
|
181
|
+
* if (!(err instanceof BatchEstimateError)) throw err;
|
|
182
|
+
* if (err.code === 'unsupported') {
|
|
183
|
+
* // this host has no batch estimate — price each cell with estimate()
|
|
184
|
+
* } else {
|
|
185
|
+
* // 'timeout' (a slow host, not an old one) or 'failed': show the estimate
|
|
186
|
+
* // as unavailable and retry later — never one estimate() per cell
|
|
187
|
+
* }
|
|
188
|
+
* }
|
|
189
|
+
*/
|
|
190
|
+
export declare function useBatchEstimate(): UseBatchEstimate;
|
|
191
|
+
//# sourceMappingURL=useBatchEstimate.d.ts.map
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { WorkflowEstimateError } from './useBuzzWorkflow.js';
|
|
6
|
+
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
7
|
+
/**
|
|
8
|
+
* The most bodies one {@link UseBatchEstimate.estimateBatch} call may carry.
|
|
9
|
+
*
|
|
10
|
+
* The host's own cap (`BLOCK_ESTIMATE_BATCH_MAX_CELLS` in civitai/civitai). The
|
|
11
|
+
* hook refuses a longer list before sending it, with the code the host would
|
|
12
|
+
* have answered with a round trip later. A larger grid is priced in several
|
|
13
|
+
* calls; each call is still one request against the estimate allowance.
|
|
14
|
+
*/
|
|
15
|
+
export const BATCH_ESTIMATE_MAX_CELLS = 16;
|
|
16
|
+
/**
|
|
17
|
+
* The host's generic reply to a request it registers no handler for. The copy is
|
|
18
|
+
* the host's (`BRIDGE_NACK_NO_HANDLER` in civitai/civitai), matched here ONCE so
|
|
19
|
+
* no app has to match it: branch on {@link BatchEstimateError.code} instead.
|
|
20
|
+
*/
|
|
21
|
+
const HOST_UNSUPPORTED_REPLY = 'unsupported on this host';
|
|
22
|
+
/**
|
|
23
|
+
* The host's `snapshot.error` for a batch cell its time budget ran out on before
|
|
24
|
+
* the cell priced. The copy is the host's (`BLOCK_ESTIMATE_BATCH_TIMEOUT_ERROR`
|
|
25
|
+
* in civitai/civitai `src/server/routers/blocks.router.ts`). This is the ONE
|
|
26
|
+
* place the SDK spells it: the hook maps it to {@link BatchEstimateCell}'s
|
|
27
|
+
* `timedOut`, and the mock host answers with it. Not exported from the package:
|
|
28
|
+
* apps branch on `timedOut`, never on this string.
|
|
29
|
+
*/
|
|
30
|
+
export const BATCH_ESTIMATE_CELL_TIMEOUT_ERROR = 'estimate timed out';
|
|
31
|
+
/**
|
|
32
|
+
* Thrown by {@link UseBatchEstimate.estimateBatch} when the WHOLE call failed.
|
|
33
|
+
*
|
|
34
|
+
* One cell failing is NOT this: a cell that could not be priced comes back as an
|
|
35
|
+
* `ok: false` entry in {@link BatchEstimateResult.cells} and the call resolves.
|
|
36
|
+
*
|
|
37
|
+
* Same three-audience split as `WorkflowEstimateError`:
|
|
38
|
+
* - {@link BatchEstimateError.code} is what you branch on.
|
|
39
|
+
* - {@link BatchEstimateError.hostError} is the host's or server's own text. It
|
|
40
|
+
* is server-authored and unsanitised: log it, never render it.
|
|
41
|
+
* - `message` is a generic developer-facing summary whose wording is not a
|
|
42
|
+
* contract.
|
|
43
|
+
*/
|
|
44
|
+
export class BatchEstimateError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
/** The host's or server's reason, verbatim, when it gave one. */
|
|
47
|
+
hostError;
|
|
48
|
+
constructor(code, hostError) {
|
|
49
|
+
super(`batch estimate failed (${code})${hostError === undefined ? '' : ' — reason on .hostError'}`);
|
|
50
|
+
this.name = 'BatchEstimateError';
|
|
51
|
+
this.code = code;
|
|
52
|
+
if (hostError !== undefined)
|
|
53
|
+
this.hostError = hostError;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function toCell(snapshot) {
|
|
57
|
+
// The rule `useBuzzWorkflow().estimate()` applies to one reply, per cell.
|
|
58
|
+
if (snapshot.status === 'failed') {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: new WorkflowEstimateError(snapshot, 'failed'),
|
|
62
|
+
// The one place the host's per-cell timeout string is matched.
|
|
63
|
+
timedOut: snapshot.error === BATCH_ESTIMATE_CELL_TIMEOUT_ERROR,
|
|
64
|
+
snapshot,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const cost = snapshot.cost;
|
|
68
|
+
if (!cost || typeof cost.total !== 'number') {
|
|
69
|
+
return {
|
|
70
|
+
ok: false,
|
|
71
|
+
error: new WorkflowEstimateError(snapshot, 'no-cost'),
|
|
72
|
+
timedOut: false,
|
|
73
|
+
snapshot,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
return { ok: true, cost, snapshot };
|
|
77
|
+
}
|
|
78
|
+
function toBatchEstimateError(err) {
|
|
79
|
+
if (err instanceof BatchEstimateError)
|
|
80
|
+
return err;
|
|
81
|
+
// Structural, not a message match — see `RequestTimeoutError`.
|
|
82
|
+
if (err instanceof RequestTimeoutError)
|
|
83
|
+
return new BatchEstimateError('timeout', err.message);
|
|
84
|
+
return new BatchEstimateError('failed', err instanceof Error ? err.message : String(err));
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Price a grid of generations in ONE request: a list of workflow bodies in, one
|
|
88
|
+
* result per body (a cost, or that cell's error) plus a run total out. Backed by
|
|
89
|
+
* the `ESTIMATE_WORKFLOW_BATCH` → `ESTIMATE_BATCH_RESULT` bridge.
|
|
90
|
+
*
|
|
91
|
+
* Use it where an app would otherwise call `useBuzzWorkflow().estimate()` once
|
|
92
|
+
* per cell. Every rule a single estimate enforces still applies to each cell;
|
|
93
|
+
* what changes is the accounting and the round trips:
|
|
94
|
+
*
|
|
95
|
+
* - **One request against the estimate allowance**, whatever the number of
|
|
96
|
+
* cells. On a page app that allowance is shared by every viewer of the app and
|
|
97
|
+
* by the host's other reads for it, so a 16-cell grid used to take 16 of it per
|
|
98
|
+
* pricing pass.
|
|
99
|
+
* - **Cells are metered separately.** The host also counts the cells of every
|
|
100
|
+
* batch against an allowance of their own, shared like the estimate allowance
|
|
101
|
+
* (on a page app, by every viewer of the app), so a batch is not a way around
|
|
102
|
+
* it. A call over either is refused whole (`code: 'failed'`, the host's
|
|
103
|
+
* rate-limit text on `hostError`); retry in a few seconds.
|
|
104
|
+
* - **At most {@link BATCH_ESTIMATE_MAX_CELLS} bodies per call.**
|
|
105
|
+
* - **`kind: 'training'` bodies are refused per cell.** Estimate a training run
|
|
106
|
+
* with `useBuzzWorkflow().estimate()`.
|
|
107
|
+
*
|
|
108
|
+
* 🔴 ESTIMATE ONLY. There is no batch submit and no server-side id for the
|
|
109
|
+
* list. To watch and cancel the submitted cells together, use
|
|
110
|
+
* `useWorkflowBatch()`. The total is a quote: nothing reserves it and each cell
|
|
111
|
+
* is priced again at `submit()`.
|
|
112
|
+
*
|
|
113
|
+
* Like `estimate()`, this does NOT prompt for consent. It needs the
|
|
114
|
+
* `ai:write:budgeted` scope; without it the call rejects with `code: 'failed'`.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* const { estimateBatch } = useBatchEstimate();
|
|
118
|
+
* try {
|
|
119
|
+
* const { cells, aggregate } = await estimateBatch(bodies);
|
|
120
|
+
* // cells[i] answers bodies[i]; a cell with `!c.ok && c.timedOut` was not
|
|
121
|
+
* // priced in time — show it as unavailable and retry later
|
|
122
|
+
* } catch (err) {
|
|
123
|
+
* if (!(err instanceof BatchEstimateError)) throw err;
|
|
124
|
+
* if (err.code === 'unsupported') {
|
|
125
|
+
* // this host has no batch estimate — price each cell with estimate()
|
|
126
|
+
* } else {
|
|
127
|
+
* // 'timeout' (a slow host, not an old one) or 'failed': show the estimate
|
|
128
|
+
* // as unavailable and retry later — never one estimate() per cell
|
|
129
|
+
* }
|
|
130
|
+
* }
|
|
131
|
+
*/
|
|
132
|
+
export function useBatchEstimate() {
|
|
133
|
+
const [pending, setPending] = useState(false);
|
|
134
|
+
const [result, setResult] = useState(null);
|
|
135
|
+
const [error, setError] = useState(null);
|
|
136
|
+
// Latest call wins: an app re-prices on a form edit, so an older reply landing
|
|
137
|
+
// after a newer one must not put the older total back on display.
|
|
138
|
+
const sequencer = useRequestSequencer();
|
|
139
|
+
const estimateBatch = useCallback(async (bodies, options) => {
|
|
140
|
+
const call = sequencer.begin();
|
|
141
|
+
if (sequencer.isCurrent(call)) {
|
|
142
|
+
setPending(true);
|
|
143
|
+
setError(null);
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
if (!Array.isArray(bodies) || bodies.length === 0) {
|
|
147
|
+
throw new BatchEstimateError('invalid-request', 'invalid estimate batch');
|
|
148
|
+
}
|
|
149
|
+
if (bodies.length > BATCH_ESTIMATE_MAX_CELLS) {
|
|
150
|
+
throw new BatchEstimateError('invalid-request', 'estimate batch too large');
|
|
151
|
+
}
|
|
152
|
+
const reply = await sendTypedRequest(getTransport(), { type: 'ESTIMATE_WORKFLOW_BATCH', payload: { bodies } }, 'ESTIMATE_BATCH_RESULT',
|
|
153
|
+
// Server-bound like the single estimate, and slower: up to 16 cost
|
|
154
|
+
// quotes behind one reply.
|
|
155
|
+
{ timeoutMs: options?.timeoutMs ?? WORKFLOW_REQUEST_TIMEOUT_MS });
|
|
156
|
+
if (reply.error !== undefined) {
|
|
157
|
+
if (reply.error === HOST_UNSUPPORTED_REPLY) {
|
|
158
|
+
throw new BatchEstimateError('unsupported', reply.error);
|
|
159
|
+
}
|
|
160
|
+
if (reply.error === 'invalid estimate batch' ||
|
|
161
|
+
reply.error === 'estimate batch too large') {
|
|
162
|
+
throw new BatchEstimateError('invalid-request', reply.error);
|
|
163
|
+
}
|
|
164
|
+
throw new BatchEstimateError('failed', reply.error);
|
|
165
|
+
}
|
|
166
|
+
const { snapshots, aggregate } = reply;
|
|
167
|
+
if (!snapshots || !aggregate || snapshots.length !== bodies.length) {
|
|
168
|
+
// A reply that does not answer every body cannot be lined up with the
|
|
169
|
+
// grid, so it is refused rather than shown shifted by a cell.
|
|
170
|
+
throw new BatchEstimateError('failed', 'the host returned a malformed batch estimate');
|
|
171
|
+
}
|
|
172
|
+
const next = { cells: snapshots.map(toCell), aggregate };
|
|
173
|
+
if (sequencer.isCurrent(call))
|
|
174
|
+
setResult(next);
|
|
175
|
+
return next;
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
const wrapped = toBatchEstimateError(err);
|
|
179
|
+
if (sequencer.isCurrent(call)) {
|
|
180
|
+
setError(wrapped);
|
|
181
|
+
// Fail closed: a confirm gate reading `result` must not keep quoting the
|
|
182
|
+
// previous list's total after this one failed.
|
|
183
|
+
setResult(null);
|
|
184
|
+
}
|
|
185
|
+
// Thrown even when superseded or unmounted — the caller's `await` is not
|
|
186
|
+
// the component.
|
|
187
|
+
throw wrapped;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (sequencer.isCurrent(call))
|
|
191
|
+
setPending(false);
|
|
192
|
+
}
|
|
193
|
+
}, [sequencer]);
|
|
194
|
+
return { estimateBatch, pending, result, error };
|
|
195
|
+
}
|
|
196
|
+
//# sourceMappingURL=useBatchEstimate.js.map
|
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
/** What {@link useBlockAnalytics} returns. */
|
|
2
2
|
export interface UseBlockAnalytics {
|
|
3
|
+
/**
|
|
4
|
+
* Send one custom event. Fire-and-forget: returns nothing, and nothing tells
|
|
5
|
+
* you whether it was recorded. See {@link useBlockAnalytics}
|
|
6
|
+
* for what is kept.
|
|
7
|
+
*/
|
|
3
8
|
track: (eventName: string, properties?: Record<string, unknown>) => void;
|
|
4
9
|
}
|
|
5
10
|
/**
|
|
6
|
-
* Fire-and-forget
|
|
7
|
-
*
|
|
8
|
-
*
|
|
11
|
+
* Custom events for your app's analytics panel. Fire-and-forget: the host
|
|
12
|
+
* forwards the event and the block gets no acknowledgement, so never wait on
|
|
13
|
+
* one or branch on it.
|
|
14
|
+
*
|
|
15
|
+
* 🔴 **Only what your manifest DECLARES is recorded.** Declare every event, and
|
|
16
|
+
* every property it carries, under `analytics.events` in `block.manifest.json`.
|
|
17
|
+
* The declaration is reviewed with the rest of the manifest, and events are
|
|
18
|
+
* accepted against the APPROVED version:
|
|
19
|
+
*
|
|
20
|
+
* - **An undeclared event** is counted as dropped. Its name and properties are
|
|
21
|
+
* not stored; the owner sees only a count of drops.
|
|
22
|
+
* - **A declared event keeps only its declared properties**, each only when the
|
|
23
|
+
* value has the declared type. An undeclared property, or a value of the
|
|
24
|
+
* wrong type, is dropped and the rest of the event is kept.
|
|
25
|
+
* - **Property types are `enum`, `number` and `boolean`.** There is no free-text
|
|
26
|
+
* string: a string is kept only when it is one of the enum's declared values.
|
|
27
|
+
* So a prompt, an email or a username cannot be recorded. A `number` is not
|
|
28
|
+
* range-limited, so do not use one to carry an identifier.
|
|
29
|
+
* - Names are lowercase snake_case: a letter, then up to 63 of `a-z`, `0-9`, `_`.
|
|
30
|
+
* At most 50 events per app and 10 properties per event.
|
|
31
|
+
*
|
|
32
|
+
* **Limits.** Each mount of your app may send at most 10 events per second and
|
|
33
|
+
* 500 in total; the host batches events into bodies of at most 32 KiB. An event
|
|
34
|
+
* over a limit is dropped in the browser and is not counted anywhere, so the
|
|
35
|
+
* panel's numbers are floors. Events are recorded only from the production
|
|
36
|
+
* site.
|
|
37
|
+
*
|
|
38
|
+
* **What owners see.** Aggregates in the app's analytics panel: per declared
|
|
39
|
+
* event, the count, unique signed-in viewers, and the declared properties'
|
|
40
|
+
* breakdowns. Signed-out viewers are counted as events but never identified,
|
|
41
|
+
* and the team's own activity is excluded by default. Individual events are
|
|
42
|
+
* never shown.
|
|
43
|
+
*
|
|
44
|
+
* **Check locally.** In `dev:live`, pass your manifest to `createLiveHost`
|
|
45
|
+
* (`createLiveHost({ blockToken, manifest })`) and the console warns once per
|
|
46
|
+
* undeclared event, undeclared property, or value of the wrong type.
|
|
9
47
|
*
|
|
10
48
|
* @example
|
|
49
|
+
* // block.manifest.json
|
|
50
|
+
* // "analytics": { "events": { "generate_clicked": { "properties": {
|
|
51
|
+
* // "mode": { "type": "enum", "values": ["txt2img", "img2img"] },
|
|
52
|
+
* // "batch": { "type": "number" } } } } }
|
|
11
53
|
* const { track } = useBlockAnalytics();
|
|
12
|
-
* track('generate_clicked', {
|
|
54
|
+
* track('generate_clicked', { mode: 'txt2img', batch: 4 });
|
|
13
55
|
*/
|
|
14
56
|
export declare function useBlockAnalytics(): UseBlockAnalytics;
|
|
15
57
|
//# sourceMappingURL=useBlockAnalytics.d.ts.map
|
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
/**
|
|
4
|
-
* Fire-and-forget
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Custom events for your app's analytics panel. Fire-and-forget: the host
|
|
5
|
+
* forwards the event and the block gets no acknowledgement, so never wait on
|
|
6
|
+
* one or branch on it.
|
|
7
|
+
*
|
|
8
|
+
* 🔴 **Only what your manifest DECLARES is recorded.** Declare every event, and
|
|
9
|
+
* every property it carries, under `analytics.events` in `block.manifest.json`.
|
|
10
|
+
* The declaration is reviewed with the rest of the manifest, and events are
|
|
11
|
+
* accepted against the APPROVED version:
|
|
12
|
+
*
|
|
13
|
+
* - **An undeclared event** is counted as dropped. Its name and properties are
|
|
14
|
+
* not stored; the owner sees only a count of drops.
|
|
15
|
+
* - **A declared event keeps only its declared properties**, each only when the
|
|
16
|
+
* value has the declared type. An undeclared property, or a value of the
|
|
17
|
+
* wrong type, is dropped and the rest of the event is kept.
|
|
18
|
+
* - **Property types are `enum`, `number` and `boolean`.** There is no free-text
|
|
19
|
+
* string: a string is kept only when it is one of the enum's declared values.
|
|
20
|
+
* So a prompt, an email or a username cannot be recorded. A `number` is not
|
|
21
|
+
* range-limited, so do not use one to carry an identifier.
|
|
22
|
+
* - Names are lowercase snake_case: a letter, then up to 63 of `a-z`, `0-9`, `_`.
|
|
23
|
+
* At most 50 events per app and 10 properties per event.
|
|
24
|
+
*
|
|
25
|
+
* **Limits.** Each mount of your app may send at most 10 events per second and
|
|
26
|
+
* 500 in total; the host batches events into bodies of at most 32 KiB. An event
|
|
27
|
+
* over a limit is dropped in the browser and is not counted anywhere, so the
|
|
28
|
+
* panel's numbers are floors. Events are recorded only from the production
|
|
29
|
+
* site.
|
|
30
|
+
*
|
|
31
|
+
* **What owners see.** Aggregates in the app's analytics panel: per declared
|
|
32
|
+
* event, the count, unique signed-in viewers, and the declared properties'
|
|
33
|
+
* breakdowns. Signed-out viewers are counted as events but never identified,
|
|
34
|
+
* and the team's own activity is excluded by default. Individual events are
|
|
35
|
+
* never shown.
|
|
36
|
+
*
|
|
37
|
+
* **Check locally.** In `dev:live`, pass your manifest to `createLiveHost`
|
|
38
|
+
* (`createLiveHost({ blockToken, manifest })`) and the console warns once per
|
|
39
|
+
* undeclared event, undeclared property, or value of the wrong type.
|
|
7
40
|
*
|
|
8
41
|
* @example
|
|
42
|
+
* // block.manifest.json
|
|
43
|
+
* // "analytics": { "events": { "generate_clicked": { "properties": {
|
|
44
|
+
* // "mode": { "type": "enum", "values": ["txt2img", "img2img"] },
|
|
45
|
+
* // "batch": { "type": "number" } } } } }
|
|
9
46
|
* const { track } = useBlockAnalytics();
|
|
10
|
-
* track('generate_clicked', {
|
|
47
|
+
* track('generate_clicked', { mode: 'txt2img', batch: 4 });
|
|
11
48
|
*/
|
|
12
49
|
export function useBlockAnalytics() {
|
|
13
50
|
const track = useCallback((eventName, properties) => {
|
|
@@ -207,7 +207,10 @@ export declare class WorkflowEstimateError extends Error {
|
|
|
207
207
|
* rather than on prose. It is the ONLY stable branch target here:
|
|
208
208
|
* `snapshot.error` is server-authored and can change without notice, and
|
|
209
209
|
* {@link WorkflowEstimateError.message} is a generic developer-facing summary
|
|
210
|
-
* whose exact wording is not a contract either.
|
|
210
|
+
* whose exact wording is not a contract either. The one carve-out is a batch
|
|
211
|
+
* cell the host's time budget ran out on: it keeps `code: 'failed'`, and its
|
|
212
|
+
* stable signal is `timedOut: true` on the `useBatchEstimate()` cell, never a
|
|
213
|
+
* match on `snapshot.error`.
|
|
211
214
|
*
|
|
212
215
|
* 🔴 THIS IS WHAT A VIEWER-FACING MESSAGE MUST BE DERIVED FROM. Branch on
|
|
213
216
|
* `code` and return a string your app owns and localises; never render
|
|
@@ -191,7 +191,10 @@ export class WorkflowEstimateError extends Error {
|
|
|
191
191
|
* rather than on prose. It is the ONLY stable branch target here:
|
|
192
192
|
* `snapshot.error` is server-authored and can change without notice, and
|
|
193
193
|
* {@link WorkflowEstimateError.message} is a generic developer-facing summary
|
|
194
|
-
* whose exact wording is not a contract either.
|
|
194
|
+
* whose exact wording is not a contract either. The one carve-out is a batch
|
|
195
|
+
* cell the host's time budget ran out on: it keeps `code: 'failed'`, and its
|
|
196
|
+
* stable signal is `timedOut: true` on the `useBatchEstimate()` cell, never a
|
|
197
|
+
* match on `snapshot.error`.
|
|
195
198
|
*
|
|
196
199
|
* 🔴 THIS IS WHAT A VIEWER-FACING MESSAGE MUST BE DERIVED FROM. Branch on
|
|
197
200
|
* `code` and return a string your app owns and localises; never render
|