@civitai/blocks-react 0.72.0 → 0.72.2
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 +46 -0
- package/dist/hooks/useAppStorage.js +2 -0
- package/dist/hooks/useAppWorkflows.js +2 -0
- package/dist/hooks/useBatchEstimate.js +2 -0
- package/dist/hooks/useBlockAnalytics.js +2 -0
- package/dist/hooks/useBlockBreakpoint.js +2 -0
- package/dist/hooks/useBlockContext.js +2 -0
- package/dist/hooks/useBlockResize.js +2 -0
- package/dist/hooks/useBlockSettings.js +2 -0
- package/dist/hooks/useBlockTheme.js +2 -0
- package/dist/hooks/useBlockToken.js +2 -0
- package/dist/hooks/useBuzzAccounts.js +2 -0
- package/dist/hooks/useBuzzBalance.js +2 -0
- package/dist/hooks/useBuzzPurchase.js +2 -0
- package/dist/hooks/useBuzzTransactions.js +2 -0
- package/dist/hooks/useBuzzWorkflow.js +2 -0
- package/dist/hooks/useCheckpointPicker.js +2 -0
- package/dist/hooks/useCivitaiNavigate.js +2 -0
- package/dist/hooks/useCivitaiRoute.js +2 -0
- package/dist/hooks/useCollectionFollow.js +2 -0
- package/dist/hooks/useConsentUnavailable.js +2 -0
- package/dist/hooks/useCreatePostFromApp.js +2 -0
- package/dist/hooks/useDailyCompensation.js +2 -0
- package/dist/hooks/useDirectLoad.js +2 -0
- package/dist/hooks/useDomainMaturity.js +2 -0
- package/dist/hooks/useEntitlements.js +2 -0
- package/dist/hooks/useGatedImages.js +2 -0
- package/dist/hooks/useGenerationResources.js +2 -0
- package/dist/hooks/useGoodPurchase.js +2 -0
- package/dist/hooks/useHostOrigin.js +2 -0
- package/dist/hooks/useImageUpload.js +2 -0
- package/dist/hooks/useNestedDocument.js +2 -0
- package/dist/hooks/usePrepareTrainingDataset.js +2 -0
- package/dist/hooks/usePublishGenerationOutputs.js +2 -0
- package/dist/hooks/useRequestConsent.js +2 -0
- package/dist/hooks/useRequestSignIn.js +2 -0
- package/dist/hooks/useResourcePicker.js +2 -0
- package/dist/hooks/useRunConfirmedStep.js +2 -0
- package/dist/hooks/useRunTraining.js +2 -0
- package/dist/hooks/useSaveImage.js +2 -0
- package/dist/hooks/useSharedStorage.js +2 -0
- package/dist/hooks/useTip.js +2 -0
- package/dist/hooks/useTipAllowance.js +2 -0
- package/dist/hooks/useTransportConnection.d.ts +43 -0
- package/dist/hooks/useTransportConnection.js +59 -0
- package/dist/hooks/useUploadImageBytes.js +2 -0
- package/dist/hooks/useViewer.js +2 -0
- package/dist/hooks/useWildcardPack.js +2 -0
- package/dist/hooks/useWorkflowBatch.js +2 -0
- package/dist/transport/iframeTransport.js +13 -0
- package/dist/transport/singleton.d.ts +6 -0
- package/dist/transport/singleton.js +20 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -117,6 +117,33 @@ viewer's signed-in civitai.com session, which the local harness does not have, s
|
|
|
117
117
|
host — `createMockHost` or `Harness` from `@civitai/blocks-react/testing`, with
|
|
118
118
|
the `publishImageIds` / `publishError` options.
|
|
119
119
|
|
|
120
|
+
### Required at mount: the host handshake
|
|
121
|
+
|
|
122
|
+
The SDK must be **connected at mount**. When the transport is created it posts
|
|
123
|
+
`BLOCK_HELLO` to the host, and it answers the host's first valid `BLOCK_INIT`
|
|
124
|
+
with `BLOCK_READY`. The host waits **10 seconds** for that answer; without it,
|
|
125
|
+
the viewer sees "This app didn't load in time", the host retries twice, and then
|
|
126
|
+
gives up. `"bootSkeleton": true` only removes the host's loading cover — it does
|
|
127
|
+
**not** extend or lift that deadline.
|
|
128
|
+
|
|
129
|
+
You do not have to do anything for this: **every hook exported from the package
|
|
130
|
+
root connects the transport when its component mounts**, so a block that calls
|
|
131
|
+
any one of them — even only `useBuzzWorkflow()`, whose requests all happen in
|
|
132
|
+
callbacks — answers the host before anyone clicks. (In earlier releases, request-only
|
|
133
|
+
hooks connected lazily on their first call, so a block whose only SDK use was
|
|
134
|
+
such a hook rendered fine and was then timed out by the host.)
|
|
135
|
+
|
|
136
|
+
What this means for you:
|
|
137
|
+
|
|
138
|
+
- A block that renders but uses **no** hook from this package never connects.
|
|
139
|
+
Call `useBlockContext()` at your root (you will want its `ready` gate anyway).
|
|
140
|
+
- A dev harness that injects options with `getTransport({ allowedParentOrigins })`
|
|
141
|
+
must still do so **before** the first component that uses a hook mounts — only
|
|
142
|
+
the first call's options are honoured.
|
|
143
|
+
- If no parent allowlist is configured, the mount-time connect logs one
|
|
144
|
+
`console.error` and the block keeps rendering; the first bridge call then
|
|
145
|
+
throws the same configuration error it always did.
|
|
146
|
+
|
|
120
147
|
### `useBlockContext()`
|
|
121
148
|
|
|
122
149
|
The primary hook. Returns everything the host delivered in `BLOCK_INIT` plus a
|
|
@@ -546,6 +573,25 @@ try {
|
|
|
546
573
|
- A refused `kind: 'workflow'` body sets its own codes, in the same shape — see
|
|
547
574
|
the next section.
|
|
548
575
|
|
|
576
|
+
#### A registered step with no price quote: `step_quote_*`
|
|
577
|
+
|
|
578
|
+
A **registered** `kind: 'step'` body (one with a `step` id, such as
|
|
579
|
+
`'chat-completion'` or `'convert-image'`) that the host has no price quote for
|
|
580
|
+
fails on `estimate()` (`WorkflowEstimateError`) and as a batch-estimate cell,
|
|
581
|
+
as `{ workflowId: 'failed', status: 'failed', error, errorCode }` with no
|
|
582
|
+
`cost`. `submit()` of the same body is unchanged. The pass-through arm (a
|
|
583
|
+
`$type`, no `step`) does not set these codes.
|
|
584
|
+
|
|
585
|
+
| `errorCode` | Means | `error` | Retry the same body? |
|
|
586
|
+
|---|---|---|---|
|
|
587
|
+
| `'step_quote_rejected'` | the orchestrator refused the request | `No price quote for this step: the orchestrator rejected the request: <reason>` when the reason is safe to show, else `No price quote for this step: the orchestrator rejected the request; check the step inputs and references (retrying the same body will not help)` | no — change the body |
|
|
588
|
+
| `'step_quote_unavailable'` | no usable answer (an outage, a timeout, a rate limit, …) | `No price quote for this step: none could be obtained; try again` | yes |
|
|
589
|
+
| `'step_quote_unpriced'` | the orchestrator answered without a price | `No price quote for this step: the orchestrator answered without one` | not expected to help |
|
|
590
|
+
|
|
591
|
+
An older host has none of the three: it resolves the estimate priced at the
|
|
592
|
+
step's declared estimate. The mock host always quotes a registered step (at
|
|
593
|
+
`generation.costPerGen`), so it never sends these codes.
|
|
594
|
+
|
|
549
595
|
#### Several steps as one workflow: `kind: 'workflow'`
|
|
550
596
|
|
|
551
597
|
A `kind: 'workflow'` body submits **several orchestrator steps as one
|
|
@@ -2,6 +2,7 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { throwOnFailedReply, throwOnReplyError } from '../internal/replyError.js';
|
|
4
4
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Per-(block instance, viewer) KV datastore. Calls flow through the host's
|
|
7
8
|
* postMessage bridge — the block never sees the apps DB credentials.
|
|
@@ -47,6 +48,7 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
47
48
|
* const quota = await storage.getQuota(); // { usedBytes, rowCount, limitBytes, limitRows }
|
|
48
49
|
*/
|
|
49
50
|
export function useAppStorage() {
|
|
51
|
+
useTransportConnection();
|
|
50
52
|
return useMemo(() => {
|
|
51
53
|
const transport = getTransport();
|
|
52
54
|
return {
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read (and cancel within) the calling app's OWN generator SUBQUEUE — the
|
|
7
8
|
* tag-scoped list of generations THIS app produced for the viewer, newest-first —
|
|
@@ -24,6 +25,7 @@ import { useRequestSequencer } from './useRequestSequencer.js';
|
|
|
24
25
|
* // …render each workflow's images; await cancel(w.workflowId) on a Cancel click.
|
|
25
26
|
*/
|
|
26
27
|
export function useAppWorkflows(params) {
|
|
28
|
+
useTransportConnection();
|
|
27
29
|
const [workflows, setWorkflows] = useState([]);
|
|
28
30
|
const [cursor, setCursor] = useState(null);
|
|
29
31
|
const [loading, setLoading] = useState(true);
|
|
@@ -4,6 +4,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
4
4
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
import { WorkflowEstimateError } from './useBuzzWorkflow.js';
|
|
6
6
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* The most bodies one {@link UseBatchEstimate.estimateBatch} call may carry.
|
|
9
10
|
*
|
|
@@ -134,6 +135,7 @@ function toBatchEstimateError(err) {
|
|
|
134
135
|
* }
|
|
135
136
|
*/
|
|
136
137
|
export function useBatchEstimate() {
|
|
138
|
+
useTransportConnection();
|
|
137
139
|
const [pending, setPending] = useState(false);
|
|
138
140
|
const [result, setResult] = useState(null);
|
|
139
141
|
const [error, setError] = useState(null);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Custom events for your app's analytics panel. Fire-and-forget: the host
|
|
5
6
|
* forwards the event and the block gets no acknowledgement, so never wait on
|
|
@@ -47,6 +48,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
47
48
|
* track('generate_clicked', { mode: 'txt2img', batch: 4 });
|
|
48
49
|
*/
|
|
49
50
|
export function useBlockAnalytics() {
|
|
51
|
+
useTransportConnection();
|
|
50
52
|
const track = useCallback((eventName, properties) => {
|
|
51
53
|
getTransport().sendMessage({ type: 'TRACK_EVENT', payload: { eventName, properties } });
|
|
52
54
|
}, []);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BREAKPOINT_KEYS, breakpoints } from '@civitai/theme';
|
|
2
2
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/** Ascending tier order. `base` first; the rest follow `BREAKPOINT_KEYS`. */
|
|
4
5
|
const TIER_ORDER = ['base', ...BREAKPOINT_KEYS];
|
|
5
6
|
/**
|
|
@@ -66,6 +67,7 @@ export function resolveBlockTier(width) {
|
|
|
66
67
|
* );
|
|
67
68
|
*/
|
|
68
69
|
export function useBlockBreakpoint(ref) {
|
|
70
|
+
useTransportConnection();
|
|
69
71
|
// `null` means UNMEASURED — distinct from a measured `'base'`. See `measured`.
|
|
70
72
|
const [tier, setTier] = useState(null);
|
|
71
73
|
// 🔴 THE EFFECT MUST NOT DEPEND ON THE REF WRAPPER'S IDENTITY. Unlike
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Subscribe a hook to the singleton transport. All hooks build on this —
|
|
5
6
|
* `useSyncExternalStore` gives concurrent-mode safety and minimal re-renders.
|
|
@@ -42,6 +43,7 @@ function useTransportSnapshot() {
|
|
|
42
43
|
* return <div data-theme={theme}>{isSignedIn(viewer) ? 'Hi there' : 'Hi anon'}</div>;
|
|
43
44
|
*/
|
|
44
45
|
export function useBlockContext() {
|
|
46
|
+
useTransportConnection();
|
|
45
47
|
const snap = useTransportSnapshot();
|
|
46
48
|
return {
|
|
47
49
|
ready: snap.ready,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useRef } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Observes the referenced element's height and asks the host to resize on
|
|
5
6
|
* every change. Attach to the block's root DOM element.
|
|
@@ -44,6 +45,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
44
45
|
* return <div ref={rootRef}>…</div>;
|
|
45
46
|
*/
|
|
46
47
|
export function useBlockResize(ref) {
|
|
48
|
+
useTransportConnection();
|
|
47
49
|
/** The element the live observer is watching. `null` = watching nothing. */
|
|
48
50
|
const observedRef = useRef(null);
|
|
49
51
|
const observerRef = useRef(null);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
2
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
2
3
|
/**
|
|
3
4
|
* Shorthand for `useBlockContext().settings`. Returns the publisher- and
|
|
4
5
|
* user-controlled settings the host forwarded at init. Read-only from the
|
|
@@ -19,6 +20,7 @@ import { useTransportSnapshot } from './useBlockContext.js';
|
|
|
19
20
|
* const { publisherSettings, userSettings } = useBlockSettings();
|
|
20
21
|
*/
|
|
21
22
|
export function useBlockSettings() {
|
|
23
|
+
useTransportConnection();
|
|
22
24
|
return useTransportSnapshot().settings;
|
|
23
25
|
}
|
|
24
26
|
//# sourceMappingURL=useBlockSettings.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
2
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
2
3
|
/**
|
|
3
4
|
* The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
|
|
4
5
|
* life of the block ON THE IFRAME TRANSPORT.
|
|
@@ -36,6 +37,7 @@ import { useTransportSnapshot } from './useBlockContext.js';
|
|
|
36
37
|
* return <div data-theme={theme}>…</div>;
|
|
37
38
|
*/
|
|
38
39
|
export function useBlockTheme() {
|
|
40
|
+
useTransportConnection();
|
|
39
41
|
return useTransportSnapshot().theme;
|
|
40
42
|
}
|
|
41
43
|
//# sourceMappingURL=useBlockTheme.js.map
|
|
@@ -2,6 +2,7 @@ import { useEffect } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/** Refresh fires `REFRESH_LEAD_MS` before the token's `expiresAt`. */
|
|
6
7
|
const REFRESH_LEAD_MS = 2 * 60 * 1000;
|
|
7
8
|
/**
|
|
@@ -37,6 +38,7 @@ const REFRESH_LEAD_MS = 2 * 60 * 1000;
|
|
|
37
38
|
* }
|
|
38
39
|
*/
|
|
39
40
|
export function useBlockToken() {
|
|
41
|
+
useTransportConnection();
|
|
40
42
|
const snap = useTransportSnapshot();
|
|
41
43
|
const token = snap.token;
|
|
42
44
|
const instanceId = snap.blockInstanceId;
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read the signed-in viewer's ALL-pool Buzz balances through the host-mediated
|
|
7
8
|
* `GET_BUZZ_ACCOUNTS` → `BUZZ_ACCOUNTS_RESULT` bridge. Token-bound: the host
|
|
@@ -19,6 +20,7 @@ import { useRequestSequencer } from './useRequestSequencer.js';
|
|
|
19
20
|
* const { accounts, loading, error } = useBuzzAccounts();
|
|
20
21
|
*/
|
|
21
22
|
export function useBuzzAccounts() {
|
|
23
|
+
useTransportConnection();
|
|
22
24
|
const [accounts, setAccounts] = useState(null);
|
|
23
25
|
const [loading, setLoading] = useState(true);
|
|
24
26
|
const [error, setError] = useState(null);
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read the signed-in viewer's per-pool Buzz balance ({ blue, green, yellow })
|
|
7
8
|
* through the host-mediated `GET_BUZZ_BALANCE` → `BUZZ_BALANCE_RESULT` bridge.
|
|
@@ -23,6 +24,7 @@ import { useRequestSequencer } from './useRequestSequencer.js';
|
|
|
23
24
|
* return <span>Yellow: {balance?.yellow ?? 0}</span>;
|
|
24
25
|
*/
|
|
25
26
|
export function useBuzzBalance() {
|
|
27
|
+
useTransportConnection();
|
|
26
28
|
const [balance, setBalance] = useState(null);
|
|
27
29
|
const [loading, setLoading] = useState(true);
|
|
28
30
|
const [error, setError] = useState(null);
|
|
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Opens the Civitai Buzz purchase modal on the host. Resolves with the
|
|
7
8
|
* outcome when the user closes the modal — `purchased: true` means the
|
|
@@ -22,6 +23,7 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
22
23
|
* if (purchased) { /* retry the generation *\/ }
|
|
23
24
|
*/
|
|
24
25
|
export function useBuzzPurchase() {
|
|
26
|
+
useTransportConnection();
|
|
25
27
|
const openPurchaseModal = useCallback(async (suggestedAmount) => {
|
|
26
28
|
const { purchased, newBalance } = await sendTypedRequest(getTransport(), { type: 'OPEN_BUZZ_PURCHASE', payload: { suggestedAmount } }, 'BUZZ_PURCHASE_RESULT', { timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
27
29
|
return { purchased, newBalance };
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/** Rehydrate the host's `date`/`cursor` (a `Date` instance OR an ISO string). */
|
|
6
7
|
function toDate(v) {
|
|
7
8
|
return v instanceof Date ? v : new Date(v);
|
|
@@ -31,6 +32,7 @@ function toIso(v) {
|
|
|
31
32
|
* const { transactions, cursor, loading, error } = useBuzzTransactions({ type: 'Tip', limit: 20 });
|
|
32
33
|
*/
|
|
33
34
|
export function useBuzzTransactions(params) {
|
|
35
|
+
useTransportConnection();
|
|
34
36
|
const [transactions, setTransactions] = useState(null);
|
|
35
37
|
const [cursor, setCursor] = useState(null);
|
|
36
38
|
const [loading, setLoading] = useState(true);
|
|
@@ -4,6 +4,7 @@ import { withConsentRetry } from '../internal/withConsentRetry.js';
|
|
|
4
4
|
import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
5
|
import { getTransport } from '../transport/singleton.js';
|
|
6
6
|
import { resolveIdempotencyKey, sendTypedRequest } from '../transport/transport.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* The consent-gated scope {@link UseBuzzWorkflow.submit} requires.
|
|
9
10
|
*
|
|
@@ -622,6 +623,7 @@ export class WorkflowSubmitError extends Error {
|
|
|
622
623
|
* }
|
|
623
624
|
*/
|
|
624
625
|
export function useBuzzWorkflow() {
|
|
626
|
+
useTransportConnection();
|
|
625
627
|
const [status, setStatus] = useState('idle');
|
|
626
628
|
const [result, setResult] = useState(null);
|
|
627
629
|
const [error, setError] = useState(null);
|
|
@@ -3,6 +3,7 @@ import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
|
3
3
|
import { throwOnFailedReply } from '../internal/replyError.js';
|
|
4
4
|
import { getTransport } from '../transport/singleton.js';
|
|
5
5
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
6
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
6
7
|
/**
|
|
7
8
|
* Drives the platform-side Checkpoint picker and the persist-override flow.
|
|
8
9
|
*
|
|
@@ -34,6 +35,7 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
34
35
|
* const { selected } = await open({ baseModelGroup: checkpoint.baseModel });
|
|
35
36
|
*/
|
|
36
37
|
export function useCheckpointPicker() {
|
|
38
|
+
useTransportConnection();
|
|
37
39
|
const open = useCallback(async (opts) => {
|
|
38
40
|
// Both keys are spread conditionally, and an empty or whitespace-only
|
|
39
41
|
// family is normalized to absent.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Requests a navigation from the host: the hook sends a `NAVIGATE` message and
|
|
5
6
|
* returns. Fire-and-forget — the host doesn't reply, so the block never learns
|
|
@@ -38,6 +39,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
38
39
|
* navigate('detail/7', 'new_tab'); // the pre-`scope` shape, still app-scoped
|
|
39
40
|
*/
|
|
40
41
|
export function useCivitaiNavigate() {
|
|
42
|
+
useTransportConnection();
|
|
41
43
|
const navigate = useCallback((path, options = {}) => {
|
|
42
44
|
// A bare string is the pre-`scope` call shape (`navigate(path, 'new_tab')`)
|
|
43
45
|
// and stays supported: there are live callers, and the whole point of the
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
2
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
2
3
|
/**
|
|
3
4
|
* The sub-path below your app's root that is CURRENTLY showing, kept live for
|
|
4
5
|
* the whole life of the block ON THE IFRAME TRANSPORT.
|
|
@@ -56,6 +57,7 @@ import { useTransportSnapshot } from './useBlockContext.js';
|
|
|
56
57
|
* moves and nothing renders.
|
|
57
58
|
*/
|
|
58
59
|
export function useCivitaiRoute() {
|
|
60
|
+
useTransportConnection();
|
|
59
61
|
const context = useTransportSnapshot().context;
|
|
60
62
|
// A PRESENCE test, not a slot-id test, and not a cast. `BlockContext` is a
|
|
61
63
|
// union whose `UnknownSlotContext` arm declares `slotId` and nothing else, and
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* The closed set of HOST refusal codes, as a runtime Set.
|
|
7
8
|
*
|
|
@@ -134,6 +135,7 @@ export class CollectionFollowError extends Error {
|
|
|
134
135
|
* };
|
|
135
136
|
*/
|
|
136
137
|
export function useCollectionFollow() {
|
|
138
|
+
useTransportConnection();
|
|
137
139
|
const [pending, setPending] = useState(false);
|
|
138
140
|
const [error, setError] = useState(null);
|
|
139
141
|
// Same guard as `useTip`/`useWildcardPack`: this request can outlive the
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { armConsentRefusalLatch, clearConsentRefusalLatch, readConsentRefusalLatch, } from '../internal/consentRefusalLatch.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { subscribeTyped } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Subscribe to the host's `CONSENT_UNAVAILABLE` push — the signal that a
|
|
7
8
|
* `REQUEST_CONSENT` this block sent can **never** be granted in this
|
|
@@ -62,6 +63,7 @@ import { subscribeTyped } from '../transport/transport.js';
|
|
|
62
63
|
* return <button onClick={() => requestConsent({ scopes: ['ai:write:budgeted'] })}>Generate</button>;
|
|
63
64
|
*/
|
|
64
65
|
export function useConsentUnavailable() {
|
|
66
|
+
useTransportConnection();
|
|
65
67
|
const [refusal, setRefusal] = useState(null);
|
|
66
68
|
useEffect(() => {
|
|
67
69
|
const transport = getTransport();
|
|
@@ -4,6 +4,7 @@ import { withConsentRetry } from '../internal/withConsentRetry.js';
|
|
|
4
4
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
5
|
import { getTransport } from '../transport/singleton.js';
|
|
6
6
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* The consent-gated scope this bridge requires — `posts:write:self`, named in
|
|
9
10
|
* `@civitai/app-sdk`'s own `CREATE_POST_FROM_APP` message docs ("Scope
|
|
@@ -156,6 +157,7 @@ export class CreatePostError extends Error {
|
|
|
156
157
|
* };
|
|
157
158
|
*/
|
|
158
159
|
export function useCreatePostFromApp() {
|
|
160
|
+
useTransportConnection();
|
|
159
161
|
const [pending, setPending] = useState(false);
|
|
160
162
|
const [error, setError] = useState(null);
|
|
161
163
|
// Same guard as `useCollectionFollow`: this request can outlive the component
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read the signed-in viewer's per-modelVersion generation compensation for the
|
|
7
8
|
* MONTH containing `params.date`, through the host-mediated
|
|
@@ -19,6 +20,7 @@ import { useRequestSequencer } from './useRequestSequencer.js';
|
|
|
19
20
|
* const { resources, hasPublishedResources } = useDailyCompensation({ date: '2026-07-01' });
|
|
20
21
|
*/
|
|
21
22
|
export function useDailyCompensation(params) {
|
|
23
|
+
useTransportConnection();
|
|
22
24
|
const [resources, setResources] = useState(null);
|
|
23
25
|
const [hasPublishedResources, setHasPublishedResources] = useState(null);
|
|
24
26
|
const [loading, setLoading] = useState(true);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { DIRECT_LOAD_TIMEOUT_MS } from '../transport/directLoad.js';
|
|
3
3
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
4
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
4
5
|
/**
|
|
5
6
|
* True iff the current window is the TOP-LEVEL browsing context — i.e. the
|
|
6
7
|
* block is NOT running inside an iframe.
|
|
@@ -47,6 +48,7 @@ function isTopLevel() {
|
|
|
47
48
|
* `ready` is `true`, so a late init can't leave a stuck fallback.
|
|
48
49
|
*/
|
|
49
50
|
export function useDirectLoad(options) {
|
|
51
|
+
useTransportConnection();
|
|
50
52
|
const timeoutMs = options?.timeoutMs ?? DIRECT_LOAD_TIMEOUT_MS;
|
|
51
53
|
const ready = useTransportSnapshot().ready;
|
|
52
54
|
// Sampled once per mount — top-level-ness doesn't change during a page's life.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { effectiveBrowsingCeiling, isSfwCeiling, isLevelAllowed as isLevelAllowedCeiling, } from '@civitai/app-sdk/blocks';
|
|
2
2
|
import { useTransportSnapshot } from './useBlockContext.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Read the maturity ceiling in force for the current viewer, as the host
|
|
5
6
|
* projects it into `BLOCK_INIT`, so a block can hide/blur mature affordances.
|
|
@@ -40,6 +41,7 @@ import { useTransportSnapshot } from './useBlockContext.js';
|
|
|
40
41
|
* const hiddenByYourSettings = effectiveBrowsingLevel !== maxBrowsingLevel;
|
|
41
42
|
*/
|
|
42
43
|
export function useDomainMaturity() {
|
|
44
|
+
useTransportConnection();
|
|
43
45
|
const snap = useTransportSnapshot();
|
|
44
46
|
const maxBrowsingLevel = snap.maxBrowsingLevel;
|
|
45
47
|
// The one number every gate below is computed from. `effectiveBrowsingCeiling`
|
|
@@ -4,6 +4,7 @@ import { useBlockContext } from './useBlockContext.js';
|
|
|
4
4
|
import { useBlockToken } from './useBlockToken.js';
|
|
5
5
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
6
6
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* Backstop timeout for the direct REST entitlements GET (see {@link useTip} /
|
|
9
10
|
* {@link useGenerationResources} for why direct-fetch hooks need their own bound).
|
|
@@ -51,6 +52,7 @@ const HOST_ORIGIN_WAIT_MS = 30_000;
|
|
|
51
52
|
* return owns('extra-slots') ? <Unlocked /> : <BuyButton onDone={refetch} />;
|
|
52
53
|
*/
|
|
53
54
|
export function useEntitlements() {
|
|
55
|
+
useTransportConnection();
|
|
54
56
|
const host = useHostOrigin();
|
|
55
57
|
const { raw } = useBlockToken();
|
|
56
58
|
const { ready, viewer } = useBlockContext();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
4
5
|
/**
|
|
5
6
|
* Read per-viewer gated display data for a list of image ids via the
|
|
6
7
|
* host-mediated `GET_IMAGES_BY_IDS` → `IMAGES_RESULT` bridge — the read side of
|
|
@@ -39,6 +40,7 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
39
40
|
* }
|
|
40
41
|
*/
|
|
41
42
|
export function useGatedImages() {
|
|
43
|
+
useTransportConnection();
|
|
42
44
|
const getImages = useCallback(async (imageIds) => {
|
|
43
45
|
const reply = await sendTypedRequest(getTransport(), { type: 'GET_IMAGES_BY_IDS', payload: { imageIds } }, 'IMAGES_RESULT');
|
|
44
46
|
if (reply.error || !reply.result) {
|
|
@@ -2,6 +2,7 @@ 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
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Backstop timeout for the direct REST fetch. Unlike the postMessage hooks
|
|
7
8
|
* (which inherit the transport's 30s request timeout), this hook talks to the
|
|
@@ -32,6 +33,7 @@ const GENERATION_RESOURCES_TIMEOUT_MS = 30_000;
|
|
|
32
33
|
* // resources[0].strength / .minStrength / .maxStrength / .trainedWords / .clipSkip
|
|
33
34
|
*/
|
|
34
35
|
export function useGenerationResources() {
|
|
36
|
+
useTransportConnection();
|
|
35
37
|
const host = useHostOrigin();
|
|
36
38
|
const { raw } = useBlockToken();
|
|
37
39
|
// In-flight AbortControllers, aborted on unmount so a pending fetch never
|
|
@@ -6,6 +6,7 @@ import { resolveIdempotencyKey } from '../transport/transport.js';
|
|
|
6
6
|
import { useBlockToken } from './useBlockToken.js';
|
|
7
7
|
import { useBuzzPurchase } from './useBuzzPurchase.js';
|
|
8
8
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
9
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
9
10
|
/**
|
|
10
11
|
* The consent-gated scope a purchase needs.
|
|
11
12
|
*
|
|
@@ -80,6 +81,7 @@ export class GoodPurchaseRefusal extends Error {
|
|
|
80
81
|
* );
|
|
81
82
|
*/
|
|
82
83
|
export function useGoodPurchase() {
|
|
84
|
+
useTransportConnection();
|
|
83
85
|
const host = useHostOrigin();
|
|
84
86
|
const { raw } = useBlockToken();
|
|
85
87
|
const { openPurchaseModal } = useBuzzPurchase();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Returns the validated host (parent) origin the block may safely direct-fetch
|
|
5
6
|
* the civitai App Blocks HTTP API against — `undefined` until the transport
|
|
@@ -33,6 +34,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
33
34
|
* moment init lands.
|
|
34
35
|
*/
|
|
35
36
|
export function useHostOrigin() {
|
|
37
|
+
useTransportConnection();
|
|
36
38
|
const transport = getTransport();
|
|
37
39
|
const origin = useSyncExternalStore((cb) => transport.subscribe(cb), () => transport.getHostOrigin(),
|
|
38
40
|
// Server snapshot: no host origin is known before hydration.
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { sendTypedRequest, subscribeTyped } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Generous hook-side backstop for {@link BlockImageScanResult} delivery. The
|
|
7
8
|
* host-side scan POLL budget is authoritative and the consuming app applies its
|
|
@@ -28,6 +29,7 @@ function isTerminalVerdict(v) {
|
|
|
28
29
|
return v.status === 'scanned' || v.status === 'blocked';
|
|
29
30
|
}
|
|
30
31
|
export function useImageUpload(options) {
|
|
32
|
+
useTransportConnection();
|
|
31
33
|
const purpose = options?.purpose;
|
|
32
34
|
const asyncScan = options?.asyncScan === true && purpose !== 'generationSource';
|
|
33
35
|
// Per-instance scan tracking, keyed by imageId. Persists across renders; a
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { fetchNestedDocument } from '@civitai/app-sdk/blocks';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
const IDLE = { status: 'idle', srcDoc: null, error: null };
|
|
4
5
|
const LOADING = { status: 'loading', srcDoc: null, error: null };
|
|
5
6
|
/**
|
|
@@ -61,6 +62,7 @@ const NESTED_DOCUMENT_TIMEOUT_MS = 30_000;
|
|
|
61
62
|
* ```
|
|
62
63
|
*/
|
|
63
64
|
export function useNestedDocument(options) {
|
|
65
|
+
useTransportConnection();
|
|
64
66
|
const src = options?.src;
|
|
65
67
|
const baseUrl = options?.baseUrl;
|
|
66
68
|
const [state, setState] = useState(src ? LOADING : IDLE);
|
|
@@ -4,6 +4,7 @@ import { withConsentRetry } from '../internal/withConsentRetry.js';
|
|
|
4
4
|
import { WORKFLOW_REQUEST_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
5
|
import { getTransport } from '../transport/singleton.js';
|
|
6
6
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* The consent-gated scope the training flow requires — `ai:write:budgeted`. The
|
|
9
10
|
* server refuses a dataset request from a token without it (it is the spend
|
|
@@ -101,6 +102,7 @@ export class PrepareTrainingDatasetError extends Error {
|
|
|
101
102
|
* }
|
|
102
103
|
*/
|
|
103
104
|
export function usePrepareTrainingDataset() {
|
|
105
|
+
useTransportConnection();
|
|
104
106
|
const [pending, setPending] = useState(false);
|
|
105
107
|
const [error, setError] = useState(null);
|
|
106
108
|
// The consent wait can outlive the component; see `useCreatePostFromApp`.
|
|
@@ -2,6 +2,7 @@ import { useCallback } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Publish selected outputs of one of the calling app's OWN generations into bare,
|
|
7
8
|
* real-scanned public `Image` rows via the host-mediated
|
|
@@ -28,6 +29,7 @@ import { sendTypedRequest } from '../transport/transport.js';
|
|
|
28
29
|
* // …store imageIds via useSharedStorage() so the grid can read them back gated.
|
|
29
30
|
*/
|
|
30
31
|
export function usePublishGenerationOutputs() {
|
|
32
|
+
useTransportConnection();
|
|
31
33
|
const publish = useCallback(async (args) => {
|
|
32
34
|
const reply = await sendTypedRequest(getTransport(), {
|
|
33
35
|
type: 'PUBLISH_GENERATION_OUTPUTS',
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { sendRequestConsent } from '../internal/withConsentRetry.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
4
5
|
/**
|
|
5
6
|
* Lazy consent. Asks the host to open civitai.com's consent UI when a
|
|
6
7
|
* LOGGED-IN viewer clicks an action whose consent-gated scope the block token
|
|
@@ -43,6 +44,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
43
44
|
* requestConsent({ scopes: ['ai:write:budgeted', 'buzz:read:self'] });
|
|
44
45
|
*/
|
|
45
46
|
export function useRequestConsent() {
|
|
47
|
+
useTransportConnection();
|
|
46
48
|
const requestConsent = useCallback((payload) => {
|
|
47
49
|
// Single-sourced with the SDK's own automatic prompt-and-retry
|
|
48
50
|
// (`internal/withConsentRetry.ts`), which posts the identical message. The
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* Anonymous conversion. Asks the host to start civitai.com's login flow when a
|
|
5
6
|
* logged-out viewer (`useBlockContext().viewer === null`) clicks an action that
|
|
@@ -19,6 +20,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
19
20
|
* if (viewer === null) return <button onClick={() => requestSignIn()}>Sign in</button>;
|
|
20
21
|
*/
|
|
21
22
|
export function useRequestSignIn() {
|
|
23
|
+
useTransportConnection();
|
|
22
24
|
const requestSignIn = useCallback((payload) => {
|
|
23
25
|
getTransport().sendMessage({
|
|
24
26
|
type: 'REQUEST_SIGN_IN',
|
|
@@ -3,6 +3,7 @@ import { throwOnReplyError } from '../internal/replyError.js';
|
|
|
3
3
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
4
4
|
import { getTransport } from '../transport/singleton.js';
|
|
5
5
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
6
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
6
7
|
/**
|
|
7
8
|
* The most resources ONE multi-select `open({ multiple })` can return — the
|
|
8
9
|
* same cap the generation body puts on `additionalResources`, so a full batch
|
|
@@ -86,6 +87,7 @@ const MULTIPLE_RESOURCE_TYPES = new Set(['lora', 'locon', 'dora']);
|
|
|
86
87
|
* }
|
|
87
88
|
*/
|
|
88
89
|
export function useResourcePicker() {
|
|
90
|
+
useTransportConnection();
|
|
89
91
|
const open = useCallback(async (opts) => {
|
|
90
92
|
if (opts.multiple == null) {
|
|
91
93
|
const { selected } = await sendTypedRequest(getTransport(), {
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* The host's CLOSED refusal codes for `RUN_CONFIRMED_STEP`, as a runtime list.
|
|
7
8
|
* Mirrors civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`.
|
|
@@ -209,6 +210,7 @@ export class RunConfirmedStepError extends Error {
|
|
|
209
210
|
* }
|
|
210
211
|
*/
|
|
211
212
|
export function useRunConfirmedStep() {
|
|
213
|
+
useTransportConnection();
|
|
212
214
|
const [pending, setPending] = useState(false);
|
|
213
215
|
const [error, setError] = useState(null);
|
|
214
216
|
// This request can outlive the component by up to ten minutes (the dialog may
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
3
|
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* The host's CLOSED refusal codes for `RUN_TRAINING`, as a runtime list. Mirrors
|
|
7
8
|
* civitai/civitai's `RUN_TRAINING_HOST_ERRORS`.
|
|
@@ -145,6 +146,7 @@ export class RunTrainingError extends Error {
|
|
|
145
146
|
* }
|
|
146
147
|
*/
|
|
147
148
|
export function useRunTraining() {
|
|
149
|
+
useTransportConnection();
|
|
148
150
|
const [pending, setPending] = useState(false);
|
|
149
151
|
const [error, setError] = useState(null);
|
|
150
152
|
// This request can outlive the component by up to ten minutes (the dialog may
|
|
@@ -3,6 +3,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
3
3
|
import { throwOnFailedReply } from '../internal/replyError.js';
|
|
4
4
|
import { SAVE_BYTES_MAX_BYTES, SAVE_BYTES_TOO_LARGE_ERROR } from '../internal/saveBytes.js';
|
|
5
5
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
6
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
6
7
|
/**
|
|
7
8
|
* Build the wire payload. The ONLY client-side refusal is the cap, so an
|
|
8
9
|
* over-cap buffer is never copied across `postMessage` just to be refused.
|
|
@@ -42,6 +43,7 @@ function toWirePayload(input) {
|
|
|
42
43
|
* await saveImage({ bytes: healedPng, filename: 'healed.png' });
|
|
43
44
|
*/
|
|
44
45
|
export function useSaveImage() {
|
|
46
|
+
useTransportConnection();
|
|
45
47
|
const saveImage = useCallback(async (input) => {
|
|
46
48
|
const payload = toWirePayload(input);
|
|
47
49
|
const reply = await sendTypedRequest(getTransport(), { type: 'SAVE_IMAGE', payload }, 'SAVE_IMAGE_RESULT');
|
|
@@ -3,6 +3,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
3
3
|
import { throwOnFailedReply, throwOnReplyError } from '../internal/replyError.js';
|
|
4
4
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
import { parseSharedStorageRateLimit } from '../transport/validate.js';
|
|
6
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
6
7
|
/**
|
|
7
8
|
* A shared-storage write the server REFUSED because a per-(viewer, app) budget
|
|
8
9
|
* is empty — thrown by `append`, `update`, `vote`, `unvote`, `withdraw` and
|
|
@@ -95,6 +96,7 @@ function rehydrateSharedItem(item) {
|
|
|
95
96
|
};
|
|
96
97
|
}
|
|
97
98
|
export function useSharedStorage() {
|
|
99
|
+
useTransportConnection();
|
|
98
100
|
return useMemo(() => {
|
|
99
101
|
const transport = getTransport();
|
|
100
102
|
return {
|
package/dist/hooks/useTip.js
CHANGED
|
@@ -5,6 +5,7 @@ import { getTransport } from '../transport/singleton.js';
|
|
|
5
5
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
6
6
|
import { useBlockToken } from './useBlockToken.js';
|
|
7
7
|
import { resolveIdempotencyKey } from '../transport/transport.js';
|
|
8
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
8
9
|
/** The consent-gated scope a tip needs (`social:tip:self`). */
|
|
9
10
|
const TIP_SCOPES = [BLOCK_SCOPES.SOCIAL_TIP_SELF];
|
|
10
11
|
/**
|
|
@@ -60,6 +61,7 @@ const TIP_REQUEST_TIMEOUT_MS = 30_000;
|
|
|
60
61
|
* await tip({ toUserId: 123, amount: 50 }, { idempotencyKey: keyRef.current });
|
|
61
62
|
*/
|
|
62
63
|
export function useTip() {
|
|
64
|
+
useTransportConnection();
|
|
63
65
|
const host = useHostOrigin();
|
|
64
66
|
const { raw } = useBlockToken();
|
|
65
67
|
const [loading, setLoading] = useState(false);
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
|
|
|
2
2
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
3
3
|
import { useBlockToken } from './useBlockToken.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Backstop timeout for the direct REST allowance GET (see {@link useTip} /
|
|
7
8
|
* {@link useGenerationResources} for why direct-fetch hooks need their own bound).
|
|
@@ -45,6 +46,7 @@ const HOST_ORIGIN_WAIT_MS = 30_000;
|
|
|
45
46
|
* // allowance?.remaining — Buzz the viewer may still tip today
|
|
46
47
|
*/
|
|
47
48
|
export function useTipAllowance() {
|
|
49
|
+
useTransportConnection();
|
|
48
50
|
const host = useHostOrigin();
|
|
49
51
|
const { raw } = useBlockToken();
|
|
50
52
|
const [allowance, setAllowance] = useState(null);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connects the block to its host at MOUNT. Internal — every public hook calls
|
|
3
|
+
* it, and it is deliberately not exported from the package entry.
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY EVERY HOOK, AND WHY AT MOUNT. The host waits 10 s for the block's
|
|
6
|
+
* handshake — `BLOCK_HELLO` on transport construction, `BLOCK_READY` on the
|
|
7
|
+
* first valid `BLOCK_INIT` — and then shows "This app didn't load in time",
|
|
8
|
+
* retries twice and gives up (`bootSkeleton` hides the cover, not the
|
|
9
|
+
* deadline). Both messages are sent by the transport, and the transport is a
|
|
10
|
+
* lazy singleton: nothing goes out until something calls `getTransport()`.
|
|
11
|
+
* Request-only hooks (`useBuzzWorkflow`, `useTip`, …) used to call it only
|
|
12
|
+
* inside their callbacks, so an app whose only SDK use was one of them never
|
|
13
|
+
* answered the host and was torn down before anyone could click. Calling this
|
|
14
|
+
* from every public hook makes "uses any SDK hook" imply "is connected".
|
|
15
|
+
* `test/connectsAtMount.test.tsx` pins it over the whole exported hook set.
|
|
16
|
+
*
|
|
17
|
+
* 🔴 IT IS CALLED EVEN WHERE IT IS REDUNDANT, ON PURPOSE. About half the hooks
|
|
18
|
+
* already reach `getTransport()` at render or in a mount fetch; they call this
|
|
19
|
+
* anyway so the rule is "every public hook", which needs no case analysis. A
|
|
20
|
+
* mount fetch can be conditional (`useWildcardPack` skips it for an invalid id),
|
|
21
|
+
* so "it already fetches on mount" is not a safe reason to leave one out.
|
|
22
|
+
*
|
|
23
|
+
* 🔴 WHY AN EFFECT, NOT RENDER. `useBlockContext` constructs during render, and
|
|
24
|
+
* that is fine for it — but a render-time call throws during SSR (the iframe
|
|
25
|
+
* transport refuses to construct without a `window`), and these hooks have
|
|
26
|
+
* always been safe to render on a server. An effect never runs there, and on
|
|
27
|
+
* the client it runs at mount, long before the host's deadline.
|
|
28
|
+
*
|
|
29
|
+
* 🔴 WHY NOT AT MODULE LOAD. `getTransport(opts)` honours only its FIRST call;
|
|
30
|
+
* dev harnesses and tests inject their allowlist (`installDevTransport()`,
|
|
31
|
+
* `getTransport({ allowedParentOrigins })`) after importing this package and
|
|
32
|
+
* before the first hook runs. Constructing on import would win that race and
|
|
33
|
+
* silently discard their options.
|
|
34
|
+
*
|
|
35
|
+
* 🔴 WHY A FAILURE IS LOGGED, NOT THROWN. Construction throws when no parent
|
|
36
|
+
* allowlist is configured. Before this hook existed, such an app still rendered
|
|
37
|
+
* and only failed on its first bridge call — and it still does: `getTransport()`
|
|
38
|
+
* caches nothing on a throw, so that call throws the same error as before. This
|
|
39
|
+
* hook only ADDS the connection; it must not turn a misconfigured block that
|
|
40
|
+
* renders into one that crashes on mount.
|
|
41
|
+
*/
|
|
42
|
+
export declare function useTransportConnection(): void;
|
|
43
|
+
//# sourceMappingURL=useTransportConnection.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { claimConnectFailureReport, getTransport } from '../transport/singleton.js';
|
|
3
|
+
/**
|
|
4
|
+
* Connects the block to its host at MOUNT. Internal — every public hook calls
|
|
5
|
+
* it, and it is deliberately not exported from the package entry.
|
|
6
|
+
*
|
|
7
|
+
* 🔴 WHY EVERY HOOK, AND WHY AT MOUNT. The host waits 10 s for the block's
|
|
8
|
+
* handshake — `BLOCK_HELLO` on transport construction, `BLOCK_READY` on the
|
|
9
|
+
* first valid `BLOCK_INIT` — and then shows "This app didn't load in time",
|
|
10
|
+
* retries twice and gives up (`bootSkeleton` hides the cover, not the
|
|
11
|
+
* deadline). Both messages are sent by the transport, and the transport is a
|
|
12
|
+
* lazy singleton: nothing goes out until something calls `getTransport()`.
|
|
13
|
+
* Request-only hooks (`useBuzzWorkflow`, `useTip`, …) used to call it only
|
|
14
|
+
* inside their callbacks, so an app whose only SDK use was one of them never
|
|
15
|
+
* answered the host and was torn down before anyone could click. Calling this
|
|
16
|
+
* from every public hook makes "uses any SDK hook" imply "is connected".
|
|
17
|
+
* `test/connectsAtMount.test.tsx` pins it over the whole exported hook set.
|
|
18
|
+
*
|
|
19
|
+
* 🔴 IT IS CALLED EVEN WHERE IT IS REDUNDANT, ON PURPOSE. About half the hooks
|
|
20
|
+
* already reach `getTransport()` at render or in a mount fetch; they call this
|
|
21
|
+
* anyway so the rule is "every public hook", which needs no case analysis. A
|
|
22
|
+
* mount fetch can be conditional (`useWildcardPack` skips it for an invalid id),
|
|
23
|
+
* so "it already fetches on mount" is not a safe reason to leave one out.
|
|
24
|
+
*
|
|
25
|
+
* 🔴 WHY AN EFFECT, NOT RENDER. `useBlockContext` constructs during render, and
|
|
26
|
+
* that is fine for it — but a render-time call throws during SSR (the iframe
|
|
27
|
+
* transport refuses to construct without a `window`), and these hooks have
|
|
28
|
+
* always been safe to render on a server. An effect never runs there, and on
|
|
29
|
+
* the client it runs at mount, long before the host's deadline.
|
|
30
|
+
*
|
|
31
|
+
* 🔴 WHY NOT AT MODULE LOAD. `getTransport(opts)` honours only its FIRST call;
|
|
32
|
+
* dev harnesses and tests inject their allowlist (`installDevTransport()`,
|
|
33
|
+
* `getTransport({ allowedParentOrigins })`) after importing this package and
|
|
34
|
+
* before the first hook runs. Constructing on import would win that race and
|
|
35
|
+
* silently discard their options.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 WHY A FAILURE IS LOGGED, NOT THROWN. Construction throws when no parent
|
|
38
|
+
* allowlist is configured. Before this hook existed, such an app still rendered
|
|
39
|
+
* and only failed on its first bridge call — and it still does: `getTransport()`
|
|
40
|
+
* caches nothing on a throw, so that call throws the same error as before. This
|
|
41
|
+
* hook only ADDS the connection; it must not turn a misconfigured block that
|
|
42
|
+
* renders into one that crashes on mount.
|
|
43
|
+
*/
|
|
44
|
+
export function useTransportConnection() {
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
try {
|
|
47
|
+
getTransport();
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
// One console line per transport lifecycle, not one per hook instance:
|
|
51
|
+
// a block calling ten hooks would otherwise log ten identical errors.
|
|
52
|
+
if (claimConnectFailureReport()) {
|
|
53
|
+
// eslint-disable-next-line no-console -- developer-facing diagnostic; the host will time this block out
|
|
54
|
+
console.error('@civitai/blocks-react: could not connect to the host at mount.', err);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}, []);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=useTransportConnection.js.map
|
|
@@ -4,6 +4,7 @@ import { UPLOAD_BYTES_MAX_BYTES, UPLOAD_BYTES_TOO_LARGE_ERROR } from '../interna
|
|
|
4
4
|
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
5
5
|
import { getTransport } from '../transport/singleton.js';
|
|
6
6
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
7
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
7
8
|
/**
|
|
8
9
|
* The SDK's own rejection when a reply carries neither an image nor an
|
|
9
10
|
* `error`. A host that predates the `bytes` variant opens its picker instead,
|
|
@@ -43,6 +44,7 @@ export const UPLOAD_IMAGE_BYTES_NO_IMAGE_ERROR = 'the host returned no uploaded
|
|
|
43
44
|
* await createPost({ sources: [{ kind: 'published', imageIds: [image.imageId] }] });
|
|
44
45
|
*/
|
|
45
46
|
export function useUploadImageBytes() {
|
|
47
|
+
useTransportConnection();
|
|
46
48
|
const upload = useCallback(async (bytes, options) => {
|
|
47
49
|
// The ONLY client-side refusal, mirroring `useSaveImage`'s cap pre-check:
|
|
48
50
|
// everything else is forwarded and judged by the host.
|
package/dist/hooks/useViewer.js
CHANGED
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* Read the signed-in viewer (id / username / status + optional buzzBudget)
|
|
7
8
|
* through the host-mediated `GET_VIEWER` → `VIEWER_RESULT` bridge. Token-bound:
|
|
@@ -29,6 +30,7 @@ import { useRequestSequencer } from './useRequestSequencer.js';
|
|
|
29
30
|
* return <span>{viewer?.username}</span>;
|
|
30
31
|
*/
|
|
31
32
|
export function useViewer() {
|
|
33
|
+
useTransportConnection();
|
|
32
34
|
const [viewer, setViewer] = useState(null);
|
|
33
35
|
const [loading, setLoading] = useState(true);
|
|
34
36
|
const [error, setError] = useState(null);
|
|
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useState } from 'react';
|
|
|
2
2
|
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
5
6
|
/**
|
|
6
7
|
* A wildcard-pack import failure. Unlike the buzz bridges' free-text errors, the
|
|
7
8
|
* host returns a DISCRIMINATED code — this error carries it on `.code` so a block
|
|
@@ -37,6 +38,7 @@ export class WildcardPackError extends Error {
|
|
|
37
38
|
* if (error instanceof WildcardPackError && error.code === 'busy') return <RetryLater />;
|
|
38
39
|
*/
|
|
39
40
|
export function useWildcardPack(modelVersionId) {
|
|
41
|
+
useTransportConnection();
|
|
40
42
|
const [pack, setPack] = useState(null);
|
|
41
43
|
const [loading, setLoading] = useState(true);
|
|
42
44
|
const [error, setError] = useState(null);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { useBuzzWorkflow } from './useBuzzWorkflow.js';
|
|
3
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
3
4
|
/**
|
|
4
5
|
* The most workflows one {@link useWorkflowBatch} may watch.
|
|
5
6
|
*
|
|
@@ -145,6 +146,7 @@ function summarise(cells) {
|
|
|
145
146
|
* if (!result.complete) showRetryCancel(result);
|
|
146
147
|
*/
|
|
147
148
|
export function useWorkflowBatch(workflowIds, options) {
|
|
149
|
+
useTransportConnection();
|
|
148
150
|
validateIds(workflowIds);
|
|
149
151
|
// A private `useBuzzWorkflow()` instance, so the app's own instance's
|
|
150
152
|
// `result`/`status` are not overwritten by 16 cells' updates.
|
|
@@ -130,6 +130,19 @@ export class IframeTransport {
|
|
|
130
130
|
this.resolveInit = resolve;
|
|
131
131
|
this.rejectInit = reject;
|
|
132
132
|
});
|
|
133
|
+
// 🔴 THE TRANSPORT HANDLES ITS OWN INIT TIMEOUT. Every hook connects the
|
|
134
|
+
// transport at mount, and most blocks never call `waitForInit()`, so without
|
|
135
|
+
// a handler here the 10s timeout is an UNHANDLED rejection. That fails the
|
|
136
|
+
// Vitest run of any block app whose tests mount a hook with no host. This
|
|
137
|
+
// handler makes the rejection handled and keeps the diagnostic (it names the
|
|
138
|
+
// configured and observed origins) on the console as a warning. It is a
|
|
139
|
+
// separate branch of the promise: `waitForInit()` still returns
|
|
140
|
+
// `initPromise` itself, so its callers still receive the rejection.
|
|
141
|
+
// Pinned by `test/initTimeoutUnhandled.test.tsx`.
|
|
142
|
+
this.initPromise.catch((err) => {
|
|
143
|
+
// eslint-disable-next-line no-console -- developer-facing diagnostic; the host shows its own fallback
|
|
144
|
+
console.warn(err instanceof Error ? err.message : err);
|
|
145
|
+
});
|
|
133
146
|
this.initTimeoutId = setTimeout(() => {
|
|
134
147
|
if (!this.initResolved) {
|
|
135
148
|
this.initResolved = true;
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { type DetectOptions } from './detector.js';
|
|
2
2
|
import type { BlockTransport } from './transport.js';
|
|
3
|
+
/**
|
|
4
|
+
* Returns true the FIRST time it is called in this transport lifecycle, false
|
|
5
|
+
* after. Internal — used by `useTransportConnection` to log a failed connect
|
|
6
|
+
* once instead of once per hook instance.
|
|
7
|
+
*/
|
|
8
|
+
export declare function claimConnectFailureReport(): boolean;
|
|
3
9
|
/**
|
|
4
10
|
* Returns the process-wide transport, instantiating it on first call.
|
|
5
11
|
*
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { BlockTransportDetector } from './detector.js';
|
|
2
2
|
let cached = null;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a failed MOUNT-TIME connect (`useTransportConnection`) has already been
|
|
5
|
+
* logged. Lives here, not in the hook, so `__resetTransport()` clears it with the
|
|
6
|
+
* transport it describes: one log per transport lifecycle — once per page in
|
|
7
|
+
* production, once per reset in tests. A latch that outlived the reset would
|
|
8
|
+
* silence every misconfigured case in a test worker after the first.
|
|
9
|
+
*/
|
|
10
|
+
let connectFailureReported = false;
|
|
11
|
+
/**
|
|
12
|
+
* Returns true the FIRST time it is called in this transport lifecycle, false
|
|
13
|
+
* after. Internal — used by `useTransportConnection` to log a failed connect
|
|
14
|
+
* once instead of once per hook instance.
|
|
15
|
+
*/
|
|
16
|
+
export function claimConnectFailureReport() {
|
|
17
|
+
if (connectFailureReported)
|
|
18
|
+
return false;
|
|
19
|
+
connectFailureReported = true;
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
3
22
|
/**
|
|
4
23
|
* Returns the process-wide transport, instantiating it on first call.
|
|
5
24
|
*
|
|
@@ -24,5 +43,6 @@ export function __resetTransport() {
|
|
|
24
43
|
const disposable = cached;
|
|
25
44
|
disposable?.dispose?.();
|
|
26
45
|
cached = null;
|
|
46
|
+
connectFailureReported = false;
|
|
27
47
|
}
|
|
28
48
|
//# sourceMappingURL=singleton.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@civitai/blocks-react",
|
|
3
|
-
"version": "0.72.
|
|
3
|
+
"version": "0.72.2",
|
|
4
4
|
"description": "React hooks and iframe transport for Civitai Apps. Pairs with @civitai/app-sdk/blocks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^8.0.14",
|
|
60
60
|
"vitest": "^4.1.11",
|
|
61
|
-
"@civitai/app-sdk": "^0.
|
|
61
|
+
"@civitai/app-sdk": "^0.68.0"
|
|
62
62
|
},
|
|
63
63
|
"publishConfig": {
|
|
64
64
|
"access": "public"
|