@civitai/blocks-react 0.56.0 → 0.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -2
- package/dist/api/generationResources.d.ts +4 -3
- package/dist/hooks/returnTypeLedger.d.ts +2 -0
- package/dist/hooks/returnTypeLedger.js +2 -0
- package/dist/hooks/useAppStorage.js +2 -2
- package/dist/hooks/useAppWorkflows.d.ts +5 -2
- package/dist/hooks/useAppWorkflows.js +18 -7
- package/dist/hooks/useBlockAnalytics.d.ts +5 -3
- package/dist/hooks/useBlockAnalytics.js +1 -1
- package/dist/hooks/useBlockBreakpoint.d.ts +7 -1
- package/dist/hooks/useBlockContext.d.ts +13 -2
- package/dist/hooks/useBlockContext.js +1 -1
- package/dist/hooks/useBlockResize.d.ts +7 -1
- package/dist/hooks/useBlockResize.js +1 -1
- package/dist/hooks/useBlockSettings.d.ts +6 -1
- package/dist/hooks/useBlockTheme.d.ts +6 -1
- package/dist/hooks/useBlockToken.d.ts +8 -3
- package/dist/hooks/useBlockToken.js +2 -2
- package/dist/hooks/useBuzzAccounts.d.ts +3 -2
- package/dist/hooks/useBuzzAccounts.js +16 -15
- package/dist/hooks/useBuzzBalance.d.ts +3 -2
- package/dist/hooks/useBuzzBalance.js +16 -18
- package/dist/hooks/useBuzzPurchase.d.ts +8 -6
- package/dist/hooks/useBuzzPurchase.js +3 -3
- package/dist/hooks/useBuzzTransactions.d.ts +4 -2
- package/dist/hooks/useBuzzTransactions.js +16 -15
- package/dist/hooks/useBuzzWorkflow.d.ts +71 -28
- package/dist/hooks/useBuzzWorkflow.js +31 -17
- package/dist/hooks/useCheckpointPicker.d.ts +18 -16
- package/dist/hooks/useCheckpointPicker.js +3 -3
- package/dist/hooks/useCivitaiNavigate.d.ts +5 -3
- package/dist/hooks/useCivitaiNavigate.js +1 -1
- package/dist/hooks/useCollectionFollow.js +4 -4
- package/dist/hooks/useConsentUnavailable.js +2 -2
- package/dist/hooks/useCreatePostFromApp.js +4 -4
- package/dist/hooks/useDailyCompensation.d.ts +4 -2
- package/dist/hooks/useDailyCompensation.js +17 -15
- package/dist/hooks/useDirectLoad.d.ts +7 -1
- package/dist/hooks/useDirectLoad.js +1 -1
- package/dist/hooks/useDomainMaturity.d.ts +6 -2
- package/dist/hooks/useGatedImages.d.ts +8 -0
- package/dist/hooks/useGatedImages.js +10 -2
- package/dist/hooks/useGenerationResources.d.ts +5 -3
- package/dist/hooks/useHostOrigin.d.ts +7 -1
- package/dist/hooks/useHostOrigin.js +1 -1
- package/dist/hooks/useImageUpload.d.ts +44 -17
- package/dist/hooks/useImageUpload.js +24 -4
- package/dist/hooks/usePublishGenerationOutputs.js +4 -4
- package/dist/hooks/useRequestConsent.d.ts +7 -5
- package/dist/hooks/useRequestConsent.js +1 -1
- package/dist/hooks/useRequestSequencer.d.ts +71 -0
- package/dist/hooks/useRequestSequencer.js +28 -0
- package/dist/hooks/useRequestSignIn.d.ts +7 -5
- package/dist/hooks/useRequestSignIn.js +1 -1
- package/dist/hooks/useResourcePicker.d.ts +16 -14
- package/dist/hooks/useResourcePicker.js +3 -3
- package/dist/hooks/useSaveImage.js +2 -2
- package/dist/hooks/useSharedStorage.js +2 -2
- package/dist/hooks/useTip.js +1 -1
- package/dist/hooks/useTipAllowance.d.ts +13 -1
- package/dist/hooks/useTipAllowance.js +49 -11
- package/dist/hooks/useViewer.d.ts +3 -2
- package/dist/hooks/useViewer.js +16 -18
- package/dist/hooks/useWildcardPack.d.ts +3 -1
- package/dist/hooks/useWildcardPack.js +19 -14
- package/dist/index.d.ts +30 -14
- package/dist/index.js +7 -7
- package/dist/internal/consentRefusalLatch.d.ts +1 -1
- package/dist/internal/consentRefusalLatch.js +1 -1
- package/dist/internal/liveHost.d.ts +63 -11
- package/dist/internal/liveHost.js +240 -37
- package/dist/internal/mockHost.js +16 -15
- package/dist/internal/pickerOverlay.d.ts +52 -4
- package/dist/internal/pickerOverlay.js +14 -3
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +1 -1
- package/dist/{internal → transport}/iframeTransport.d.ts +43 -5
- package/dist/{internal → transport}/iframeTransport.js +142 -12
- package/dist/transport/originMatcher.d.ts +92 -0
- package/dist/transport/originMatcher.js +222 -0
- package/dist/transport/requestId.d.ts +113 -0
- package/dist/transport/requestId.js +117 -0
- package/dist/{internal → transport}/validate.d.ts +34 -0
- package/dist/{internal → transport}/validate.js +189 -48
- package/dist/ui/BlockGate.js +1 -1
- package/dist/ui/SettingsForm.d.ts +8 -2
- package/dist/ui/SettingsForm.js +49 -3
- package/package.json +7 -148
- package/dist/api/generationResources.d.ts.map +0 -1
- package/dist/api/generationResources.js.map +0 -1
- package/dist/hooks/SfwGate.d.ts.map +0 -1
- package/dist/hooks/SfwGate.js.map +0 -1
- package/dist/hooks/useAppStorage.d.ts.map +0 -1
- package/dist/hooks/useAppStorage.js.map +0 -1
- package/dist/hooks/useAppWorkflows.d.ts.map +0 -1
- package/dist/hooks/useAppWorkflows.js.map +0 -1
- package/dist/hooks/useBlockAnalytics.d.ts.map +0 -1
- package/dist/hooks/useBlockAnalytics.js.map +0 -1
- package/dist/hooks/useBlockBreakpoint.d.ts.map +0 -1
- package/dist/hooks/useBlockBreakpoint.js.map +0 -1
- package/dist/hooks/useBlockContext.d.ts.map +0 -1
- package/dist/hooks/useBlockContext.js.map +0 -1
- package/dist/hooks/useBlockResize.d.ts.map +0 -1
- package/dist/hooks/useBlockResize.js.map +0 -1
- package/dist/hooks/useBlockSettings.d.ts.map +0 -1
- package/dist/hooks/useBlockSettings.js.map +0 -1
- package/dist/hooks/useBlockTheme.d.ts.map +0 -1
- package/dist/hooks/useBlockTheme.js.map +0 -1
- package/dist/hooks/useBlockToken.d.ts.map +0 -1
- package/dist/hooks/useBlockToken.js.map +0 -1
- package/dist/hooks/useBuzzAccounts.d.ts.map +0 -1
- package/dist/hooks/useBuzzAccounts.js.map +0 -1
- package/dist/hooks/useBuzzBalance.d.ts.map +0 -1
- package/dist/hooks/useBuzzBalance.js.map +0 -1
- package/dist/hooks/useBuzzPurchase.d.ts.map +0 -1
- package/dist/hooks/useBuzzPurchase.js.map +0 -1
- package/dist/hooks/useBuzzTransactions.d.ts.map +0 -1
- package/dist/hooks/useBuzzTransactions.js.map +0 -1
- package/dist/hooks/useBuzzWorkflow.d.ts.map +0 -1
- package/dist/hooks/useBuzzWorkflow.js.map +0 -1
- package/dist/hooks/useCheckpointPicker.d.ts.map +0 -1
- package/dist/hooks/useCheckpointPicker.js.map +0 -1
- package/dist/hooks/useCivitaiNavigate.d.ts.map +0 -1
- package/dist/hooks/useCivitaiNavigate.js.map +0 -1
- package/dist/hooks/useCollectionFollow.d.ts.map +0 -1
- package/dist/hooks/useCollectionFollow.js.map +0 -1
- package/dist/hooks/useConsentUnavailable.d.ts.map +0 -1
- package/dist/hooks/useConsentUnavailable.js.map +0 -1
- package/dist/hooks/useCreatePostFromApp.d.ts.map +0 -1
- package/dist/hooks/useCreatePostFromApp.js.map +0 -1
- package/dist/hooks/useDailyCompensation.d.ts.map +0 -1
- package/dist/hooks/useDailyCompensation.js.map +0 -1
- package/dist/hooks/useDirectLoad.d.ts.map +0 -1
- package/dist/hooks/useDirectLoad.js.map +0 -1
- package/dist/hooks/useDomainMaturity.d.ts.map +0 -1
- package/dist/hooks/useDomainMaturity.js.map +0 -1
- package/dist/hooks/useGatedImages.d.ts.map +0 -1
- package/dist/hooks/useGatedImages.js.map +0 -1
- package/dist/hooks/useGenerationResources.d.ts.map +0 -1
- package/dist/hooks/useGenerationResources.js.map +0 -1
- package/dist/hooks/useHostOrigin.d.ts.map +0 -1
- package/dist/hooks/useHostOrigin.js.map +0 -1
- package/dist/hooks/useImageUpload.d.ts.map +0 -1
- package/dist/hooks/useImageUpload.js.map +0 -1
- package/dist/hooks/usePublishGenerationOutputs.d.ts.map +0 -1
- package/dist/hooks/usePublishGenerationOutputs.js.map +0 -1
- package/dist/hooks/useRequestConsent.d.ts.map +0 -1
- package/dist/hooks/useRequestConsent.js.map +0 -1
- package/dist/hooks/useRequestSignIn.d.ts.map +0 -1
- package/dist/hooks/useRequestSignIn.js.map +0 -1
- package/dist/hooks/useResourcePicker.d.ts.map +0 -1
- package/dist/hooks/useResourcePicker.js.map +0 -1
- package/dist/hooks/useSaveImage.d.ts.map +0 -1
- package/dist/hooks/useSaveImage.js.map +0 -1
- package/dist/hooks/useSharedStorage.d.ts.map +0 -1
- package/dist/hooks/useSharedStorage.js.map +0 -1
- package/dist/hooks/useTip.d.ts.map +0 -1
- package/dist/hooks/useTip.js.map +0 -1
- package/dist/hooks/useTipAllowance.d.ts.map +0 -1
- package/dist/hooks/useTipAllowance.js.map +0 -1
- package/dist/hooks/useViewer.d.ts.map +0 -1
- package/dist/hooks/useViewer.js.map +0 -1
- package/dist/hooks/useWildcardPack.d.ts.map +0 -1
- package/dist/hooks/useWildcardPack.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal/catalog.d.ts.map +0 -1
- package/dist/internal/catalog.js.map +0 -1
- package/dist/internal/consent.d.ts.map +0 -1
- package/dist/internal/consent.js.map +0 -1
- package/dist/internal/consentRefusalLatch.d.ts.map +0 -1
- package/dist/internal/consentRefusalLatch.js.map +0 -1
- package/dist/internal/detector.d.ts.map +0 -1
- package/dist/internal/detector.js.map +0 -1
- package/dist/internal/directLoad.d.ts.map +0 -1
- package/dist/internal/directLoad.js.map +0 -1
- package/dist/internal/iframeTransport.d.ts.map +0 -1
- package/dist/internal/iframeTransport.js.map +0 -1
- package/dist/internal/inlineTransport.d.ts.map +0 -1
- package/dist/internal/inlineTransport.js.map +0 -1
- package/dist/internal/liveHost.d.ts.map +0 -1
- package/dist/internal/liveHost.js.map +0 -1
- package/dist/internal/mockHost.d.ts.map +0 -1
- package/dist/internal/mockHost.js.map +0 -1
- package/dist/internal/originMatcher.d.ts +0 -28
- package/dist/internal/originMatcher.d.ts.map +0 -1
- package/dist/internal/originMatcher.js +0 -89
- package/dist/internal/originMatcher.js.map +0 -1
- package/dist/internal/pickerOverlay.d.ts.map +0 -1
- package/dist/internal/pickerOverlay.js.map +0 -1
- package/dist/internal/replyError.d.ts.map +0 -1
- package/dist/internal/replyError.js.map +0 -1
- package/dist/internal/requestTimeouts.d.ts.map +0 -1
- package/dist/internal/requestTimeouts.js.map +0 -1
- package/dist/internal/singleton.d.ts.map +0 -1
- package/dist/internal/singleton.js.map +0 -1
- package/dist/internal/transport.d.ts.map +0 -1
- package/dist/internal/transport.js.map +0 -1
- package/dist/internal/validate.d.ts.map +0 -1
- package/dist/internal/validate.js.map +0 -1
- package/dist/live.d.ts.map +0 -1
- package/dist/live.js.map +0 -1
- package/dist/testing.d.ts.map +0 -1
- package/dist/testing.js.map +0 -1
- package/dist/ui/Alert.d.ts.map +0 -1
- package/dist/ui/Alert.js.map +0 -1
- package/dist/ui/Badge.d.ts.map +0 -1
- package/dist/ui/Badge.js.map +0 -1
- package/dist/ui/BlockGate.d.ts.map +0 -1
- package/dist/ui/BlockGate.js.map +0 -1
- package/dist/ui/Button.d.ts.map +0 -1
- package/dist/ui/Button.js.map +0 -1
- package/dist/ui/Card.d.ts.map +0 -1
- package/dist/ui/Card.js.map +0 -1
- package/dist/ui/Collapse.d.ts.map +0 -1
- package/dist/ui/Collapse.js.map +0 -1
- package/dist/ui/FollowButton.d.ts.map +0 -1
- package/dist/ui/FollowButton.js.map +0 -1
- package/dist/ui/Group.d.ts.map +0 -1
- package/dist/ui/Group.js.map +0 -1
- package/dist/ui/Loader.d.ts.map +0 -1
- package/dist/ui/Loader.js.map +0 -1
- package/dist/ui/Modal.d.ts.map +0 -1
- package/dist/ui/Modal.js.map +0 -1
- package/dist/ui/NumberInput.d.ts.map +0 -1
- package/dist/ui/NumberInput.js.map +0 -1
- package/dist/ui/ReportButton.d.ts.map +0 -1
- package/dist/ui/ReportButton.js.map +0 -1
- package/dist/ui/ResourceCard.d.ts.map +0 -1
- package/dist/ui/ResourceCard.js.map +0 -1
- package/dist/ui/SegmentedControl.d.ts.map +0 -1
- package/dist/ui/SegmentedControl.js.map +0 -1
- package/dist/ui/Select.d.ts.map +0 -1
- package/dist/ui/Select.js.map +0 -1
- package/dist/ui/SettingsForm.d.ts.map +0 -1
- package/dist/ui/SettingsForm.js.map +0 -1
- package/dist/ui/Slider.d.ts.map +0 -1
- package/dist/ui/Slider.js.map +0 -1
- package/dist/ui/Stack.d.ts.map +0 -1
- package/dist/ui/Stack.js.map +0 -1
- package/dist/ui/TextInput.d.ts.map +0 -1
- package/dist/ui/TextInput.js.map +0 -1
- package/dist/ui/Textarea.d.ts.map +0 -1
- package/dist/ui/Textarea.js.map +0 -1
- package/dist/ui/TipButton.d.ts.map +0 -1
- package/dist/ui/TipButton.js.map +0 -1
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/styles.d.ts.map +0 -1
- package/dist/ui/styles.js.map +0 -1
- /package/dist/{internal → transport}/detector.d.ts +0 -0
- /package/dist/{internal → transport}/detector.js +0 -0
- /package/dist/{internal → transport}/directLoad.d.ts +0 -0
- /package/dist/{internal → transport}/directLoad.js +0 -0
- /package/dist/{internal → transport}/inlineTransport.d.ts +0 -0
- /package/dist/{internal → transport}/inlineTransport.js +0 -0
- /package/dist/{internal → transport}/requestTimeouts.d.ts +0 -0
- /package/dist/{internal → transport}/requestTimeouts.js +0 -0
- /package/dist/{internal → transport}/singleton.d.ts +0 -0
- /package/dist/{internal → transport}/singleton.js +0 -0
- /package/dist/{internal → transport}/transport.d.ts +0 -0
- /package/dist/{internal → transport}/transport.js +0 -0
|
@@ -43,6 +43,14 @@ export interface UseGatedImages {
|
|
|
43
43
|
* who can't see the image, and the block must render a placeholder for any
|
|
44
44
|
* `hidden` entry.
|
|
45
45
|
*
|
|
46
|
+
* 🔴 A `hidden` ENTRY IS NARROWED TO `{ imageId, status }` BEFORE IT REACHES
|
|
47
|
+
* HERE, and that is enforced in code rather than asserted in prose: the
|
|
48
|
+
* transport runs `projectInboundPayload` on every `IMAGES_RESULT` before
|
|
49
|
+
* delivery (`src/transport/validate.ts`), so a `previewUrl`, `src`, `imageUrl` or
|
|
50
|
+
* any other key a host attaches to a withheld image is DROPPED, not forwarded.
|
|
51
|
+
* Fields are dropped rather than the reply rejected so a future host-side field
|
|
52
|
+
* addition cannot hang this call — see `projectGatedImage`'s docblock.
|
|
53
|
+
*
|
|
46
54
|
* 🔴 `nsfwLevel` AND `contentRating` ARE OPTIONAL, AND A MISSING ONE IS NOT "G".
|
|
47
55
|
* They are absent exactly when `ratingPending` is present. Treating absent as a
|
|
48
56
|
* safe default is the bug this state exists to stop: an image published seconds
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { getTransport } from '../
|
|
3
|
-
import { sendTypedRequest } from '../
|
|
2
|
+
import { getTransport } from '../transport/singleton.js';
|
|
3
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
4
4
|
/**
|
|
5
5
|
* Read per-viewer gated display data for a list of image ids via the
|
|
6
6
|
* host-mediated `GET_IMAGES_BY_IDS` → `IMAGES_RESULT` bridge — the read side of
|
|
@@ -14,6 +14,14 @@ import { sendTypedRequest } from '../internal/transport.js';
|
|
|
14
14
|
* who can't see the image, and the block must render a placeholder for any
|
|
15
15
|
* `hidden` entry.
|
|
16
16
|
*
|
|
17
|
+
* 🔴 A `hidden` ENTRY IS NARROWED TO `{ imageId, status }` BEFORE IT REACHES
|
|
18
|
+
* HERE, and that is enforced in code rather than asserted in prose: the
|
|
19
|
+
* transport runs `projectInboundPayload` on every `IMAGES_RESULT` before
|
|
20
|
+
* delivery (`src/transport/validate.ts`), so a `previewUrl`, `src`, `imageUrl` or
|
|
21
|
+
* any other key a host attaches to a withheld image is DROPPED, not forwarded.
|
|
22
|
+
* Fields are dropped rather than the reply rejected so a future host-side field
|
|
23
|
+
* addition cannot hang this call — see `projectGatedImage`'s docblock.
|
|
24
|
+
*
|
|
17
25
|
* 🔴 `nsfwLevel` AND `contentRating` ARE OPTIONAL, AND A MISSING ONE IS NOT "G".
|
|
18
26
|
* They are absent exactly when `ratingPending` is present. Treating absent as a
|
|
19
27
|
* safe default is the bug this state exists to stop: an image published seconds
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { BlockResourceInfo } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/** What {@link useGenerationResources} returns. */
|
|
3
|
+
export interface UseGenerationResources {
|
|
4
|
+
fetch: (versionIds: number[]) => Promise<BlockResourceInfo[]>;
|
|
5
|
+
}
|
|
2
6
|
/**
|
|
3
7
|
* Rehydrate a saved set of generation resources by version id, WITHOUT
|
|
4
8
|
* re-opening the resource picker. `fetch(versionIds)` GETs the block-token-gated
|
|
@@ -21,7 +25,5 @@ import type { BlockResourceInfo } from '@civitai/app-sdk/blocks';
|
|
|
21
25
|
* const resources = await fetch([691639, 666002]); // by saved versionIds
|
|
22
26
|
* // resources[0].strength / .minStrength / .maxStrength / .trainedWords / .clipSkip
|
|
23
27
|
*/
|
|
24
|
-
export declare function useGenerationResources():
|
|
25
|
-
fetch: (versionIds: number[]) => Promise<BlockResourceInfo[]>;
|
|
26
|
-
};
|
|
28
|
+
export declare function useGenerationResources(): UseGenerationResources;
|
|
27
29
|
//# sourceMappingURL=useGenerationResources.d.ts.map
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What {@link useHostOrigin} returns: the validated host origin, or
|
|
3
|
+
* `undefined` before `BLOCK_INIT` lands. Named so a consumer can write it —
|
|
4
|
+
* see `./returnTypeLedger.js`.
|
|
5
|
+
*/
|
|
6
|
+
export type UseHostOrigin = string | undefined;
|
|
1
7
|
/**
|
|
2
8
|
* Returns the validated host (parent) origin the block may safely direct-fetch
|
|
3
9
|
* the civitai App Blocks HTTP API against — `undefined` until the transport
|
|
@@ -30,5 +36,5 @@
|
|
|
30
36
|
* same tick it applies `BLOCK_INIT`, so the hook re-renders with the origin the
|
|
31
37
|
* moment init lands.
|
|
32
38
|
*/
|
|
33
|
-
export declare function useHostOrigin():
|
|
39
|
+
export declare function useHostOrigin(): UseHostOrigin;
|
|
34
40
|
//# sourceMappingURL=useHostOrigin.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
|
-
import { getTransport } from '../
|
|
2
|
+
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
/**
|
|
4
4
|
* Returns the validated host (parent) origin the block may safely direct-fetch
|
|
5
5
|
* the civitai App Blocks HTTP API against — `undefined` until the transport
|
|
@@ -1,4 +1,45 @@
|
|
|
1
1
|
import type { BlockGenerationSourceImageInfo, BlockImageScanResult, BlockPendingImageInfo, BlockUploadedImageInfo, BlockUploadPurpose } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/**
|
|
3
|
+
* What {@link useImageUpload} returns for the DEFAULT (`purpose: 'display'`,
|
|
4
|
+
* blocking-scan) call — `useImageUpload()` with no arguments.
|
|
5
|
+
*
|
|
6
|
+
* 🔴 THIS HOOK IS OVERLOADED, SO ITS RETURN TYPE IS A FAMILY, NOT A SINGLETON
|
|
7
|
+
* (#380). Each overload names its own: {@link UseImageUploadGenerationSource},
|
|
8
|
+
* {@link UseImageUploadAsyncScan}, and this one. A single union would be worse
|
|
9
|
+
* than the anonymous literals it replaces — it would hand every caller a
|
|
10
|
+
* `scanStatus` that only one overload actually provides.
|
|
11
|
+
*/
|
|
12
|
+
export interface UseImageUpload {
|
|
13
|
+
open: () => Promise<BlockUploadedImageInfo | null>;
|
|
14
|
+
}
|
|
15
|
+
/** What {@link useImageUpload} returns for `{ purpose: 'generationSource' }`. */
|
|
16
|
+
export interface UseImageUploadGenerationSource {
|
|
17
|
+
open: () => Promise<BlockGenerationSourceImageInfo | null>;
|
|
18
|
+
}
|
|
19
|
+
/** What {@link useImageUpload} returns for `{ asyncScan: true }`. */
|
|
20
|
+
export interface UseImageUploadAsyncScan {
|
|
21
|
+
/** Early-resolve handle (image persisted, NOT yet scanned) or `null` (dismissed). */
|
|
22
|
+
open: () => Promise<BlockPendingImageInfo | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the async scan verdict for a handle returned by `open()`. Re-callable
|
|
25
|
+
* for retry: the host emits the verdict once and the hook buffers it, so a
|
|
26
|
+
* re-call after an `'error'`/timeout re-awaits (or immediately returns) the same
|
|
27
|
+
* verdict. An unknown/expired handle resolves to a retryable `'error'`, and so
|
|
28
|
+
* does a call that is still awaiting when the component UNMOUNTS — the promise
|
|
29
|
+
* always settles, never rejects, so no caller needs a `try`/`catch` (#393).
|
|
30
|
+
*/
|
|
31
|
+
scanStatus: (handle: BlockPendingImageInfo) => Promise<BlockImageScanResult>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The IMPLEMENTATION signature's return — not a call signature any consumer can
|
|
35
|
+
* reach, and deliberately not on the package entry. `scanStatus` is optional
|
|
36
|
+
* here so the two non-async overloads (which return `{ open }` only) stay
|
|
37
|
+
* compatible; the async overload types it required.
|
|
38
|
+
*/
|
|
39
|
+
export interface UseImageUploadImplementation {
|
|
40
|
+
open: () => Promise<BlockUploadedImageInfo | BlockGenerationSourceImageInfo | BlockPendingImageInfo | null>;
|
|
41
|
+
scanStatus?: (handle: BlockPendingImageInfo) => Promise<BlockImageScanResult>;
|
|
42
|
+
}
|
|
2
43
|
/** Options for {@link useImageUpload}. */
|
|
3
44
|
export interface UseImageUploadOptions {
|
|
4
45
|
/**
|
|
@@ -79,26 +120,12 @@ export interface UseImageUploadOptions {
|
|
|
79
120
|
*/
|
|
80
121
|
export declare function useImageUpload(options: {
|
|
81
122
|
purpose: 'generationSource';
|
|
82
|
-
}):
|
|
83
|
-
open: () => Promise<BlockGenerationSourceImageInfo | null>;
|
|
84
|
-
};
|
|
123
|
+
}): UseImageUploadGenerationSource;
|
|
85
124
|
export declare function useImageUpload(options: {
|
|
86
125
|
purpose?: 'display';
|
|
87
126
|
asyncScan: true;
|
|
88
|
-
}):
|
|
89
|
-
/** Early-resolve handle (image persisted, NOT yet scanned) or `null` (dismissed). */
|
|
90
|
-
open: () => Promise<BlockPendingImageInfo | null>;
|
|
91
|
-
/**
|
|
92
|
-
* Resolve the async scan verdict for a handle returned by `open()`. Re-callable
|
|
93
|
-
* for retry: the host emits the verdict once and the hook buffers it, so a
|
|
94
|
-
* re-call after an `'error'`/timeout re-awaits (or immediately returns) the same
|
|
95
|
-
* verdict. An unknown/expired handle resolves to a retryable `'error'`.
|
|
96
|
-
*/
|
|
97
|
-
scanStatus: (handle: BlockPendingImageInfo) => Promise<BlockImageScanResult>;
|
|
98
|
-
};
|
|
127
|
+
}): UseImageUploadAsyncScan;
|
|
99
128
|
export declare function useImageUpload(options?: {
|
|
100
129
|
purpose?: 'display';
|
|
101
|
-
}):
|
|
102
|
-
open: () => Promise<BlockUploadedImageInfo | null>;
|
|
103
|
-
};
|
|
130
|
+
}): UseImageUpload;
|
|
104
131
|
//# sourceMappingURL=useImageUpload.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
-
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../
|
|
3
|
-
import { getTransport } from '../
|
|
4
|
-
import { sendTypedRequest, subscribeTyped } from '../
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { sendTypedRequest, subscribeTyped } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* Generous hook-side backstop for {@link BlockImageScanResult} delivery. The
|
|
7
7
|
* host-side scan POLL budget is authoritative and the consuming app applies its
|
|
@@ -64,10 +64,30 @@ export function useImageUpload(options) {
|
|
|
64
64
|
});
|
|
65
65
|
return () => {
|
|
66
66
|
unsubscribe();
|
|
67
|
+
// 🔴 #393 — SETTLE, DON'T JUST DISARM. This cleanup used to `clearTimeout`
|
|
68
|
+
// each waiter and stop there, which removed the ONLY remaining path to a
|
|
69
|
+
// settled promise: the listener is gone, so no verdict can ever arrive,
|
|
70
|
+
// and the backstop that would have resolved it has just been cancelled.
|
|
71
|
+
// An `await scanStatus(handle)` in flight at unmount therefore hung for
|
|
72
|
+
// the life of the page.
|
|
73
|
+
//
|
|
74
|
+
// RESOLVE, never reject: every other terminal path in this hook hands the
|
|
75
|
+
// caller a `BlockImageScanResult`, so rejecting here would be a new failure
|
|
76
|
+
// mode requiring a `try`/`catch` no existing caller has. `'error'` is the
|
|
77
|
+
// hook's RETRYABLE status, which is the honest verdict — the scan itself
|
|
78
|
+
// may well still be running server-side; this block just stopped watching.
|
|
67
79
|
for (const entry of scans.values()) {
|
|
68
|
-
for (const w of entry.waiters)
|
|
80
|
+
for (const w of entry.waiters) {
|
|
69
81
|
clearTimeout(w.timeoutId);
|
|
82
|
+
w.resolve({ status: 'error', message: 'scan status unavailable (the upload hook unmounted)' });
|
|
83
|
+
}
|
|
84
|
+
entry.waiters = [];
|
|
70
85
|
}
|
|
86
|
+
// Drop the tracking map too, so a `scanStatus()` call made AFTER unmount
|
|
87
|
+
// takes the immediate unknown-handle path rather than registering a waiter
|
|
88
|
+
// against a listener that no longer exists (which would sit for the full
|
|
89
|
+
// SCAN_STATUS_TIMEOUT_MS — settled, but ten minutes late).
|
|
90
|
+
scans.clear();
|
|
71
91
|
};
|
|
72
92
|
}, []);
|
|
73
93
|
const open = useCallback(async () => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../
|
|
3
|
-
import { getTransport } from '../
|
|
4
|
-
import { sendTypedRequest } from '../
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* Publish selected outputs of one of the calling app's OWN generations into bare,
|
|
7
7
|
* real-scanned public `Image` rows via the host-mediated
|
|
@@ -37,7 +37,7 @@ export function usePublishGenerationOutputs() {
|
|
|
37
37
|
// on screen. The generation was already billed and a dead publish
|
|
38
38
|
// bridge has no refund path, so the charge stood and the outputs were
|
|
39
39
|
// lost. Every human-gated request opts out the same way — the ledger is
|
|
40
|
-
// `HUMAN_GATED_REQUEST_TYPES` in `
|
|
40
|
+
// `HUMAN_GATED_REQUEST_TYPES` in `transport/requestTimeouts.ts`.
|
|
41
41
|
{ timeoutMs: HUMAN_INTERACTION_TIMEOUT_MS });
|
|
42
42
|
if (reply.error || !reply.result) {
|
|
43
43
|
// `||`, not `??`: `isValidPublishResult` gates `error` on SHAPE only, so a
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** What {@link useRequestConsent} returns. */
|
|
2
|
+
export interface UseRequestConsent {
|
|
3
|
+
requestConsent: (payload?: {
|
|
4
|
+
scopes?: string[];
|
|
5
|
+
}) => void;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* Lazy consent. Asks the host to open civitai.com's consent UI when a
|
|
3
9
|
* LOGGED-IN viewer clicks an action whose consent-gated scope the block token
|
|
@@ -39,9 +45,5 @@
|
|
|
39
45
|
* // viewer is logged in but the token lacks the spend scopes:
|
|
40
46
|
* requestConsent({ scopes: ['ai:write:budgeted', 'buzz:read:self'] });
|
|
41
47
|
*/
|
|
42
|
-
export declare function useRequestConsent():
|
|
43
|
-
requestConsent: (payload?: {
|
|
44
|
-
scopes?: string[];
|
|
45
|
-
}) => void;
|
|
46
|
-
};
|
|
48
|
+
export declare function useRequestConsent(): UseRequestConsent;
|
|
47
49
|
//# sourceMappingURL=useRequestConsent.d.ts.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { armConsentRefusalLatch } from '../internal/consentRefusalLatch.js';
|
|
3
|
-
import { getTransport } from '../
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
4
|
/**
|
|
5
5
|
* Lazy consent. Asks the host to open civitai.com's consent UI when a
|
|
6
6
|
* LOGGED-IN viewer clicks an action whose consent-gated scope the block token
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE latest-wins guard for every hook that auto-issues a request and writes the
|
|
3
|
+
* reply into component state (#392).
|
|
4
|
+
*
|
|
5
|
+
* 🔴 WHY THIS IS A SHARED HELPER AND NOT A PATTERN TO COPY. Before it, eight
|
|
6
|
+
* hooks each open-coded a `mountedRef` and every one of them got the same thing
|
|
7
|
+
* wrong in the same direction: `mountedRef` answers "is this component still
|
|
8
|
+
* here?", which is not the question a reply has to pass. The question is "is
|
|
9
|
+
* this reply still the one I am waiting for?" — and a superseded request's reply
|
|
10
|
+
* passes the mount check every time, because nothing unmounted. A predicate
|
|
11
|
+
* duplicated across N call sites regenerates the same bug at N sites; this is
|
|
12
|
+
* the consolidation that makes it one site.
|
|
13
|
+
*
|
|
14
|
+
* THE FAILURE IT CLOSES. `refetch`'s identity changes (a `paramsKey` moves when
|
|
15
|
+
* the viewer clicks "next page"), the mount effect re-runs, and request B goes
|
|
16
|
+
* out while A is still in flight. B answers first and paints page 2. A answers
|
|
17
|
+
* second and — with only a mount guard — repaints page 1 *and* rewrites `cursor`
|
|
18
|
+
* to page 2's value, so "next" re-fetches the page already shown and the viewer
|
|
19
|
+
* is wedged. The transport correlates each reply to its own request correctly;
|
|
20
|
+
* the defect is purely in which reply the hook lets write state.
|
|
21
|
+
*
|
|
22
|
+
* WHAT IT SUPERSEDES. `isCurrent` is FALSE after unmount as well as for a
|
|
23
|
+
* superseded request, so it replaces `mountedRef` rather than sitting next to
|
|
24
|
+
* it — one predicate, one place, both hazards.
|
|
25
|
+
*
|
|
26
|
+
* ⚠️ HONEST LABEL ON THE MOUNT HALF: it is DEFENSIVE, not load-bearing, and no
|
|
27
|
+
* test in this package can kill it. Mutating `mountedRef.current && token ===
|
|
28
|
+
* latestRef.current` down to just the latest-wins half leaves 84 tests green,
|
|
29
|
+
* because on React 18+ a `setState` against an unmounted component is a silent
|
|
30
|
+
* no-op — there is no warning to catch and no render to observe. It is kept
|
|
31
|
+
* because it is what the hooks did before (it avoids the pointless work), NOT
|
|
32
|
+
* because anything measures it. The LATEST-WINS half is the one that carries
|
|
33
|
+
* weight: mutating THAT away reddens 24 cases.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const seq = useRequestSequencer();
|
|
37
|
+
* const refetch = useCallback(() => {
|
|
38
|
+
* const token = seq.begin(); // supersedes any earlier request
|
|
39
|
+
* setLoading(true);
|
|
40
|
+
* sendTypedRequest(...)
|
|
41
|
+
* .then((reply) => {
|
|
42
|
+
* if (!seq.isCurrent(token)) return; // stale or unmounted — drop it
|
|
43
|
+
* setData(reply.result);
|
|
44
|
+
* setLoading(false);
|
|
45
|
+
* });
|
|
46
|
+
* }, [seq, ...]);
|
|
47
|
+
*/
|
|
48
|
+
export interface RequestSequencer {
|
|
49
|
+
/**
|
|
50
|
+
* Open a new request and SUPERSEDE every earlier one. Returns the token that
|
|
51
|
+
* identifies it; hand that token to {@link RequestSequencer.isCurrent} before
|
|
52
|
+
* writing any state derived from the reply.
|
|
53
|
+
*/
|
|
54
|
+
begin: () => number;
|
|
55
|
+
/**
|
|
56
|
+
* `true` only when `token` names the most recently begun request AND the
|
|
57
|
+
* component is still mounted. Every other case — a superseded request, a reply
|
|
58
|
+
* after unmount — is `false`.
|
|
59
|
+
*/
|
|
60
|
+
isCurrent: (token: number) => boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create a {@link RequestSequencer} for one hook instance.
|
|
64
|
+
*
|
|
65
|
+
* The returned object is STABLE for the life of the component (both methods are
|
|
66
|
+
* `useCallback`-pinned with no dependencies, and the object itself is memoized),
|
|
67
|
+
* so a consumer can list it in a `useCallback` dependency array without
|
|
68
|
+
* retriggering the very effect it guards.
|
|
69
|
+
*/
|
|
70
|
+
export declare function useRequestSequencer(): RequestSequencer;
|
|
71
|
+
//# sourceMappingURL=useRequestSequencer.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Create a {@link RequestSequencer} for one hook instance.
|
|
4
|
+
*
|
|
5
|
+
* The returned object is STABLE for the life of the component (both methods are
|
|
6
|
+
* `useCallback`-pinned with no dependencies, and the object itself is memoized),
|
|
7
|
+
* so a consumer can list it in a `useCallback` dependency array without
|
|
8
|
+
* retriggering the very effect it guards.
|
|
9
|
+
*/
|
|
10
|
+
export function useRequestSequencer() {
|
|
11
|
+
// Monotonic request counter. `0` is the "no request yet" value and is never
|
|
12
|
+
// handed out, so `isCurrent(0)` is false by construction.
|
|
13
|
+
const latestRef = useRef(0);
|
|
14
|
+
const mountedRef = useRef(true);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
mountedRef.current = true;
|
|
17
|
+
return () => {
|
|
18
|
+
mountedRef.current = false;
|
|
19
|
+
};
|
|
20
|
+
}, []);
|
|
21
|
+
const begin = useCallback(() => {
|
|
22
|
+
latestRef.current += 1;
|
|
23
|
+
return latestRef.current;
|
|
24
|
+
}, []);
|
|
25
|
+
const isCurrent = useCallback((token) => mountedRef.current && token === latestRef.current, []);
|
|
26
|
+
return useMemo(() => ({ begin, isCurrent }), [begin, isCurrent]);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=useRequestSequencer.js.map
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** What {@link useRequestSignIn} returns. */
|
|
2
|
+
export interface UseRequestSignIn {
|
|
3
|
+
requestSignIn: (payload?: {
|
|
4
|
+
returnUrl?: string;
|
|
5
|
+
}) => void;
|
|
6
|
+
}
|
|
1
7
|
/**
|
|
2
8
|
* Anonymous conversion. Asks the host to start civitai.com's login flow when a
|
|
3
9
|
* logged-out viewer (`useBlockContext().viewer === null`) clicks an action that
|
|
@@ -16,9 +22,5 @@
|
|
|
16
22
|
* const { requestSignIn } = useRequestSignIn();
|
|
17
23
|
* if (viewer === null) return <button onClick={() => requestSignIn()}>Sign in</button>;
|
|
18
24
|
*/
|
|
19
|
-
export declare function useRequestSignIn():
|
|
20
|
-
requestSignIn: (payload?: {
|
|
21
|
-
returnUrl?: string;
|
|
22
|
-
}) => void;
|
|
23
|
-
};
|
|
25
|
+
export declare function useRequestSignIn(): UseRequestSignIn;
|
|
24
26
|
//# sourceMappingURL=useRequestSignIn.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { getTransport } from '../
|
|
2
|
+
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
/**
|
|
4
4
|
* Anonymous conversion. Asks the host to start civitai.com's login flow when a
|
|
5
5
|
* logged-out viewer (`useBlockContext().viewer === null`) clicks an action that
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { BlockResourceInfo, BlockResourcePickerType } from '@civitai/app-sdk/blocks';
|
|
2
|
+
/** What {@link useResourcePicker} returns. */
|
|
3
|
+
export interface UseResourcePicker {
|
|
4
|
+
open: (opts: {
|
|
5
|
+
/** Which resource type to pick. v1: `'Checkpoint' | 'LORA'` only — the
|
|
6
|
+
* host rejects any other type (the modal never opens). */
|
|
7
|
+
resourceType: BlockResourcePickerType;
|
|
8
|
+
/**
|
|
9
|
+
* Optional base-model family hint — an ecosystem key (e.g. 'Flux1', 'SDXL')
|
|
10
|
+
* OR a baseModel name (e.g. 'Flux.1 D'); the host collapses it to the
|
|
11
|
+
* ecosystem family. Use the chosen checkpoint's `baseModel` to constrain a
|
|
12
|
+
* LoRA pick to the same family. Omit for an unconstrained pick of the type.
|
|
13
|
+
*/
|
|
14
|
+
baseModelGroup?: string;
|
|
15
|
+
}) => Promise<BlockResourceInfo | null>;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Drives the platform-side resource picker for PAGE App Blocks (Design 1 —
|
|
4
19
|
* host-chrome). Generalizes {@link useCheckpointPicker} from Checkpoint-only to
|
|
@@ -27,18 +42,5 @@ import type { BlockResourceInfo, BlockResourcePickerType } from '@civitai/app-sd
|
|
|
27
42
|
* if (!picked) return; // user dismissed
|
|
28
43
|
* // feed picked.versionId into body.additionalResources and submit
|
|
29
44
|
*/
|
|
30
|
-
export declare function useResourcePicker():
|
|
31
|
-
open: (opts: {
|
|
32
|
-
/** Which resource type to pick. v1: `'Checkpoint' | 'LORA'` only — the
|
|
33
|
-
* host rejects any other type (the modal never opens). */
|
|
34
|
-
resourceType: BlockResourcePickerType;
|
|
35
|
-
/**
|
|
36
|
-
* Optional base-model family hint — an ecosystem key (e.g. 'Flux1', 'SDXL')
|
|
37
|
-
* OR a baseModel name (e.g. 'Flux.1 D'); the host collapses it to the
|
|
38
|
-
* ecosystem family. Use the chosen checkpoint's `baseModel` to constrain a
|
|
39
|
-
* LoRA pick to the same family. Omit for an unconstrained pick of the type.
|
|
40
|
-
*/
|
|
41
|
-
baseModelGroup?: string;
|
|
42
|
-
}) => Promise<BlockResourceInfo | null>;
|
|
43
|
-
};
|
|
45
|
+
export declare function useResourcePicker(): UseResourcePicker;
|
|
44
46
|
//# sourceMappingURL=useResourcePicker.d.ts.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../
|
|
3
|
-
import { getTransport } from '../
|
|
4
|
-
import { sendTypedRequest } from '../
|
|
2
|
+
import { HUMAN_INTERACTION_TIMEOUT_MS } from '../transport/requestTimeouts.js';
|
|
3
|
+
import { getTransport } from '../transport/singleton.js';
|
|
4
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* Drives the platform-side resource picker for PAGE App Blocks (Design 1 —
|
|
7
7
|
* host-chrome). Generalizes {@link useCheckpointPicker} from Checkpoint-only to
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { getTransport } from '../
|
|
2
|
+
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { throwOnFailedReply } from '../internal/replyError.js';
|
|
4
|
-
import { sendTypedRequest } from '../
|
|
4
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* Download an image via the host-mediated `SAVE_IMAGE` → `SAVE_IMAGE_RESULT`
|
|
7
7
|
* bridge. See {@link SaveImageInput} for the url-vs-id security posture.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import { getTransport } from '../
|
|
2
|
+
import { getTransport } from '../transport/singleton.js';
|
|
3
3
|
import { throwOnFailedReply, throwOnReplyError } from '../internal/replyError.js';
|
|
4
|
-
import { sendTypedRequest } from '../
|
|
4
|
+
import { sendTypedRequest } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* App-scoped, append-only, community-votable SHARED datastore. Sibling of
|
|
7
7
|
* {@link useAppStorage} (the per-viewer KV store) — same postMessage bridge,
|
package/dist/hooks/useTip.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
3
3
|
import { useBlockToken } from './useBlockToken.js';
|
|
4
|
-
import { generateIdempotencyKey } from '../
|
|
4
|
+
import { generateIdempotencyKey } from '../transport/transport.js';
|
|
5
5
|
/**
|
|
6
6
|
* Backstop timeout for the direct REST tip POST. Like {@link useGenerationResources}
|
|
7
7
|
* (and unlike the postMessage hooks), this talks to the HTTP API directly, so it
|
|
@@ -15,7 +15,14 @@ export interface UseTipAllowance {
|
|
|
15
15
|
allowance: TipAllowance | null;
|
|
16
16
|
/** `true` while a fetch (initial or `refetch`) is in flight. */
|
|
17
17
|
loading: boolean;
|
|
18
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* The last fetch's failure, or `null`. Cleared at the start of the next fetch.
|
|
20
|
+
*
|
|
21
|
+
* Also carries the NO-HOST-ORIGIN terminal state (#398): if `BLOCK_INIT` never
|
|
22
|
+
* lands — a direct/unembedded load, or `InlineTransport` before bootstrap —
|
|
23
|
+
* `loading` drops to `false` and this becomes a named `Error` after
|
|
24
|
+
* {@link HOST_ORIGIN_WAIT_MS}, instead of the hook spinning forever.
|
|
25
|
+
*/
|
|
19
26
|
error: Error | null;
|
|
20
27
|
/** Re-read the allowance (e.g. after the viewer tips). */
|
|
21
28
|
refetch: () => void;
|
|
@@ -32,6 +39,11 @@ export interface UseTipAllowance {
|
|
|
32
39
|
* (`localStorage` is inert in the opaque-origin sandbox). Fetches once on mount
|
|
33
40
|
* and exposes `refetch` (call it after a successful `useTip().tip(...)`).
|
|
34
41
|
*
|
|
42
|
+
* Only the LATEST request may write state: a reply superseded by a newer
|
|
43
|
+
* `refetch` — or one that lands after unmount — is dropped (#392). If the host
|
|
44
|
+
* origin never arrives the hook reaches a TERMINAL state rather than spinning:
|
|
45
|
+
* see `error` above (#398).
|
|
46
|
+
*
|
|
35
47
|
* @example
|
|
36
48
|
* const { allowance, refetch } = useTipAllowance();
|
|
37
49
|
* // allowance?.remaining — Buzz the viewer may still tip today
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { useHostOrigin } from './useHostOrigin.js';
|
|
3
3
|
import { useBlockToken } from './useBlockToken.js';
|
|
4
|
+
import { useRequestSequencer } from './useRequestSequencer.js';
|
|
4
5
|
/**
|
|
5
6
|
* Backstop timeout for the direct REST allowance GET (see {@link useTip} /
|
|
6
7
|
* {@link useGenerationResources} for why direct-fetch hooks need their own bound).
|
|
7
8
|
*/
|
|
8
9
|
const TIP_ALLOWANCE_TIMEOUT_MS = 30_000;
|
|
10
|
+
/**
|
|
11
|
+
* How long the hook waits for `useHostOrigin()` before declaring the block
|
|
12
|
+
* un-embedded (#398).
|
|
13
|
+
*
|
|
14
|
+
* 🔴 A BOUND, NOT AN IMMEDIATE ERROR, AND THE DIFFERENCE MATTERS. The host
|
|
15
|
+
* origin is absent on EVERY boot — it lands a tick or two after mount, with
|
|
16
|
+
* `BLOCK_INIT` — so erroring on the first `!host` would flash a spurious error
|
|
17
|
+
* on every healthy embedded block. Only its CONTINUED absence is the fault, and
|
|
18
|
+
* that is what this measures.
|
|
19
|
+
*
|
|
20
|
+
* Equal to {@link TIP_ALLOWANCE_TIMEOUT_MS} by coincidence, not by derivation:
|
|
21
|
+
* this bounds a wait for the HOST to introduce itself, that one bounds a request
|
|
22
|
+
* already in flight. Two independent questions, so two constants — tuning either
|
|
23
|
+
* must not silently retune the other.
|
|
24
|
+
*/
|
|
25
|
+
const HOST_ORIGIN_WAIT_MS = 30_000;
|
|
9
26
|
/**
|
|
10
27
|
* Read the viewer's REAL remaining daily tip allowance `{ cap, spent, remaining }`
|
|
11
28
|
* through the block-token-gated `GET /api/v1/blocks/tip-allowance` REST endpoint
|
|
@@ -18,6 +35,11 @@ const TIP_ALLOWANCE_TIMEOUT_MS = 30_000;
|
|
|
18
35
|
* (`localStorage` is inert in the opaque-origin sandbox). Fetches once on mount
|
|
19
36
|
* and exposes `refetch` (call it after a successful `useTip().tip(...)`).
|
|
20
37
|
*
|
|
38
|
+
* Only the LATEST request may write state: a reply superseded by a newer
|
|
39
|
+
* `refetch` — or one that lands after unmount — is dropped (#392). If the host
|
|
40
|
+
* origin never arrives the hook reaches a TERMINAL state rather than spinning:
|
|
41
|
+
* see `error` above (#398).
|
|
42
|
+
*
|
|
21
43
|
* @example
|
|
22
44
|
* const { allowance, refetch } = useTipAllowance();
|
|
23
45
|
* // allowance?.remaining — Buzz the viewer may still tip today
|
|
@@ -28,26 +50,42 @@ export function useTipAllowance() {
|
|
|
28
50
|
const [allowance, setAllowance] = useState(null);
|
|
29
51
|
const [loading, setLoading] = useState(true);
|
|
30
52
|
const [error, setError] = useState(null);
|
|
31
|
-
|
|
53
|
+
// 🔴 `inFlight` IS NOT A SEQUENCING GUARD, and #392's triage note read it as
|
|
54
|
+
// one. It is drained only by the unmount cleanup below, so two overlapping
|
|
55
|
+
// `refetch()`es neither abort each other nor correlate their replies — the
|
|
56
|
+
// slower one simply wrote last. `useRequestSequencer` is what supplies the
|
|
57
|
+
// latest-wins predicate; this set exists to abort on unmount and to let a
|
|
58
|
+
// superseded request's socket be closed rather than left running.
|
|
32
59
|
const inFlight = useRef(new Set());
|
|
60
|
+
const seq = useRequestSequencer();
|
|
33
61
|
useEffect(() => {
|
|
34
|
-
mountedRef.current = true;
|
|
35
62
|
const controllers = inFlight.current;
|
|
36
63
|
return () => {
|
|
37
|
-
mountedRef.current = false;
|
|
38
64
|
for (const c of controllers)
|
|
39
65
|
c.abort();
|
|
40
66
|
controllers.clear();
|
|
41
67
|
};
|
|
42
68
|
}, []);
|
|
69
|
+
// #398 — bounded wait for the host origin. Re-armed whenever `host` changes,
|
|
70
|
+
// and cleared the moment one arrives, so the terminal error is reachable ONLY
|
|
71
|
+
// when the origin is still absent a full HOST_ORIGIN_WAIT_MS after mount.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (host)
|
|
74
|
+
return;
|
|
75
|
+
const id = setTimeout(() => {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
setError(new Error(`useTipAllowance: host origin not established after ${HOST_ORIGIN_WAIT_MS}ms (no BLOCK_INIT — the block is probably not embedded).`));
|
|
78
|
+
}, HOST_ORIGIN_WAIT_MS);
|
|
79
|
+
return () => clearTimeout(id);
|
|
80
|
+
}, [host, seq]);
|
|
43
81
|
const refetch = useCallback(() => {
|
|
44
|
-
// Wait for the host origin (established at BLOCK_INIT) before the first
|
|
82
|
+
// Wait for the host origin (established at BLOCK_INIT) before the first
|
|
83
|
+
// fetch. The effect above is what stops this from being an eternal spinner.
|
|
45
84
|
if (!host)
|
|
46
85
|
return;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
86
|
+
const token = seq.begin();
|
|
87
|
+
setLoading(true);
|
|
88
|
+
setError(null);
|
|
51
89
|
const controller = new AbortController();
|
|
52
90
|
inFlight.current.add(controller);
|
|
53
91
|
const timeoutId = setTimeout(() => controller.abort(), TIP_ALLOWANCE_TIMEOUT_MS);
|
|
@@ -57,7 +95,7 @@ export function useTipAllowance() {
|
|
|
57
95
|
})
|
|
58
96
|
.then(async (res) => {
|
|
59
97
|
const body = (await res.json().catch(() => null));
|
|
60
|
-
if (!
|
|
98
|
+
if (!seq.isCurrent(token))
|
|
61
99
|
return;
|
|
62
100
|
if (!res.ok || body == null || typeof body.remaining !== 'number') {
|
|
63
101
|
setError(new Error(body?.error ?? `tip-allowance request failed (${res.status})`));
|
|
@@ -68,7 +106,7 @@ export function useTipAllowance() {
|
|
|
68
106
|
setLoading(false);
|
|
69
107
|
})
|
|
70
108
|
.catch((err) => {
|
|
71
|
-
if (!
|
|
109
|
+
if (!seq.isCurrent(token))
|
|
72
110
|
return;
|
|
73
111
|
setError(controller.signal.aborted
|
|
74
112
|
? new Error(`useTipAllowance: request aborted (timed out after ${TIP_ALLOWANCE_TIMEOUT_MS}ms or the hook unmounted).`)
|
|
@@ -81,7 +119,7 @@ export function useTipAllowance() {
|
|
|
81
119
|
clearTimeout(timeoutId);
|
|
82
120
|
inFlight.current.delete(controller);
|
|
83
121
|
});
|
|
84
|
-
}, [host, raw]);
|
|
122
|
+
}, [host, raw, seq]);
|
|
85
123
|
useEffect(() => {
|
|
86
124
|
refetch();
|
|
87
125
|
}, [refetch]);
|
|
@@ -32,8 +32,9 @@ export interface UseViewer {
|
|
|
32
32
|
*
|
|
33
33
|
* Fetches once on mount and exposes `refetch` for on-demand refreshes. A host
|
|
34
34
|
* that never answers surfaces as an `error` after the transport's request
|
|
35
|
-
* timeout — the hook never hangs.
|
|
36
|
-
*
|
|
35
|
+
* timeout — the hook never hangs. Only the LATEST request may write state: a
|
|
36
|
+
* reply superseded by a newer `refetch` — or one that lands after unmount — is
|
|
37
|
+
* dropped (#392).
|
|
37
38
|
*
|
|
38
39
|
* @example
|
|
39
40
|
* const { viewer, loading, error, refetch } = useViewer();
|