@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
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
* --- BEGIN DERIVED ---
|
|
47
47
|
* fetch chokepoints ..... 2
|
|
48
48
|
* REST endpoints ........ GET /api/v1/blocks/me
|
|
49
|
-
* tRPC procedures .......
|
|
50
|
-
* switch case labels ....
|
|
49
|
+
* tRPC procedures ....... 29 = blocks.* (14) + apps.shared.* (10) + apps.storage.* (5)
|
|
50
|
+
* switch case labels .... 48, of which 10 REFUSE (enumerated under SCOPE below)
|
|
51
51
|
* --- END DERIVED ---
|
|
52
52
|
*
|
|
53
53
|
* PICKERS (Phase 1 of "make dev:live a faithful local host"): the live host
|
|
@@ -83,6 +83,12 @@
|
|
|
83
83
|
* the switch's `default: return` with no reply at all, so `useSharedStorage()`
|
|
84
84
|
* hung to the 30s protocol timeout in dev:live for code that worked in dev:mock.
|
|
85
85
|
*
|
|
86
|
+
* TRACK_EVENT: recorded only in production, so the live host forwards nothing
|
|
87
|
+
* and replies nothing (production replies nothing either). What it does is
|
|
88
|
+
* CHECK each event against the manifest passed as `options.manifest` and warn
|
|
89
|
+
* once per distinct problem production would silently drop β see
|
|
90
|
+
* `trackEventCheck.ts`.
|
|
91
|
+
*
|
|
86
92
|
* π΄ SCOPE β the capabilities live mode still cannot SERVE. Each REFUSES on its
|
|
87
93
|
* own reply channel β `logOnce` (or `console.info`) plus an honest error, never
|
|
88
94
|
* a fabricated success and never silence. Two guards stand behind this list:
|
|
@@ -129,6 +135,8 @@
|
|
|
129
135
|
import { consentUnavailablePayload, resolveUngrantableConsentNotice } from './consent.js';
|
|
130
136
|
import { hostContextWithTheme } from '../transport/transport.js';
|
|
131
137
|
import { isRoutableRequestId } from '../transport/requestId.js';
|
|
138
|
+
import { parseSharedStorageRateLimit } from '../transport/validate.js';
|
|
139
|
+
import { createTrackEventChecker } from './trackEventCheck.js';
|
|
132
140
|
import { openPickerOverlay, } from './pickerOverlay.js';
|
|
133
141
|
/** Default civitai backend the live host forwards to. */
|
|
134
142
|
const DEFAULT_BACKEND_BASE_URL = 'https://civitai.com';
|
|
@@ -225,6 +233,9 @@ function errorSnapshot(error) {
|
|
|
225
233
|
* deploy (the dev token is fine; the pod just hasn't loaded the verifier yet) β
|
|
226
234
|
* the exact blip the round-5 dogfood hit. Anything β₯ 500, plus 408 (timeout)
|
|
227
235
|
* and 429 (rate-limit), is also transient.
|
|
236
|
+
*
|
|
237
|
+
* Only {@link pollWithRetry} reads `transient`; a shared-storage 429 is a
|
|
238
|
+
* write-budget refusal whose wait is minutes, so it must never be retried.
|
|
228
239
|
*/
|
|
229
240
|
function isTransientHttpStatus(status) {
|
|
230
241
|
return status >= 500 || status === 408 || status === 429 || status === 401;
|
|
@@ -323,6 +334,8 @@ export function createLiveHost(options) {
|
|
|
323
334
|
'likely minted from an OAuth login. Real generation needs a full-scope personal API key: ' +
|
|
324
335
|
'`civitai login --token <key>` (https://civitai.com/user/account), then re-mint the dev token.');
|
|
325
336
|
}
|
|
337
|
+
// `TRACK_EVENT` vs the manifest's `analytics.events`; dedupes its own warnings.
|
|
338
|
+
const trackEventChecker = createTrackEventChecker(options.manifest);
|
|
326
339
|
// One-time "not supported in live v1" log gating per capability.
|
|
327
340
|
const loggedOnce = new Set();
|
|
328
341
|
const logOnce = (key, message) => {
|
|
@@ -377,7 +390,12 @@ export function createLiveHost(options) {
|
|
|
377
390
|
}
|
|
378
391
|
if (!res.ok) {
|
|
379
392
|
const msg = extractTrpcError(parsed) ?? `${procedure} failed: HTTP ${res.status}`;
|
|
380
|
-
|
|
393
|
+
const rateLimit = parseSharedStorageRateLimit(extractTrpcErrorData(parsed)?.rateLimit);
|
|
394
|
+
return {
|
|
395
|
+
error: msg,
|
|
396
|
+
transient: isTransientHttpStatus(res.status),
|
|
397
|
+
...(rateLimit ? { rateLimit } : {}),
|
|
398
|
+
};
|
|
381
399
|
}
|
|
382
400
|
return { parsed };
|
|
383
401
|
}
|
|
@@ -411,8 +429,13 @@ export function createLiveHost(options) {
|
|
|
411
429
|
*/
|
|
412
430
|
async function callTrpcData(procedure, input, method) {
|
|
413
431
|
const raw = await rawTrpcCall(procedure, input, method);
|
|
414
|
-
if (raw.error !== undefined)
|
|
415
|
-
return {
|
|
432
|
+
if (raw.error !== undefined) {
|
|
433
|
+
return {
|
|
434
|
+
error: raw.error,
|
|
435
|
+
transient: raw.transient,
|
|
436
|
+
...(raw.rateLimit ? { rateLimit: raw.rateLimit } : {}),
|
|
437
|
+
};
|
|
438
|
+
}
|
|
416
439
|
const data = extractData(raw.parsed);
|
|
417
440
|
if (data === undefined) {
|
|
418
441
|
return { error: `${procedure}: malformed response (no data)` };
|
|
@@ -659,16 +682,32 @@ export function createLiveHost(options) {
|
|
|
659
682
|
fetchImpl,
|
|
660
683
|
...(params.baseModelGroup != null ? { baseModelGroup: params.baseModelGroup } : {}),
|
|
661
684
|
...(params.currentVersionId != null ? { currentVersionId: params.currentVersionId } : {}),
|
|
685
|
+
...(params.multiple ? { multiple: params.multiple } : {}),
|
|
662
686
|
...(win.document ? { document: win.document } : {}),
|
|
663
687
|
...(options.onPickerReady ? { onReady: options.onPickerReady } : {}),
|
|
664
688
|
onResolve: (selection) => {
|
|
665
689
|
openOverlays.delete(handle);
|
|
666
690
|
if (torn)
|
|
667
691
|
return;
|
|
692
|
+
if (params.multiple) {
|
|
693
|
+
// MULTI-select: the reply shape follows the REQUEST β always a list,
|
|
694
|
+
// `[]` on dismissal, and never `selected` (the production contract).
|
|
695
|
+
dispatchToBlock({
|
|
696
|
+
type: resultType,
|
|
697
|
+
payload: {
|
|
698
|
+
requestId,
|
|
699
|
+
selectedResources: selection && 'selectedResources' in selection ? selection.selectedResources : [],
|
|
700
|
+
},
|
|
701
|
+
});
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
668
704
|
dispatchToBlock({
|
|
669
705
|
type: resultType,
|
|
670
706
|
// A dismissal omits `selected` (the hooks resolve to undefined/null).
|
|
671
|
-
payload: {
|
|
707
|
+
payload: {
|
|
708
|
+
requestId,
|
|
709
|
+
...(selection && 'selected' in selection ? { selected: selection.selected } : {}),
|
|
710
|
+
},
|
|
672
711
|
});
|
|
673
712
|
},
|
|
674
713
|
});
|
|
@@ -723,6 +762,27 @@ export function createLiveHost(options) {
|
|
|
723
762
|
});
|
|
724
763
|
return;
|
|
725
764
|
}
|
|
765
|
+
case 'ESTIMATE_WORKFLOW_BATCH': {
|
|
766
|
+
// Forwarded to the token-bound `blocks.estimateWorkflowBatch`, the
|
|
767
|
+
// batch twin of the estimate above. Its reply is a plain
|
|
768
|
+
// `{ snapshots, aggregate }` (unwrapped with `callTrpcData`); a
|
|
769
|
+
// whole-call failure is FREE-TEXT `error`, a per-cell failure rides in
|
|
770
|
+
// `snapshots`. Estimate only β nothing is submitted. Unroutable
|
|
771
|
+
// without a requestId. Against a civitai that predates the procedure
|
|
772
|
+
// the server's own not-found error comes back here as `error`.
|
|
773
|
+
if (!isRoutableRequestId(requestId))
|
|
774
|
+
return;
|
|
775
|
+
void callTrpcData('blocks.estimateWorkflowBatch', { blockToken: rawToken, bodies: typed.payload?.bodies }, 'POST').then((r) => {
|
|
776
|
+
const data = r.data;
|
|
777
|
+
dispatchToBlock({
|
|
778
|
+
type: 'ESTIMATE_BATCH_RESULT',
|
|
779
|
+
payload: data && Array.isArray(data.snapshots) && data.aggregate
|
|
780
|
+
? { requestId, snapshots: data.snapshots, aggregate: data.aggregate }
|
|
781
|
+
: { requestId, error: r.error ?? 'batch estimate failed' },
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
726
786
|
case 'SUBMIT_WORKFLOW': {
|
|
727
787
|
const body = typed.payload?.body;
|
|
728
788
|
// π΄ KNOWN GAP, DELIBERATELY NOT CHANGED HERE β see below before
|
|
@@ -1158,11 +1218,37 @@ export function createLiveHost(options) {
|
|
|
1158
1218
|
// (or no `selected` on dismissal). DISCOVERY ONLY β re-priced +
|
|
1159
1219
|
// re-validated server-side at estimate/submit.
|
|
1160
1220
|
const resourceType = typed.payload?.resourceType === 'Checkpoint' ? 'Checkpoint' : 'LORA';
|
|
1221
|
+
// MULTI-select (`multiple: { max }`) β the production host's rules:
|
|
1222
|
+
// LoRA only, `max` a whole number β₯ 1 clamped to 5, and a refusal is
|
|
1223
|
+
// ANSWERED with `error` (the hook throws on it) rather than opening
|
|
1224
|
+
// a single-pick overlay the block did not ask for.
|
|
1225
|
+
const rawMultiple = typed.payload?.multiple;
|
|
1226
|
+
let multiple;
|
|
1227
|
+
if (rawMultiple !== undefined && rawMultiple !== null) {
|
|
1228
|
+
const max = typeof rawMultiple === 'object' ? rawMultiple.max : undefined;
|
|
1229
|
+
const refusal = resourceType === 'Checkpoint'
|
|
1230
|
+
? 'OPEN_RESOURCE_PICKER: multiple is only supported for LoRA-family resource types, not Checkpoint.'
|
|
1231
|
+
: typeof max !== 'number' || !Number.isInteger(max) || max < 1
|
|
1232
|
+
? 'OPEN_RESOURCE_PICKER: multiple.max must be a whole number of at least 1.'
|
|
1233
|
+
: null;
|
|
1234
|
+
if (refusal !== null) {
|
|
1235
|
+
// A refusal nobody can route is not worth sending.
|
|
1236
|
+
if (!isRoutableRequestId(requestId))
|
|
1237
|
+
return;
|
|
1238
|
+
dispatchToBlock({
|
|
1239
|
+
type: 'RESOURCE_PICKER_RESULT',
|
|
1240
|
+
payload: { requestId, error: refusal },
|
|
1241
|
+
});
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1244
|
+
multiple = { max: Math.min(max, 5) };
|
|
1245
|
+
}
|
|
1161
1246
|
openPicker({
|
|
1162
1247
|
type: resourceType,
|
|
1163
1248
|
...(typeof typed.payload?.baseModelGroup === 'string'
|
|
1164
1249
|
? { baseModelGroup: typed.payload.baseModelGroup }
|
|
1165
1250
|
: {}),
|
|
1251
|
+
...(multiple ? { multiple } : {}),
|
|
1166
1252
|
}, 'RESOURCE_PICKER_RESULT', requestId ?? '');
|
|
1167
1253
|
return;
|
|
1168
1254
|
}
|
|
@@ -1523,7 +1609,7 @@ export function createLiveHost(options) {
|
|
|
1523
1609
|
if (r.error) {
|
|
1524
1610
|
dispatchToBlock({
|
|
1525
1611
|
type: 'SHARED_APPEND_RESULT',
|
|
1526
|
-
payload: { requestId: requestId ?? '', key: '',
|
|
1612
|
+
payload: { requestId: requestId ?? '', key: '', ...sharedRefusal(r) },
|
|
1527
1613
|
});
|
|
1528
1614
|
return;
|
|
1529
1615
|
}
|
|
@@ -1542,7 +1628,7 @@ export function createLiveHost(options) {
|
|
|
1542
1628
|
dispatchToBlock({
|
|
1543
1629
|
type: 'SHARED_UPDATE_RESULT',
|
|
1544
1630
|
payload: r.error
|
|
1545
|
-
? { requestId: requestId ?? '', ok: false,
|
|
1631
|
+
? { requestId: requestId ?? '', ok: false, ...sharedRefusal(r) }
|
|
1546
1632
|
: { requestId: requestId ?? '', ok: true },
|
|
1547
1633
|
});
|
|
1548
1634
|
});
|
|
@@ -1554,7 +1640,7 @@ export function createLiveHost(options) {
|
|
|
1554
1640
|
if (r.error) {
|
|
1555
1641
|
dispatchToBlock({
|
|
1556
1642
|
type: 'SHARED_VOTE_RESULT',
|
|
1557
|
-
payload: { requestId: requestId ?? '', count: 0,
|
|
1643
|
+
payload: { requestId: requestId ?? '', count: 0, ...sharedRefusal(r) },
|
|
1558
1644
|
});
|
|
1559
1645
|
return;
|
|
1560
1646
|
}
|
|
@@ -1572,7 +1658,7 @@ export function createLiveHost(options) {
|
|
|
1572
1658
|
if (r.error) {
|
|
1573
1659
|
dispatchToBlock({
|
|
1574
1660
|
type: 'SHARED_UNVOTE_RESULT',
|
|
1575
|
-
payload: { requestId: requestId ?? '', count: 0,
|
|
1661
|
+
payload: { requestId: requestId ?? '', count: 0, ...sharedRefusal(r) },
|
|
1576
1662
|
});
|
|
1577
1663
|
return;
|
|
1578
1664
|
}
|
|
@@ -1594,7 +1680,7 @@ export function createLiveHost(options) {
|
|
|
1594
1680
|
requestId: requestId ?? '',
|
|
1595
1681
|
ok: false,
|
|
1596
1682
|
deleted: false,
|
|
1597
|
-
|
|
1683
|
+
...sharedRefusal(r),
|
|
1598
1684
|
},
|
|
1599
1685
|
});
|
|
1600
1686
|
return;
|
|
@@ -1632,7 +1718,7 @@ export function createLiveHost(options) {
|
|
|
1632
1718
|
dispatchToBlock({
|
|
1633
1719
|
type: 'SHARED_REPORT_RESULT',
|
|
1634
1720
|
payload: r.error
|
|
1635
|
-
? { requestId: requestId ?? '', ok: false,
|
|
1721
|
+
? { requestId: requestId ?? '', ok: false, ...sharedRefusal(r) }
|
|
1636
1722
|
: { requestId: requestId ?? '', ok: true },
|
|
1637
1723
|
});
|
|
1638
1724
|
});
|
|
@@ -1865,9 +1951,19 @@ export function createLiveHost(options) {
|
|
|
1865
1951
|
return;
|
|
1866
1952
|
}
|
|
1867
1953
|
case 'TRACK_EVENT':
|
|
1954
|
+
// Fire-and-forget, no reply β as in production, where the host
|
|
1955
|
+
// forwards it and the ingest keeps only what the manifest's
|
|
1956
|
+
// `analytics.events` declares. Nothing is sent anywhere from here;
|
|
1957
|
+
// the only effect is a once-per-problem console warning for what
|
|
1958
|
+
// production would drop (see `trackEventCheck.ts`).
|
|
1959
|
+
for (const message of trackEventChecker.check(typed.payload)) {
|
|
1960
|
+
// eslint-disable-next-line no-console
|
|
1961
|
+
console.warn(`[createLiveHost] ${message}`);
|
|
1962
|
+
}
|
|
1963
|
+
return;
|
|
1868
1964
|
case 'REQUEST_SIGN_IN':
|
|
1869
1965
|
case 'BLOCK_ERROR':
|
|
1870
|
-
// No-op.
|
|
1966
|
+
// No-op. BLOCK_ERROR is fire-and-forget; REQUEST_SIGN_IN
|
|
1871
1967
|
// can't open a login in dev (the viewer comes from the token β a
|
|
1872
1968
|
// failed viewer fetch already warns in resolveViewer).
|
|
1873
1969
|
return;
|
|
@@ -2051,6 +2147,21 @@ function extractData(parsed) {
|
|
|
2051
2147
|
* `{ error: { message } }`. Returns undefined when no message is found.
|
|
2052
2148
|
*/
|
|
2053
2149
|
function extractTrpcError(parsed) {
|
|
2150
|
+
const message = trpcErrorShape(parsed)?.message;
|
|
2151
|
+
return typeof message === 'string' ? message : undefined;
|
|
2152
|
+
}
|
|
2153
|
+
/**
|
|
2154
|
+
* The `data` object of a tRPC error response (`{ error: { json: { data } } }`
|
|
2155
|
+
* or `{ error: { data } }`), or undefined. The platform's error formatter puts
|
|
2156
|
+
* machine-readable refusal fields here β `rateLimit` on a rate-limited
|
|
2157
|
+
* shared-storage write β which {@link extractTrpcError} alone dropped.
|
|
2158
|
+
*/
|
|
2159
|
+
function extractTrpcErrorData(parsed) {
|
|
2160
|
+
const data = trpcErrorShape(parsed)?.data;
|
|
2161
|
+
return typeof data === 'object' && data !== null ? data : undefined;
|
|
2162
|
+
}
|
|
2163
|
+
/** The unwrapped tRPC error shape (superjson `{ json }` or plain), or undefined. */
|
|
2164
|
+
function trpcErrorShape(parsed) {
|
|
2054
2165
|
if (typeof parsed !== 'object' || parsed === null)
|
|
2055
2166
|
return undefined;
|
|
2056
2167
|
const error = parsed.error;
|
|
@@ -2059,7 +2170,14 @@ function extractTrpcError(parsed) {
|
|
|
2059
2170
|
const inner = 'json' in error ? error.json : error;
|
|
2060
2171
|
if (typeof inner !== 'object' || inner === null)
|
|
2061
2172
|
return undefined;
|
|
2062
|
-
|
|
2063
|
-
|
|
2173
|
+
return inner;
|
|
2174
|
+
}
|
|
2175
|
+
/**
|
|
2176
|
+
* The error fields of a shared-storage WRITE reply β `error`, plus `rateLimit`
|
|
2177
|
+
* only when the refusal carried it. The dev-host twin of the production host's
|
|
2178
|
+
* `sharedStorageErrorReply`, so the six write bridges build the refusal one way.
|
|
2179
|
+
*/
|
|
2180
|
+
function sharedRefusal(r) {
|
|
2181
|
+
return { error: r.error ?? '', ...(r.rateLimit ? { rateLimit: r.rateLimit } : {}) };
|
|
2064
2182
|
}
|
|
2065
2183
|
//# sourceMappingURL=liveHost.js.map
|
|
@@ -93,6 +93,23 @@ export interface MockGenerationScenario {
|
|
|
93
93
|
* a `(body) => number`. Overrides the legacy top-level `cost`. Default `8`.
|
|
94
94
|
*/
|
|
95
95
|
costPerGen?: CostSpec;
|
|
96
|
+
/**
|
|
97
|
+
* The app author's per-generation fee, itemised as `cost.authorFee` on a
|
|
98
|
+
* QUOTE β the `ESTIMATE_RESULT` and the {@link submitCapRefusal} reply β and
|
|
99
|
+
* added into that quote's `cost.total` on top of {@link costPerGen}, as the
|
|
100
|
+
* real host does. A number, or a `(body) => number`. Default `0`, which is
|
|
101
|
+
* reported as `authorFee: 0` ("no fee on this request").
|
|
102
|
+
*
|
|
103
|
+
* Like the real host, only a `textToImage` or registered-`step` body is
|
|
104
|
+
* itemised; `customComfy`, pass-through `step` and `training` quotes carry no
|
|
105
|
+
* `authorFee`. The succeeded snapshot's `cost.total` stays {@link costPerGen}
|
|
106
|
+
* (a submitted workflow reports the generation's own cost), while a simulated
|
|
107
|
+
* {@link MockBuzzScenario.balance} is debited `costPerGen + authorFee`.
|
|
108
|
+
*
|
|
109
|
+
* `false` simulates a host that PREDATES the field: `authorFee` is omitted
|
|
110
|
+
* everywhere and no fee is added β use it to test your "not itemised" branch.
|
|
111
|
+
*/
|
|
112
|
+
authorFee?: CostSpec | false;
|
|
96
113
|
/**
|
|
97
114
|
* Synthetic latency before the SUBMITTEDβsucceeded transition lands, in ms.
|
|
98
115
|
* A single number, or a `[min, max]` range (uniform random). Applied to the
|
|
@@ -149,6 +166,58 @@ export interface MockGenerationScenario {
|
|
|
149
166
|
* resource-compatibility rejection) to check how your block renders one.
|
|
150
167
|
*/
|
|
151
168
|
failEstimateMessage?: string;
|
|
169
|
+
/**
|
|
170
|
+
* How the mock answers `ESTIMATE_WORKFLOW_BATCH` (`useBatchEstimate()`), so a
|
|
171
|
+
* block can exercise its per-cell fallback locally.
|
|
172
|
+
*
|
|
173
|
+
* - `'supported'` (default) β prices each cell exactly as an
|
|
174
|
+
* `ESTIMATE_WORKFLOW` of that body would be priced (`costPerGen`,
|
|
175
|
+
* `failEstimate` and the training rules all apply per cell) and replies with
|
|
176
|
+
* the snapshots in order plus the host's aggregate.
|
|
177
|
+
* - `'unsupported'` β replies `{ error: 'unsupported on this host' }`, the
|
|
178
|
+
* host's generic answer for a message it has no handler for.
|
|
179
|
+
* `estimateBatch()` rejects with `BatchEstimateError` code `'unsupported'`.
|
|
180
|
+
* - `'silent'` β never replies, which is what a host that PREDATES the message
|
|
181
|
+
* does. `estimateBatch()` rejects with code `'timeout'` once its wait ends;
|
|
182
|
+
* pass a short `timeoutMs` to it in a test.
|
|
183
|
+
*
|
|
184
|
+
* Live-tunable via `setScenario({ generation: { batchEstimate } })`.
|
|
185
|
+
*/
|
|
186
|
+
batchEstimate?: 'supported' | 'unsupported' | 'silent';
|
|
187
|
+
/**
|
|
188
|
+
* Indices of batch cells the mock answers as NOT PRICED IN TIME β the snapshot
|
|
189
|
+
* the host returns for a cell when the call's time budget runs out before that
|
|
190
|
+
* cell priced: `{ workflowId:'failed', status:'failed', error:'estimate timed out' }`
|
|
191
|
+
* (the host's string, held once in `BATCH_ESTIMATE_CELL_TIMEOUT_ERROR`).
|
|
192
|
+
* The other cells price normally and the call still resolves, so this tests a
|
|
193
|
+
* PARTIAL grid: the cell is `ok: false`, `timedOut: true`, with
|
|
194
|
+
* `WorkflowEstimateError` code `'failed'`. Applies only with
|
|
195
|
+
* `batchEstimate: 'supported'`. Default: unset (no cell times out).
|
|
196
|
+
*/
|
|
197
|
+
batchEstimateTimedOutCells?: number[];
|
|
198
|
+
/**
|
|
199
|
+
* Make `CANCEL_WORKFLOW` answer the way the real host answers a cancel it
|
|
200
|
+
* REFUSED because the estimate allowance is used up: `{ workflowId, status:
|
|
201
|
+
* 'processing' }`. The cancel is not applied, so later polls keep reporting
|
|
202
|
+
* the workflow as running. `true` refuses every cancel; an array refuses only
|
|
203
|
+
* those workflow ids. Use it to test a cancel button's "not cancelled, try
|
|
204
|
+
* again" path, including `useWorkflowBatch().cancelAll()`'s `refused` and
|
|
205
|
+
* `notAttempted` lists.
|
|
206
|
+
*
|
|
207
|
+
* Live-tunable via `setScenario({ generation: { cancelRefused } })`. Default:
|
|
208
|
+
* unset (every cancel applies).
|
|
209
|
+
*/
|
|
210
|
+
cancelRefused?: boolean | readonly string[];
|
|
211
|
+
/**
|
|
212
|
+
* The terminal status each polled workflow ends with, so a grid can be tested
|
|
213
|
+
* with a mix of outcomes. Called once per workflow, on the poll that finishes
|
|
214
|
+
* it, with the workflow id and the body it was submitted with (`undefined`
|
|
215
|
+
* for an id this mock did not submit, such as one restored after a reload).
|
|
216
|
+
*
|
|
217
|
+
* `'failed'` and `'expired'` reply `{ workflowId, status, error }` with no
|
|
218
|
+
* `cost` and no images. Default: every workflow `'succeeded'`.
|
|
219
|
+
*/
|
|
220
|
+
workflowOutcome?: (workflowId: string, body: WorkflowBody | undefined) => 'succeeded' | 'failed' | 'expired';
|
|
152
221
|
/**
|
|
153
222
|
* Force every SUBMIT to come back as a caught server EXCEPTION β the host's
|
|
154
223
|
* real `failureSnapshot(err)` shape: `{ workflowId:'failed', status:'failed',
|
|
@@ -424,6 +493,27 @@ export interface MockSharedScenario {
|
|
|
424
493
|
* error UX.
|
|
425
494
|
*/
|
|
426
495
|
failNext?: number;
|
|
496
|
+
/**
|
|
497
|
+
* Refuse the next N shared WRITES (`append`/`update`/`vote`/`unvote`/
|
|
498
|
+
* `withdraw`/`report`) as RATE-LIMITED (counts down), the way the platform
|
|
499
|
+
* does when a per-(viewer, app) budget is empty: the reply carries the
|
|
500
|
+
* server's refusal text plus `rateLimit` metadata, so the hook rejects with
|
|
501
|
+
* `SharedStorageRateLimitError`. Checked before {@link failNext}, as the
|
|
502
|
+
* server checks the budget before attempting the write. Live-tunable via
|
|
503
|
+
* `setScenario({ shared: { rateLimitNext } })`.
|
|
504
|
+
*/
|
|
505
|
+
rateLimitNext?: number;
|
|
506
|
+
/**
|
|
507
|
+
* The metadata a {@link rateLimitNext} refusal carries. `retryAfterSeconds`
|
|
508
|
+
* defaults to 60; `retryAt` defaults to now + `retryAfterSeconds`, computed
|
|
509
|
+
* when each refusal is sent. Pass a malformed value to exercise a block's
|
|
510
|
+
* fallback: the hook parses it, ignores the malformed value and throws a plain
|
|
511
|
+
* `Error`.
|
|
512
|
+
*/
|
|
513
|
+
rateLimit?: {
|
|
514
|
+
retryAfterSeconds?: number;
|
|
515
|
+
retryAt?: string;
|
|
516
|
+
};
|
|
427
517
|
}
|
|
428
518
|
/**
|
|
429
519
|
* Drives `createMockHost`. Every field is optional with a sensible default so
|
|
@@ -477,6 +567,18 @@ export interface MockHostOptions {
|
|
|
477
567
|
* for that type. Defaults to a curated Checkpoint + LoRA pick.
|
|
478
568
|
*/
|
|
479
569
|
cannedPicks?: Partial<Record<BlockResourcePickerType, CannedPick | null>>;
|
|
570
|
+
/**
|
|
571
|
+
* What a MULTI-select `OPEN_RESOURCE_PICKER` (`multiple: { max }`, LoRA only)
|
|
572
|
+
* answers with: these picks, in this order, cut to the request's `max` (itself
|
|
573
|
+
* clamped to 5) β the real host's `selectedResources`. `null` or `[]`
|
|
574
|
+
* simulates a dismissed picker (β an empty list). Defaults to two curated
|
|
575
|
+
* LoRAs, or to `[]` when `cannedPicks.LORA` is `null`, so one "the viewer
|
|
576
|
+
* dismissed" setting covers both pick modes.
|
|
577
|
+
*
|
|
578
|
+
* The mock applies the real host's refusals too: `multiple` with a Checkpoint,
|
|
579
|
+
* or a `max` that is not a whole number β₯ 1, is answered with an `error`.
|
|
580
|
+
*/
|
|
581
|
+
cannedMultiPicks?: CannedPick[] | null;
|
|
480
582
|
/**
|
|
481
583
|
* The canned moderated image returned from `OPEN_IMAGE_UPLOAD` when the block
|
|
482
584
|
* requests `purpose:'display'` (the default) β what `useImageUpload().open()`
|
|
@@ -1054,7 +1156,7 @@ export interface MockHostOptions {
|
|
|
1054
1156
|
* {@link MockHost.setTheme}, which pushes a real `THEME_CHANGE` β the mock of
|
|
1055
1157
|
* the viewer toggling dark mode with the block already mounted.)
|
|
1056
1158
|
*/
|
|
1057
|
-
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'createPostResult' | 'createPostError' | 'trainingDatasetRejected' | 'trainingDatasetError' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes' | 'saveImageError' | 'uploadImageBytesResult' | 'uploadImageBytesError'>;
|
|
1159
|
+
export type MockHostScenarioPatch = Pick<MockHostOptions, 'consentGrantable' | 'failMode' | 'cost' | 'pollsUntilDone' | 'cannedPicks' | 'cannedMultiPicks' | 'cannedImageUpload' | 'cannedGenerationSourceUpload' | 'cannedImageScan' | 'generation' | 'buzz' | 'storage' | 'shared' | 'buzzBalanceError' | 'viewerResult' | 'viewerError' | 'buzzTransactions' | 'buzzAccounts' | 'dailyCompensation' | 'buzzReadError' | 'wildcardPack' | 'wildcardPackError' | 'collectionFollowError' | 'createPostResult' | 'createPostError' | 'trainingDatasetRejected' | 'trainingDatasetError' | 'trainingQuoteTotal' | 'runTrainingError' | 'runTrainingCapRefusal' | 'appWorkflows' | 'appWorkflowsError' | 'publishImageIds' | 'publishError' | 'gatedImages' | 'gatedImagesError' | 'disallowedAccountTypes' | 'saveImageError' | 'uploadImageBytesResult' | 'uploadImageBytesError'>;
|
|
1058
1160
|
/** Runtime Buzz-balance handle exposed on {@link MockHost.buzz}. */
|
|
1059
1161
|
export interface MockBuzzHandle {
|
|
1060
1162
|
/** Current simulated balance, or `undefined` when balance isn't simulated. */
|
|
@@ -1099,7 +1201,7 @@ export declare function disallowedAccountError(accountType: BuzzAccountType): st
|
|
|
1099
1201
|
/**
|
|
1100
1202
|
* Reads the URL query toggles the gen-matrix dev harness uses, so a starter's
|
|
1101
1203
|
* dev harness keeps working with `?viewer/?consent/?fail/?theme/?pick/?pickCkpt`.
|
|
1102
|
-
* Layer-1 additions: `?balance/?latency/?costPerGen/?failNext/?failRate/?seed`
|
|
1204
|
+
* Layer-1 additions: `?balance/?latency/?costPerGen/?authorFee/?failNext/?failRate/?seed`
|
|
1103
1205
|
* map onto the new scenario groups so a dev can flip out-of-Buzz /
|
|
1104
1206
|
* failures / latency without editing code. `?capRefusal=1` (or `=<text>`) sets
|
|
1105
1207
|
* {@link MockGenerationScenario.submitCapRefusal}.
|