@civitai/blocks-react 0.71.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 +308 -15
- package/dist/hooks/useAppStorage.js +2 -0
- package/dist/hooks/useAppWorkflows.js +2 -0
- package/dist/hooks/useBatchEstimate.d.ts +4 -0
- package/dist/hooks/useBatchEstimate.js +6 -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.d.ts +35 -3
- package/dist/hooks/useBuzzWorkflow.js +32 -3
- package/dist/hooks/useCheckpointPicker.d.ts +2 -0
- package/dist/hooks/useCheckpointPicker.js +4 -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.d.ts +209 -0
- package/dist/hooks/useRunConfirmedStep.js +289 -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/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/internal/hostSurface.d.ts +37 -0
- package/dist/internal/hostSurface.js +38 -0
- package/dist/internal/liveHost.d.ts +15 -6
- package/dist/internal/liveHost.js +151 -69
- package/dist/internal/mockHost.d.ts +123 -10
- package/dist/internal/mockHost.js +377 -14
- package/dist/internal/mockWorkflowGraph.d.ts +23 -10
- package/dist/internal/mockWorkflowGraph.js +34 -11
- package/dist/testing.d.ts +1 -1
- package/dist/transport/iframeTransport.js +13 -0
- package/dist/transport/requestTimeouts.js +6 -0
- package/dist/transport/singleton.d.ts +6 -0
- package/dist/transport/singleton.js +20 -0
- package/dist/transport/validate.d.ts +13 -0
- package/dist/transport/validate.js +76 -0
- package/package.json +4 -4
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { BlockRunConfirmedStepHostError, BlockStepQuote, BlockWorkflowSnapshot, WorkflowBodyPassThroughStep } from '@civitai/app-sdk/blocks';
|
|
2
|
+
export type { BlockRunConfirmedStepHostError, BlockStepQuote, WorkflowBodyPassThroughStep };
|
|
3
|
+
/**
|
|
4
|
+
* A pass-through step body ready to RUN as a confirmed job:
|
|
5
|
+
* {@link WorkflowBodyPassThroughStep} with the `quoteId` its estimate returned
|
|
6
|
+
* (`snapshot.stepQuote.quoteId`). Required here so a body without one is a
|
|
7
|
+
* compile error rather than a refusal.
|
|
8
|
+
*/
|
|
9
|
+
export type QuotedStepBody = WorkflowBodyPassThroughStep & {
|
|
10
|
+
quoteId: string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* The host's CLOSED refusal codes for `RUN_CONFIRMED_STEP`, as a runtime list.
|
|
14
|
+
* Mirrors civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`.
|
|
15
|
+
*
|
|
16
|
+
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
|
|
17
|
+
* a body or price that differs from the quoted one) arrive in the same field —
|
|
18
|
+
* see {@link isRunConfirmedStepErrorCode}.
|
|
19
|
+
*/
|
|
20
|
+
export declare const RUN_CONFIRMED_STEP_ERROR_CODES: readonly ["review-mode", "block is not ready", "sign in to run", "invalid confirmed step request", "no block token", "declined", "daily-limit-exceeded", "submission-unconfirmed"];
|
|
21
|
+
/**
|
|
22
|
+
* The shape of a confirmed-step quote id: `sq_` + 32 lowercase hex characters.
|
|
23
|
+
*
|
|
24
|
+
* A COPY of `@civitai/app-sdk/blocks`'s `BLOCK_STEP_QUOTE_ID_REGEX`, deliberately
|
|
25
|
+
* not value-imported: a value import would raise this package's
|
|
26
|
+
* `@civitai/app-sdk` peer floor for one regular expression (see
|
|
27
|
+
* `PEER_FLOOR.md`). Pinned to the SDK's by `test/confirmedStepFlow.test.tsx`,
|
|
28
|
+
* which imports both.
|
|
29
|
+
*/
|
|
30
|
+
export declare const STEP_QUOTE_ID_PATTERN: RegExp;
|
|
31
|
+
/** `true` when `value` is a string shaped like a confirmed-step quote id. */
|
|
32
|
+
export declare function isStepQuoteId(value: unknown): value is string;
|
|
33
|
+
/**
|
|
34
|
+
* The message of the {@link RunConfirmedStepError} `runConfirmedStep` rejects
|
|
35
|
+
* with when the body carries no `sq_`-shaped `quoteId`.
|
|
36
|
+
*
|
|
37
|
+
* 🔴 REFUSED BEFORE THE SEND. The host refuses the same body with `invalid
|
|
38
|
+
* confirmed step request`, so nothing is lost by refusing here — and one thing is
|
|
39
|
+
* gained: a `tq_` training quote, an `undefined` from an estimate that returned
|
|
40
|
+
* no `stepQuote`, or a stale id from storage is named at the call site instead of
|
|
41
|
+
* surfacing as a host refusal. Nothing was sent, so nothing was charged.
|
|
42
|
+
*/
|
|
43
|
+
export declare const CONFIRMED_STEP_QUOTE_ID_REFUSAL = "runConfirmedStep() needs the quoteId its estimate returned (snapshot.stepQuote.quoteId, \"sq_\" + 32 hex characters) \u2014 estimate the body with useBuzzWorkflow().estimate() first. Nothing was sent.";
|
|
44
|
+
/**
|
|
45
|
+
* The `code` a {@link RunConfirmedStepError} can carry: one of the host's closed
|
|
46
|
+
* refusal codes, or `'refused'` — assigned by THIS SDK, never sent by the host,
|
|
47
|
+
* for a submit the server refused at a spend cap or as temporarily unavailable
|
|
48
|
+
* (see {@link RunConfirmedStepError.refused}).
|
|
49
|
+
*/
|
|
50
|
+
export type RunConfirmedStepErrorCode = BlockRunConfirmedStepHostError | 'refused';
|
|
51
|
+
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
52
|
+
export declare function isRunConfirmedStepErrorCode(error: string): error is BlockRunConfirmedStepHostError;
|
|
53
|
+
/**
|
|
54
|
+
* A confirmed step that did not come back with a snapshot.
|
|
55
|
+
*
|
|
56
|
+
* 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
|
|
57
|
+
* this error with a money meaning, and they point in opposite directions:
|
|
58
|
+
*
|
|
59
|
+
* - `.declined` — the viewer dismissed the confirm. NO JOB WAS SUBMITTED,
|
|
60
|
+
* guaranteed by the host's consent latch, and the quote is still runnable
|
|
61
|
+
* until it expires. Render nothing.
|
|
62
|
+
* - `.dailyLimitExceeded` — the job would exceed what remains today of the
|
|
63
|
+
* daily Buzz limit the VIEWER set for this app. NO JOB, nothing charged. The
|
|
64
|
+
* host has already explained it to the viewer and pointed at where the limit
|
|
65
|
+
* is raised; a confirm cannot override it. Do not retry, and do not offer a
|
|
66
|
+
* Buzz purchase — more Buzz does not lift a limit.
|
|
67
|
+
* - `.unconfirmed` — the job MAY BE RUNNING AND CHARGED: the host sent the
|
|
68
|
+
* submit and could not learn its outcome (`submission-unconfirmed`), or no
|
|
69
|
+
* reply arrived at all (`.timedOut` — the viewer may have confirmed a dialog
|
|
70
|
+
* this block had stopped waiting for). 🔴 NEVER RETRY AUTOMATICALLY and do not
|
|
71
|
+
* tell the viewer it failed. Every confirmation is its own job, so a retry —
|
|
72
|
+
* which needs a new estimate and a new confirmation — is a SECOND, separately
|
|
73
|
+
* charged job if the first one did start. Check the viewer's generations
|
|
74
|
+
* first (`useAppWorkflows()` lists this app's runs).
|
|
75
|
+
* - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
|
|
76
|
+
* cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
|
|
77
|
+
* the app's daily spend or rate limit). Its reservation was refunded: NO JOB,
|
|
78
|
+
* nothing charged. `.message` is the server's reason and `.snapshot` the
|
|
79
|
+
* refusal it arrived in. Retrying needs a new estimate and usually a later
|
|
80
|
+
* time.
|
|
81
|
+
* - `.signInRequired` — no session. Route into `useRequestSignIn()`.
|
|
82
|
+
* - anything else — a refusal before any submit (an expired or used quote, a
|
|
83
|
+
* body that no longer matches its quote, a price that changed, or the
|
|
84
|
+
* client-side {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}). Estimate again before
|
|
85
|
+
* retrying; `.message` is the host's or server's text.
|
|
86
|
+
*/
|
|
87
|
+
export declare class RunConfirmedStepError extends Error {
|
|
88
|
+
/**
|
|
89
|
+
* The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
|
|
90
|
+
* for a server/transport error.
|
|
91
|
+
*/
|
|
92
|
+
readonly code?: RunConfirmedStepErrorCode;
|
|
93
|
+
/**
|
|
94
|
+
* The server refused the submit (a spend cap, or a temporary-availability deny)
|
|
95
|
+
* and refunded it — no job exists. The quote is used up: estimate again.
|
|
96
|
+
* See the class docs; `.message` is the server's reason.
|
|
97
|
+
*/
|
|
98
|
+
readonly refused: boolean;
|
|
99
|
+
/** The refusal snapshot the host replied with, when {@link refused}. */
|
|
100
|
+
readonly snapshot?: BlockWorkflowSnapshot;
|
|
101
|
+
/** The viewer dismissed the confirm — no job was submitted. */
|
|
102
|
+
readonly declined: boolean;
|
|
103
|
+
/**
|
|
104
|
+
* The job would exceed the viewer's own daily Buzz limit for this app — no job
|
|
105
|
+
* was submitted and nothing was charged. Set for the host code
|
|
106
|
+
* `daily-limit-exceeded` and for a refusal snapshot whose `errorCode` is that
|
|
107
|
+
* value.
|
|
108
|
+
*/
|
|
109
|
+
readonly dailyLimitExceeded: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* The job may exist and may have been charged — `submission-unconfirmed`, or a
|
|
112
|
+
* transport timeout. Check the viewer's generations before retrying.
|
|
113
|
+
*/
|
|
114
|
+
readonly unconfirmed: boolean;
|
|
115
|
+
/** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
|
|
116
|
+
readonly timedOut: boolean;
|
|
117
|
+
/** There is no session (`sign in to run`, or a bare `UNAUTHORIZED`). */
|
|
118
|
+
readonly signInRequired: boolean;
|
|
119
|
+
constructor(error: string, opts?: {
|
|
120
|
+
timedOut?: boolean;
|
|
121
|
+
refusedSnapshot?: BlockWorkflowSnapshot;
|
|
122
|
+
code?: BlockRunConfirmedStepHostError;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
/** What {@link useRunConfirmedStep} returns. */
|
|
126
|
+
export interface UseRunConfirmedStep {
|
|
127
|
+
/**
|
|
128
|
+
* Ask the host to run a quoted pass-through step. Civitai shows the viewer a
|
|
129
|
+
* confirm with the server's price; on their click it submits, and this
|
|
130
|
+
* resolves with the submitted workflow's snapshot — poll it with
|
|
131
|
+
* `useBuzzWorkflow().watch(snapshot.workflowId)`.
|
|
132
|
+
*
|
|
133
|
+
* REJECTS with a {@link RunConfirmedStepError} — including `declined` and
|
|
134
|
+
* `dailyLimitExceeded` (no job), `refused` (a spend-cap refusal; no job) and
|
|
135
|
+
* `unconfirmed` (a job may exist). Read the flags before rendering anything. A
|
|
136
|
+
* resolved snapshot is always a submitted job with a pollable `workflowId`.
|
|
137
|
+
*
|
|
138
|
+
* A body whose `quoteId` is missing or not `sq_`-shaped is refused HERE,
|
|
139
|
+
* before anything is sent — see {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}.
|
|
140
|
+
*/
|
|
141
|
+
runConfirmedStep: (body: QuotedStepBody) => Promise<BlockWorkflowSnapshot>;
|
|
142
|
+
/** `true` while a request is in flight (including the viewer's dialog). */
|
|
143
|
+
pending: boolean;
|
|
144
|
+
/** The last request's failure, or `null`. Cleared at the start of the next call. */
|
|
145
|
+
error: RunConfirmedStepError | null;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Run ONE pass-through `kind: 'step'` job whose price is above what the app may
|
|
149
|
+
* spend without asking — through the host-mediated `RUN_CONFIRMED_STEP` →
|
|
150
|
+
* `CONFIRMED_STEP_RESULT` bridge, after the viewer confirms the exact price in
|
|
151
|
+
* Civitai's own dialog.
|
|
152
|
+
*
|
|
153
|
+
* 🔴 AVAILABILITY: page apps only, behind the host flag
|
|
154
|
+
* `app-blocks-confirmed-step` (ships off), with `ai:write:budgeted` declared and
|
|
155
|
+
* granted by a signed-in viewer, on a host carrying civitai/civitai#5786. Refused
|
|
156
|
+
* from `dev:live` and from review sessions — build the flow against the mock
|
|
157
|
+
* host. Where it is unavailable the estimate simply carries no `stepQuote`.
|
|
158
|
+
*
|
|
159
|
+
* The flow: `useBuzzWorkflow().estimate(body)` (its `stepQuote.quoteId`) →
|
|
160
|
+
* `runConfirmedStep({ ...body, quoteId })` → `useBuzzWorkflow().watch(workflowId)`.
|
|
161
|
+
* Change nothing in the body between the estimate and the run: the server checks
|
|
162
|
+
* it against the quoted one.
|
|
163
|
+
*
|
|
164
|
+
* 🔴 ONLY A FIXED PRICE IS CONFIRMABLE. `stepQuote` is present when the
|
|
165
|
+
* orchestrator quoted a fixed price above the token's per-call budget and at
|
|
166
|
+
* most `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB` (5,000 Buzz). A variable or
|
|
167
|
+
* metered price, or no price at all, gets no quote and cannot be run this way.
|
|
168
|
+
*
|
|
169
|
+
* 🔴 ESTIMATE THE BODY ON ITS OWN. A batch estimate (`useBatchEstimate()`) never
|
|
170
|
+
* carries a `stepQuote`. And where a quote would be granted, the estimate
|
|
171
|
+
* REJECTS instead for a training `$type` (`training` / `imageResourceTraining`,
|
|
172
|
+
* any casing — use `kind: 'training'`) and for an `input` that is not plain JSON
|
|
173
|
+
* (a `Date`, `Map`, `Set`, `BigInt`, `NaN`, an empty slot in a sparse array, …). Both are server messages with no code; see
|
|
174
|
+
* `BlockWorkflowSnapshot.stepQuote` for the exact text.
|
|
175
|
+
*
|
|
176
|
+
* 🔴 THE CONFIRM IS CIVITAI'S, AND IT IS THE CHARGE. The price on it is read back
|
|
177
|
+
* from the server's stored quote — never from your body. A quote is single-use
|
|
178
|
+
* and expires 15 minutes after the estimate; the server prices the job again at
|
|
179
|
+
* submit and refuses a higher price.
|
|
180
|
+
*
|
|
181
|
+
* 🔴 NO AUTOMATIC RETRY OF ANY KIND, on purpose. Every confirmation is its own
|
|
182
|
+
* job — there is no idempotency key, the orchestrator id comes from the confirmed
|
|
183
|
+
* quote — and one outcome (`submission-unconfirmed`) means a job may already be
|
|
184
|
+
* running. So this hook never re-sends, and neither should you: a retry is a
|
|
185
|
+
* NEW, separately charged job.
|
|
186
|
+
*
|
|
187
|
+
* Sent under the 10-minute human-interaction bound: the host replies only when
|
|
188
|
+
* the viewer clicks or dismisses its dialog.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* const { estimate, watch } = useBuzzWorkflow();
|
|
192
|
+
* const { runConfirmedStep } = useRunConfirmedStep();
|
|
193
|
+
* const quote = (await estimate(body)).stepQuote;
|
|
194
|
+
* if (!quote) return submit(body); // within budget — the ordinary path
|
|
195
|
+
* try {
|
|
196
|
+
* const started = await runConfirmedStep({ ...body, quoteId: quote.quoteId });
|
|
197
|
+
* await watch(started.workflowId, { onUpdate: render });
|
|
198
|
+
* } catch (e) {
|
|
199
|
+
* if (!(e instanceof RunConfirmedStepError)) throw e;
|
|
200
|
+
* if (e.declined) return; // no job — say nothing
|
|
201
|
+
* if (e.dailyLimitExceeded) return; // no job — Civitai already told the viewer
|
|
202
|
+
* if (e.signInRequired) return requestSignIn();
|
|
203
|
+
* if (e.unconfirmed) return showCheckYourGenerations(); // may be running — never auto-retry
|
|
204
|
+
* if (e.refused) return showError(e.message); // a cap refusal; no job, nothing charged
|
|
205
|
+
* showError('Could not start this job. Get a new price and try again.');
|
|
206
|
+
* }
|
|
207
|
+
*/
|
|
208
|
+
export declare function useRunConfirmedStep(): UseRunConfirmedStep;
|
|
209
|
+
//# sourceMappingURL=useRunConfirmedStep.d.ts.map
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { RequestTimeoutError, sendTypedRequest } from '../transport/transport.js';
|
|
5
|
+
import { useTransportConnection } from './useTransportConnection.js';
|
|
6
|
+
/**
|
|
7
|
+
* The host's CLOSED refusal codes for `RUN_CONFIRMED_STEP`, as a runtime list.
|
|
8
|
+
* Mirrors civitai/civitai's `RUN_CONFIRMED_STEP_HOST_ERRORS`.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 THE ERROR CHANNEL IS NOT AN ENUM: server messages (an expired or used quote,
|
|
11
|
+
* a body or price that differs from the quoted one) arrive in the same field —
|
|
12
|
+
* see {@link isRunConfirmedStepErrorCode}.
|
|
13
|
+
*/
|
|
14
|
+
export const RUN_CONFIRMED_STEP_ERROR_CODES = [
|
|
15
|
+
'review-mode',
|
|
16
|
+
'block is not ready',
|
|
17
|
+
'sign in to run',
|
|
18
|
+
'invalid confirmed step request',
|
|
19
|
+
'no block token',
|
|
20
|
+
'declined',
|
|
21
|
+
'daily-limit-exceeded',
|
|
22
|
+
'submission-unconfirmed',
|
|
23
|
+
];
|
|
24
|
+
const CODE_SET = new Set(RUN_CONFIRMED_STEP_ERROR_CODES);
|
|
25
|
+
/**
|
|
26
|
+
* The shape of a confirmed-step quote id: `sq_` + 32 lowercase hex characters.
|
|
27
|
+
*
|
|
28
|
+
* A COPY of `@civitai/app-sdk/blocks`'s `BLOCK_STEP_QUOTE_ID_REGEX`, deliberately
|
|
29
|
+
* not value-imported: a value import would raise this package's
|
|
30
|
+
* `@civitai/app-sdk` peer floor for one regular expression (see
|
|
31
|
+
* `PEER_FLOOR.md`). Pinned to the SDK's by `test/confirmedStepFlow.test.tsx`,
|
|
32
|
+
* which imports both.
|
|
33
|
+
*/
|
|
34
|
+
export const STEP_QUOTE_ID_PATTERN = /^sq_[a-f0-9]{32}$/;
|
|
35
|
+
/** `true` when `value` is a string shaped like a confirmed-step quote id. */
|
|
36
|
+
export function isStepQuoteId(value) {
|
|
37
|
+
return typeof value === 'string' && STEP_QUOTE_ID_PATTERN.test(value);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The message of the {@link RunConfirmedStepError} `runConfirmedStep` rejects
|
|
41
|
+
* with when the body carries no `sq_`-shaped `quoteId`.
|
|
42
|
+
*
|
|
43
|
+
* 🔴 REFUSED BEFORE THE SEND. The host refuses the same body with `invalid
|
|
44
|
+
* confirmed step request`, so nothing is lost by refusing here — and one thing is
|
|
45
|
+
* gained: a `tq_` training quote, an `undefined` from an estimate that returned
|
|
46
|
+
* no `stepQuote`, or a stale id from storage is named at the call site instead of
|
|
47
|
+
* surfacing as a host refusal. Nothing was sent, so nothing was charged.
|
|
48
|
+
*/
|
|
49
|
+
export const CONFIRMED_STEP_QUOTE_ID_REFUSAL = 'runConfirmedStep() needs the quoteId its estimate returned (snapshot.stepQuote.quoteId, "sq_" + 32 hex characters) — estimate the body with useBuzzWorkflow().estimate() first. Nothing was sent.';
|
|
50
|
+
/**
|
|
51
|
+
* The `workflowId` the host's confirmed submit stamps on a snapshot that is NOT
|
|
52
|
+
* a run — its cap refusals resolve `{ workflowId: 'failed', status: 'failed',
|
|
53
|
+
* cost, error }`, the same shape the training submit uses. Compared with `===`:
|
|
54
|
+
* a real orchestrator id is a workflow that may exist.
|
|
55
|
+
*/
|
|
56
|
+
const HOST_REFUSAL_WORKFLOW_ID = 'failed';
|
|
57
|
+
/** The viewer's own daily-limit refusal, as a host code and as a snapshot `errorCode`. */
|
|
58
|
+
const DAILY_LIMIT_CODE = 'daily-limit-exceeded';
|
|
59
|
+
/** `true` when `error` is one of the host's closed refusal codes rather than a server message. */
|
|
60
|
+
export function isRunConfirmedStepErrorCode(error) {
|
|
61
|
+
return CODE_SET.has(error);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* A confirmed step that did not come back with a snapshot.
|
|
65
|
+
*
|
|
66
|
+
* 🔴 READ THE FLAGS BEFORE SAYING ANYTHING ABOUT MONEY — they are the only part of
|
|
67
|
+
* this error with a money meaning, and they point in opposite directions:
|
|
68
|
+
*
|
|
69
|
+
* - `.declined` — the viewer dismissed the confirm. NO JOB WAS SUBMITTED,
|
|
70
|
+
* guaranteed by the host's consent latch, and the quote is still runnable
|
|
71
|
+
* until it expires. Render nothing.
|
|
72
|
+
* - `.dailyLimitExceeded` — the job would exceed what remains today of the
|
|
73
|
+
* daily Buzz limit the VIEWER set for this app. NO JOB, nothing charged. The
|
|
74
|
+
* host has already explained it to the viewer and pointed at where the limit
|
|
75
|
+
* is raised; a confirm cannot override it. Do not retry, and do not offer a
|
|
76
|
+
* Buzz purchase — more Buzz does not lift a limit.
|
|
77
|
+
* - `.unconfirmed` — the job MAY BE RUNNING AND CHARGED: the host sent the
|
|
78
|
+
* submit and could not learn its outcome (`submission-unconfirmed`), or no
|
|
79
|
+
* reply arrived at all (`.timedOut` — the viewer may have confirmed a dialog
|
|
80
|
+
* this block had stopped waiting for). 🔴 NEVER RETRY AUTOMATICALLY and do not
|
|
81
|
+
* tell the viewer it failed. Every confirmation is its own job, so a retry —
|
|
82
|
+
* which needs a new estimate and a new confirmation — is a SECOND, separately
|
|
83
|
+
* charged job if the first one did start. Check the viewer's generations
|
|
84
|
+
* first (`useAppWorkflows()` lists this app's runs).
|
|
85
|
+
* - `.refused` (`code: 'refused'`) — the server REFUSED the submit at a spend
|
|
86
|
+
* cap after the viewer confirmed (the viewer's daily or private-run Buzz cap,
|
|
87
|
+
* the app's daily spend or rate limit). Its reservation was refunded: NO JOB,
|
|
88
|
+
* nothing charged. `.message` is the server's reason and `.snapshot` the
|
|
89
|
+
* refusal it arrived in. Retrying needs a new estimate and usually a later
|
|
90
|
+
* time.
|
|
91
|
+
* - `.signInRequired` — no session. Route into `useRequestSignIn()`.
|
|
92
|
+
* - anything else — a refusal before any submit (an expired or used quote, a
|
|
93
|
+
* body that no longer matches its quote, a price that changed, or the
|
|
94
|
+
* client-side {@link CONFIRMED_STEP_QUOTE_ID_REFUSAL}). Estimate again before
|
|
95
|
+
* retrying; `.message` is the host's or server's text.
|
|
96
|
+
*/
|
|
97
|
+
export class RunConfirmedStepError extends Error {
|
|
98
|
+
/**
|
|
99
|
+
* The closed host refusal code, `'refused'` for a cap refusal, or `undefined`
|
|
100
|
+
* for a server/transport error.
|
|
101
|
+
*/
|
|
102
|
+
code;
|
|
103
|
+
/**
|
|
104
|
+
* The server refused the submit (a spend cap, or a temporary-availability deny)
|
|
105
|
+
* and refunded it — no job exists. The quote is used up: estimate again.
|
|
106
|
+
* See the class docs; `.message` is the server's reason.
|
|
107
|
+
*/
|
|
108
|
+
refused;
|
|
109
|
+
/** The refusal snapshot the host replied with, when {@link refused}. */
|
|
110
|
+
snapshot;
|
|
111
|
+
/** The viewer dismissed the confirm — no job was submitted. */
|
|
112
|
+
declined;
|
|
113
|
+
/**
|
|
114
|
+
* The job would exceed the viewer's own daily Buzz limit for this app — no job
|
|
115
|
+
* was submitted and nothing was charged. Set for the host code
|
|
116
|
+
* `daily-limit-exceeded` and for a refusal snapshot whose `errorCode` is that
|
|
117
|
+
* value.
|
|
118
|
+
*/
|
|
119
|
+
dailyLimitExceeded;
|
|
120
|
+
/**
|
|
121
|
+
* The job may exist and may have been charged — `submission-unconfirmed`, or a
|
|
122
|
+
* transport timeout. Check the viewer's generations before retrying.
|
|
123
|
+
*/
|
|
124
|
+
unconfirmed;
|
|
125
|
+
/** No reply arrived within the 10-minute consent bound. Implies {@link unconfirmed}. */
|
|
126
|
+
timedOut;
|
|
127
|
+
/** There is no session (`sign in to run`, or a bare `UNAUTHORIZED`). */
|
|
128
|
+
signInRequired;
|
|
129
|
+
constructor(error, opts) {
|
|
130
|
+
super(error);
|
|
131
|
+
this.name = 'RunConfirmedStepError';
|
|
132
|
+
this.timedOut = opts?.timedOut === true;
|
|
133
|
+
this.refused = opts?.refusedSnapshot !== undefined;
|
|
134
|
+
if (opts?.refusedSnapshot !== undefined) {
|
|
135
|
+
this.code = 'refused';
|
|
136
|
+
this.snapshot = opts.refusedSnapshot;
|
|
137
|
+
}
|
|
138
|
+
else if (opts?.code !== undefined) {
|
|
139
|
+
this.code = opts.code;
|
|
140
|
+
}
|
|
141
|
+
else if (isRunConfirmedStepErrorCode(error)) {
|
|
142
|
+
this.code = error;
|
|
143
|
+
}
|
|
144
|
+
this.declined = error === 'declined';
|
|
145
|
+
this.dailyLimitExceeded =
|
|
146
|
+
error === DAILY_LIMIT_CODE || opts?.refusedSnapshot?.errorCode === DAILY_LIMIT_CODE;
|
|
147
|
+
this.unconfirmed = this.timedOut || error === 'submission-unconfirmed';
|
|
148
|
+
this.signInRequired = error === 'sign in to run' || error === 'UNAUTHORIZED';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Run ONE pass-through `kind: 'step'` job whose price is above what the app may
|
|
153
|
+
* spend without asking — through the host-mediated `RUN_CONFIRMED_STEP` →
|
|
154
|
+
* `CONFIRMED_STEP_RESULT` bridge, after the viewer confirms the exact price in
|
|
155
|
+
* Civitai's own dialog.
|
|
156
|
+
*
|
|
157
|
+
* 🔴 AVAILABILITY: page apps only, behind the host flag
|
|
158
|
+
* `app-blocks-confirmed-step` (ships off), with `ai:write:budgeted` declared and
|
|
159
|
+
* granted by a signed-in viewer, on a host carrying civitai/civitai#5786. Refused
|
|
160
|
+
* from `dev:live` and from review sessions — build the flow against the mock
|
|
161
|
+
* host. Where it is unavailable the estimate simply carries no `stepQuote`.
|
|
162
|
+
*
|
|
163
|
+
* The flow: `useBuzzWorkflow().estimate(body)` (its `stepQuote.quoteId`) →
|
|
164
|
+
* `runConfirmedStep({ ...body, quoteId })` → `useBuzzWorkflow().watch(workflowId)`.
|
|
165
|
+
* Change nothing in the body between the estimate and the run: the server checks
|
|
166
|
+
* it against the quoted one.
|
|
167
|
+
*
|
|
168
|
+
* 🔴 ONLY A FIXED PRICE IS CONFIRMABLE. `stepQuote` is present when the
|
|
169
|
+
* orchestrator quoted a fixed price above the token's per-call budget and at
|
|
170
|
+
* most `BLOCK_CONFIRMED_STEP_MAX_BUZZ_PER_JOB` (5,000 Buzz). A variable or
|
|
171
|
+
* metered price, or no price at all, gets no quote and cannot be run this way.
|
|
172
|
+
*
|
|
173
|
+
* 🔴 ESTIMATE THE BODY ON ITS OWN. A batch estimate (`useBatchEstimate()`) never
|
|
174
|
+
* carries a `stepQuote`. And where a quote would be granted, the estimate
|
|
175
|
+
* REJECTS instead for a training `$type` (`training` / `imageResourceTraining`,
|
|
176
|
+
* any casing — use `kind: 'training'`) and for an `input` that is not plain JSON
|
|
177
|
+
* (a `Date`, `Map`, `Set`, `BigInt`, `NaN`, an empty slot in a sparse array, …). Both are server messages with no code; see
|
|
178
|
+
* `BlockWorkflowSnapshot.stepQuote` for the exact text.
|
|
179
|
+
*
|
|
180
|
+
* 🔴 THE CONFIRM IS CIVITAI'S, AND IT IS THE CHARGE. The price on it is read back
|
|
181
|
+
* from the server's stored quote — never from your body. A quote is single-use
|
|
182
|
+
* and expires 15 minutes after the estimate; the server prices the job again at
|
|
183
|
+
* submit and refuses a higher price.
|
|
184
|
+
*
|
|
185
|
+
* 🔴 NO AUTOMATIC RETRY OF ANY KIND, on purpose. Every confirmation is its own
|
|
186
|
+
* job — there is no idempotency key, the orchestrator id comes from the confirmed
|
|
187
|
+
* quote — and one outcome (`submission-unconfirmed`) means a job may already be
|
|
188
|
+
* running. So this hook never re-sends, and neither should you: a retry is a
|
|
189
|
+
* NEW, separately charged job.
|
|
190
|
+
*
|
|
191
|
+
* Sent under the 10-minute human-interaction bound: the host replies only when
|
|
192
|
+
* the viewer clicks or dismisses its dialog.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* const { estimate, watch } = useBuzzWorkflow();
|
|
196
|
+
* const { runConfirmedStep } = useRunConfirmedStep();
|
|
197
|
+
* const quote = (await estimate(body)).stepQuote;
|
|
198
|
+
* if (!quote) return submit(body); // within budget — the ordinary path
|
|
199
|
+
* try {
|
|
200
|
+
* const started = await runConfirmedStep({ ...body, quoteId: quote.quoteId });
|
|
201
|
+
* await watch(started.workflowId, { onUpdate: render });
|
|
202
|
+
* } catch (e) {
|
|
203
|
+
* if (!(e instanceof RunConfirmedStepError)) throw e;
|
|
204
|
+
* if (e.declined) return; // no job — say nothing
|
|
205
|
+
* if (e.dailyLimitExceeded) return; // no job — Civitai already told the viewer
|
|
206
|
+
* if (e.signInRequired) return requestSignIn();
|
|
207
|
+
* if (e.unconfirmed) return showCheckYourGenerations(); // may be running — never auto-retry
|
|
208
|
+
* if (e.refused) return showError(e.message); // a cap refusal; no job, nothing charged
|
|
209
|
+
* showError('Could not start this job. Get a new price and try again.');
|
|
210
|
+
* }
|
|
211
|
+
*/
|
|
212
|
+
export function useRunConfirmedStep() {
|
|
213
|
+
useTransportConnection();
|
|
214
|
+
const [pending, setPending] = useState(false);
|
|
215
|
+
const [error, setError] = useState(null);
|
|
216
|
+
// This request can outlive the component by up to ten minutes (the dialog may
|
|
217
|
+
// stay open), so a `pending` toggled on an unmounted control is state nobody
|
|
218
|
+
// can clear.
|
|
219
|
+
const mountedRef = useRef(true);
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
mountedRef.current = true;
|
|
222
|
+
return () => {
|
|
223
|
+
mountedRef.current = false;
|
|
224
|
+
};
|
|
225
|
+
}, []);
|
|
226
|
+
const runConfirmedStep = useCallback(async (body) => {
|
|
227
|
+
// See `CONFIRMED_STEP_QUOTE_ID_REFUSAL`. Before the `pending` flip and the
|
|
228
|
+
// send: nothing about this call may reach the wire. `body` is read through
|
|
229
|
+
// `unknown` because a JavaScript caller, or one that cast its way past the
|
|
230
|
+
// type, is exactly who this is for.
|
|
231
|
+
if (!isStepQuoteId(body?.quoteId)) {
|
|
232
|
+
const refused = new RunConfirmedStepError(CONFIRMED_STEP_QUOTE_ID_REFUSAL, {
|
|
233
|
+
code: 'invalid confirmed step request',
|
|
234
|
+
});
|
|
235
|
+
if (mountedRef.current)
|
|
236
|
+
setError(refused);
|
|
237
|
+
throw refused;
|
|
238
|
+
}
|
|
239
|
+
if (mountedRef.current) {
|
|
240
|
+
setPending(true);
|
|
241
|
+
setError(null);
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
const reply = await sendTypedRequest(getTransport(), { type: 'RUN_CONFIRMED_STEP', payload: { body } }, 'CONFIRMED_STEP_RESULT', { timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS }).catch((err) => {
|
|
245
|
+
if (err instanceof RequestTimeoutError) {
|
|
246
|
+
throw new RunConfirmedStepError(err.message, { timedOut: true });
|
|
247
|
+
}
|
|
248
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
249
|
+
});
|
|
250
|
+
if (reply.error || !reply.snapshot) {
|
|
251
|
+
// `||`, not `??`: `error: ''` is a shape-valid reply. Its fallback is the
|
|
252
|
+
// CAUTIOUS code — with neither a snapshot nor a reason, nothing says a
|
|
253
|
+
// job was not started, so the block must not be told it was safe to
|
|
254
|
+
// retry.
|
|
255
|
+
throw new RunConfirmedStepError(reply.error || 'submission-unconfirmed');
|
|
256
|
+
}
|
|
257
|
+
// 🔴 A RESOLVED SNAPSHOT IS NOT ALWAYS A RUN. The host's confirmed submit
|
|
258
|
+
// answers its spend-cap refusals (refunded, nothing started) with a
|
|
259
|
+
// resolved `{ workflowId: 'failed', status: 'failed', cost, error }`.
|
|
260
|
+
// Resolving that would hand the caller a workflow id to `watch()` that
|
|
261
|
+
// names nothing, and drop the reason. Exact match on the host's
|
|
262
|
+
// sentinel: a failed snapshot with a REAL id is a workflow that may
|
|
263
|
+
// exist, and keeps resolving so it can be watched.
|
|
264
|
+
if (reply.snapshot.workflowId === HOST_REFUSAL_WORKFLOW_ID &&
|
|
265
|
+
reply.snapshot.status === 'failed') {
|
|
266
|
+
throw new RunConfirmedStepError(reply.snapshot.error || 'confirmed step refused', {
|
|
267
|
+
refusedSnapshot: reply.snapshot,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return reply.snapshot;
|
|
271
|
+
}
|
|
272
|
+
catch (err) {
|
|
273
|
+
const wrapped = err instanceof RunConfirmedStepError
|
|
274
|
+
? err
|
|
275
|
+
: new RunConfirmedStepError(err instanceof Error ? err.message : String(err));
|
|
276
|
+
if (mountedRef.current)
|
|
277
|
+
setError(wrapped);
|
|
278
|
+
// Thrown even when unmounted: the caller's `await` must still learn the
|
|
279
|
+
// outcome of a possibly-charged job.
|
|
280
|
+
throw wrapped;
|
|
281
|
+
}
|
|
282
|
+
finally {
|
|
283
|
+
if (mountedRef.current)
|
|
284
|
+
setPending(false);
|
|
285
|
+
}
|
|
286
|
+
}, []);
|
|
287
|
+
return { runConfirmedStep, pending, error };
|
|
288
|
+
}
|
|
289
|
+
//# sourceMappingURL=useRunConfirmedStep.js.map
|
|
@@ -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
|