@civitai/blocks-react 0.26.0 → 0.28.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 +117 -0
- package/dist/hooks/useBlockToken.d.ts +6 -0
- package/dist/hooks/useBlockToken.d.ts.map +1 -1
- package/dist/hooks/useBlockToken.js +24 -11
- package/dist/hooks/useBlockToken.js.map +1 -1
- package/dist/hooks/useGenerationResources.d.ts.map +1 -1
- package/dist/hooks/useGenerationResources.js +45 -8
- package/dist/hooks/useGenerationResources.js.map +1 -1
- package/dist/hooks/useImageUpload.d.ts +42 -4
- package/dist/hooks/useImageUpload.d.ts.map +1 -1
- package/dist/hooks/useImageUpload.js +124 -4
- package/dist/hooks/useImageUpload.js.map +1 -1
- package/dist/internal/iframeTransport.d.ts +7 -0
- package/dist/internal/iframeTransport.d.ts.map +1 -1
- package/dist/internal/iframeTransport.js +39 -0
- package/dist/internal/iframeTransport.js.map +1 -1
- package/dist/internal/inlineTransport.d.ts +1 -0
- package/dist/internal/inlineTransport.d.ts.map +1 -1
- package/dist/internal/inlineTransport.js +5 -0
- package/dist/internal/inlineTransport.js.map +1 -1
- package/dist/internal/liveHost.d.ts.map +1 -1
- package/dist/internal/liveHost.js +203 -0
- package/dist/internal/liveHost.js.map +1 -1
- package/dist/internal/mockHost.d.ts +25 -1
- package/dist/internal/mockHost.d.ts.map +1 -1
- package/dist/internal/mockHost.js +67 -3
- package/dist/internal/mockHost.js.map +1 -1
- package/dist/internal/transport.d.ts +21 -0
- package/dist/internal/transport.d.ts.map +1 -1
- package/dist/internal/transport.js +10 -0
- package/dist/internal/transport.js.map +1 -1
- package/dist/internal/validate.d.ts +92 -0
- package/dist/internal/validate.d.ts.map +1 -1
- package/dist/internal/validate.js +414 -2
- 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
package/README.md
CHANGED
|
@@ -307,6 +307,21 @@ const { keys } = await storage.list({ prefix: 'note-' });
|
|
|
307
307
|
const quota = await storage.getQuota(); // { usedBytes, rowCount, limitBytes, limitRows }
|
|
308
308
|
```
|
|
309
309
|
|
|
310
|
+
### `useSharedStorage()`
|
|
311
|
+
|
|
312
|
+
App-scoped, append-only, community-votable SHARED datastore (every viewer sees
|
|
313
|
+
the same list). Sibling of `useAppStorage`; anonymous viewers get the read path
|
|
314
|
+
and a hard reject on mutations.
|
|
315
|
+
|
|
316
|
+
```tsx
|
|
317
|
+
const shared = useSharedStorage();
|
|
318
|
+
const { key } = await shared.append({ title: 'Add dark mode', body: 'please' });
|
|
319
|
+
const { items } = await shared.list({ limit: 20 }); // newest-first
|
|
320
|
+
const count = await shared.vote(key); // idempotent up-vote
|
|
321
|
+
await shared.unvote(key);
|
|
322
|
+
await shared.withdraw(key); // remove my own entry
|
|
323
|
+
```
|
|
324
|
+
|
|
310
325
|
### `useCheckpointPicker()`
|
|
311
326
|
|
|
312
327
|
Drive the platform Checkpoint picker + persist a viewer override.
|
|
@@ -317,6 +332,55 @@ const { selected } = await open({ baseModelGroup: 'SDXL', currentVersionId });
|
|
|
317
332
|
if (selected) await persist(selected.versionId); // null clears the override
|
|
318
333
|
```
|
|
319
334
|
|
|
335
|
+
### `useResourcePicker()`
|
|
336
|
+
|
|
337
|
+
Drive the platform resource picker for page blocks — `'Checkpoint' | 'LORA'`.
|
|
338
|
+
The viewer searches in host chrome; the block only ever sees the one resource it
|
|
339
|
+
picked. DISCOVERY ONLY — the returned `versionId` is re-validated + re-priced
|
|
340
|
+
server-side at estimate/submit.
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
const { open } = useResourcePicker();
|
|
344
|
+
const picked = await open({ resourceType: 'LORA', baseModelGroup: 'SDXL' });
|
|
345
|
+
if (picked) {
|
|
346
|
+
const versionId = picked.versionId; // feed into body.additionalResources
|
|
347
|
+
const weight = picked.strength; // recommended default weight (may be undefined)
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### `useImageUpload()`
|
|
352
|
+
|
|
353
|
+
Host-mediated image upload — the host opens its native upload modal and the
|
|
354
|
+
iframe never handles the bytes. Resolves with a moderated image (or `null` on
|
|
355
|
+
dismiss); pass `{ purpose: 'generationSource' }` for an unscanned img2img source
|
|
356
|
+
or `{ asyncScan: true }` for the early-resolve + `scanStatus()` flow.
|
|
357
|
+
|
|
358
|
+
```tsx
|
|
359
|
+
const { open } = useImageUpload();
|
|
360
|
+
const img = await open(); // BlockUploadedImageInfo | null
|
|
361
|
+
if (img) {
|
|
362
|
+
await submit({
|
|
363
|
+
kind: 'textToImage',
|
|
364
|
+
modelId,
|
|
365
|
+
modelVersionId,
|
|
366
|
+
sourceImage: { url: img.url, width: 1024, height: 1024 },
|
|
367
|
+
params: { prompt },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### `useGenerationResources()`
|
|
373
|
+
|
|
374
|
+
Rehydrate a saved set of generation resources by version id — WITHOUT re-opening
|
|
375
|
+
the picker. Returns the same widened projection `useResourcePicker` yields
|
|
376
|
+
(recommended weights, trigger words, clipSkip). DISCOVERY ONLY.
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
const { fetch } = useGenerationResources();
|
|
380
|
+
const resources = await fetch([691639, 666002]); // by saved versionIds
|
|
381
|
+
const first = resources[0]; // .versionId / .strength / .trainedWords / .clipSkip
|
|
382
|
+
```
|
|
383
|
+
|
|
320
384
|
### `useCivitaiNavigate()`
|
|
321
385
|
|
|
322
386
|
Request a navigation within civitai.com (host-mediated; fire-and-forget).
|
|
@@ -335,6 +399,56 @@ const { track } = useBlockAnalytics();
|
|
|
335
399
|
track('generate_clicked', { modelId });
|
|
336
400
|
```
|
|
337
401
|
|
|
402
|
+
### `useRequestSignIn()`
|
|
403
|
+
|
|
404
|
+
Ask the host to open its sign-in flow for an ANONYMOUS viewer (fire-and-forget).
|
|
405
|
+
On sign-in the host re-inits the block with the now-authenticated viewer.
|
|
406
|
+
|
|
407
|
+
```tsx
|
|
408
|
+
const { requestSignIn } = useRequestSignIn();
|
|
409
|
+
// e.g. onClick of a "Sign in to generate" button:
|
|
410
|
+
requestSignIn();
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### `useRequestConsent()`
|
|
414
|
+
|
|
415
|
+
Lazy consent: ask the host to open its consent UI when a LOGGED-IN viewer takes
|
|
416
|
+
an action whose consent-gated scope the block token is missing (e.g. Generate
|
|
417
|
+
needs `ai:write:budgeted` but the viewer hasn't granted it). Fire-and-forget —
|
|
418
|
+
on grant the host pushes a new token; observe `useBlockToken().scopes` and retry.
|
|
419
|
+
|
|
420
|
+
```tsx
|
|
421
|
+
const { requestConsent } = useRequestConsent();
|
|
422
|
+
requestConsent({ scopes: ['ai:write:budgeted', 'buzz:read:self'] });
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### `useDomainMaturity()`
|
|
426
|
+
|
|
427
|
+
Read the surrounding color-domain's maturity ceiling (civitai #2670) so a block
|
|
428
|
+
can hide/blur mature affordances on a SFW domain. **Fail-closed SFW** until
|
|
429
|
+
`BLOCK_INIT` lands or against a host that predates the field.
|
|
430
|
+
|
|
431
|
+
```tsx
|
|
432
|
+
const { isSfw, isLevelAllowed } = useDomainMaturity();
|
|
433
|
+
const showRSlider = isLevelAllowed(BrowsingLevel.R); // false on a SFW domain
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### `SfwGate`
|
|
437
|
+
|
|
438
|
+
Convenience component that renders `children` only when the domain permits the
|
|
439
|
+
maturity — no `level` prop gates on the SFW ceiling, a `level` prop gates on that
|
|
440
|
+
browsing-level bit. Fail-closed SFW.
|
|
441
|
+
|
|
442
|
+
```tsx
|
|
443
|
+
function MatureSection() {
|
|
444
|
+
return (
|
|
445
|
+
<SfwGate level={BrowsingLevel.R} fallback={<SafePlaceholder />}>
|
|
446
|
+
<RRatedControl />
|
|
447
|
+
</SfwGate>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
338
452
|
## The `/ui` subexport
|
|
339
453
|
|
|
340
454
|
Opinionated components, imported separately so a transport-only block stays lean.
|
|
@@ -469,6 +583,9 @@ Runnable, minimal blocks — one per feature, each with its own README:
|
|
|
469
583
|
|
|
470
584
|
| `@civitai/blocks-react` | pairs with `@civitai/app-sdk` | adds |
|
|
471
585
|
|---|---|---|
|
|
586
|
+
| `0.27.x` | `^0.23.0` | async-scan image upload; transport validators for all `SHARED_*` / `APP_STORAGE_*` / picker replies |
|
|
587
|
+
| `0.26.x` | `^0.22.0` | `useViewer()` (`GET_VIEWER`) |
|
|
588
|
+
| `0.25.x` | `^0.21.0` | `useBuzzTransactions`, `useBuzzAccounts`, `useDailyCompensation`, `useWildcardPack` |
|
|
472
589
|
| `0.5.0` | `^0.7.0` | `useBuzzWorkflow().cancel()` (real server-side cancel, gotcha #51) |
|
|
473
590
|
| `0.4.x` | `^0.6.0` | `useAppStorage`, `SettingsForm` (`/ui`) |
|
|
474
591
|
| `0.3.x` | `^0.5.0` | earlier hook set |
|
|
@@ -20,4 +20,10 @@ import type { BlockToken } from '@civitai/app-sdk/blocks';
|
|
|
20
20
|
export declare function useBlockToken(): BlockToken & {
|
|
21
21
|
refresh: () => Promise<void>;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Mint/await a token refresh for one block instance, coalescing concurrent calls
|
|
25
|
+
* for the SAME `blockInstanceId`. Exported for the inline-mode multi-instance
|
|
26
|
+
* dedup test — hooks call it via `useBlockToken().refresh`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function requestRefresh(blockInstanceId: string): Promise<void>;
|
|
23
29
|
//# sourceMappingURL=useBlockToken.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBlockToken.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockToken.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAS1D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAwB7E"}
|
|
1
|
+
{"version":3,"file":"useBlockToken.d.ts","sourceRoot":"","sources":["../../src/hooks/useBlockToken.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAS1D;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,aAAa,IAAI,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CAwB7E;AAiBD;;;;GAIG;AACH,wBAAsB,cAAc,CAAC,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAe3E"}
|
|
@@ -47,22 +47,35 @@ export function useBlockToken() {
|
|
|
47
47
|
return { ...token, refresh: () => requestRefresh(instanceId) };
|
|
48
48
|
}
|
|
49
49
|
/**
|
|
50
|
-
* In-flight dedup
|
|
51
|
-
* synchronous path can fire while a previous refresh is
|
|
52
|
-
* coalescing into a single round-trip avoids fanning out
|
|
53
|
-
* REQUEST_TOKEN messages, which would otherwise hit the host once per
|
|
50
|
+
* In-flight dedup, keyed by `blockInstanceId`. Both the scheduled-timeout path
|
|
51
|
+
* and the "already past" synchronous path can fire while a previous refresh is
|
|
52
|
+
* still pending — coalescing into a single round-trip avoids fanning out
|
|
53
|
+
* duplicate REQUEST_TOKEN messages, which would otherwise hit the host once per
|
|
54
54
|
* snapshot update.
|
|
55
|
+
*
|
|
56
|
+
* The key MUST include `blockInstanceId`: in iframe mode there is one module per
|
|
57
|
+
* iframe so any key works, but on the INLINE transport (v2) several block
|
|
58
|
+
* instances share one module context. A module-global singleton would coalesce
|
|
59
|
+
* across instances, so instance B's 401-retry `refresh()` would resolve on
|
|
60
|
+
* instance A's in-flight `REQUEST_TOKEN` and B would never mint its OWN token.
|
|
61
|
+
*/
|
|
62
|
+
const inFlightRefreshByInstance = new Map();
|
|
63
|
+
/**
|
|
64
|
+
* Mint/await a token refresh for one block instance, coalescing concurrent calls
|
|
65
|
+
* for the SAME `blockInstanceId`. Exported for the inline-mode multi-instance
|
|
66
|
+
* dedup test — hooks call it via `useBlockToken().refresh`.
|
|
55
67
|
*/
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (
|
|
59
|
-
return
|
|
68
|
+
export async function requestRefresh(blockInstanceId) {
|
|
69
|
+
const existing = inFlightRefreshByInstance.get(blockInstanceId);
|
|
70
|
+
if (existing)
|
|
71
|
+
return existing;
|
|
60
72
|
const transport = getTransport();
|
|
61
|
-
|
|
73
|
+
const refresh = sendTypedRequest(transport, { type: 'REQUEST_TOKEN', payload: { blockInstanceId } }, 'TOKEN_REFRESH_RESPONSE')
|
|
62
74
|
.then(() => undefined)
|
|
63
75
|
.finally(() => {
|
|
64
|
-
|
|
76
|
+
inFlightRefreshByInstance.delete(blockInstanceId);
|
|
65
77
|
});
|
|
66
|
-
|
|
78
|
+
inFlightRefreshByInstance.set(blockInstanceId, refresh);
|
|
79
|
+
return refresh;
|
|
67
80
|
}
|
|
68
81
|
//# sourceMappingURL=useBlockToken.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useBlockToken.js","sourceRoot":"","sources":["../../src/hooks/useBlockToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,sEAAsE;AACtE,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;IAExC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,MAAM,cAAc,GAAG,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,CAAC;QAChF,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;YACxB,iDAAiD;YACjD,oEAAoE;YACpE,6CAA6C;YAC7C,cAAc,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE;YACzB,oEAAoE;YACpE,6CAA6C;YAC7C,cAAc,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7C,CAAC,EAAE,cAAc,CAAC,CAAC;QACnB,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IAE9C,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;AACjE,CAAC;AAED
|
|
1
|
+
{"version":3,"file":"useBlockToken.js","sourceRoot":"","sources":["../../src/hooks/useBlockToken.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAIlC,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAE5D,sEAAsE;AACtE,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEtC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,aAAa;IAC3B,MAAM,IAAI,GAAG,oBAAoB,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACzB,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,CAAC;IAExC,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,IAAI,CAAC,KAAK;YAAE,OAAO;QACxB,MAAM,cAAc,GAAG,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,eAAe,CAAC;QAChF,IAAI,cAAc,IAAI,CAAC,EAAE,CAAC;YACxB,iDAAiD;YACjD,oEAAoE;YACpE,6CAA6C;YAC7C,cAAc,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAC3C,OAAO;QACT,CAAC;QACD,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE;YACzB,oEAAoE;YACpE,6CAA6C;YAC7C,cAAc,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC7C,CAAC,EAAE,cAAc,CAAC,CAAC;QACnB,OAAO,GAAG,EAAE,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IAChC,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC,CAAC;IAE9C,OAAO,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;AACjE,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,yBAAyB,GAAG,IAAI,GAAG,EAAyB,CAAC;AAEnE;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,eAAuB;IAC1D,MAAM,QAAQ,GAAG,yBAAyB,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IAChE,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,MAAM,OAAO,GAAG,gBAAgB,CAC9B,SAAS,EACT,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,eAAe,EAAE,EAAE,EACvD,wBAAwB,CACzB;SACE,IAAI,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC;SACrB,OAAO,CAAC,GAAG,EAAE;QACZ,yBAAyB,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC;IACpD,CAAC,CAAC,CAAC;IACL,yBAAyB,CAAC,GAAG,CAAC,eAAe,EAAE,OAAO,CAAC,CAAC;IACxD,OAAO,OAAO,CAAC;AACjB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGenerationResources.d.ts","sourceRoot":"","sources":["../../src/hooks/useGenerationResources.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"useGenerationResources.d.ts","sourceRoot":"","sources":["../../src/hooks/useGenerationResources.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAiBjE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,IAAI;IACxC,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CAC/D,CA0DA"}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { buildGenerationResourcesUrl, responseToResources, } from '../api/generationResources.js';
|
|
3
3
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
4
4
|
import { useBlockToken } from './useBlockToken.js';
|
|
5
|
+
/**
|
|
6
|
+
* Backstop timeout for the direct REST fetch. Unlike the postMessage hooks
|
|
7
|
+
* (which inherit the transport's 30s request timeout), this hook talks to the
|
|
8
|
+
* HTTP API directly, so it needs its OWN bound — otherwise a hung request never
|
|
9
|
+
* rejects. Matches the transport's default request timeout.
|
|
10
|
+
*/
|
|
11
|
+
const GENERATION_RESOURCES_TIMEOUT_MS = 30_000;
|
|
5
12
|
/**
|
|
6
13
|
* Rehydrate a saved set of generation resources by version id, WITHOUT
|
|
7
14
|
* re-opening the resource picker. `fetch(versionIds)` GETs the block-token-gated
|
|
@@ -27,19 +34,49 @@ import { useBlockToken } from './useBlockToken.js';
|
|
|
27
34
|
export function useGenerationResources() {
|
|
28
35
|
const host = useHostOrigin();
|
|
29
36
|
const { raw } = useBlockToken();
|
|
37
|
+
// In-flight AbortControllers, aborted on unmount so a pending fetch never
|
|
38
|
+
// resolves into an unmounted component (and its timer is cleared).
|
|
39
|
+
const inFlight = useRef(new Set());
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const controllers = inFlight.current;
|
|
42
|
+
return () => {
|
|
43
|
+
for (const c of controllers)
|
|
44
|
+
c.abort();
|
|
45
|
+
controllers.clear();
|
|
46
|
+
};
|
|
47
|
+
}, []);
|
|
30
48
|
const doFetch = useCallback(async (versionIds) => {
|
|
31
49
|
if (!host) {
|
|
32
50
|
throw new Error('useGenerationResources: host origin not established yet (wait for BLOCK_INIT).');
|
|
33
51
|
}
|
|
34
52
|
const url = host + buildGenerationResourcesUrl(versionIds);
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
53
|
+
const controller = new AbortController();
|
|
54
|
+
inFlight.current.add(controller);
|
|
55
|
+
const timeoutId = setTimeout(() => controller.abort(), GENERATION_RESOURCES_TIMEOUT_MS);
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(url, {
|
|
58
|
+
headers: { Authorization: `Bearer ${raw}` },
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
});
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
throw new Error(`generation-resources request failed (${res.status})`);
|
|
63
|
+
}
|
|
64
|
+
const body = (await res.json());
|
|
65
|
+
return responseToResources(body);
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// Distinguish an abort (timeout OR unmount) from a real network/HTTP
|
|
69
|
+
// failure so the caller gets an actionable message instead of a bare
|
|
70
|
+
// DOMException.
|
|
71
|
+
if (controller.signal.aborted) {
|
|
72
|
+
throw new Error(`useGenerationResources: request aborted (timed out after ${GENERATION_RESOURCES_TIMEOUT_MS}ms or the hook unmounted).`);
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
clearTimeout(timeoutId);
|
|
78
|
+
inFlight.current.delete(controller);
|
|
40
79
|
}
|
|
41
|
-
const body = (await res.json());
|
|
42
|
-
return responseToResources(body);
|
|
43
80
|
}, [host, raw]);
|
|
44
81
|
return { fetch: doFetch };
|
|
45
82
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGenerationResources.js","sourceRoot":"","sources":["../../src/hooks/useGenerationResources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useGenerationResources.js","sourceRoot":"","sources":["../../src/hooks/useGenerationResources.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,OAAO,CAAC;AAIvD,OAAO,EACL,2BAA2B,EAC3B,mBAAmB,GACpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;GAKG;AACH,MAAM,+BAA+B,GAAG,MAAM,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,sBAAsB;IAGpC,MAAM,IAAI,GAAG,aAAa,EAAE,CAAC;IAC7B,MAAM,EAAE,GAAG,EAAE,GAAG,aAAa,EAAE,CAAC;IAEhC,0EAA0E;IAC1E,mEAAmE;IACnE,MAAM,QAAQ,GAAG,MAAM,CAAuB,IAAI,GAAG,EAAE,CAAC,CAAC;IACzD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC;QACrC,OAAO,GAAG,EAAE;YACV,KAAK,MAAM,CAAC,IAAI,WAAW;gBAAE,CAAC,CAAC,KAAK,EAAE,CAAC;YACvC,WAAW,CAAC,KAAK,EAAE,CAAC;QACtB,CAAC,CAAC;IACJ,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CACzB,KAAK,EAAE,UAAoB,EAAgC,EAAE;QAC3D,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,KAAK,CACb,gFAAgF,CACjF,CAAC;QACJ,CAAC;QACD,MAAM,GAAG,GAAG,IAAI,GAAG,2BAA2B,CAAC,UAAU,CAAC,CAAC;QAC3D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC;QACjC,MAAM,SAAS,GAAG,UAAU,CAC1B,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EACxB,+BAA+B,CAChC,CAAC;QACF,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,GAAG,EAAE,EAAE;gBAC3C,MAAM,EAAE,UAAU,CAAC,MAAM;aAC1B,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;YACzE,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAY,CAAC;YAC3C,OAAO,mBAAmB,CAAC,IAAiD,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,qEAAqE;YACrE,qEAAqE;YACrE,gBAAgB;YAChB,IAAI,UAAU,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;gBAC9B,MAAM,IAAI,KAAK,CACb,4DAA4D,+BAA+B,4BAA4B,CACxH,CAAC;YACJ,CAAC;YACD,MAAM,GAAG,CAAC;QACZ,CAAC;gBAAS,CAAC;YACT,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,QAAQ,CAAC,OAAO,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;QACtC,CAAC;IACH,CAAC,EACD,CAAC,IAAI,EAAE,GAAG,CAAC,CACZ,CAAC;IAEF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC;AAC5B,CAAC"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockGenerationSourceImageInfo, BlockUploadedImageInfo, BlockUploadPurpose } from '@civitai/app-sdk/blocks';
|
|
1
|
+
import type { BlockGenerationSourceImageInfo, BlockImageScanResult, BlockPendingImageInfo, BlockUploadedImageInfo, BlockUploadPurpose } from '@civitai/app-sdk/blocks';
|
|
2
2
|
/** Options for {@link useImageUpload}. */
|
|
3
3
|
export interface UseImageUploadOptions {
|
|
4
4
|
/**
|
|
@@ -10,6 +10,14 @@ export interface UseImageUploadOptions {
|
|
|
10
10
|
* (`{ url, width, height }`).
|
|
11
11
|
*/
|
|
12
12
|
purpose?: BlockUploadPurpose;
|
|
13
|
+
/**
|
|
14
|
+
* Opt in to the NON-BLOCKING `'display'` flow. When `true`, `open()` resolves
|
|
15
|
+
* EARLY (on persist) with a {@link BlockPendingImageInfo} and the scan verdict
|
|
16
|
+
* is delivered separately via `scanStatus()` (see the async overload). Only
|
|
17
|
+
* meaningful for `'display'`; ignored for `'generationSource'` (that path has
|
|
18
|
+
* no host-side scan). Absent/`false` keeps the legacy blocking behavior.
|
|
19
|
+
*/
|
|
20
|
+
asyncScan?: boolean;
|
|
13
21
|
}
|
|
14
22
|
/**
|
|
15
23
|
* Drives the platform-side image-upload flow for App Blocks (Design 1 —
|
|
@@ -18,12 +26,17 @@ export interface UseImageUploadOptions {
|
|
|
18
26
|
* routes through civitai's session-authed pipeline; the host is what decides
|
|
19
27
|
* how the image is scanned/returned based on the requested `purpose`.
|
|
20
28
|
*
|
|
21
|
-
* The RETURN of `open()` is typed by `purpose`:
|
|
22
|
-
* - default / `'display'` → resolves with a MODERATED
|
|
29
|
+
* The RETURN of `open()` is typed by `purpose` / `asyncScan`:
|
|
30
|
+
* - default / `'display'` (BLOCKING) → resolves with a MODERATED
|
|
23
31
|
* {@link BlockUploadedImageInfo} (imageId/nsfwLevel/contentRating/url), or
|
|
24
32
|
* `null` when the user dismissed the modal without a successful upload. The
|
|
25
33
|
* returned `url` is a Civitai-hosted image URL usable directly as a
|
|
26
34
|
* `WorkflowBody.sourceImage.url`.
|
|
35
|
+
* - `'display'` + `asyncScan: true` (NON-BLOCKING) → resolves EARLY (on persist)
|
|
36
|
+
* with a {@link BlockPendingImageInfo} (`status:'pending'`, imageId, url), or
|
|
37
|
+
* `null` on dismiss. The image is AUTHOR-PREVIEW-ONLY until the scan verdict —
|
|
38
|
+
* obtain it with `scanStatus(handle)`. Do NOT persist the image to a
|
|
39
|
+
* cross-user surface until `scanStatus` resolves `'scanned'`.
|
|
27
40
|
* - `'generationSource'` → resolves with the source
|
|
28
41
|
* {@link BlockGenerationSourceImageInfo} (`{ url, width, height }`, UNSCANNED —
|
|
29
42
|
* the orchestrator scans it at gen time), or `null` on dismiss. Feed it
|
|
@@ -38,7 +51,7 @@ export interface UseImageUploadOptions {
|
|
|
38
51
|
* purpose to `'display'`) behaves exactly as before.
|
|
39
52
|
*
|
|
40
53
|
* @example
|
|
41
|
-
* // display (moderated public image) — default
|
|
54
|
+
* // display (moderated public image) — default, BLOCKING
|
|
42
55
|
* const { open } = useImageUpload();
|
|
43
56
|
* const img = await open();
|
|
44
57
|
* if (!img) return; // user dismissed
|
|
@@ -46,6 +59,17 @@ export interface UseImageUploadOptions {
|
|
|
46
59
|
* sourceImage: { url: img.url, width: 1024, height: 1024 }, params: { prompt } });
|
|
47
60
|
*
|
|
48
61
|
* @example
|
|
62
|
+
* // display (NON-BLOCKING) — early handle now, scan verdict streamed
|
|
63
|
+
* const { open, scanStatus } = useImageUpload({ asyncScan: true });
|
|
64
|
+
* const handle = await open(); // BlockPendingImageInfo | null
|
|
65
|
+
* if (!handle) return;
|
|
66
|
+
* // …show the author their own preview immediately…
|
|
67
|
+
* const verdict = await scanStatus(handle);
|
|
68
|
+
* if (verdict.status === 'scanned') persistBackground(verdict.image); // clean
|
|
69
|
+
* else if (verdict.status === 'blocked') showRejected(verdict.reason); // terminal
|
|
70
|
+
* else retryLater(verdict.message); // transient — re-call scanStatus(handle)
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
49
73
|
* // generationSource (private img2img source, unscanned)
|
|
50
74
|
* const { open } = useImageUpload({ purpose: 'generationSource' });
|
|
51
75
|
* const src = await open(); // { url, width, height } | null
|
|
@@ -58,6 +82,20 @@ export declare function useImageUpload(options: {
|
|
|
58
82
|
}): {
|
|
59
83
|
open: () => Promise<BlockGenerationSourceImageInfo | null>;
|
|
60
84
|
};
|
|
85
|
+
export declare function useImageUpload(options: {
|
|
86
|
+
purpose?: 'display';
|
|
87
|
+
asyncScan: true;
|
|
88
|
+
}): {
|
|
89
|
+
/** Early-resolve handle (image persisted, NOT yet scanned) or `null` (dismissed). */
|
|
90
|
+
open: () => Promise<BlockPendingImageInfo | null>;
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the async scan verdict for a handle returned by `open()`. Re-callable
|
|
93
|
+
* for retry: the host emits the verdict once and the hook buffers it, so a
|
|
94
|
+
* re-call after an `'error'`/timeout re-awaits (or immediately returns) the same
|
|
95
|
+
* verdict. An unknown/expired handle resolves to a retryable `'error'`.
|
|
96
|
+
*/
|
|
97
|
+
scanStatus: (handle: BlockPendingImageInfo) => Promise<BlockImageScanResult>;
|
|
98
|
+
};
|
|
61
99
|
export declare function useImageUpload(options?: {
|
|
62
100
|
purpose?: 'display';
|
|
63
101
|
}): {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useImageUpload.d.ts","sourceRoot":"","sources":["../../src/hooks/useImageUpload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"useImageUpload.d.ts","sourceRoot":"","sources":["../../src/hooks/useImageUpload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,8BAA8B,EAC9B,oBAAoB,EACpB,qBAAqB,EACrB,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,yBAAyB,CAAC;AAejC,0CAA0C;AAC1C,MAAM,WAAW,qBAAqB;IACpC;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,kBAAkB,CAAC;IAC7B;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AA+BD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE;IAAE,OAAO,EAAE,kBAAkB,CAAA;CAAE,GAAG;IACxE,IAAI,EAAE,MAAM,OAAO,CAAC,8BAA8B,GAAG,IAAI,CAAC,CAAC;CAC5D,CAAC;AACF,wBAAgB,cAAc,CAAC,OAAO,EAAE;IAAE,OAAO,CAAC,EAAE,SAAS,CAAC;IAAC,SAAS,EAAE,IAAI,CAAA;CAAE,GAAG;IACjF,qFAAqF;IACrF,IAAI,EAAE,MAAM,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IAClD;;;;;OAKG;IACH,UAAU,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;CAC9E,CAAC;AACF,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,SAAS,CAAA;CAAE,GAAG;IACjE,IAAI,EAAE,MAAM,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;CACpD,CAAC"}
|
|
@@ -1,10 +1,106 @@
|
|
|
1
|
-
import { useCallback } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
2
|
import { getTransport } from '../internal/singleton.js';
|
|
3
|
-
import { sendTypedRequest } from '../internal/transport.js';
|
|
3
|
+
import { sendTypedRequest, subscribeTyped } from '../internal/transport.js';
|
|
4
4
|
import { PICKER_REQUEST_TIMEOUT_MS } from './useCheckpointPicker.js';
|
|
5
|
+
/**
|
|
6
|
+
* Generous hook-side backstop for {@link BlockImageScanResult} delivery. The
|
|
7
|
+
* host-side scan POLL budget is authoritative and the consuming app applies its
|
|
8
|
+
* own (shorter) timeout; this only guards against a verdict that never arrives,
|
|
9
|
+
* resolving RETRYABLE so a later re-call can still pick up a late verdict from
|
|
10
|
+
* the buffer.
|
|
11
|
+
*/
|
|
12
|
+
const SCAN_STATUS_TIMEOUT_MS = PICKER_REQUEST_TIMEOUT_MS;
|
|
13
|
+
/**
|
|
14
|
+
* A verdict is TERMINAL when the host has reached a final answer (`scanned` /
|
|
15
|
+
* `blocked`) — the block will never poll it again. An `error` verdict is
|
|
16
|
+
* transient/retryable, so its entry is KEPT so `scanStatus()` can be re-called.
|
|
17
|
+
* Terminal entries are EVICTED from the tracking map once delivered (see below)
|
|
18
|
+
* so a long-lived block doing many uploads can't grow the map unbounded.
|
|
19
|
+
*/
|
|
20
|
+
function isTerminalVerdict(v) {
|
|
21
|
+
return v.status === 'scanned' || v.status === 'blocked';
|
|
22
|
+
}
|
|
5
23
|
export function useImageUpload(options) {
|
|
6
24
|
const purpose = options?.purpose;
|
|
25
|
+
const asyncScan = options?.asyncScan === true && purpose !== 'generationSource';
|
|
26
|
+
// Per-instance scan tracking, keyed by imageId. Persists across renders; a
|
|
27
|
+
// verdict is correlated back to its upload by imageId + the generated requestId.
|
|
28
|
+
const scansRef = useRef(new Map());
|
|
29
|
+
// Mount ONE listener for the host→block IMAGE_SCAN_RESOLVED push. The transport
|
|
30
|
+
// invokes this ONLY AFTER the message has cleared BOTH the origin allowlist and
|
|
31
|
+
// the payload validator (see IframeTransport.handleMessage), and we ADDITIONALLY
|
|
32
|
+
// honor a verdict only if it matches a request this hook instance originated
|
|
33
|
+
// (matching requestId + imageId), so concurrent instances coexist and a
|
|
34
|
+
// forged/replayed verdict is ignored.
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const scans = scansRef.current;
|
|
37
|
+
const unsubscribe = subscribeTyped(getTransport(), 'IMAGE_SCAN_RESOLVED', ({ requestId, imageId, result }) => {
|
|
38
|
+
const entry = scans.get(imageId);
|
|
39
|
+
if (!entry || entry.requestId !== requestId)
|
|
40
|
+
return; // not ours / forged
|
|
41
|
+
if (entry.verdict)
|
|
42
|
+
return; // host emits once — honor only the first
|
|
43
|
+
entry.verdict = result;
|
|
44
|
+
const hadWaiters = entry.waiters.length > 0;
|
|
45
|
+
for (const w of entry.waiters) {
|
|
46
|
+
clearTimeout(w.timeoutId);
|
|
47
|
+
w.resolve(result);
|
|
48
|
+
}
|
|
49
|
+
entry.waiters = [];
|
|
50
|
+
// Evict a TERMINAL verdict once it has been delivered to a live caller
|
|
51
|
+
// (bounded-memory: L1). If nobody was waiting yet (verdict arrived before
|
|
52
|
+
// scanStatus() was called), keep it BUFFERED so the eventual scanStatus()
|
|
53
|
+
// can still read it — that read then evicts it (see scanStatus below).
|
|
54
|
+
// `error` verdicts are always kept (retryable).
|
|
55
|
+
if (hadWaiters && isTerminalVerdict(result))
|
|
56
|
+
scans.delete(imageId);
|
|
57
|
+
});
|
|
58
|
+
return () => {
|
|
59
|
+
unsubscribe();
|
|
60
|
+
for (const entry of scans.values()) {
|
|
61
|
+
for (const w of entry.waiters)
|
|
62
|
+
clearTimeout(w.timeoutId);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}, []);
|
|
7
66
|
const open = useCallback(async () => {
|
|
67
|
+
if (asyncScan) {
|
|
68
|
+
// Omit `purpose` (display byte-compat) but opt into the early-resolve flow.
|
|
69
|
+
const { requestId, selected } = await sendTypedRequest(getTransport(), { type: 'OPEN_IMAGE_UPLOAD', payload: { asyncScan: true } }, 'IMAGE_UPLOAD_RESULT', { timeoutMs: PICKER_REQUEST_TIMEOUT_MS });
|
|
70
|
+
if (!selected)
|
|
71
|
+
return null; // dismissed
|
|
72
|
+
// NEW host: an early-resolve pending handle. Track it and wait for the push.
|
|
73
|
+
if ('status' in selected && selected.status === 'pending') {
|
|
74
|
+
scansRef.current.set(selected.imageId, { requestId, waiters: [] });
|
|
75
|
+
const handle = {
|
|
76
|
+
status: 'pending',
|
|
77
|
+
imageId: selected.imageId,
|
|
78
|
+
url: selected.url,
|
|
79
|
+
};
|
|
80
|
+
return handle;
|
|
81
|
+
}
|
|
82
|
+
// OLD host (ignored asyncScan): it blocking-resolved a MODERATED image. Wrap
|
|
83
|
+
// it in a pending-shaped handle AND pre-buffer a `scanned` verdict so
|
|
84
|
+
// scanStatus() resolves immediately — no hang. (Compat matrix case 2.)
|
|
85
|
+
if ('imageId' in selected && typeof selected.imageId === 'number') {
|
|
86
|
+
const moderated = selected;
|
|
87
|
+
scansRef.current.set(moderated.imageId, {
|
|
88
|
+
requestId,
|
|
89
|
+
verdict: { status: 'scanned', image: moderated },
|
|
90
|
+
waiters: [],
|
|
91
|
+
});
|
|
92
|
+
const handle = {
|
|
93
|
+
status: 'pending',
|
|
94
|
+
imageId: moderated.imageId,
|
|
95
|
+
url: moderated.url,
|
|
96
|
+
};
|
|
97
|
+
return handle;
|
|
98
|
+
}
|
|
99
|
+
// Any other shape on a display request (e.g. an unexpected source-only
|
|
100
|
+
// payload) → treat as dismissed rather than fabricate a handle.
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
// ---- Existing BLOCKING behavior (unchanged) ----
|
|
8
104
|
// Omit `purpose` for the default/display mode so the wire stays
|
|
9
105
|
// byte-compatible with an older host that predates the field.
|
|
10
106
|
const payload = purpose === 'generationSource' ? { purpose } : {};
|
|
@@ -12,7 +108,31 @@ export function useImageUpload(options) {
|
|
|
12
108
|
// Normalize the "dismissed" case to an explicit null so callers can
|
|
13
109
|
// `if (!img) return;` without an `undefined` ambiguity.
|
|
14
110
|
return selected ?? null;
|
|
15
|
-
}, [purpose]);
|
|
16
|
-
|
|
111
|
+
}, [asyncScan, purpose]);
|
|
112
|
+
const scanStatus = useCallback((handle) => {
|
|
113
|
+
const entry = scansRef.current.get(handle.imageId);
|
|
114
|
+
if (!entry) {
|
|
115
|
+
// No tracked upload for this handle (a foreign/expired handle, or the
|
|
116
|
+
// hook remounted). Retryable rather than a hang.
|
|
117
|
+
return Promise.resolve({ status: 'error', message: 'unknown or expired upload handle' });
|
|
118
|
+
}
|
|
119
|
+
if (entry.verdict) {
|
|
120
|
+
const buffered = entry.verdict;
|
|
121
|
+
// A BUFFERED terminal verdict (the push arrived before this call) is
|
|
122
|
+
// consumed here — evict it so the map doesn't grow (L1). `error` stays
|
|
123
|
+
// (retryable). See isTerminalVerdict + the push handler above.
|
|
124
|
+
if (isTerminalVerdict(buffered))
|
|
125
|
+
scansRef.current.delete(handle.imageId);
|
|
126
|
+
return Promise.resolve(buffered);
|
|
127
|
+
}
|
|
128
|
+
return new Promise((resolve) => {
|
|
129
|
+
const timeoutId = setTimeout(() => {
|
|
130
|
+
entry.waiters = entry.waiters.filter((w) => w.timeoutId !== timeoutId);
|
|
131
|
+
resolve({ status: 'error', message: 'scan status timed out' });
|
|
132
|
+
}, SCAN_STATUS_TIMEOUT_MS);
|
|
133
|
+
entry.waiters.push({ resolve, timeoutId });
|
|
134
|
+
});
|
|
135
|
+
}, []);
|
|
136
|
+
return { open, scanStatus };
|
|
17
137
|
}
|
|
18
138
|
//# sourceMappingURL=useImageUpload.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useImageUpload.js","sourceRoot":"","sources":["../../src/hooks/useImageUpload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"useImageUpload.js","sourceRoot":"","sources":["../../src/hooks/useImageUpload.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,12 @@ export declare class IframeTransport implements BlockTransport {
|
|
|
29
29
|
/** Messages queued before `BLOCK_INIT` lands. Flushed in arrival order. */
|
|
30
30
|
private readonly outbound;
|
|
31
31
|
private readonly pending;
|
|
32
|
+
/**
|
|
33
|
+
* Handlers for UNSOLICITED parent→block pushes (e.g. `IMAGE_SCAN_RESOLVED`) —
|
|
34
|
+
* messages the host initiates on its own schedule, NOT replies to a pending
|
|
35
|
+
* `sendRequest`. Keyed by message type; each entry a set of subscribers.
|
|
36
|
+
*/
|
|
37
|
+
private readonly pushListeners;
|
|
32
38
|
private readonly initPromise;
|
|
33
39
|
private resolveInit;
|
|
34
40
|
private rejectInit;
|
|
@@ -61,6 +67,7 @@ export declare class IframeTransport implements BlockTransport {
|
|
|
61
67
|
sendRequest(request: OutboundRequest, responseType: ParentToBlockMessageType, opts?: {
|
|
62
68
|
timeoutMs?: number;
|
|
63
69
|
}): Promise<unknown>;
|
|
70
|
+
onMessage(type: ParentToBlockMessageType, handler: (payload: unknown) => void): () => void;
|
|
64
71
|
/** Test-only: tear down listeners + reject pending. */
|
|
65
72
|
dispose(): void;
|
|
66
73
|
private dispatch;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iframeTransport.d.ts","sourceRoot":"","sources":["../../src/internal/iframeTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAEzB,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AASxB,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,cAAc;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IAEnD,yEAAyE;IACzE,OAAO,CAAC,YAAY,CAAuB;IAE3C,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;IAC1E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgC;gBAEpD,IAAI,EAAE,sBAAsB;IAoCxC,WAAW,IAAI,aAAa;IAI5B;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAIxC,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI;IAIhD,WAAW,CACT,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,wBAAwB,EACtC,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAChC,OAAO,CAAC,OAAO,CAAC;IAmBnB,uDAAuD;IACvD,OAAO,IAAI,IAAI;
|
|
1
|
+
{"version":3,"file":"iframeTransport.d.ts","sourceRoot":"","sources":["../../src/internal/iframeTransport.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAEzB,KAAK,wBAAwB,EAC9B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,gBAAgB,CAAC;AASxB,MAAM,WAAW,sBAAsB;IACrC;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,EAAE,CAAC;IAC/B,iEAAiE;IACjE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AASD;;;;;GAKG;AACH,qBAAa,eAAgB,YAAW,cAAc;IACpD,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAiC;IACjD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IAEnD,yEAAyE;IACzE,OAAO,CAAC,YAAY,CAAuB;IAE3C,2EAA2E;IAC3E,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiD;IAC1E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAsD;IAEpF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA4B;IACxD,OAAO,CAAC,WAAW,CAAuC;IAC1D,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAAS;IAE7B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgC;gBAEpD,IAAI,EAAE,sBAAsB;IAoCxC,WAAW,IAAI,aAAa;IAI5B;;;;;;;;;;;;;;;;OAgBG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI;IAI9B,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAIxC,WAAW,CAAC,OAAO,EAAE,oBAAoB,GAAG,IAAI;IAIhD,WAAW,CACT,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,wBAAwB,EACtC,IAAI,GAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAO,GAChC,OAAO,CAAC,OAAO,CAAC;IAmBnB,SAAS,CACP,IAAI,EAAE,wBAAwB,EAC9B,OAAO,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GAClC,MAAM,IAAI;IAeb,uDAAuD;IACvD,OAAO,IAAI,IAAI;IAYf,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IAKpB,OAAO,CAAC,aAAa;IA2GrB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,IAAI;CAGb"}
|