sygnal 5.3.7 → 6.0.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 (438) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +77 -47
  3. package/dist/astro/client.cjs.js +33 -8114
  4. package/dist/astro/client.cjs.js.map +1 -0
  5. package/dist/astro/client.mjs +33 -8114
  6. package/dist/astro/client.mjs.map +1 -0
  7. package/dist/astro/index.cjs.js +1039 -9
  8. package/dist/astro/index.cjs.js.map +1 -0
  9. package/dist/astro/index.mjs +1038 -9
  10. package/dist/astro/index.mjs.map +1 -0
  11. package/dist/astro/server.cjs.js +3400 -166
  12. package/dist/astro/server.cjs.js.map +1 -0
  13. package/dist/astro/server.mjs +3400 -166
  14. package/dist/astro/server.mjs.map +1 -0
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +4033 -0
  20. package/dist/diagnostics.cjs.js.map +1 -0
  21. package/dist/diagnostics.esm.js +4003 -0
  22. package/dist/diagnostics.esm.js.map +1 -0
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +15283 -6279
  67. package/dist/index.cjs.js.map +1 -0
  68. package/dist/index.d.ts +3739 -370
  69. package/dist/index.esm.js +15230 -6274
  70. package/dist/index.esm.js.map +1 -0
  71. package/dist/jsx-dev-runtime.cjs.js +27 -329
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -0
  73. package/dist/jsx-dev-runtime.esm.js +20 -322
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -0
  75. package/dist/jsx-runtime.cjs.js +27 -329
  76. package/dist/jsx-runtime.cjs.js.map +1 -0
  77. package/dist/jsx-runtime.esm.js +20 -322
  78. package/dist/jsx-runtime.esm.js.map +1 -0
  79. package/dist/jsx.cjs.js +11 -309
  80. package/dist/jsx.cjs.js.map +1 -0
  81. package/dist/jsx.esm.js +9 -310
  82. package/dist/jsx.esm.js.map +1 -0
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +2 -1
  89. package/dist/sygnal.min.js.map +1 -0
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +5 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -0
  108. package/dist/vike/ClientOnly.mjs +5 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -0
  110. package/dist/vike/{+config.js → config/+config.js} +10 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +166 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -0
  115. package/dist/vike/onRenderClient.mjs +166 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -0
  117. package/dist/vike/onRenderHtml.cjs.js +60 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -0
  119. package/dist/vike/onRenderHtml.mjs +61 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -0
  121. package/dist/vite/plugin.cjs.js +926 -65
  122. package/dist/vite/plugin.cjs.js.map +1 -0
  123. package/dist/vite/plugin.mjs +925 -65
  124. package/dist/vite/plugin.mjs.map +1 -0
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +313 -0
  130. package/package.json +109 -16
  131. package/src/astro/client.ts +40 -18
  132. package/src/astro/index.d.ts +48 -1
  133. package/src/astro/index.ts +106 -9
  134. package/src/astro/server.ts +11 -3
  135. package/src/collection.ts +6 -90
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +13 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +76 -0
  162. package/src/cycle/dom/controlledInputModule.ts +50 -0
  163. package/src/cycle/dom/enrichEventStream.ts +29 -45
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -57
  167. package/src/cycle/dom/mockDOMSource.ts +83 -9
  168. package/src/cycle/dom/modules.ts +21 -4
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +20 -4
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +109 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +67 -0
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +250 -0
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +83 -0
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +202 -0
  206. package/src/extra/diagnostics/checks/inspect.ts +427 -0
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +56 -0
  210. package/src/extra/diagnostics/checks/public.d.ts +302 -0
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
  214. package/src/extra/diagnostics/checks/shared.ts +203 -0
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +148 -0
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +55 -0
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +121 -0
  225. package/src/extra/diagnostics/codes.ts +477 -0
  226. package/src/extra/diagnostics/index.ts +353 -0
  227. package/src/extra/diagnostics/legacy.ts +76 -0
  228. package/src/extra/driverFactories.ts +172 -60
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/eventDriver.ts +4 -0
  231. package/src/extra/fetchDriver.ts +519 -0
  232. package/src/extra/flatten.ts +75 -0
  233. package/src/extra/focusWithin.ts +27 -0
  234. package/src/extra/form.ts +268 -0
  235. package/src/extra/formHelpers.ts +139 -0
  236. package/src/extra/head.ts +106 -0
  237. package/src/extra/hmr.ts +2 -3
  238. package/src/extra/owned.ts +24 -0
  239. package/src/extra/pager.ts +50 -0
  240. package/src/extra/persist.ts +133 -0
  241. package/src/extra/pwa.ts +1 -1
  242. package/src/extra/queryCache.ts +107 -0
  243. package/src/extra/reducers.ts +13 -6
  244. package/src/extra/reduxDevtools.ts +92 -0
  245. package/src/extra/replies.ts +54 -0
  246. package/src/extra/router.ts +323 -0
  247. package/src/extra/run.ts +94 -210
  248. package/src/extra/selection.ts +76 -0
  249. package/src/extra/socketDriver.ts +265 -0
  250. package/src/extra/sortable.ts +377 -0
  251. package/src/extra/ssr.ts +427 -160
  252. package/src/extra/standardSchema.ts +27 -0
  253. package/src/extra/testing.ts +3158 -190
  254. package/src/extra/timers.ts +95 -0
  255. package/src/extra/undo.ts +261 -0
  256. package/src/extra/viewTransitions.ts +38 -0
  257. package/src/extra/virtual.ts +513 -0
  258. package/src/extra/widget.ts +201 -0
  259. package/src/extra/xstreamExtras.ts +269 -0
  260. package/src/index.d.ts +3012 -87
  261. package/src/index.ts +32 -10
  262. package/src/jsx-runtime.ts +15 -1
  263. package/src/jsx.ts +2 -1
  264. package/src/lazy.ts +50 -7
  265. package/src/portal.ts +3 -1
  266. package/src/pragma/index.ts +262 -134
  267. package/src/react-peers.d.ts +5 -0
  268. package/src/react.d.ts +43 -0
  269. package/src/react.ts +110 -0
  270. package/src/shared.ts +48 -0
  271. package/src/slot.ts +1 -1
  272. package/src/suspense.ts +3 -1
  273. package/src/switchable.ts +6 -119
  274. package/src/transition.ts +3 -1
  275. package/src/ui/accordion.ts +64 -0
  276. package/src/ui/dialog.ts +129 -0
  277. package/src/ui/disclosure.ts +35 -0
  278. package/src/ui/popover.ts +45 -0
  279. package/src/ui/shared.ts +94 -0
  280. package/src/ui/tabs.ts +82 -0
  281. package/src/ui/toaster.ts +198 -0
  282. package/src/ui/tooltip.ts +70 -0
  283. package/src/ui/zag/combobox.ts +115 -0
  284. package/src/ui/zag/menu.ts +40 -0
  285. package/src/ui/zag/select.ts +54 -0
  286. package/src/ui/zag/shared.ts +43 -0
  287. package/src/ui-combobox.d.ts +20 -0
  288. package/src/ui-combobox.ts +6 -0
  289. package/src/ui-menu.d.ts +30 -0
  290. package/src/ui-menu.ts +6 -0
  291. package/src/ui-select.d.ts +17 -0
  292. package/src/ui-select.ts +6 -0
  293. package/src/ui-zag-types.d.ts +42 -0
  294. package/src/ui.d.ts +208 -0
  295. package/src/ui.ts +15 -0
  296. package/src/vike/+config.ts +9 -1
  297. package/src/vike/ClientOnly.ts +1 -1
  298. package/src/vike/onRenderClient.ts +144 -96
  299. package/src/vike/onRenderHtml.ts +69 -26
  300. package/src/vike/types.ts +27 -4
  301. package/src/vite/globalthis-shim.ts +18 -0
  302. package/src/vite/globalthis.ts +56 -0
  303. package/src/vite/plugin.d.ts +114 -2
  304. package/src/vite/plugin.ts +920 -56
  305. package/src/zag.d.ts +64 -0
  306. package/src/zag.ts +238 -0
  307. package/dist/astro/astro/client.d.ts +0 -5
  308. package/dist/astro/astro/index.d.ts +0 -16
  309. package/dist/astro/astro/server.d.ts +0 -12
  310. package/dist/astro/client.d.ts +0 -5
  311. package/dist/astro/client.esm.js +0 -4665
  312. package/dist/astro/collection.d.ts +0 -12
  313. package/dist/astro/component.d.ts +0 -22
  314. package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
  315. package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
  316. package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
  317. package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
  318. package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
  319. package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
  320. package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
  321. package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
  322. package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
  323. package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
  324. package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
  325. package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
  326. package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
  327. package/dist/astro/cycle/dom/index.d.ts +0 -8
  328. package/dist/astro/cycle/dom/isolate.d.ts +0 -9
  329. package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
  330. package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
  331. package/dist/astro/cycle/dom/modules.d.ts +0 -5
  332. package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
  333. package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
  334. package/dist/astro/cycle/dom/thunk.d.ts +0 -11
  335. package/dist/astro/cycle/dom/utils.d.ts +0 -8
  336. package/dist/astro/cycle/isolate/index.d.ts +0 -42
  337. package/dist/astro/cycle/run/adapt.d.ts +0 -6
  338. package/dist/astro/cycle/run/index.d.ts +0 -36
  339. package/dist/astro/cycle/run/internals.d.ts +0 -8
  340. package/dist/astro/cycle/run/types.d.ts +0 -49
  341. package/dist/astro/cycle/state/Collection.d.ts +0 -29
  342. package/dist/astro/cycle/state/StateSource.d.ts +0 -20
  343. package/dist/astro/cycle/state/index.d.ts +0 -5
  344. package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
  345. package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
  346. package/dist/astro/cycle/state/types.d.ts +0 -18
  347. package/dist/astro/cycle/state/withState.d.ts +0 -17
  348. package/dist/astro/extra/classes.d.ts +0 -7
  349. package/dist/astro/extra/devtools.d.ts +0 -78
  350. package/dist/astro/extra/dragDriver.d.ts +0 -20
  351. package/dist/astro/extra/driverFactories.d.ts +0 -12
  352. package/dist/astro/extra/eventDriver.d.ts +0 -9
  353. package/dist/astro/extra/exactState.d.ts +0 -1
  354. package/dist/astro/extra/hmr.d.ts +0 -17
  355. package/dist/astro/extra/logDriver.d.ts +0 -2
  356. package/dist/astro/extra/processDrag.d.ts +0 -19
  357. package/dist/astro/extra/processForm.d.ts +0 -15
  358. package/dist/astro/extra/run.d.ts +0 -13
  359. package/dist/astro/extra/xstreamCompat.d.ts +0 -5
  360. package/dist/astro/index.d.ts +0 -19
  361. package/dist/astro/index.esm.js +0 -27
  362. package/dist/astro/jsx-dev-runtime.d.ts +0 -1
  363. package/dist/astro/jsx-runtime.d.ts +0 -4
  364. package/dist/astro/jsx.d.ts +0 -2
  365. package/dist/astro/pragma/fn.d.ts +0 -7
  366. package/dist/astro/pragma/index.d.ts +0 -7
  367. package/dist/astro/pragma/is.d.ts +0 -10
  368. package/dist/astro/server.d.ts +0 -12
  369. package/dist/astro/server.esm.js +0 -25
  370. package/dist/astro/switchable.d.ts +0 -7
  371. package/dist/collection.d.ts +0 -12
  372. package/dist/component.d.ts +0 -22
  373. package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
  374. package/dist/cycle/dom/DOMSource.d.ts +0 -11
  375. package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
  376. package/dist/cycle/dom/ElementFinder.d.ts +0 -8
  377. package/dist/cycle/dom/EventDelegator.d.ts +0 -34
  378. package/dist/cycle/dom/IsolateModule.d.ts +0 -23
  379. package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
  380. package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
  381. package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
  382. package/dist/cycle/dom/SymbolTree.d.ts +0 -9
  383. package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
  384. package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
  385. package/dist/cycle/dom/fromEvent.d.ts +0 -6
  386. package/dist/cycle/dom/index.d.ts +0 -8
  387. package/dist/cycle/dom/isolate.d.ts +0 -9
  388. package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
  389. package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
  390. package/dist/cycle/dom/modules.d.ts +0 -5
  391. package/dist/cycle/dom/snabbdom.d.ts +0 -22
  392. package/dist/cycle/dom/styleModule.d.ts +0 -13
  393. package/dist/cycle/dom/thunk.d.ts +0 -11
  394. package/dist/cycle/dom/utils.d.ts +0 -8
  395. package/dist/cycle/isolate/index.d.ts +0 -42
  396. package/dist/cycle/run/adapt.d.ts +0 -6
  397. package/dist/cycle/run/index.d.ts +0 -36
  398. package/dist/cycle/run/internals.d.ts +0 -8
  399. package/dist/cycle/run/types.d.ts +0 -49
  400. package/dist/cycle/state/Collection.d.ts +0 -29
  401. package/dist/cycle/state/StateSource.d.ts +0 -20
  402. package/dist/cycle/state/index.d.ts +0 -5
  403. package/dist/cycle/state/pickCombine.d.ts +0 -3
  404. package/dist/cycle/state/pickMerge.d.ts +0 -3
  405. package/dist/cycle/state/types.d.ts +0 -18
  406. package/dist/cycle/state/withState.d.ts +0 -17
  407. package/dist/extra/classes.d.ts +0 -7
  408. package/dist/extra/devtools.d.ts +0 -78
  409. package/dist/extra/dragDriver.d.ts +0 -20
  410. package/dist/extra/driverFactories.d.ts +0 -12
  411. package/dist/extra/eventDriver.d.ts +0 -9
  412. package/dist/extra/exactState.d.ts +0 -1
  413. package/dist/extra/hmr.d.ts +0 -17
  414. package/dist/extra/logDriver.d.ts +0 -2
  415. package/dist/extra/processDrag.d.ts +0 -19
  416. package/dist/extra/processForm.d.ts +0 -15
  417. package/dist/extra/run.d.ts +0 -13
  418. package/dist/extra/xstreamCompat.d.ts +0 -5
  419. package/dist/jsx-dev-runtime.d.ts +0 -1
  420. package/dist/jsx-dev-runtime.js +0 -224
  421. package/dist/jsx-runtime.d.ts +0 -4
  422. package/dist/jsx-runtime.js +0 -224
  423. package/dist/jsx.d.ts +0 -2
  424. package/dist/pragma/fn.d.ts +0 -7
  425. package/dist/pragma/index.d.ts +0 -7
  426. package/dist/pragma/is.d.ts +0 -10
  427. package/dist/switchable.d.ts +0 -7
  428. package/dist/vike/+config.cjs.js +0 -65
  429. package/src/component.ts +0 -2256
  430. package/src/cycle/isolate/index.ts +0 -196
  431. package/src/cycle/run/index.ts +0 -151
  432. package/src/cycle/run/internals.ts +0 -143
  433. package/src/cycle/state/Collection.ts +0 -174
  434. package/src/cycle/state/index.ts +0 -5
  435. package/src/cycle/state/pickCombine.ts +0 -167
  436. package/src/cycle/state/pickMerge.ts +0 -122
  437. package/src/cycle/state/withState.ts +0 -47
  438. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,471 @@
1
+ <!-- Generated from docs/src/content/docs/guide by scripts/copy-guides.mjs; online: https://sygnal.js.org/guide/ -->
2
+ # Resources and Caching
3
+
4
+ A **resource** is a read that follows state. The component declares which request each resource needs, and [`makeFetchDriver()`](./http.md) fetches it whenever that request changes, writing the result to `state[name]`. There is no model entry for loading or for the reply.
5
+
6
+ In this demo, a stand-in server has quotes 1 to 3; any other id is a 404:
7
+
8
+ ```js live-server
9
+ const quotes = { 1: 'Start where you are.', 2: 'Less, but better.', 3: 'Done is done.' }
10
+
11
+ export default {
12
+ 'GET /api/quotes/:id': ({ params }) => {
13
+ const text = quotes[params.id]
14
+ if (!text) return { status: 404, json: { message: 'No such quote' } }
15
+ return { json: { id: Number(params.id), text }, delayMs: 1000 }
16
+ },
17
+ }
18
+ ```
19
+
20
+ ```jsx live
21
+ function Quote({ state }) {
22
+ const { status, data, refreshing } = state.quote
23
+ return (
24
+ <div className="quote">
25
+ <button className="next">Next</button>
26
+ <button className="refresh">Refresh</button>
27
+ <p className="status">{status === 'loading' ? 'Loading…' : status === 'error' ? 'Could not load the quote.' : ''}</p>
28
+ <p className="text">{data?.text}</p>
29
+ {refreshing && <p className="busy">Updating…</p>}
30
+ </div>
31
+ )
32
+ }
33
+ Quote.initialState = { id: 1 }
34
+ Quote.resources = {
35
+ quote: (state) => state.id && `/api/quotes/${state.id}`, // a URL or a request; falsy = idle
36
+ }
37
+ Quote.intent = ({ DOM }) => ({
38
+ NEXT: DOM.click('.next'),
39
+ REFRESH: DOM.click('.refresh'),
40
+ })
41
+ Quote.model = {
42
+ NEXT: (state) => ({ ...state, id: state.id + 1 }),
43
+ REFRESH: { HTTP: { refresh: 'quote' } },
44
+ }
45
+ ```
46
+
47
+ `state.quote` is `{ status, data, error, refreshing }`:
48
+
49
+ | `status` | When | `data` / `error` |
50
+ |---|---|---|
51
+ | `'idle'` | The entry returns a falsy value (or before the first request) | none |
52
+ | `'loading'` | A **new** request is in flight (the first one, or the request changed) | none: the previous record is never shown for the new one |
53
+ | `'success'` | The 2xx reply arrived | `data` is the parsed body |
54
+ | `'error'` | A non-2xx status, a network error, a timeout or a validation failure | `error` is the Error (`error.status`, `error.body`, `error.issues`); `data` is kept from the last success of the same request |
55
+
56
+ - **A refetch of the same request keeps what is shown.** `{ refresh: 'quote' }`, an [invalidation](#invalidation), a [focus or reconnect refetch](#focus-reconnect-and-polling), polling, or the same request declared again after a pause keep `status`, `data` and `error`, and set `refreshing: true` until the reply lands. Show a spinner next to the data, or treat `refreshing` like loading when the old data must not be visible.
57
+ - **A new request clears it.** When the derived request changes (another id), the resource goes to `'loading'` with no data, and the earlier request is aborted: its reply never lands. For pagination, where the previous page should stay visible, set `keepPrevious: true` on the request: the old `data` stays (with `refreshing: true`) until the new page arrives.
58
+ - **Change detection is by value.** A state change that derives an equal request (by JSON) sends nothing.
59
+ - `ok` / `error` on the request also dispatch those actions after the write, for side work such as appending pages.
60
+ - A Collection item, or any component instance, has its own resources. Removing it aborts them.
61
+ - In a hidden [Switchable](https://sygnal.js.org/guide/switchable/#hidden-pages-pause-connections-and-resources) page, resources pause: the request in flight is aborted and the resource keeps its last result (`refreshing` off; `'idle'` if nothing had arrived yet). When the page is shown again, the same request is refetched, keeping data. Mark an entry `background: true` to keep it live while hidden.
62
+
63
+ Writes stay [reply actions](./http.md): `{ url, method: 'PUT', json, ok: 'SAVED', error: 'SAVE_FAILED' }`. After a write, [`invalidates`](#invalidation) refetches the reads it changed, and [`updates`](#writing-a-reply-into-the-cache) shows its reply in them at once.
64
+
65
+ ## The query cache
66
+
67
+ The cache is off by default, and it is a separate export, so an app that only sends requests doesn't ship it. Give the driver one:
68
+
69
+ ```javascript
70
+ import { run, makeFetchDriver, queryCache } from 'sygnal'
71
+ import App from './App.jsx'
72
+
73
+ run(App, { HTTP: makeFetchDriver({ cache: queryCache() }) })
74
+ // or: queryCache({ staleTime: 30000, gcTime: 300000, refetchOnFocus: true, refetchOnReconnect: true })
75
+ ```
76
+
77
+ **The cache goes in `main.js` and in each test**: `renderComponent(App, { http: { cache: queryCache() } })` ([Testing](#testing)). A component rendered without one gets no caching. Put `staleTime` on the resource's request (`{ url: '/api/items', staleTime: 2000 }`) rather than on the cache, so the app and its tests use the same freshness; a `staleTime` sent to a driver without a `queryCache()` is reported as [SYG635](https://sygnal.js.org/reference/errors/#syg635).
78
+
79
+ With the cache on:
80
+
81
+ - **Stale-while-revalidate.** A resource whose request was fetched before shows the cached data at once (`'success'` with `refreshing: true`) and refetches it in the background. Switching back to a list or a record shows it with no spinner.
82
+ - **`staleTime`** (default `0`): how long a reply counts as fresh. A fresh entry is served without any request; a `{ refresh }` always fetches.
83
+ - **De-duplication.** Identical requests in flight share one fetch, even from different components; each component still gets its own `state[name]` (or its own `ok` action). The fetch is aborted only when no component still wants it.
84
+ - **`gcTime`** (default 5 minutes): an entry that no mounted resource uses is dropped after this long.
85
+ - The cache key is the method, the URL with its query (sorted), the body and `parse`. Resources' GET and HEAD requests are cached; a request opts out with `cache: false`.
86
+
87
+ Requests with reply actions keep one-send-one-request semantics unless they ask for the cache with `cache: true` or `staleTime`:
88
+
89
+ ```jsx
90
+ User.model = {
91
+ LOAD: { HTTP: (state) => ({ url: `/api/users/${state.id}`, ok: 'LOADED', staleTime: 60000 }) },
92
+ LOADED: (state, user) => ({ ...state, user }),
93
+ }
94
+ ```
95
+
96
+ Cache reads only. A POST, PUT, PATCH or DELETE with `cache` or `staleTime` is reported as [SYG630](https://sygnal.js.org/reference/errors/#syg630): its writes would be answered from the cache. Without a `queryCache()` on the driver, `cache`, `staleTime` and `{ prefetch }` do nothing, which is reported as [SYG635](https://sygnal.js.org/reference/errors/#syg635).
97
+
98
+ ## Prefetching
99
+
100
+ `{ prefetch: request }` on the `HTTP` sink fetches a request into the cache without a reply. A resource that reads it later starts at `'success'`, and while the entry is fresh (`staleTime`) it sends nothing. A fresh entry, or the same fetch already in flight, is not fetched again.
101
+
102
+ ```jsx
103
+ QuoteList.model = {
104
+ HOVER: { HTTP: (state, id) => ({ prefetch: `/api/quotes/${id}` }) },
105
+ }
106
+ ```
107
+
108
+ `cache.prefetch(request)` does the same from outside a component, through the driver that was given the cache.
109
+
110
+ ### Prefetching a route's data
111
+
112
+ There is no loader API: route pages derive their resources from `state.route`. To warm a page's data on link hover, keep the page's requests in one map and use it twice, in the page's `resources` and in the [router](https://sygnal.js.org/guide/router/#prefetching)'s `prefetch` option:
113
+
114
+ ```javascript
115
+ // data.js
116
+ import { makeRouter, queryCache } from 'sygnal'
117
+
118
+ export const cache = queryCache({ staleTime: 30000 })
119
+ export const routeData = {
120
+ task: (route) => [`/api/tasks/${route.params.id}`],
121
+ }
122
+ export const router = makeRouter({
123
+ routes: { home: '/', task: '/tasks/:id', notFound: '*' },
124
+ prefetch: (route) => routeData[route.name]?.(route).forEach(cache.prefetch),
125
+ })
126
+ ```
127
+
128
+ ```jsx
129
+ // TaskPage.jsx: the page reads the same request
130
+ TaskPage.resources = { task: (state) => routeData.task(state.route)[0] }
131
+
132
+ // App.jsx: { prefetch } on the ROUTER sink, e.g. on hover, warms the route's data
133
+ App.model = {
134
+ HOVER_TASK: { ROUTER: (state, id) => ({ prefetch: 'task', params: { id } }) },
135
+ }
136
+ ```
137
+
138
+ Register the driver with the same cache: `run(App, { ROUTER: router.driver, HTTP: makeFetchDriver({ cache }) })`. Navigating to the task then renders it at once, with no request while the entry is fresh.
139
+
140
+ ## Server rendering
141
+
142
+ Requests are never sent during server rendering. To render a resource with data on the server, a loader fetches it into a cache, and the snapshot travels to the client:
143
+
144
+ - `cache.set(request, data)` writes one entry, keyed like the request the component declares.
145
+ - `renderToString(App, { cache })` renders each resource found in the cache as `{ status: 'success', data }`; any other request renders as `'loading'`.
146
+ - `cache.dehydrate()` returns a snapshot, `[{ key, data, updatedAt, tags }]`, that is safe to serialise. `queryCache({ initial: snapshot })` or `cache.hydrate(snapshot)` loads it on the client.
147
+
148
+ A seeded entry keeps its `updatedAt`, so it stays fresh for `staleTime` after the server fetched it. While it is fresh, the client's first paint is `'success'` with no request. A stale entry is shown at once and refetched (`refreshing: true`). Give the client cache a `staleTime`, or every seeded page refetches right after hydration.
149
+
150
+ ### With Vike
151
+
152
+ A `+data` hook seeds a cache and puts its snapshot on `pageContext.queryCache`. The Sygnal Vike extension renders the page's resources from it, passes it to the client, and hydrates the cache of every fetch driver in the `drivers` config before the page runs. On client-side navigation it does the same with the next page's snapshot.
153
+
154
+ ```javascript
155
+ // pages/quote/@id/+data.js
156
+ import { queryCache } from 'sygnal'
157
+
158
+ export async function data(pageContext) {
159
+ const id = Number(pageContext.routeParams.id)
160
+ const cache = queryCache()
161
+ const quote = await (await fetch(`https://api.example.com/quotes/${id}`)).json()
162
+ cache.set(`/api/quotes/${id}`, quote) // the request the page's resource declares
163
+ pageContext.queryCache = cache.dehydrate()
164
+ return { id }
165
+ }
166
+ ```
167
+
168
+ ```javascript
169
+ // pages/+drivers.js: client-only; one cache for the session
170
+ import { makeFetchDriver, queryCache } from 'sygnal'
171
+
172
+ export default { HTTP: makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) }) }
173
+ ```
174
+
175
+ ```jsx
176
+ // pages/quote/@id/+Page.jsx
177
+ function Page({ state }) {
178
+ const { status, data } = state.quote
179
+ return <p className="quote">{status === 'success' ? data.text : 'Loading…'}</p>
180
+ }
181
+ Page.initialState = { id: 1 }
182
+ Page.resources = { quote: (state) => `/api/quotes/${state.id}` }
183
+ export default Page
184
+ ```
185
+
186
+ The server HTML contains the quote, and the client hydrates without fetching it again.
187
+
188
+ ## Invalidation
189
+
190
+ After a write, refresh the reads it changed. Tag the reads, and invalidate the tag:
191
+
192
+ ```jsx
193
+ QuoteList.resources = {
194
+ quotes: () => ({ url: '/api/quotes', tags: ['quotes'] }),
195
+ }
196
+
197
+ QuoteEditor.model = {
198
+ SAVE: {
199
+ HTTP: (state) => ({ url: `/api/quotes/${state.id}`, method: 'PUT', json: state.draft, ok: 'SAVED', invalidates: ['quotes'] }),
200
+ },
201
+ SAVED: (state) => ({ ...state, saved: true }),
202
+ }
203
+ ```
204
+
205
+ - `invalidates` on any request runs after a 2xx reply only.
206
+ - `{ invalidate: value }` on the `HTTP` sink invalidates right away, from any component: `HTTP: () => ({ invalidate: 'quotes' })`.
207
+ - The value is a tag (`'quotes'`), a URL prefix of the request's `url` (a string starting with `/`: `'/api/quotes'`), an array of them, or a predicate `(request) => boolean`. Tags are explicit: nothing is derived from the URL.
208
+ - Matching **mounted** resources refetch, keeping their data (`refreshing: true`); matching cache entries are marked stale, so the next use refetches. Invalidation works with or without the cache.
209
+ - **Invalidation aborts older reads.** A refetch aborts the resource's read already in flight, so a reply sent before the save never lands after it: no request ids or sequence numbers. A shared cache fetch in flight for a matching entry still answers the requests waiting on it, but never writes the cache.
210
+ - An `invalidate` that matches nothing is reported as [SYG632](https://sygnal.js.org/reference/errors/#syg632) (info).
211
+
212
+ ## Writing a reply into the cache
213
+
214
+ `updates` on a request writes its 2xx reply into this component's resources, like React Query's `setQueryData`. The saved record shows at once, without waiting for a refetch:
215
+
216
+ ```jsx
217
+ Item.model = {
218
+ SAVE: {
219
+ HTTP: (state) => ({
220
+ url: `/api/items/${state.id}`, method: 'PUT', json: { title: state.draft },
221
+ ok: 'SAVED', error: 'SAVE_FAILED',
222
+ updates: 'item', // the reply becomes state.item.data now
223
+ invalidates: '/api/items', // then the item and the list refetch, keeping data
224
+ }),
225
+ },
226
+ SAVED: (state) => ({ ...state, editing: false }),
227
+ SAVE_FAILED: (state) => ({ ...state, saveError: 'Could not save the item.' }),
228
+ }
229
+ ```
230
+
231
+ - The value names resources of the component that sends the request: `'item'`, `['item', 'detail']`, or an object whose functions derive the new data from the current one: `updates: { item: true, items: (list, saved) => list.map((i) => (i.id === saved.id ? { ...i, title: saved.title } : i)) }`.
232
+ - The write comes first: the resource becomes `'success'` with the new data, its cache entry is set, and its read in flight (if any) is aborted. Then the `ok` action runs, then `invalidates`. An invalidated resource refetches keeping the written data (`refreshing: true`), and the server's reply replaces it.
233
+ - Other mounted resources reading the same cache entry show the new data too. A resource that is idle (its entry returned a falsy value) is skipped; a paused one (a hidden page) is updated.
234
+ - Nothing is written when the request fails. The `ok` action still gets the reply.
235
+
236
+ ## Focus, reconnect and polling
237
+
238
+ - With a `queryCache()`, when the window regains focus (or the tab becomes visible) and when the browser comes back online, every **stale** mounted resource refetches, keeping its data. Turn either off with `refetchOnFocus: false` / `refetchOnReconnect: false`. Resources in hidden pages don't refetch.
239
+ - `refetchEvery: ms` on a resource request polls: it refetches that long after each reply, and skips while the document is hidden. It works with or without the cache.
240
+
241
+ ```jsx
242
+ Prices.resources = {
243
+ prices: (state) => ({ url: '/api/prices', query: { market: state.market }, refetchEvery: 10000 }),
244
+ }
245
+ ```
246
+
247
+ ## Retries
248
+
249
+ Requests are not retried by default. Ask for it per request, or for every GET and HEAD with the driver option:
250
+
251
+ ```javascript
252
+ makeFetchDriver({ retry: 2 }) // GET/HEAD only
253
+ // on a request (any method): retry: 3, or retry: { count: 3, delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }
254
+ ```
255
+
256
+ - Network errors, 408, 429 and 5xx are retried; other 4xx never are. A `Retry-After` header (in seconds) sets the wait; otherwise the delay doubles from `delayMs` up to `maxDelayMs`, varied by `jitter` (the same backoff as the socket driver's `reconnect`).
257
+ - The `error` action (or the resource's `'error'`) arrives once, after the last attempt, with `attempts`.
258
+ - `latest`, `{ abort }` and removing the component cancel a pending retry.
259
+
260
+ ## Validation
261
+
262
+ `validate` takes any [Standard Schema](https://standardschema.dev) (zod, valibot, arktype, …) and checks the parsed body before it is delivered. The delivered value is the schema's output, so transforms apply:
263
+
264
+ ```jsx
265
+ import { z } from 'zod'
266
+
267
+ const QuoteSchema = z.object({ id: z.number(), text: z.string(), author: z.string() })
268
+
269
+ Quote.resources = {
270
+ quote: (state) => state.id && { url: `/api/quotes/${state.id}`, validate: QuoteSchema },
271
+ }
272
+ ```
273
+
274
+ A body that fails goes to `'error'` (or the `error` action) with the schema's `issues`: `{ error, status, issues, request }`. A `validate` that isn't a Standard Schema fails the request and is reported as [SYG631](https://sygnal.js.org/reference/errors/#syg631).
275
+
276
+ ## Recipes
277
+
278
+ ### Pagination
279
+
280
+ `keepPrevious: true` keeps the current page on screen, with `refreshing: true`, while the next one loads. In this demo, a stand-in server pages through 12 items, 5 at a time:
281
+
282
+ ```js live-server
283
+ const items = Array.from({ length: 12 }, (_, i) => ({ title: `Item ${i + 1}` }))
284
+
285
+ export default {
286
+ 'GET /api/items': ({ query }) => {
287
+ const start = (Number(query.page) - 1) * 5
288
+ return { json: { items: items.slice(start, start + 5) }, delayMs: 1000 }
289
+ },
290
+ }
291
+ ```
292
+
293
+ ```css live
294
+ .rows.stale { opacity: 0.5; }
295
+ ```
296
+
297
+ ```jsx live
298
+ function Pages({ state }) {
299
+ const { data, refreshing } = state.list
300
+ return (
301
+ <div>
302
+ <ul className={refreshing ? 'rows stale' : 'rows'}>{(data?.items ?? []).map(item => <li>{item.title}</li>)}</ul>
303
+ <button className="prev" disabled={state.page === 1}>Previous</button>
304
+ <span className="page">Page {state.page}</span>
305
+ <button className="next">Next</button>
306
+ </div>
307
+ )
308
+ }
309
+ Pages.initialState = { page: 1 }
310
+ Pages.resources = {
311
+ list: (state) => ({ url: '/api/items', query: { page: state.page }, keepPrevious: true }),
312
+ }
313
+ Pages.intent = ({ DOM }) => ({ PREV: DOM.click('.prev'), NEXT: DOM.click('.next') })
314
+ Pages.model = {
315
+ PREV: (state) => ({ ...state, page: Math.max(1, state.page - 1) }),
316
+ NEXT: (state) => ({ ...state, page: state.page + 1 }),
317
+ }
318
+ ```
319
+
320
+ With a `queryCache()`, going back to a page fetched less than `staleTime` ago shows it without a request.
321
+
322
+ ### Infinite list
323
+
324
+ The resource fetches the current page; its `ok` action appends each page to a list the component owns:
325
+
326
+ ```jsx
327
+ import { ABORT } from 'sygnal'
328
+
329
+ function Feed({ state }) {
330
+ return (
331
+ <div>
332
+ <ul>{state.items.map(item => <li>{item.title}</li>)}</ul>
333
+ {state.feed.status === 'loading' && <p className="loading">Loading…</p>}
334
+ {state.hasMore && <button className="more">Load more</button>}
335
+ </div>
336
+ )
337
+ }
338
+ Feed.initialState = { page: 1, items: [], hasMore: true }
339
+ Feed.resources = {
340
+ feed: (state) => ({ url: '/api/feed', query: { page: state.page }, ok: 'PAGE' }),
341
+ }
342
+ Feed.intent = ({ DOM }) => ({ MORE: DOM.click('.more') })
343
+ Feed.model = {
344
+ MORE: (state) => (state.feed.status === 'loading' ? ABORT : { ...state, page: state.page + 1 }),
345
+ PAGE: (state, body) => ({ ...state, items: [...state.items, ...body.items], hasMore: body.hasMore }),
346
+ }
347
+ ```
348
+
349
+ `PAGE` runs on every successful fetch of the request, refetches included, so leave the cache's focus refetch off for such a list (`queryCache({ refetchOnFocus: false })`) or de-duplicate by id in `PAGE`.
350
+
351
+ ### List and detail with a save
352
+
353
+ Each view declares its read only while it is shown, the cache shows a known list or item at once, and a save writes the reply and refetches both. In this demo, a stand-in server keeps three items, refuses an empty title, and the driver has a `queryCache()` (the `options` export):
354
+
355
+ ```js live-server
356
+ import { queryCache } from 'sygnal'
357
+
358
+ export const options = { cache: queryCache() }
359
+
360
+ const items = [
361
+ { id: 1, title: 'Buy milk' },
362
+ { id: 2, title: 'Call Ana' },
363
+ { id: 3, title: 'Book flights' },
364
+ ]
365
+ const find = (id) => items.find((i) => i.id === Number(id))
366
+
367
+ export default {
368
+ 'GET /api/items': () => ({ json: items }),
369
+ 'GET /api/items/:id': ({ params }) => ({ json: find(params.id) }),
370
+ 'PUT /api/items/:id': ({ params, json }) => (json.title.trim() === ''
371
+ ? { status: 422, json: { title: 'Required' } }
372
+ : { json: Object.assign(find(params.id), { title: json.title }) }),
373
+ }
374
+ ```
375
+
376
+ ```jsx live
377
+ const statusOf = (r) => (r?.status === 'loading' ? 'Loading…' : r?.refreshing ? 'Updating…' : '')
378
+
379
+ function App({ state }) {
380
+ if (state.view === 'list') {
381
+ return (
382
+ <section className="list">
383
+ <p className="status">{statusOf(state.items)}</p>
384
+ <ul className="items">
385
+ {(state.items?.data ?? []).map((item) => <li><button className="open" data-id={String(item.id)}>{item.title}</button></li>)}
386
+ </ul>
387
+ </section>
388
+ )
389
+ }
390
+ return (
391
+ <section className="detail">
392
+ <button className="back">Back to list</button>
393
+ <p className="status">{statusOf(state.item)}</p>
394
+ <h2 className="item-title">{state.item?.data?.title}</h2>
395
+ <input name="title" aria-label="Title" value={state.draft} />
396
+ <button className="save">Save</button>
397
+ <p className="save-error">{state.saveError}</p>
398
+ </section>
399
+ )
400
+ }
401
+ App.initialState = { view: 'list', id: null, draft: '', saveError: '' }
402
+ App.resources = {
403
+ items: (state) => state.view === 'list' && { url: '/api/items', staleTime: 2000 },
404
+ item: (state) => state.view === 'detail' && { url: `/api/items/${state.id}`, staleTime: 2000 },
405
+ }
406
+ App.intent = ({ DOM }) => ({
407
+ OPEN: DOM.click('.open').data('id', Number),
408
+ BACK: DOM.click('.back'),
409
+ TYPE: DOM.input('input[name="title"]').value(),
410
+ SAVE: DOM.click('.save'),
411
+ })
412
+ App.model = {
413
+ OPEN: (state, id) => ({ ...state, view: 'detail', id, draft: '', saveError: '' }),
414
+ BACK: (state) => ({ ...state, view: 'list' }),
415
+ TYPE: (state, draft) => ({ ...state, draft }),
416
+ SAVE: {
417
+ STATE: (state) => ({ ...state, saveError: '' }),
418
+ HTTP: (state) => ({
419
+ url: `/api/items/${state.id}`, method: 'PUT', json: { title: state.draft },
420
+ ok: 'SAVED', error: 'SAVE_FAILED', updates: 'item', invalidates: '/api/items',
421
+ }),
422
+ },
423
+ SAVED: (state) => ({ ...state, draft: '' }),
424
+ SAVE_FAILED: (state) => ({ ...state, saveError: 'Could not save the item.' }),
425
+ }
426
+ ```
427
+
428
+ ```javascript
429
+ // main.js; the tests use renderComponent(App, { http: { cache: queryCache() } })
430
+ run(App, { HTTP: makeFetchDriver({ cache: queryCache() }) })
431
+ ```
432
+
433
+ - `state.items` and `state.item` are undefined until their first write, hence the `?.`.
434
+ - A hidden view's resource goes `'idle'`. Shown again within `staleTime`, the cached data is shown with no request; later it is shown at once and refetched (`'Updating…'`).
435
+ - The save shows the new title at once (`updates`), and the list and item refetch (`invalidates`). A read in flight from before the save is aborted, so its reply never overwrites the saved title.
436
+
437
+ For writes, see the [optimistic update and save status recipes](./http.md#recipes).
438
+
439
+ ## Testing
440
+
441
+ `renderComponent` runs the real driver over an in-memory `fetch` ([Testing](https://sygnal.js.org/integration/testing/#resources)), so every rule on this page applies in tests. Each fetch stays pending until `t.respond` or `t.fail` answers it, by the resource name, its URL or a partial request:
442
+
443
+ ```js
444
+ import { it, expect } from 'vitest'
445
+ import { renderComponent, queryCache } from 'sygnal'
446
+ import Quote from './Quote.jsx'
447
+
448
+ it('refreshes in place, and shows the cached quote when coming back', async () => {
449
+ const t = renderComponent(Quote, { http: { cache: queryCache() } })
450
+ await t.waitForState((s) => s.quote.status === 'loading')
451
+ await t.respond('HTTP', { text: 'One' }, 'quote')
452
+
453
+ t.simulateAction('REFRESH')
454
+ await t.waitForState((s) => s.quote.refreshing)
455
+ expect(t.html()).toContain('One') // still shown while it refetches
456
+ await t.respond('HTTP', { text: 'One, edited' }, 'quote')
457
+
458
+ t.simulateAction('NEXT') // not cached yet: loading
459
+ await t.waitForState((s) => s.quote.status === 'loading')
460
+ await t.respond('HTTP', { text: 'Two' }, 'quote')
461
+ expect(t.cache('HTTP').map((e) => e.key)).toEqual(['GET /api/quotes/1', 'GET /api/quotes/2'])
462
+ t.dispose()
463
+ })
464
+ ```
465
+
466
+ - A resource's request is sent after the state changes, not during the event that caused it. `t.respond` / `t.fail` by resource name right after a `simulateEvent` / `simulateAction` that hasn't produced its state yet waits (up to 1 s) for that fetch; otherwise, as for any request, they throw at the call when nothing matches.
467
+ - `renderComponent(C, { http })` passes driver options (`cache: queryCache()`, `retry`, `timeoutMs`, …) to the fake: give it the same `queryCache()` as `main.js`, or the test runs without a cache. A seeded cache: `queryCache({ initial: snapshot })`.
468
+ - `t.cache('HTTP')` lists the cache entries: `{ key, age, stale, subscribers, data, tags }`.
469
+ - A `{ prefetch }` fetch is listed in `t.requests('HTTP')` as `{ url, ...request, prefetch: true }`; answer it like any other, e.g. `t.respond('HTTP', data, '/api/quotes/2')`.
470
+ - `t.focus()` and `t.online()` simulate the browser events; the test's own window events never trigger refetches.
471
+ - Retries and `refetchEvery` run on the test's timers, fake timers included. `t.requests('HTTP')` lists what the component sent (and each resource fetch), not each retry.