@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.
Files changed (262) hide show
  1. package/README.md +34 -2
  2. package/dist/api/generationResources.d.ts +4 -3
  3. package/dist/hooks/returnTypeLedger.d.ts +2 -0
  4. package/dist/hooks/returnTypeLedger.js +2 -0
  5. package/dist/hooks/useAppStorage.js +2 -2
  6. package/dist/hooks/useAppWorkflows.d.ts +5 -2
  7. package/dist/hooks/useAppWorkflows.js +18 -7
  8. package/dist/hooks/useBlockAnalytics.d.ts +5 -3
  9. package/dist/hooks/useBlockAnalytics.js +1 -1
  10. package/dist/hooks/useBlockBreakpoint.d.ts +7 -1
  11. package/dist/hooks/useBlockContext.d.ts +13 -2
  12. package/dist/hooks/useBlockContext.js +1 -1
  13. package/dist/hooks/useBlockResize.d.ts +7 -1
  14. package/dist/hooks/useBlockResize.js +1 -1
  15. package/dist/hooks/useBlockSettings.d.ts +6 -1
  16. package/dist/hooks/useBlockTheme.d.ts +6 -1
  17. package/dist/hooks/useBlockToken.d.ts +8 -3
  18. package/dist/hooks/useBlockToken.js +2 -2
  19. package/dist/hooks/useBuzzAccounts.d.ts +3 -2
  20. package/dist/hooks/useBuzzAccounts.js +16 -15
  21. package/dist/hooks/useBuzzBalance.d.ts +3 -2
  22. package/dist/hooks/useBuzzBalance.js +16 -18
  23. package/dist/hooks/useBuzzPurchase.d.ts +8 -6
  24. package/dist/hooks/useBuzzPurchase.js +3 -3
  25. package/dist/hooks/useBuzzTransactions.d.ts +4 -2
  26. package/dist/hooks/useBuzzTransactions.js +16 -15
  27. package/dist/hooks/useBuzzWorkflow.d.ts +71 -28
  28. package/dist/hooks/useBuzzWorkflow.js +31 -17
  29. package/dist/hooks/useCheckpointPicker.d.ts +18 -16
  30. package/dist/hooks/useCheckpointPicker.js +3 -3
  31. package/dist/hooks/useCivitaiNavigate.d.ts +5 -3
  32. package/dist/hooks/useCivitaiNavigate.js +1 -1
  33. package/dist/hooks/useCollectionFollow.js +4 -4
  34. package/dist/hooks/useConsentUnavailable.js +2 -2
  35. package/dist/hooks/useCreatePostFromApp.js +4 -4
  36. package/dist/hooks/useDailyCompensation.d.ts +4 -2
  37. package/dist/hooks/useDailyCompensation.js +17 -15
  38. package/dist/hooks/useDirectLoad.d.ts +7 -1
  39. package/dist/hooks/useDirectLoad.js +1 -1
  40. package/dist/hooks/useDomainMaturity.d.ts +6 -2
  41. package/dist/hooks/useGatedImages.d.ts +8 -0
  42. package/dist/hooks/useGatedImages.js +10 -2
  43. package/dist/hooks/useGenerationResources.d.ts +5 -3
  44. package/dist/hooks/useHostOrigin.d.ts +7 -1
  45. package/dist/hooks/useHostOrigin.js +1 -1
  46. package/dist/hooks/useImageUpload.d.ts +44 -17
  47. package/dist/hooks/useImageUpload.js +24 -4
  48. package/dist/hooks/usePublishGenerationOutputs.js +4 -4
  49. package/dist/hooks/useRequestConsent.d.ts +7 -5
  50. package/dist/hooks/useRequestConsent.js +1 -1
  51. package/dist/hooks/useRequestSequencer.d.ts +71 -0
  52. package/dist/hooks/useRequestSequencer.js +28 -0
  53. package/dist/hooks/useRequestSignIn.d.ts +7 -5
  54. package/dist/hooks/useRequestSignIn.js +1 -1
  55. package/dist/hooks/useResourcePicker.d.ts +16 -14
  56. package/dist/hooks/useResourcePicker.js +3 -3
  57. package/dist/hooks/useSaveImage.js +2 -2
  58. package/dist/hooks/useSharedStorage.js +2 -2
  59. package/dist/hooks/useTip.js +1 -1
  60. package/dist/hooks/useTipAllowance.d.ts +13 -1
  61. package/dist/hooks/useTipAllowance.js +49 -11
  62. package/dist/hooks/useViewer.d.ts +3 -2
  63. package/dist/hooks/useViewer.js +16 -18
  64. package/dist/hooks/useWildcardPack.d.ts +3 -1
  65. package/dist/hooks/useWildcardPack.js +19 -14
  66. package/dist/index.d.ts +30 -14
  67. package/dist/index.js +7 -7
  68. package/dist/internal/consentRefusalLatch.d.ts +1 -1
  69. package/dist/internal/consentRefusalLatch.js +1 -1
  70. package/dist/internal/liveHost.d.ts +63 -11
  71. package/dist/internal/liveHost.js +240 -37
  72. package/dist/internal/mockHost.js +16 -15
  73. package/dist/internal/pickerOverlay.d.ts +52 -4
  74. package/dist/internal/pickerOverlay.js +14 -3
  75. package/dist/testing.d.ts +1 -1
  76. package/dist/testing.js +1 -1
  77. package/dist/{internal → transport}/iframeTransport.d.ts +43 -5
  78. package/dist/{internal → transport}/iframeTransport.js +142 -12
  79. package/dist/transport/originMatcher.d.ts +92 -0
  80. package/dist/transport/originMatcher.js +222 -0
  81. package/dist/transport/requestId.d.ts +113 -0
  82. package/dist/transport/requestId.js +117 -0
  83. package/dist/{internal → transport}/validate.d.ts +34 -0
  84. package/dist/{internal → transport}/validate.js +189 -48
  85. package/dist/ui/BlockGate.js +1 -1
  86. package/dist/ui/SettingsForm.d.ts +8 -2
  87. package/dist/ui/SettingsForm.js +49 -3
  88. package/package.json +7 -148
  89. package/dist/api/generationResources.d.ts.map +0 -1
  90. package/dist/api/generationResources.js.map +0 -1
  91. package/dist/hooks/SfwGate.d.ts.map +0 -1
  92. package/dist/hooks/SfwGate.js.map +0 -1
  93. package/dist/hooks/useAppStorage.d.ts.map +0 -1
  94. package/dist/hooks/useAppStorage.js.map +0 -1
  95. package/dist/hooks/useAppWorkflows.d.ts.map +0 -1
  96. package/dist/hooks/useAppWorkflows.js.map +0 -1
  97. package/dist/hooks/useBlockAnalytics.d.ts.map +0 -1
  98. package/dist/hooks/useBlockAnalytics.js.map +0 -1
  99. package/dist/hooks/useBlockBreakpoint.d.ts.map +0 -1
  100. package/dist/hooks/useBlockBreakpoint.js.map +0 -1
  101. package/dist/hooks/useBlockContext.d.ts.map +0 -1
  102. package/dist/hooks/useBlockContext.js.map +0 -1
  103. package/dist/hooks/useBlockResize.d.ts.map +0 -1
  104. package/dist/hooks/useBlockResize.js.map +0 -1
  105. package/dist/hooks/useBlockSettings.d.ts.map +0 -1
  106. package/dist/hooks/useBlockSettings.js.map +0 -1
  107. package/dist/hooks/useBlockTheme.d.ts.map +0 -1
  108. package/dist/hooks/useBlockTheme.js.map +0 -1
  109. package/dist/hooks/useBlockToken.d.ts.map +0 -1
  110. package/dist/hooks/useBlockToken.js.map +0 -1
  111. package/dist/hooks/useBuzzAccounts.d.ts.map +0 -1
  112. package/dist/hooks/useBuzzAccounts.js.map +0 -1
  113. package/dist/hooks/useBuzzBalance.d.ts.map +0 -1
  114. package/dist/hooks/useBuzzBalance.js.map +0 -1
  115. package/dist/hooks/useBuzzPurchase.d.ts.map +0 -1
  116. package/dist/hooks/useBuzzPurchase.js.map +0 -1
  117. package/dist/hooks/useBuzzTransactions.d.ts.map +0 -1
  118. package/dist/hooks/useBuzzTransactions.js.map +0 -1
  119. package/dist/hooks/useBuzzWorkflow.d.ts.map +0 -1
  120. package/dist/hooks/useBuzzWorkflow.js.map +0 -1
  121. package/dist/hooks/useCheckpointPicker.d.ts.map +0 -1
  122. package/dist/hooks/useCheckpointPicker.js.map +0 -1
  123. package/dist/hooks/useCivitaiNavigate.d.ts.map +0 -1
  124. package/dist/hooks/useCivitaiNavigate.js.map +0 -1
  125. package/dist/hooks/useCollectionFollow.d.ts.map +0 -1
  126. package/dist/hooks/useCollectionFollow.js.map +0 -1
  127. package/dist/hooks/useConsentUnavailable.d.ts.map +0 -1
  128. package/dist/hooks/useConsentUnavailable.js.map +0 -1
  129. package/dist/hooks/useCreatePostFromApp.d.ts.map +0 -1
  130. package/dist/hooks/useCreatePostFromApp.js.map +0 -1
  131. package/dist/hooks/useDailyCompensation.d.ts.map +0 -1
  132. package/dist/hooks/useDailyCompensation.js.map +0 -1
  133. package/dist/hooks/useDirectLoad.d.ts.map +0 -1
  134. package/dist/hooks/useDirectLoad.js.map +0 -1
  135. package/dist/hooks/useDomainMaturity.d.ts.map +0 -1
  136. package/dist/hooks/useDomainMaturity.js.map +0 -1
  137. package/dist/hooks/useGatedImages.d.ts.map +0 -1
  138. package/dist/hooks/useGatedImages.js.map +0 -1
  139. package/dist/hooks/useGenerationResources.d.ts.map +0 -1
  140. package/dist/hooks/useGenerationResources.js.map +0 -1
  141. package/dist/hooks/useHostOrigin.d.ts.map +0 -1
  142. package/dist/hooks/useHostOrigin.js.map +0 -1
  143. package/dist/hooks/useImageUpload.d.ts.map +0 -1
  144. package/dist/hooks/useImageUpload.js.map +0 -1
  145. package/dist/hooks/usePublishGenerationOutputs.d.ts.map +0 -1
  146. package/dist/hooks/usePublishGenerationOutputs.js.map +0 -1
  147. package/dist/hooks/useRequestConsent.d.ts.map +0 -1
  148. package/dist/hooks/useRequestConsent.js.map +0 -1
  149. package/dist/hooks/useRequestSignIn.d.ts.map +0 -1
  150. package/dist/hooks/useRequestSignIn.js.map +0 -1
  151. package/dist/hooks/useResourcePicker.d.ts.map +0 -1
  152. package/dist/hooks/useResourcePicker.js.map +0 -1
  153. package/dist/hooks/useSaveImage.d.ts.map +0 -1
  154. package/dist/hooks/useSaveImage.js.map +0 -1
  155. package/dist/hooks/useSharedStorage.d.ts.map +0 -1
  156. package/dist/hooks/useSharedStorage.js.map +0 -1
  157. package/dist/hooks/useTip.d.ts.map +0 -1
  158. package/dist/hooks/useTip.js.map +0 -1
  159. package/dist/hooks/useTipAllowance.d.ts.map +0 -1
  160. package/dist/hooks/useTipAllowance.js.map +0 -1
  161. package/dist/hooks/useViewer.d.ts.map +0 -1
  162. package/dist/hooks/useViewer.js.map +0 -1
  163. package/dist/hooks/useWildcardPack.d.ts.map +0 -1
  164. package/dist/hooks/useWildcardPack.js.map +0 -1
  165. package/dist/index.d.ts.map +0 -1
  166. package/dist/index.js.map +0 -1
  167. package/dist/internal/catalog.d.ts.map +0 -1
  168. package/dist/internal/catalog.js.map +0 -1
  169. package/dist/internal/consent.d.ts.map +0 -1
  170. package/dist/internal/consent.js.map +0 -1
  171. package/dist/internal/consentRefusalLatch.d.ts.map +0 -1
  172. package/dist/internal/consentRefusalLatch.js.map +0 -1
  173. package/dist/internal/detector.d.ts.map +0 -1
  174. package/dist/internal/detector.js.map +0 -1
  175. package/dist/internal/directLoad.d.ts.map +0 -1
  176. package/dist/internal/directLoad.js.map +0 -1
  177. package/dist/internal/iframeTransport.d.ts.map +0 -1
  178. package/dist/internal/iframeTransport.js.map +0 -1
  179. package/dist/internal/inlineTransport.d.ts.map +0 -1
  180. package/dist/internal/inlineTransport.js.map +0 -1
  181. package/dist/internal/liveHost.d.ts.map +0 -1
  182. package/dist/internal/liveHost.js.map +0 -1
  183. package/dist/internal/mockHost.d.ts.map +0 -1
  184. package/dist/internal/mockHost.js.map +0 -1
  185. package/dist/internal/originMatcher.d.ts +0 -28
  186. package/dist/internal/originMatcher.d.ts.map +0 -1
  187. package/dist/internal/originMatcher.js +0 -89
  188. package/dist/internal/originMatcher.js.map +0 -1
  189. package/dist/internal/pickerOverlay.d.ts.map +0 -1
  190. package/dist/internal/pickerOverlay.js.map +0 -1
  191. package/dist/internal/replyError.d.ts.map +0 -1
  192. package/dist/internal/replyError.js.map +0 -1
  193. package/dist/internal/requestTimeouts.d.ts.map +0 -1
  194. package/dist/internal/requestTimeouts.js.map +0 -1
  195. package/dist/internal/singleton.d.ts.map +0 -1
  196. package/dist/internal/singleton.js.map +0 -1
  197. package/dist/internal/transport.d.ts.map +0 -1
  198. package/dist/internal/transport.js.map +0 -1
  199. package/dist/internal/validate.d.ts.map +0 -1
  200. package/dist/internal/validate.js.map +0 -1
  201. package/dist/live.d.ts.map +0 -1
  202. package/dist/live.js.map +0 -1
  203. package/dist/testing.d.ts.map +0 -1
  204. package/dist/testing.js.map +0 -1
  205. package/dist/ui/Alert.d.ts.map +0 -1
  206. package/dist/ui/Alert.js.map +0 -1
  207. package/dist/ui/Badge.d.ts.map +0 -1
  208. package/dist/ui/Badge.js.map +0 -1
  209. package/dist/ui/BlockGate.d.ts.map +0 -1
  210. package/dist/ui/BlockGate.js.map +0 -1
  211. package/dist/ui/Button.d.ts.map +0 -1
  212. package/dist/ui/Button.js.map +0 -1
  213. package/dist/ui/Card.d.ts.map +0 -1
  214. package/dist/ui/Card.js.map +0 -1
  215. package/dist/ui/Collapse.d.ts.map +0 -1
  216. package/dist/ui/Collapse.js.map +0 -1
  217. package/dist/ui/FollowButton.d.ts.map +0 -1
  218. package/dist/ui/FollowButton.js.map +0 -1
  219. package/dist/ui/Group.d.ts.map +0 -1
  220. package/dist/ui/Group.js.map +0 -1
  221. package/dist/ui/Loader.d.ts.map +0 -1
  222. package/dist/ui/Loader.js.map +0 -1
  223. package/dist/ui/Modal.d.ts.map +0 -1
  224. package/dist/ui/Modal.js.map +0 -1
  225. package/dist/ui/NumberInput.d.ts.map +0 -1
  226. package/dist/ui/NumberInput.js.map +0 -1
  227. package/dist/ui/ReportButton.d.ts.map +0 -1
  228. package/dist/ui/ReportButton.js.map +0 -1
  229. package/dist/ui/ResourceCard.d.ts.map +0 -1
  230. package/dist/ui/ResourceCard.js.map +0 -1
  231. package/dist/ui/SegmentedControl.d.ts.map +0 -1
  232. package/dist/ui/SegmentedControl.js.map +0 -1
  233. package/dist/ui/Select.d.ts.map +0 -1
  234. package/dist/ui/Select.js.map +0 -1
  235. package/dist/ui/SettingsForm.d.ts.map +0 -1
  236. package/dist/ui/SettingsForm.js.map +0 -1
  237. package/dist/ui/Slider.d.ts.map +0 -1
  238. package/dist/ui/Slider.js.map +0 -1
  239. package/dist/ui/Stack.d.ts.map +0 -1
  240. package/dist/ui/Stack.js.map +0 -1
  241. package/dist/ui/TextInput.d.ts.map +0 -1
  242. package/dist/ui/TextInput.js.map +0 -1
  243. package/dist/ui/Textarea.d.ts.map +0 -1
  244. package/dist/ui/Textarea.js.map +0 -1
  245. package/dist/ui/TipButton.d.ts.map +0 -1
  246. package/dist/ui/TipButton.js.map +0 -1
  247. package/dist/ui/index.d.ts.map +0 -1
  248. package/dist/ui/index.js.map +0 -1
  249. package/dist/ui/styles.d.ts.map +0 -1
  250. package/dist/ui/styles.js.map +0 -1
  251. /package/dist/{internal → transport}/detector.d.ts +0 -0
  252. /package/dist/{internal → transport}/detector.js +0 -0
  253. /package/dist/{internal → transport}/directLoad.d.ts +0 -0
  254. /package/dist/{internal → transport}/directLoad.js +0 -0
  255. /package/dist/{internal → transport}/inlineTransport.d.ts +0 -0
  256. /package/dist/{internal → transport}/inlineTransport.js +0 -0
  257. /package/dist/{internal → transport}/requestTimeouts.d.ts +0 -0
  258. /package/dist/{internal → transport}/requestTimeouts.js +0 -0
  259. /package/dist/{internal → transport}/singleton.d.ts +0 -0
  260. /package/dist/{internal → transport}/singleton.js +0 -0
  261. /package/dist/{internal → transport}/transport.d.ts +0 -0
  262. /package/dist/{internal → transport}/transport.js +0 -0
package/README.md CHANGED
@@ -1313,6 +1313,38 @@ For non-React or advanced use, the transport primitives are exported too:
1313
1313
  `readAllowedOriginsFromEnv`, `getTransport`, and `sendTypedRequest`. Hooks are the
1314
1314
  recommended surface; reach for these only when a hook doesn't fit.
1315
1315
 
1316
+ ### How `allowedParentOrigins` entries are read
1317
+
1318
+ Each entry is canonicalised with the URL parser before it is compared to
1319
+ `event.origin`, so these spellings all mean the same thing and all match a host
1320
+ frame at `https://civitai.com`:
1321
+
1322
+ | Written as | Also matches |
1323
+ |---|---|
1324
+ | `https://civitai.com/` | trailing slash is ignored |
1325
+ | `HTTPS://CIVITAI.COM` | scheme and host are case-insensitive |
1326
+ | `https://civitai.com:443`, `http://x:80` | an explicit **default** port is ignored |
1327
+ | `https://пример.com` | normalised to the punycode a browser reports |
1328
+
1329
+ These stay **significant** — they are different origins, not spellings:
1330
+
1331
+ - a **non-default** port: `https://civitai.com:8443` does not match `https://civitai.com`
1332
+ - the scheme: `http://` never matches `https://`
1333
+ - a trailing-dot host: `https://civitai.com.` is its own origin
1334
+ - any host that merely *contains* an allowed one: `https://civitai.com.evil.com`
1335
+ never matches `https://civitai.com`
1336
+
1337
+ An entry that is not a bare origin **throws at construction** rather than being
1338
+ quietly skipped — no scheme (`civitai.com`), a path/query/fragment
1339
+ (`https://civitai.com/embed`), credentials (`https://u:p@civitai.com`), or a
1340
+ scheme with no origin of its own (`file://…`). A silently dropped entry is the
1341
+ failure this rule exists to prevent: the allowlist would be missing an origin
1342
+ you believe is on it.
1343
+
1344
+ If `BLOCK_INIT` never lands, the 10s timeout error names the origins that
1345
+ actually arrived and were rejected, next to the allowlist it checked them
1346
+ against — start there.
1347
+
1316
1348
  ## The `/testing` subexport
1317
1349
 
1318
1350
  `@civitai/blocks-react/testing` is the **host-simulation** entry point: it stands
@@ -1586,8 +1618,8 @@ Runnable, minimal blocks — one per feature, each with its own README:
1586
1618
 
1587
1619
  | `@civitai/blocks-react` | pairs with `@civitai/app-sdk` | adds |
1588
1620
  |---|---|---|
1589
- | `0.50.x` | `^0.40.0` | `useCreatePostFromApp()` (`CREATE_POST_FROM_APP`) + the `posts:write:self` scope. 🔴 Same wide `peerDependencies` floor as the row below, so **npm will not warn you**: pairing this with an SDK below `0.40.0` fails at `tsc` with `Cannot find name 'BlockCreatePostHostError'`, not at install. |
1590
- | `0.48.x` | `^0.38.0` | `useCollectionFollow()` + `<FollowButton>` / `<TipButton>` (`SET_COLLECTION_FOLLOW`). 🔴 The `peerDependencies` floor stays the deliberately-wide `>=0.29.0 <1.0.0` (#206 — a per-minor floor forced a major on consumers), so **npm will not warn you**: pairing this with an SDK below `0.38.0` fails at `tsc` with `Cannot find name 'BlockCollectionFollowErrorCode'`, not at install. |
1621
+ | `0.50.x` | `^0.40.0` | `useCreatePostFromApp()` (`CREATE_POST_FROM_APP`) + the `posts:write:self` scope. Pairing `0.50.x` with an SDK below `0.40.0` fails at `tsc` with `Cannot find name 'BlockCreatePostHostError'`, not at install. |
1622
+ | `0.48.x` | `^0.38.0` | `useCollectionFollow()` + `<FollowButton>` / `<TipButton>` (`SET_COLLECTION_FOLLOW`). Pairing `0.48.x` with an SDK below `0.38.0` fails at `tsc` with `Cannot find name 'BlockCollectionFollowErrorCode'`, not at install — the floor those releases declared was deliberately wide (#206 — a per-minor floor forced a major on consumers). |
1591
1623
  | `0.36.x` | `^0.27.0` | auto-installs the SDK's opaque-origin web-storage shim (`@civitai/app-sdk/safe-storage`) on import |
1592
1624
  | `0.29.x` | `^0.24.0` | `useAppWorkflows()` — app generator subqueue read + cancel (`QUERY_APP_WORKFLOWS` / `CANCEL_APP_WORKFLOW`) |
1593
1625
  | `0.27.x`–`0.28.x` | `^0.23.0` | async-scan image upload; transport validators for all `SHARED_*` / `APP_STORAGE_*` / picker replies |
@@ -30,7 +30,8 @@ export declare const MAX_GENERATION_RESOURCE_IDS = 30;
30
30
  * Pure + deterministic. `base` defaults to {@link GENERATION_RESOURCES_API_BASE}.
31
31
  */
32
32
  export declare function buildGenerationResourcesUrl(versionIds: number[], base?: string): string;
33
- interface RawGenerationResource {
33
+ /** One row of the rehydrate response. See the note above: wire shape, not a contract. */
34
+ export interface RawGenerationResource {
34
35
  versionId?: number;
35
36
  modelId?: number;
36
37
  modelName?: string;
@@ -43,7 +44,8 @@ interface RawGenerationResource {
43
44
  trainedWords?: string[];
44
45
  clipSkip?: number | null;
45
46
  }
46
- interface RawGenerationResourcesResponse {
47
+ /** The rehydrate response body — the parameter {@link responseToResources} takes. */
48
+ export interface RawGenerationResourcesResponse {
47
49
  items?: RawGenerationResource[];
48
50
  maturity?: {
49
51
  browsingLevel?: number;
@@ -58,5 +60,4 @@ interface RawGenerationResourcesResponse {
58
60
  * `projectSafeGenerationResource`. Never throws.
59
61
  */
60
62
  export declare function responseToResources(raw: RawGenerationResourcesResponse | null | undefined): BlockResourceInfo[];
61
- export {};
62
63
  //# sourceMappingURL=generationResources.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=returnTypeLedger.d.ts.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=returnTypeLedger.js.map
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
3
  import { throwOnFailedReply, throwOnReplyError } from '../internal/replyError.js';
4
- import { sendTypedRequest } from '../internal/transport.js';
4
+ import { sendTypedRequest } from '../transport/transport.js';
5
5
  /**
6
6
  * Per-(block instance, viewer) KV datastore. Calls flow through the host's
7
7
  * postMessage bridge — the block never sees the apps DB credentials.
@@ -43,8 +43,11 @@ export interface UseAppWorkflows {
43
43
  *
44
44
  * Fetches on mount and whenever `params` change (by value), and exposes `refetch`.
45
45
  * A host that never answers surfaces as an `error` after the transport's request
46
- * timeout — the hook never hangs. Late responses that arrive after unmount are
47
- * ignored.
46
+ * timeout — the hook never hangs. Only the LATEST list request may write state:
47
+ * a reply superseded by a newer `refetch` / params change — or one that lands
48
+ * after unmount — is dropped (#392). `cancel` is caller-driven and keeps its own
49
+ * mount guard: its functional `setWorkflows` splice is order-independent, so it
50
+ * is not a sequencing hazard.
48
51
  *
49
52
  * @example
50
53
  * const { workflows, cursor, loading, error, cancel } = useAppWorkflows({ limit: 20 });
@@ -1,6 +1,7 @@
1
1
  import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
3
- import { sendTypedRequest } from '../internal/transport.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
+ import { sendTypedRequest } from '../transport/transport.js';
4
+ import { useRequestSequencer } from './useRequestSequencer.js';
4
5
  /**
5
6
  * Read (and cancel within) the calling app's OWN generator SUBQUEUE — the
6
7
  * tag-scoped list of generations THIS app produced for the viewer, newest-first —
@@ -12,8 +13,11 @@ import { sendTypedRequest } from '../internal/transport.js';
12
13
  *
13
14
  * Fetches on mount and whenever `params` change (by value), and exposes `refetch`.
14
15
  * A host that never answers surfaces as an `error` after the transport's request
15
- * timeout — the hook never hangs. Late responses that arrive after unmount are
16
- * ignored.
16
+ * timeout — the hook never hangs. Only the LATEST list request may write state:
17
+ * a reply superseded by a newer `refetch` / params change — or one that lands
18
+ * after unmount — is dropped (#392). `cancel` is caller-driven and keeps its own
19
+ * mount guard: its functional `setWorkflows` splice is order-independent, so it
20
+ * is not a sequencing hazard.
17
21
  *
18
22
  * @example
19
23
  * const { workflows, cursor, loading, error, cancel } = useAppWorkflows({ limit: 20 });
@@ -24,6 +28,9 @@ export function useAppWorkflows(params) {
24
28
  const [cursor, setCursor] = useState(null);
25
29
  const [loading, setLoading] = useState(true);
26
30
  const [error, setError] = useState(null);
31
+ // `cancel` is caller-driven, not auto-issued, and its state write is a
32
+ // functional splice that is safe in any order — so it keeps a plain mount
33
+ // guard rather than joining the list request's sequence.
27
34
  const mountedRef = useRef(true);
28
35
  useEffect(() => {
29
36
  mountedRef.current = true;
@@ -31,18 +38,22 @@ export function useAppWorkflows(params) {
31
38
  mountedRef.current = false;
32
39
  };
33
40
  }, []);
41
+ // Latest-wins + unmount guard for the LIST request (#392): a reply may write
42
+ // state only if it answers the request this hook is CURRENTLY waiting for.
43
+ const seq = useRequestSequencer();
34
44
  // Serialize the params to a stable key so `refetch`'s identity only changes
35
45
  // when the params VALUE changes (not on every render's fresh object). The
36
46
  // callback re-parses the key so it closes over NOTHING but the key.
37
47
  const paramsKey = params ? JSON.stringify(params) : '';
38
48
  const refetch = useCallback(() => {
49
+ const token = seq.begin();
39
50
  setLoading(true);
40
51
  setError(null);
41
52
  const parsed = paramsKey ? JSON.parse(paramsKey) : undefined;
42
53
  const payload = parsed ? { params: parsed } : {};
43
54
  sendTypedRequest(getTransport(), { type: 'QUERY_APP_WORKFLOWS', payload }, 'APP_WORKFLOWS_RESULT')
44
55
  .then((result) => {
45
- if (!mountedRef.current)
56
+ if (!seq.isCurrent(token))
46
57
  return;
47
58
  if (result.error || !result.result) {
48
59
  // `||`, not `??`: the reply validator gates `error` on SHAPE only, so a
@@ -57,12 +68,12 @@ export function useAppWorkflows(params) {
57
68
  setLoading(false);
58
69
  })
59
70
  .catch((err) => {
60
- if (!mountedRef.current)
71
+ if (!seq.isCurrent(token))
61
72
  return;
62
73
  setError(err instanceof Error ? err : new Error(String(err)));
63
74
  setLoading(false);
64
75
  });
65
- }, [paramsKey]);
76
+ }, [paramsKey, seq]);
66
77
  useEffect(() => {
67
78
  refetch();
68
79
  }, [refetch]);
@@ -1,3 +1,7 @@
1
+ /** What {@link useBlockAnalytics} returns. */
2
+ export interface UseBlockAnalytics {
3
+ track: (eventName: string, properties?: Record<string, unknown>) => void;
4
+ }
1
5
  /**
2
6
  * Fire-and-forget analytics tracking. The host forwards events to its
3
7
  * analytics pipeline (ClickHouse in production); the block doesn't see
@@ -7,7 +11,5 @@
7
11
  * const { track } = useBlockAnalytics();
8
12
  * track('generate_clicked', { modelId });
9
13
  */
10
- export declare function useBlockAnalytics(): {
11
- track: (eventName: string, properties?: Record<string, unknown>) => void;
12
- };
14
+ export declare function useBlockAnalytics(): UseBlockAnalytics;
13
15
  //# sourceMappingURL=useBlockAnalytics.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { useCallback } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
3
  /**
4
4
  * Fire-and-forget analytics tracking. The host forwards events to its
5
5
  * analytics pipeline (ClickHouse in production); the block doesn't see
@@ -23,6 +23,12 @@ export type BlockSizeTier = 'base' | BreakpointKey;
23
23
  * `display: none` ancestor) resolves to `'base'` — the most conservative tier.
24
24
  */
25
25
  export declare function resolveBlockTier(width: number): BlockSizeTier;
26
+ /**
27
+ * What {@link useBlockBreakpoint} returns. An alias for the long-standing
28
+ * {@link BlockBreakpoint} name, which stays exported and unchanged — see
29
+ * `./returnTypeLedger.js`.
30
+ */
31
+ export type UseBlockBreakpoint = BlockBreakpoint;
26
32
  export interface BlockBreakpoint {
27
33
  /**
28
34
  * The current tier. `'base'` while unmeasured — see `measured`.
@@ -88,5 +94,5 @@ export interface BlockBreakpoint {
88
94
  * </div>
89
95
  * );
90
96
  */
91
- export declare function useBlockBreakpoint(ref?: RefObject<HTMLElement | null>): BlockBreakpoint;
97
+ export declare function useBlockBreakpoint(ref?: RefObject<HTMLElement | null>): UseBlockBreakpoint;
92
98
  //# sourceMappingURL=useBlockBreakpoint.d.ts.map
@@ -1,9 +1,20 @@
1
- import type { BlockSnapshot } from '../internal/transport.js';
1
+ import type { BlockSnapshot } from '../transport/transport.js';
2
2
  /**
3
3
  * Subscribe a hook to the singleton transport. All hooks build on this —
4
4
  * `useSyncExternalStore` gives concurrent-mode safety and minimal re-renders.
5
5
  */
6
6
  declare function useTransportSnapshot(): BlockSnapshot;
7
+ /**
8
+ * What {@link useBlockContext} returns — the projection of {@link BlockSnapshot}
9
+ * the hook exposes.
10
+ *
11
+ * 🔴 DERIVED FROM `BlockSnapshot`, NOT RETYPED. A hand-written copy of these ten
12
+ * fields would drift silently the first time the snapshot's shape moved; the
13
+ * `Pick` re-resolves against the transport's own type on every build. Before
14
+ * #380 this expression was written inline on the hook's return annotation, so
15
+ * a consumer wrapping `useBlockContext()` had nothing to name.
16
+ */
17
+ export type UseBlockContext = Pick<BlockSnapshot, 'ready' | 'renderMode' | 'context' | 'token' | 'settings' | 'viewer' | 'theme' | 'blockId' | 'blockInstanceId' | 'appId'>;
7
18
  /**
8
19
  * Primary hook for a block app. Returns the full per-instance context
9
20
  * delivered by the host plus a `ready` gate the UI should respect — fields
@@ -34,7 +45,7 @@ declare function useTransportSnapshot(): BlockSnapshot;
34
45
  * // scheduled for removal. This snippet used to do exactly that.
35
46
  * return <div data-theme={theme}>{isSignedIn(viewer) ? 'Hi there' : 'Hi anon'}</div>;
36
47
  */
37
- export declare function useBlockContext(): Pick<BlockSnapshot, 'ready' | 'renderMode' | 'context' | 'token' | 'settings' | 'viewer' | 'theme' | 'blockId' | 'blockInstanceId' | 'appId'>;
48
+ export declare function useBlockContext(): UseBlockContext;
38
49
  /** Re-exported so other hooks in this package can share the subscription. */
39
50
  export { useTransportSnapshot };
40
51
  //# sourceMappingURL=useBlockContext.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { useSyncExternalStore } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
3
  /**
4
4
  * Subscribe a hook to the singleton transport. All hooks build on this —
5
5
  * `useSyncExternalStore` gives concurrent-mode safety and minimal re-renders.
@@ -1,4 +1,10 @@
1
1
  import { type RefObject } from 'react';
2
+ /**
3
+ * What {@link useBlockResize} returns: nothing. It is named anyway so the
4
+ * convention has NO exceptions to remember — see `./returnTypeLedger.js`. A wrapper
5
+ * that forwards this hook's result can still annotate it.
6
+ */
7
+ export type UseBlockResize = void;
2
8
  /**
3
9
  * Observes the referenced element's height and asks the host to resize on
4
10
  * every change. Attach to the block's root DOM element.
@@ -42,5 +48,5 @@ import { type RefObject } from 'react';
42
48
  * if (!ready) return <div>Loading…</div>; // no ref needed on this branch
43
49
  * return <div ref={rootRef}>…</div>;
44
50
  */
45
- export declare function useBlockResize(ref: RefObject<HTMLElement | null>): void;
51
+ export declare function useBlockResize(ref: RefObject<HTMLElement | null>): UseBlockResize;
46
52
  //# sourceMappingURL=useBlockResize.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useRef } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
3
  /**
4
4
  * Observes the referenced element's height and asks the host to resize on
5
5
  * every change. Attach to the block's root DOM element.
@@ -1,4 +1,9 @@
1
1
  import type { BlockSettings } from '@civitai/app-sdk/blocks';
2
+ /**
3
+ * What {@link useBlockSettings} returns. An alias — see
4
+ * `./returnTypeLedger.js` for why every hook on the entry has one of these.
5
+ */
6
+ export type UseBlockSettings = BlockSettings;
2
7
  /**
3
8
  * Shorthand for `useBlockContext().settings`. Returns the publisher- and
4
9
  * user-controlled settings the host forwarded at init. Read-only from the
@@ -8,5 +13,5 @@ import type { BlockSettings } from '@civitai/app-sdk/blocks';
8
13
  * @example
9
14
  * const { publisherSettings, userSettings } = useBlockSettings();
10
15
  */
11
- export declare function useBlockSettings(): BlockSettings;
16
+ export declare function useBlockSettings(): UseBlockSettings;
12
17
  //# sourceMappingURL=useBlockSettings.d.ts.map
@@ -1,4 +1,9 @@
1
1
  import type { Theme } from '@civitai/app-sdk/blocks';
2
+ /**
3
+ * What {@link useBlockTheme} returns. An alias — see `./returnTypeLedger.js` for why
4
+ * every hook on the entry has one of these.
5
+ */
6
+ export type UseBlockTheme = Theme;
2
7
  /**
3
8
  * The host's CURRENT site theme (`'light' | 'dark'`), kept live for the whole
4
9
  * life of the block ON THE IFRAME TRANSPORT.
@@ -35,5 +40,5 @@ import type { Theme } from '@civitai/app-sdk/blocks';
35
40
  * const theme = useBlockTheme();
36
41
  * return <div data-theme={theme}>…</div>;
37
42
  */
38
- export declare function useBlockTheme(): Theme;
43
+ export declare function useBlockTheme(): UseBlockTheme;
39
44
  //# sourceMappingURL=useBlockTheme.d.ts.map
@@ -1,4 +1,11 @@
1
1
  import type { BlockToken } from '@civitai/app-sdk/blocks';
2
+ /**
3
+ * What {@link useBlockToken} returns: the live block token plus a manual
4
+ * `refresh`. See `./returnTypeLedger.js`.
5
+ */
6
+ export type UseBlockToken = BlockToken & {
7
+ refresh: () => Promise<void>;
8
+ };
2
9
  /**
3
10
  * Returns the current block-scoped JWT and keeps it fresh.
4
11
  *
@@ -17,9 +24,7 @@ import type { BlockToken } from '@civitai/app-sdk/blocks';
17
24
  * const { raw, scopes, expiresAt, buzzBudget, refresh } = useBlockToken();
18
25
  * // after a 401: await refresh(); then retry the request once with the new `raw`.
19
26
  */
20
- export declare function useBlockToken(): BlockToken & {
21
- refresh: () => Promise<void>;
22
- };
27
+ export declare function useBlockToken(): UseBlockToken;
23
28
  /**
24
29
  * Mint/await a token refresh for one block instance, coalescing concurrent calls
25
30
  * for the SAME `blockInstanceId`. Exported for the inline-mode multi-instance
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
3
- import { sendTypedRequest } from '../internal/transport.js';
2
+ import { getTransport } from '../transport/singleton.js';
3
+ import { sendTypedRequest } from '../transport/transport.js';
4
4
  import { useTransportSnapshot } from './useBlockContext.js';
5
5
  /** Refresh fires `REFRESH_LEAD_MS` before the token's `expiresAt`. */
6
6
  const REFRESH_LEAD_MS = 2 * 60 * 1000;
@@ -29,8 +29,9 @@ export interface UseBuzzAccounts {
29
29
  * the creator payout pools — as `{ accountType, balance }` rows.
30
30
  *
31
31
  * Fetches once on mount and exposes `refetch`. A host that never answers surfaces
32
- * as an `error` after the transport timeout; late post-unmount responses are
33
- * ignored.
32
+ * as an `error` after the transport timeout. Only the LATEST request may write
33
+ * state: a reply superseded by a newer `refetch` — or one that lands after
34
+ * unmount — is dropped (#392).
34
35
  *
35
36
  * @example
36
37
  * const { accounts, loading, error } = useBuzzAccounts();
@@ -1,6 +1,7 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
3
- import { sendTypedRequest } from '../internal/transport.js';
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { getTransport } from '../transport/singleton.js';
3
+ import { sendTypedRequest } from '../transport/transport.js';
4
+ import { useRequestSequencer } from './useRequestSequencer.js';
4
5
  /**
5
6
  * Read the signed-in viewer's ALL-pool Buzz balances through the host-mediated
6
7
  * `GET_BUZZ_ACCOUNTS` → `BUZZ_ACCOUNTS_RESULT` bridge. Token-bound: the host
@@ -10,8 +11,9 @@ import { sendTypedRequest } from '../internal/transport.js';
10
11
  * the creator payout pools — as `{ accountType, balance }` rows.
11
12
  *
12
13
  * Fetches once on mount and exposes `refetch`. A host that never answers surfaces
13
- * as an `error` after the transport timeout; late post-unmount responses are
14
- * ignored.
14
+ * as an `error` after the transport timeout. Only the LATEST request may write
15
+ * state: a reply superseded by a newer `refetch` — or one that lands after
16
+ * unmount — is dropped (#392).
15
17
  *
16
18
  * @example
17
19
  * const { accounts, loading, error } = useBuzzAccounts();
@@ -20,19 +22,18 @@ export function useBuzzAccounts() {
20
22
  const [accounts, setAccounts] = useState(null);
21
23
  const [loading, setLoading] = useState(true);
22
24
  const [error, setError] = useState(null);
23
- const mountedRef = useRef(true);
24
- useEffect(() => {
25
- mountedRef.current = true;
26
- return () => {
27
- mountedRef.current = false;
28
- };
29
- }, []);
25
+ // Latest-wins + unmount guard in one predicate (#392): a reply may write state
26
+ // only if it answers the request this hook is CURRENTLY waiting for. A bare
27
+ // mount check would let a superseded `refetch`'s slow reply overwrite newer
28
+ // state — nothing unmounted, so it passes.
29
+ const seq = useRequestSequencer();
30
30
  const refetch = useCallback(() => {
31
+ const token = seq.begin();
31
32
  setLoading(true);
32
33
  setError(null);
33
34
  sendTypedRequest(getTransport(), { type: 'GET_BUZZ_ACCOUNTS', payload: {} }, 'BUZZ_ACCOUNTS_RESULT')
34
35
  .then((result) => {
35
- if (!mountedRef.current)
36
+ if (!seq.isCurrent(token))
36
37
  return;
37
38
  if (result.error || !result.result) {
38
39
  // `||`, not `??`: the reply validator gates `error` on SHAPE only, so a
@@ -46,12 +47,12 @@ export function useBuzzAccounts() {
46
47
  setLoading(false);
47
48
  })
48
49
  .catch((err) => {
49
- if (!mountedRef.current)
50
+ if (!seq.isCurrent(token))
50
51
  return;
51
52
  setError(err instanceof Error ? err : new Error(String(err)));
52
53
  setLoading(false);
53
54
  });
54
- }, []);
55
+ }, [seq]);
55
56
  useEffect(() => {
56
57
  refetch();
57
58
  }, [refetch]);
@@ -35,8 +35,9 @@ export interface UseBuzzBalance {
35
35
  *
36
36
  * Fetches once on mount and exposes `refetch` for on-demand refreshes (e.g.
37
37
  * after a generation debits the balance). A host that never answers surfaces as
38
- * an `error` after the transport's request timeout — the hook never hangs. Late
39
- * responses that arrive after unmount are ignored (no state update).
38
+ * an `error` after the transport's request timeout — the hook never hangs. Only
39
+ * the LATEST request may write state: a reply superseded by a newer `refetch` —
40
+ * or one that lands after unmount — is dropped (#392).
40
41
  *
41
42
  * @example
42
43
  * const { balance, loading, error, refetch } = useBuzzBalance();
@@ -1,6 +1,7 @@
1
- import { useCallback, useEffect, useRef, useState } from 'react';
2
- import { getTransport } from '../internal/singleton.js';
3
- import { sendTypedRequest } from '../internal/transport.js';
1
+ import { useCallback, useEffect, useState } from 'react';
2
+ import { getTransport } from '../transport/singleton.js';
3
+ import { sendTypedRequest } from '../transport/transport.js';
4
+ import { useRequestSequencer } from './useRequestSequencer.js';
4
5
  /**
5
6
  * Read the signed-in viewer's per-pool Buzz balance ({ blue, green, yellow })
6
7
  * through the host-mediated `GET_BUZZ_BALANCE` → `BUZZ_BALANCE_RESULT` bridge.
@@ -11,8 +12,9 @@ import { sendTypedRequest } from '../internal/transport.js';
11
12
  *
12
13
  * Fetches once on mount and exposes `refetch` for on-demand refreshes (e.g.
13
14
  * after a generation debits the balance). A host that never answers surfaces as
14
- * an `error` after the transport's request timeout — the hook never hangs. Late
15
- * responses that arrive after unmount are ignored (no state update).
15
+ * an `error` after the transport's request timeout — the hook never hangs. Only
16
+ * the LATEST request may write state: a reply superseded by a newer `refetch` —
17
+ * or one that lands after unmount — is dropped (#392).
16
18
  *
17
19
  * @example
18
20
  * const { balance, loading, error, refetch } = useBuzzBalance();
@@ -24,22 +26,18 @@ export function useBuzzBalance() {
24
26
  const [balance, setBalance] = useState(null);
25
27
  const [loading, setLoading] = useState(true);
26
28
  const [error, setError] = useState(null);
27
- // Guards against a late response resolving after the component unmounted
28
- // (React would warn about a state update on an unmounted component, and the
29
- // work is wasted anyway).
30
- const mountedRef = useRef(true);
31
- useEffect(() => {
32
- mountedRef.current = true;
33
- return () => {
34
- mountedRef.current = false;
35
- };
36
- }, []);
29
+ // Latest-wins + unmount guard in one predicate (#392): a reply may write state
30
+ // only if it answers the request this hook is CURRENTLY waiting for. A bare
31
+ // mount check would let a superseded `refetch`'s slow reply overwrite newer
32
+ // state — nothing unmounted, so it passes.
33
+ const seq = useRequestSequencer();
37
34
  const refetch = useCallback(() => {
35
+ const token = seq.begin();
38
36
  setLoading(true);
39
37
  setError(null);
40
38
  sendTypedRequest(getTransport(), { type: 'GET_BUZZ_BALANCE', payload: {} }, 'BUZZ_BALANCE_RESULT')
41
39
  .then((result) => {
42
- if (!mountedRef.current)
40
+ if (!seq.isCurrent(token))
43
41
  return;
44
42
  if (result.error || !result.balance) {
45
43
  // `||`, not `??`: the reply validator gates `error` on SHAPE only, so a
@@ -53,12 +51,12 @@ export function useBuzzBalance() {
53
51
  setLoading(false);
54
52
  })
55
53
  .catch((err) => {
56
- if (!mountedRef.current)
54
+ if (!seq.isCurrent(token))
57
55
  return;
58
56
  setError(err instanceof Error ? err : new Error(String(err)));
59
57
  setLoading(false);
60
58
  });
61
- }, []);
59
+ }, [seq]);
62
60
  useEffect(() => {
63
61
  refetch();
64
62
  }, [refetch]);
@@ -1,3 +1,10 @@
1
+ /** What {@link useBuzzPurchase} returns. */
2
+ export interface UseBuzzPurchase {
3
+ openPurchaseModal: (suggestedAmount?: number) => Promise<{
4
+ purchased: boolean;
5
+ newBalance?: number;
6
+ }>;
7
+ }
1
8
  /**
2
9
  * Opens the Civitai Buzz purchase modal on the host. Resolves with the
3
10
  * outcome when the user closes the modal — `purchased: true` means the
@@ -15,10 +22,5 @@
15
22
  * const { purchased, newBalance } = await openPurchaseModal(suggestedAmount);
16
23
  * if (purchased) { /* retry the generation *\/ }
17
24
  */
18
- export declare function useBuzzPurchase(): {
19
- openPurchaseModal: (suggestedAmount?: number) => Promise<{
20
- purchased: boolean;
21
- newBalance?: number;
22
- }>;
23
- };
25
+ export declare function useBuzzPurchase(): UseBuzzPurchase;
24
26
  //# sourceMappingURL=useBuzzPurchase.d.ts.map
@@ -1,7 +1,7 @@
1
1
  import { useCallback } from 'react';
2
- import { HUMAN_INTERACTION_TIMEOUT_MS } from '../internal/requestTimeouts.js';
3
- import { getTransport } from '../internal/singleton.js';
4
- import { sendTypedRequest } from '../internal/transport.js';
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
  * Opens the Civitai Buzz purchase modal on the host. Resolves with the
7
7
  * outcome when the user closes the modal — `purchased: true` means the
@@ -41,8 +41,10 @@ export interface UseBuzzTransactions {
41
41
  *
42
42
  * Fetches on mount and whenever `params` change (by value), and exposes `refetch`.
43
43
  * A host that never answers surfaces as an `error` after the transport's request
44
- * timeout — the hook never hangs. Late responses that arrive after unmount are
45
- * ignored. Transaction `date`s are rehydrated to `Date`; `cursor` is normalized
44
+ * timeout — the hook never hangs. Only the LATEST request may write state: page
45
+ * 1's slow reply cannot repaint (or rewind `cursor` behind) the page 2 a newer
46
+ * request already painted, and a reply that lands after unmount is dropped
47
+ * (#392). Transaction `date`s are rehydrated to `Date`; `cursor` is normalized
46
48
  * to an ISO string for round-tripping.
47
49
  *
48
50
  * @example