@civitai/blocks-react 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hooks/SfwGate.d.ts +44 -0
- package/dist/hooks/SfwGate.d.ts.map +1 -0
- package/dist/hooks/SfwGate.js +33 -0
- package/dist/hooks/SfwGate.js.map +1 -0
- package/dist/hooks/useDomainMaturity.d.ts +56 -0
- package/dist/hooks/useDomainMaturity.d.ts.map +1 -0
- package/dist/hooks/useDomainMaturity.js +35 -0
- package/dist/hooks/useDomainMaturity.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/mockHost.d.ts +197 -18
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +419 -43
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/transport.d.ts +14 -1
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +2 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +15 -0
- package/dist/internal/validate.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js.map +1 -1
- package/package.json +3 -3
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* a block in a cross-origin iframe and answers its `postMessage` protocol:
|
|
7
7
|
* mints a token, runs the lazy-consent round-trip, brokers the orchestrator
|
|
8
8
|
* money path (estimate → submit → poll), opens the native Buzz-purchase and
|
|
9
|
-
* resource-picker modals
|
|
10
|
-
* harness — there is no host, so this plays
|
|
9
|
+
* resource-picker modals, and serves the App-Blocks KV datastore. Locally — in
|
|
10
|
+
* a `vitest` test OR a starter's dev harness — there is no host, so this plays
|
|
11
|
+
* one.
|
|
11
12
|
*
|
|
12
13
|
* It is the portable core that the React `<Harness>` (a.k.a. `<MockHostProvider>`
|
|
13
14
|
* in `../testing`) wraps. Every block app used to hand-roll ~250 lines of this;
|
|
@@ -22,11 +23,33 @@
|
|
|
22
23
|
* `window.location.origin` (the React `<Harness>` is documented for that).
|
|
23
24
|
* 3. Dispatches a configurable `BLOCK_INIT`, then answers the full protocol.
|
|
24
25
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
26
|
+
* SCENARIOS (Layer 1 of the local-dev DX): the {@link MockHostOptions.generation},
|
|
27
|
+
* {@link MockHostOptions.buzz}, and {@link MockHostOptions.storage} groups let a
|
|
28
|
+
* dev simulate REAL costs, slow gens, FAILURES, an insufficient-Buzz balance,
|
|
29
|
+
* and a working KV store with a quota — entirely synthetically, so the full
|
|
30
|
+
* money / error / storage UX is exercisable locally without spending a single
|
|
31
|
+
* Buzz or touching the network. The returned {@link MockHost} exposes
|
|
32
|
+
* `setScenario()` + a `buzz` handle so a harness UI can flip scenarios
|
|
33
|
+
* mid-session.
|
|
34
|
+
*
|
|
35
|
+
* PURE + SYNTHETIC: NOT a real RS256 JWT, NO real Buzz, NO orchestrator, and
|
|
36
|
+
* — asserted by the test suite — NO network (`fetch`/`XMLHttpRequest` are never
|
|
37
|
+
* called). Only the postMessage bridge round-trips are exercised. Never import
|
|
38
|
+
* this from production code.
|
|
27
39
|
*/
|
|
40
|
+
import { BrowsingLevel, SFW_LEVELS, } from '@civitai/app-sdk/blocks';
|
|
41
|
+
/** The full all-levels ceiling a `red` domain projects (mirrors the server). */
|
|
42
|
+
const ALL_LEVELS = BrowsingLevel.PG |
|
|
43
|
+
BrowsingLevel.PG13 |
|
|
44
|
+
BrowsingLevel.R |
|
|
45
|
+
BrowsingLevel.X |
|
|
46
|
+
BrowsingLevel.XXX;
|
|
28
47
|
const DEV_TOKEN = 'dev.mockhost.mock.jwt.NOT.A.REAL.RS256';
|
|
29
48
|
const BUDGETED_SCOPE = 'ai:write:budgeted';
|
|
49
|
+
/** v0 host storage ceilings (mirror civitai/civitai's APP_STORAGE limits). */
|
|
50
|
+
const DEFAULT_STORAGE_QUOTA_BYTES = 50 * 1024 * 1024; // 50 MB per app
|
|
51
|
+
const DEFAULT_STORAGE_VALUE_CAP_BYTES = 64 * 1024; // 64 KB per value
|
|
52
|
+
const DEFAULT_STORAGE_LIMIT_ROWS = 1_000_000;
|
|
30
53
|
const DEFAULT_CHECKPOINT_PICK = {
|
|
31
54
|
versionId: 691639,
|
|
32
55
|
modelId: 618692,
|
|
@@ -44,9 +67,24 @@ const DEFAULT_LORA_PICK = {
|
|
|
44
67
|
modelType: 'LORA',
|
|
45
68
|
};
|
|
46
69
|
const DEFAULT_VIEWER = { id: 2, username: 'dev-viewer', status: 'active' };
|
|
70
|
+
const INSUFFICIENT_BUZZ_ERROR = 'Insufficient Buzz to run this generation.';
|
|
71
|
+
const GENERIC_GEN_ERROR = 'Generation failed (simulated).';
|
|
72
|
+
/** Byte size of a JSON value as the mock store would persist it. */
|
|
73
|
+
function jsonByteSize(value) {
|
|
74
|
+
try {
|
|
75
|
+
return new TextEncoder().encode(JSON.stringify(value ?? null)).length;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
47
81
|
/**
|
|
48
82
|
* Reads the URL query toggles the gen-matrix dev harness uses, so a starter's
|
|
49
83
|
* dev harness keeps working with `?viewer/?consent/?fail/?theme/?pick/?pickCkpt`.
|
|
84
|
+
* Layer-1 additions: `?balance/?latency/?costPerGen/?failNext/?failRate/?seed`
|
|
85
|
+
* map onto the new scenario groups so a dev can flip insufficient-buzz /
|
|
86
|
+
* failures / latency without editing code.
|
|
87
|
+
*
|
|
50
88
|
* Returns a partial overlay applied ON TOP of explicit {@link MockHostOptions}
|
|
51
89
|
* (URL wins — it's the interactive dev knob). No-op outside a browser.
|
|
52
90
|
*/
|
|
@@ -68,6 +106,14 @@ export function readMockHostUrlOptions(win = globalThis
|
|
|
68
106
|
out.theme = 'light';
|
|
69
107
|
else if (params.get('theme') === 'dark')
|
|
70
108
|
out.theme = 'dark';
|
|
109
|
+
// ?domain=green|blue|red projects a color-domain (and its derived ceiling);
|
|
110
|
+
// ?maturity=sfw|mature sets the ceiling directly.
|
|
111
|
+
const domain = params.get('domain');
|
|
112
|
+
if (domain === 'green' || domain === 'blue' || domain === 'red')
|
|
113
|
+
out.domain = domain;
|
|
114
|
+
const maturity = params.get('maturity');
|
|
115
|
+
if (maturity === 'sfw' || maturity === 'mature')
|
|
116
|
+
out.maturity = maturity;
|
|
71
117
|
// ?pick (LoRA) / ?pickCkpt (Checkpoint): 'cancel' → dismissed; 'pony' → an
|
|
72
118
|
// incompatible Pony LoRA; any other value → the default curated pick.
|
|
73
119
|
const pick = params.get('pick');
|
|
@@ -93,6 +139,56 @@ export function readMockHostUrlOptions(win = globalThis
|
|
|
93
139
|
cannedPicks.Checkpoint = DEFAULT_CHECKPOINT_PICK;
|
|
94
140
|
out.cannedPicks = cannedPicks;
|
|
95
141
|
}
|
|
142
|
+
// --- Layer-1 scenario toggles ---
|
|
143
|
+
const generation = {};
|
|
144
|
+
const buzz = {};
|
|
145
|
+
const balance = params.get('balance');
|
|
146
|
+
if (balance !== null && balance.trim() !== '' && Number.isFinite(Number(balance))) {
|
|
147
|
+
buzz.balance = Number(balance);
|
|
148
|
+
}
|
|
149
|
+
if (params.get('insufficient') === '1' || params.get('insufficient') === 'true') {
|
|
150
|
+
buzz.insufficient = true;
|
|
151
|
+
}
|
|
152
|
+
const latency = params.get('latency');
|
|
153
|
+
if (latency !== null && latency.trim() !== '') {
|
|
154
|
+
// ?latency=2000 or ?latency=500-2000
|
|
155
|
+
const range = latency.split('-').map((s) => Number(s.trim()));
|
|
156
|
+
const lo = range[0] ?? NaN;
|
|
157
|
+
const hi = range[1] ?? NaN;
|
|
158
|
+
if (range.length === 2 && Number.isFinite(lo) && Number.isFinite(hi)) {
|
|
159
|
+
generation.latencyMs = [lo, hi];
|
|
160
|
+
}
|
|
161
|
+
else if (Number.isFinite(lo)) {
|
|
162
|
+
generation.latencyMs = lo;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const costPerGen = params.get('costPerGen') ?? params.get('cost');
|
|
166
|
+
if (costPerGen !== null && Number.isFinite(Number(costPerGen))) {
|
|
167
|
+
generation.costPerGen = Number(costPerGen);
|
|
168
|
+
}
|
|
169
|
+
const failNext = params.get('failNext');
|
|
170
|
+
if (failNext !== null && Number.isInteger(Number(failNext))) {
|
|
171
|
+
generation.failNext = Number(failNext);
|
|
172
|
+
}
|
|
173
|
+
const failRate = params.get('failRate');
|
|
174
|
+
if (failRate !== null && Number.isFinite(Number(failRate))) {
|
|
175
|
+
generation.failRate = Number(failRate);
|
|
176
|
+
}
|
|
177
|
+
if (Object.keys(generation).length > 0)
|
|
178
|
+
out.generation = generation;
|
|
179
|
+
if (Object.keys(buzz).length > 0)
|
|
180
|
+
out.buzz = buzz;
|
|
181
|
+
const seed = params.get('seed');
|
|
182
|
+
if (seed) {
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(seed);
|
|
185
|
+
if (parsed && typeof parsed === 'object')
|
|
186
|
+
out.storage = { seed: parsed };
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* ignore malformed ?seed= */
|
|
190
|
+
}
|
|
191
|
+
}
|
|
96
192
|
return out;
|
|
97
193
|
}
|
|
98
194
|
/**
|
|
@@ -102,9 +198,11 @@ export function readMockHostUrlOptions(win = globalThis
|
|
|
102
198
|
* from a node/jsdom/happy-dom test OR a browser dev harness.
|
|
103
199
|
*
|
|
104
200
|
* @example
|
|
105
|
-
* const host = createMockHost({
|
|
201
|
+
* const host = createMockHost({ generation: { failNext: 1, latencyMs: 1500 }, buzz: { balance: 5 } });
|
|
106
202
|
* const uninstall = host.install();
|
|
107
203
|
* // … drive the block / assertions …
|
|
204
|
+
* host.buzz.setBalance(0); // flip to insufficient mid-session
|
|
205
|
+
* host.setScenario({ generation: { failRate: 1 } });
|
|
108
206
|
* uninstall();
|
|
109
207
|
*/
|
|
110
208
|
export function createMockHost(options = {}) {
|
|
@@ -115,15 +213,62 @@ export function createMockHost(options = {}) {
|
|
|
115
213
|
// Bind to a non-nullable local so the `install()` closure keeps the narrowing.
|
|
116
214
|
const win = maybeWin;
|
|
117
215
|
const viewer = options.viewer === undefined ? DEFAULT_VIEWER : options.viewer;
|
|
118
|
-
const failMode = options.failMode ?? 'none';
|
|
119
|
-
const pollsUntilDone = options.pollsUntilDone ?? 2;
|
|
120
|
-
const cost = options.cost ?? 8;
|
|
121
216
|
const buzzBudget = options.buzzBudget ?? 200;
|
|
122
217
|
const theme = options.theme ?? 'dark';
|
|
123
|
-
const cannedPicks = options.cannedPicks ?? { Checkpoint: DEFAULT_CHECKPOINT_PICK, LORA: DEFAULT_LORA_PICK };
|
|
124
218
|
const blockInstanceId = options.blockInstanceId ?? 'page_mock';
|
|
125
219
|
const blockId = options.blockId ?? 'mock-block';
|
|
126
220
|
const appId = options.appId ?? 'app_dev';
|
|
221
|
+
// ---- LIVE, mutable scenario state (so setScenario / buzz.setBalance work) ----
|
|
222
|
+
// Legacy + scenario knobs are merged into one mutable record; `setScenario`
|
|
223
|
+
// rewrites these in place without re-installing.
|
|
224
|
+
let failMode = options.failMode ?? 'none';
|
|
225
|
+
let pollsUntilDone = options.pollsUntilDone ?? 2;
|
|
226
|
+
let gen = { ...(options.generation ?? {}) };
|
|
227
|
+
let buzz = { ...(options.buzz ?? {}) };
|
|
228
|
+
// Legacy `cost` feeds costPerGen unless the scenario set its own.
|
|
229
|
+
let legacyCost = options.cost ?? 8;
|
|
230
|
+
let cannedPicks = options.cannedPicks ?? { Checkpoint: DEFAULT_CHECKPOINT_PICK, LORA: DEFAULT_LORA_PICK };
|
|
231
|
+
// ---- Storage scenario (in-memory KV backend) ----
|
|
232
|
+
const storageScenario = { ...(options.storage ?? {}) };
|
|
233
|
+
const store = new Map();
|
|
234
|
+
const seedNow = new Date().toISOString();
|
|
235
|
+
for (const [k, v] of Object.entries(storageScenario.seed ?? {})) {
|
|
236
|
+
store.set(k, { value: v, updatedAt: seedNow });
|
|
237
|
+
}
|
|
238
|
+
let storageFailNext = storageScenario.failNext ?? 0;
|
|
239
|
+
const quotaBytes = storageScenario.quotaBytes ?? DEFAULT_STORAGE_QUOTA_BYTES;
|
|
240
|
+
const valueCapBytes = storageScenario.valueCapBytes ?? DEFAULT_STORAGE_VALUE_CAP_BYTES;
|
|
241
|
+
const limitRows = storageScenario.limitRows ?? DEFAULT_STORAGE_LIMIT_ROWS;
|
|
242
|
+
const usedBytes = () => {
|
|
243
|
+
let total = 0;
|
|
244
|
+
for (const [k, row] of store)
|
|
245
|
+
total += jsonByteSize(row.value) + k.length;
|
|
246
|
+
return total;
|
|
247
|
+
};
|
|
248
|
+
// Resolve a per-gen cost from the scenario (or legacy `cost`).
|
|
249
|
+
const costFor = (body) => {
|
|
250
|
+
const spec = gen.costPerGen ?? legacyCost;
|
|
251
|
+
return typeof spec === 'function' ? spec(body) : (spec ?? legacyCost);
|
|
252
|
+
};
|
|
253
|
+
const latencyFor = () => {
|
|
254
|
+
const l = gen.latencyMs;
|
|
255
|
+
if (l === undefined)
|
|
256
|
+
return 0;
|
|
257
|
+
if (Array.isArray(l)) {
|
|
258
|
+
const [min, max] = l;
|
|
259
|
+
return Math.round(min + Math.random() * Math.max(0, max - min));
|
|
260
|
+
}
|
|
261
|
+
return l;
|
|
262
|
+
};
|
|
263
|
+
const imagesFor = (workflowId, body) => {
|
|
264
|
+
if (gen.images)
|
|
265
|
+
return typeof gen.images === 'function' ? gen.images(body) : gen.images;
|
|
266
|
+
if (gen.image)
|
|
267
|
+
return [typeof gen.image === 'function' ? gen.image(body) : gen.image];
|
|
268
|
+
return [
|
|
269
|
+
`https://placehold.co/512x512/1971c2/ffffff/png?text=${encodeURIComponent(workflowId.slice(-4))}`,
|
|
270
|
+
];
|
|
271
|
+
};
|
|
127
272
|
let installed = false;
|
|
128
273
|
let teardown = () => { };
|
|
129
274
|
function install() {
|
|
@@ -135,11 +280,19 @@ export function createMockHost(options = {}) {
|
|
|
135
280
|
let consentGranted = !!options.consentGranted;
|
|
136
281
|
let tokenSerial = 0;
|
|
137
282
|
let submitCount = 0;
|
|
283
|
+
// body + cost remembered per workflow so the succeeded snapshot can echo them.
|
|
138
284
|
const workflows = new Map();
|
|
139
285
|
const timers = new Set();
|
|
140
286
|
const dispatchToBlock = (data) => {
|
|
141
287
|
win.dispatchEvent(new MessageEvent('message', { data, origin: parentOrigin }));
|
|
142
288
|
};
|
|
289
|
+
const after = (ms, fn) => {
|
|
290
|
+
const t = setTimeout(() => {
|
|
291
|
+
timers.delete(t);
|
|
292
|
+
fn();
|
|
293
|
+
}, ms);
|
|
294
|
+
timers.add(t);
|
|
295
|
+
};
|
|
143
296
|
const nextToken = () => {
|
|
144
297
|
tokenSerial += 1;
|
|
145
298
|
return {
|
|
@@ -149,14 +302,17 @@ export function createMockHost(options = {}) {
|
|
|
149
302
|
...(consentGranted ? { buzzBudget } : {}),
|
|
150
303
|
};
|
|
151
304
|
};
|
|
152
|
-
const succeededSnapshot = (workflowId) =>
|
|
153
|
-
workflowId
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
305
|
+
const succeededSnapshot = (workflowId) => {
|
|
306
|
+
const wf = workflows.get(workflowId);
|
|
307
|
+
const cost = wf?.cost ?? legacyCost;
|
|
308
|
+
const body = wf?.body ?? {};
|
|
309
|
+
return {
|
|
310
|
+
workflowId,
|
|
311
|
+
status: 'succeeded',
|
|
312
|
+
cost: { total: cost },
|
|
313
|
+
imageUrls: imagesFor(workflowId, body),
|
|
314
|
+
};
|
|
315
|
+
};
|
|
160
316
|
const parentMock = {
|
|
161
317
|
postMessage: (msg) => {
|
|
162
318
|
if (typeof msg !== 'object' ||
|
|
@@ -179,30 +335,56 @@ export function createMockHost(options = {}) {
|
|
|
179
335
|
// host-initiated TOKEN_REFRESH carrying it (the App's auto-resume
|
|
180
336
|
// depends on seeing the new scope on its token).
|
|
181
337
|
consentGranted = true;
|
|
182
|
-
|
|
338
|
+
after(0, () => {
|
|
183
339
|
dispatchToBlock({ type: 'TOKEN_REFRESH', payload: { token: nextToken() } });
|
|
184
|
-
}
|
|
185
|
-
timers.add(t);
|
|
340
|
+
});
|
|
186
341
|
return;
|
|
187
342
|
}
|
|
188
343
|
case 'REQUEST_SIGN_IN':
|
|
189
344
|
// The real host opens its login UI; nothing to reply.
|
|
190
345
|
return;
|
|
191
|
-
case 'ESTIMATE_WORKFLOW':
|
|
346
|
+
case 'ESTIMATE_WORKFLOW': {
|
|
347
|
+
const body = typed.payload?.body ?? {};
|
|
192
348
|
dispatchToBlock({
|
|
193
349
|
type: 'ESTIMATE_RESULT',
|
|
194
350
|
payload: {
|
|
195
351
|
requestId,
|
|
196
|
-
snapshot: {
|
|
352
|
+
snapshot: {
|
|
353
|
+
workflowId: 'wf_estimate',
|
|
354
|
+
status: 'pending',
|
|
355
|
+
cost: { total: costFor(body) },
|
|
356
|
+
},
|
|
197
357
|
},
|
|
198
358
|
});
|
|
199
359
|
return;
|
|
360
|
+
}
|
|
200
361
|
case 'SUBMIT_WORKFLOW': {
|
|
201
362
|
submitCount += 1;
|
|
202
|
-
const
|
|
363
|
+
const body = typed.payload?.body ?? {};
|
|
364
|
+
const cost = costFor(body);
|
|
365
|
+
// Insufficient-Buzz path: legacy failMode, the buzz scenario's
|
|
366
|
+
// force flag, OR a simulated balance that can't cover this gen.
|
|
367
|
+
const balanceSimulated = typeof buzz.balance === 'number';
|
|
368
|
+
const insufficient = failMode === 'all' ||
|
|
203
369
|
failMode === 'insufficient' ||
|
|
204
|
-
|
|
205
|
-
|
|
370
|
+
buzz.insufficient === true ||
|
|
371
|
+
(balanceSimulated && buzz.balance < cost);
|
|
372
|
+
// Generic generation failure: failNext countdown, failRate dice, or
|
|
373
|
+
// the legacy 'some' (~1 in 3) mode.
|
|
374
|
+
let genericFail = false;
|
|
375
|
+
if (!insufficient) {
|
|
376
|
+
if ((gen.failNext ?? 0) > 0) {
|
|
377
|
+
gen.failNext = gen.failNext - 1;
|
|
378
|
+
genericFail = true;
|
|
379
|
+
}
|
|
380
|
+
else if (typeof gen.failRate === 'number' && Math.random() < gen.failRate) {
|
|
381
|
+
genericFail = true;
|
|
382
|
+
}
|
|
383
|
+
else if (failMode === 'some' && submitCount % 3 === 0) {
|
|
384
|
+
genericFail = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (insufficient) {
|
|
206
388
|
dispatchToBlock({
|
|
207
389
|
type: 'WORKFLOW_SUBMITTED',
|
|
208
390
|
payload: {
|
|
@@ -210,14 +392,31 @@ export function createMockHost(options = {}) {
|
|
|
210
392
|
snapshot: {
|
|
211
393
|
workflowId: `wf_fail_${submitCount}`,
|
|
212
394
|
status: 'failed',
|
|
213
|
-
error:
|
|
395
|
+
error: INSUFFICIENT_BUZZ_ERROR,
|
|
214
396
|
},
|
|
215
397
|
},
|
|
216
398
|
});
|
|
217
399
|
return;
|
|
218
400
|
}
|
|
401
|
+
if (genericFail) {
|
|
402
|
+
dispatchToBlock({
|
|
403
|
+
type: 'WORKFLOW_SUBMITTED',
|
|
404
|
+
payload: {
|
|
405
|
+
requestId,
|
|
406
|
+
snapshot: {
|
|
407
|
+
workflowId: `wf_fail_${submitCount}`,
|
|
408
|
+
status: 'failed',
|
|
409
|
+
error: GENERIC_GEN_ERROR,
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
// Success path: debit the simulated balance + remember body/cost.
|
|
416
|
+
if (balanceSimulated)
|
|
417
|
+
buzz.balance = buzz.balance - cost;
|
|
219
418
|
const workflowId = `wf_${submitCount}_${Date.now()}`;
|
|
220
|
-
workflows.set(workflowId, { polls: 0 });
|
|
419
|
+
workflows.set(workflowId, { polls: 0, cost, body });
|
|
221
420
|
dispatchToBlock({
|
|
222
421
|
type: 'WORKFLOW_SUBMITTED',
|
|
223
422
|
payload: { requestId, snapshot: { workflowId, status: 'pending' } },
|
|
@@ -230,10 +429,28 @@ export function createMockHost(options = {}) {
|
|
|
230
429
|
const polls = (wf?.polls ?? 0) + 1;
|
|
231
430
|
if (wf)
|
|
232
431
|
wf.polls = polls;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
432
|
+
if (polls >= pollsUntilDone) {
|
|
433
|
+
// Apply synthetic latency on the terminal (succeeded) poll only.
|
|
434
|
+
const delay = latencyFor();
|
|
435
|
+
if (delay > 0) {
|
|
436
|
+
after(delay, () => dispatchToBlock({
|
|
437
|
+
type: 'WORKFLOW_STATUS',
|
|
438
|
+
payload: { requestId, snapshot: succeededSnapshot(workflowId) },
|
|
439
|
+
}));
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
dispatchToBlock({
|
|
443
|
+
type: 'WORKFLOW_STATUS',
|
|
444
|
+
payload: { requestId, snapshot: succeededSnapshot(workflowId) },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
else {
|
|
449
|
+
dispatchToBlock({
|
|
450
|
+
type: 'WORKFLOW_STATUS',
|
|
451
|
+
payload: { requestId, snapshot: { workflowId, status: 'processing' } },
|
|
452
|
+
});
|
|
453
|
+
}
|
|
237
454
|
return;
|
|
238
455
|
}
|
|
239
456
|
case 'CANCEL_WORKFLOW': {
|
|
@@ -245,12 +462,18 @@ export function createMockHost(options = {}) {
|
|
|
245
462
|
});
|
|
246
463
|
return;
|
|
247
464
|
}
|
|
248
|
-
case 'OPEN_BUZZ_PURCHASE':
|
|
465
|
+
case 'OPEN_BUZZ_PURCHASE': {
|
|
466
|
+
// Refill the simulated balance so the post-top-up retry succeeds.
|
|
467
|
+
const newBalance = typeof buzz.balance === 'number' ? buzz.balance + 1000 : 1000;
|
|
468
|
+
if (typeof buzz.balance === 'number')
|
|
469
|
+
buzz.balance = newBalance;
|
|
470
|
+
buzz.insufficient = false;
|
|
249
471
|
dispatchToBlock({
|
|
250
472
|
type: 'BUZZ_PURCHASE_RESULT',
|
|
251
|
-
payload: { requestId, purchased: true, newBalance
|
|
473
|
+
payload: { requestId, purchased: true, newBalance },
|
|
252
474
|
});
|
|
253
475
|
return;
|
|
476
|
+
}
|
|
254
477
|
case 'OPEN_CHECKPOINT_PICKER': {
|
|
255
478
|
const selected = cannedPicks.Checkpoint;
|
|
256
479
|
dispatchToBlock({
|
|
@@ -281,6 +504,111 @@ export function createMockHost(options = {}) {
|
|
|
281
504
|
});
|
|
282
505
|
return;
|
|
283
506
|
}
|
|
507
|
+
// ---- App Blocks KV datastore (W4) — in-memory backend ----
|
|
508
|
+
case 'APP_STORAGE_GET': {
|
|
509
|
+
const key = typed.payload?.key ?? '';
|
|
510
|
+
const row = store.get(key);
|
|
511
|
+
dispatchToBlock({
|
|
512
|
+
type: 'APP_STORAGE_GET_RESULT',
|
|
513
|
+
payload: { requestId, value: row ? row.value : null },
|
|
514
|
+
});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
case 'APP_STORAGE_SET': {
|
|
518
|
+
const key = typed.payload?.key ?? '';
|
|
519
|
+
const value = typed.payload?.value;
|
|
520
|
+
if (storageFailNext > 0) {
|
|
521
|
+
storageFailNext -= 1;
|
|
522
|
+
dispatchToBlock({
|
|
523
|
+
type: 'APP_STORAGE_SET_RESULT',
|
|
524
|
+
payload: { requestId, ok: false, error: 'STORAGE_UNAVAILABLE' },
|
|
525
|
+
});
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const sizeBytes = jsonByteSize(value);
|
|
529
|
+
if (sizeBytes > valueCapBytes) {
|
|
530
|
+
dispatchToBlock({
|
|
531
|
+
type: 'APP_STORAGE_SET_RESULT',
|
|
532
|
+
payload: { requestId, ok: false, error: 'PAYLOAD_TOO_LARGE' },
|
|
533
|
+
});
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// Quota check: projected usage after this upsert.
|
|
537
|
+
const existing = store.get(key);
|
|
538
|
+
const existingBytes = existing ? jsonByteSize(existing.value) + key.length : 0;
|
|
539
|
+
const projected = usedBytes() - existingBytes + sizeBytes + key.length;
|
|
540
|
+
if (projected > quotaBytes) {
|
|
541
|
+
dispatchToBlock({
|
|
542
|
+
type: 'APP_STORAGE_SET_RESULT',
|
|
543
|
+
payload: { requestId, ok: false, error: 'PAYLOAD_TOO_LARGE' },
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
store.set(key, { value, updatedAt: new Date().toISOString() });
|
|
548
|
+
dispatchToBlock({
|
|
549
|
+
type: 'APP_STORAGE_SET_RESULT',
|
|
550
|
+
payload: { requestId, ok: true, sizeBytes },
|
|
551
|
+
});
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
case 'APP_STORAGE_DELETE': {
|
|
555
|
+
const key = typed.payload?.key ?? '';
|
|
556
|
+
if (storageFailNext > 0) {
|
|
557
|
+
storageFailNext -= 1;
|
|
558
|
+
dispatchToBlock({
|
|
559
|
+
type: 'APP_STORAGE_DELETE_RESULT',
|
|
560
|
+
payload: { requestId, ok: false, deleted: false, error: 'STORAGE_UNAVAILABLE' },
|
|
561
|
+
});
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
const had = store.delete(key);
|
|
565
|
+
dispatchToBlock({
|
|
566
|
+
type: 'APP_STORAGE_DELETE_RESULT',
|
|
567
|
+
payload: { requestId, ok: true, deleted: had },
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
case 'APP_STORAGE_LIST': {
|
|
572
|
+
const prefix = typed.payload?.prefix ?? '';
|
|
573
|
+
const limit = typed.payload?.limit ?? 100;
|
|
574
|
+
const cursor = typed.payload?.cursor;
|
|
575
|
+
// Cursor = base64 of the last returned key (matches the hook's
|
|
576
|
+
// documented `nextCursor` contract).
|
|
577
|
+
const afterKey = cursor ? safeAtob(cursor) : undefined;
|
|
578
|
+
const allKeys = [...store.entries()]
|
|
579
|
+
.filter(([k]) => k.startsWith(prefix))
|
|
580
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
581
|
+
const startIdx = afterKey
|
|
582
|
+
? allKeys.findIndex(([k]) => k > afterKey)
|
|
583
|
+
: 0;
|
|
584
|
+
const slice = (startIdx < 0 ? [] : allKeys.slice(startIdx)).slice(0, limit);
|
|
585
|
+
const keys = slice.map(([key, row]) => ({ key, updatedAt: row.updatedAt }));
|
|
586
|
+
const last = slice[slice.length - 1]?.[0];
|
|
587
|
+
const hasMore = last !== undefined &&
|
|
588
|
+
allKeys.findIndex(([k]) => k === last) < allKeys.length - 1;
|
|
589
|
+
dispatchToBlock({
|
|
590
|
+
type: 'APP_STORAGE_LIST_RESULT',
|
|
591
|
+
payload: {
|
|
592
|
+
requestId,
|
|
593
|
+
keys,
|
|
594
|
+
...(hasMore && last ? { nextCursor: safeBtoa(last) } : {}),
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
case 'APP_STORAGE_QUOTA': {
|
|
600
|
+
dispatchToBlock({
|
|
601
|
+
type: 'APP_STORAGE_QUOTA_RESULT',
|
|
602
|
+
payload: {
|
|
603
|
+
requestId,
|
|
604
|
+
usedBytes: usedBytes(),
|
|
605
|
+
rowCount: store.size,
|
|
606
|
+
limitBytes: quotaBytes,
|
|
607
|
+
limitRows,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
284
612
|
default:
|
|
285
613
|
return;
|
|
286
614
|
}
|
|
@@ -291,14 +619,24 @@ export function createMockHost(options = {}) {
|
|
|
291
619
|
configurable: true,
|
|
292
620
|
writable: true,
|
|
293
621
|
});
|
|
294
|
-
// Merge theme
|
|
622
|
+
// Merge theme into the init context.
|
|
295
623
|
const baseContext = options.context ?? { slotId: 'app.page' };
|
|
296
|
-
const context = {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
624
|
+
const context = { ...baseContext, theme };
|
|
625
|
+
// Color-domain maturity (civitai #2670). Resolve the ceiling by precedence:
|
|
626
|
+
// explicit maxBrowsingLevel > maturity convenience > domain-derived. Only
|
|
627
|
+
// EMIT a field when the corresponding option was set, so the default mock
|
|
628
|
+
// host stays a #2670-predating host (the hook fail-closes to SFW).
|
|
629
|
+
const resolvedCeiling = options.maxBrowsingLevel !== undefined
|
|
630
|
+
? options.maxBrowsingLevel
|
|
631
|
+
: options.maturity === 'sfw'
|
|
632
|
+
? SFW_LEVELS
|
|
633
|
+
: options.maturity === 'mature'
|
|
634
|
+
? ALL_LEVELS
|
|
635
|
+
: options.domain !== undefined
|
|
636
|
+
? options.domain === 'red'
|
|
637
|
+
? ALL_LEVELS
|
|
638
|
+
: SFW_LEVELS
|
|
639
|
+
: undefined;
|
|
302
640
|
const initPayload = {
|
|
303
641
|
blockInstanceId,
|
|
304
642
|
blockId,
|
|
@@ -309,9 +647,10 @@ export function createMockHost(options = {}) {
|
|
|
309
647
|
viewer,
|
|
310
648
|
theme,
|
|
311
649
|
renderMode: 'iframe',
|
|
650
|
+
...(options.domain !== undefined ? { domain: options.domain } : {}),
|
|
651
|
+
...(resolvedCeiling !== undefined ? { maxBrowsingLevel: resolvedCeiling } : {}),
|
|
312
652
|
};
|
|
313
|
-
|
|
314
|
-
timers.add(initTimer);
|
|
653
|
+
after(0, () => dispatchToBlock({ type: 'BLOCK_INIT', payload: initPayload }));
|
|
315
654
|
let torn = false;
|
|
316
655
|
teardown = () => {
|
|
317
656
|
if (torn)
|
|
@@ -329,6 +668,43 @@ export function createMockHost(options = {}) {
|
|
|
329
668
|
};
|
|
330
669
|
return teardown;
|
|
331
670
|
}
|
|
332
|
-
|
|
671
|
+
function setScenario(patch) {
|
|
672
|
+
if (patch.failMode !== undefined)
|
|
673
|
+
failMode = patch.failMode;
|
|
674
|
+
if (patch.pollsUntilDone !== undefined)
|
|
675
|
+
pollsUntilDone = patch.pollsUntilDone;
|
|
676
|
+
if (patch.cost !== undefined)
|
|
677
|
+
legacyCost = patch.cost;
|
|
678
|
+
if (patch.cannedPicks !== undefined)
|
|
679
|
+
cannedPicks = patch.cannedPicks;
|
|
680
|
+
if (patch.generation !== undefined)
|
|
681
|
+
gen = { ...gen, ...patch.generation };
|
|
682
|
+
if (patch.buzz !== undefined)
|
|
683
|
+
buzz = { ...buzz, ...patch.buzz };
|
|
684
|
+
if (patch.storage !== undefined) {
|
|
685
|
+
// Only the live-tunable storage knob (`failNext`) is applied mid-session;
|
|
686
|
+
// seed/quota are install-time (re-install to change the backing store).
|
|
687
|
+
if (patch.storage.failNext !== undefined)
|
|
688
|
+
storageFailNext = patch.storage.failNext;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const buzzHandle = {
|
|
692
|
+
getBalance: () => buzz.balance,
|
|
693
|
+
setBalance: (n) => {
|
|
694
|
+
buzz.balance = n;
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
return { install, setScenario, buzz: buzzHandle };
|
|
698
|
+
}
|
|
699
|
+
/** btoa/atob that work in both browser + node (happy-dom + vitest). */
|
|
700
|
+
function safeBtoa(s) {
|
|
701
|
+
if (typeof btoa === 'function')
|
|
702
|
+
return btoa(s);
|
|
703
|
+
return Buffer.from(s, 'utf-8').toString('base64');
|
|
704
|
+
}
|
|
705
|
+
function safeAtob(s) {
|
|
706
|
+
if (typeof atob === 'function')
|
|
707
|
+
return atob(s);
|
|
708
|
+
return Buffer.from(s, 'base64').toString('utf-8');
|
|
333
709
|
}
|
|
334
710
|
//# sourceMappingURL=mockHost.js.map
|