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.
- package/CHANGELOG.md +646 -0
- package/README.md +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +20 -4
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +109 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +13 -6
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +94 -210
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +3158 -190
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +262 -134
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -119
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- 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.
|