sygnal 5.4.0 → 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 +24 -9
- package/dist/astro/client.cjs.js +16 -9
- package/dist/astro/client.cjs.js.map +1 -1
- package/dist/astro/client.mjs +16 -9
- package/dist/astro/client.mjs.map +1 -1
- package/dist/astro/index.cjs.js +362 -91
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +362 -92
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/server.cjs.js +3396 -165
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +3396 -165
- package/dist/astro/server.mjs.map +1 -1
- 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 +3032 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3032 -296
- package/dist/diagnostics.esm.js.map +1 -1
- 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 +13178 -5769
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +2908 -276
- package/dist/index.esm.js +13149 -5779
- package/dist/index.esm.js.map +1 -1
- package/dist/jsx-dev-runtime.cjs.js +26 -334
- package/dist/jsx-dev-runtime.cjs.js.map +1 -1
- package/dist/jsx-dev-runtime.esm.js +19 -327
- package/dist/jsx-dev-runtime.esm.js.map +1 -1
- package/dist/jsx-runtime.cjs.js +26 -334
- package/dist/jsx-runtime.cjs.js.map +1 -1
- package/dist/jsx-runtime.esm.js +19 -327
- package/dist/jsx-runtime.esm.js.map +1 -1
- package/dist/jsx.cjs.js +10 -314
- package/dist/jsx.cjs.js.map +1 -1
- package/dist/jsx.esm.js +8 -315
- package/dist/jsx.esm.js.map +1 -1
- 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 +1 -1
- package/dist/sygnal.min.js.map +1 -1
- 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 +4 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -1
- package/dist/vike/ClientOnly.mjs +4 -14
- package/dist/vike/ClientOnly.mjs.map +1 -1
- package/dist/vike/{+config.js → config/+config.js} +9 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +165 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -1
- package/dist/vike/onRenderClient.mjs +165 -95
- package/dist/vike/onRenderClient.mjs.map +1 -1
- package/dist/vike/onRenderHtml.cjs.js +59 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -1
- package/dist/vike/onRenderHtml.mjs +60 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -1
- package/dist/vite/plugin.cjs.js +323 -88
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +323 -89
- package/dist/vite/plugin.mjs.map +1 -1
- 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 +166 -103
- package/package.json +98 -14
- package/src/astro/client.ts +27 -16
- package/src/astro/index.d.ts +24 -0
- package/src/astro/index.ts +69 -3
- package/src/astro/server.ts +8 -2
- package/src/collection.ts +6 -93
- 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 +9 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +9 -7
- package/src/cycle/dom/controlledInputModule.ts +23 -6
- package/src/cycle/dom/enrichEventStream.ts +25 -47
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -56
- package/src/cycle/dom/mockDOMSource.ts +21 -1
- package/src/cycle/dom/modules.ts +16 -3
- 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 +19 -3
- 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 +101 -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 +1 -1
- 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 +20 -11
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +17 -2
- 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 +97 -4
- package/src/extra/diagnostics/checks/inspect.ts +140 -32
- 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 +10 -5
- package/src/extra/diagnostics/checks/public.d.ts +98 -7
- 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 +1 -1
- package/src/extra/diagnostics/checks/shared.ts +86 -2
- 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 +100 -7
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +10 -67
- 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 +66 -10
- package/src/extra/diagnostics/codes.ts +268 -23
- package/src/extra/diagnostics/index.ts +34 -9
- package/src/extra/diagnostics/legacy.ts +17 -0
- package/src/extra/driverFactories.ts +83 -19
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/fetchDriver.ts +519 -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 +1 -1
- 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 +90 -217
- 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 +2433 -175
- 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/index.d.ts +2631 -117
- package/src/index.ts +25 -4
- 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 +256 -133
- 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 -121
- 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 +7 -1
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +22 -3
- package/src/vite/plugin.ts +222 -24
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/vike/+config.cjs.js +0 -66
- package/dist/vike/+config.cjs.js.map +0 -1
- package/dist/vike/+config.js.map +0 -1
- package/src/component.ts +0 -2301
- 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 -184
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -201
- 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,519 @@
|
|
|
1
|
+
import xs, {Stream} from 'xstream';
|
|
2
|
+
import {senderOf, keepSender, allowed, makeReplies} from './replies';
|
|
3
|
+
import {backoff} from './backoff';
|
|
4
|
+
import {isHidden} from './browserSignals';
|
|
5
|
+
import {validateWith} from './standardSchema';
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* makeFetchDriver(options?) (PLAN-2 E2): an opt-in HTTP driver over `fetch`. Docs on the
|
|
9
|
+
* declarations in src/index.d.ts and docs/guide/drivers.md. Summary:
|
|
10
|
+
*
|
|
11
|
+
* - Request (sink value): `{ url, ok?, error?, key?, category?, method?, headers?, query?, json?,
|
|
12
|
+
* body?, latest?, timeoutMs?, parse?, init? }` or a URL string. `json` is stringified with a JSON
|
|
13
|
+
* content-type; method defaults to POST with a body/json, else GET. `query` is appended as a
|
|
14
|
+
* query string (null/undefined values skipped, arrays repeat the key, before any #fragment).
|
|
15
|
+
* Other fetch options (credentials, mode, cache, ...) go under `init` (R4-5); other top-level
|
|
16
|
+
* keys are the app's own: not sent, but returned on `request`.
|
|
17
|
+
* - Reply actions (PLAN-3, D57/D58): a component's request naming `ok` / `error` actions gets its
|
|
18
|
+
* reply as that action, delivered to exactly the sending instance (./replies.ts), never to
|
|
19
|
+
* select()/errors(): `ok` data is the parsed body (the Response with parse: 'response'),
|
|
20
|
+
* `error` data `{ error, status?, body?, request }`. A request naming only one of them sends
|
|
21
|
+
* the other outcome down the plain path (select()/errors()). `latest` acts per (sender,
|
|
22
|
+
* `key ?? ok ?? error`); `{ abort: 'LOADED' }` / `{ abort: true, key }` cancel the sender's
|
|
23
|
+
* requests under that key; when the sender is disposed its requests with reply actions are aborted.
|
|
24
|
+
* Requests with a `then` / `catch` key are refused (SYG610).
|
|
25
|
+
* - `{ category?, abort: true }` cancels the component's requests in flight in that category
|
|
26
|
+
* (all of its requests without a category); a cancelled request delivers nothing.
|
|
27
|
+
* - `latest: true` (per request, or the driver option): a new request aborts the component's
|
|
28
|
+
* requests still in flight in its category; their late responses/errors are never delivered.
|
|
29
|
+
* - Responses on `select(category?)`: `{ category, value, status, request }`. 2xx only.
|
|
30
|
+
* - Failures on `errors(category?)`: `{ error, category, request, status?, body? }` for a
|
|
31
|
+
* non-2xx status (`error.status`, `error.body` too), a network error, a body that doesn't
|
|
32
|
+
* parse, a timeout (`error.name === 'TimeoutError'`) or no fetch. Never delivered for an
|
|
33
|
+
* aborted/superseded request. Unheard failures are console.error'd.
|
|
34
|
+
* - Isolation (R4-2, D54, like @cycle/http): an isolated component's requests are tagged with
|
|
35
|
+
* its scope (isolateSink); its select()/errors() see only the replies to its own and its
|
|
36
|
+
* descendants' requests (isolateSource); latest/abort act per (scope, category). The root
|
|
37
|
+
* component sees every reply, as an unisolated app always did.
|
|
38
|
+
* - parse: 'auto' (default: JSON when the content-type says json, else text; 204 → null),
|
|
39
|
+
* 'json', 'text', 'response' (the Response itself) or `res => value`. A non-2xx status is
|
|
40
|
+
* always a failure (its body parsed with 'auto').
|
|
41
|
+
* - Early replies (before anything listens) are held until the first select() listener;
|
|
42
|
+
* early failures until the first errors() listener or the next macrotask (R4-4, G-092).
|
|
43
|
+
* - Resources (PLAN-3 3-A; D78): the core sends a component's `resources` static as
|
|
44
|
+
* `{ resources: { name: request | falsy } }` (sender-stamped). Per (sender, name), a request
|
|
45
|
+
* that changed (by JSON) is fetched with latest semantics (the stale one aborted); falsy aborts
|
|
46
|
+
* it. Each step is the reply action RESOURCE `{ name, status, data, error, refreshing }`:
|
|
47
|
+
* - a new request (key change): 'loading', data and error cleared, unless `keepPrevious: true`;
|
|
48
|
+
* - a refetch of the same request (`{ refresh: name | names }`, invalidation, focus, polling,
|
|
49
|
+
* or the same request declared again after a pause): `data` and `error` kept, `status`
|
|
50
|
+
* unchanged, `refreshing: true` (a resource with no result yet stays 'loading');
|
|
51
|
+
* - 'success' (data: the parsed body, validated), 'error' (error: the Error, with
|
|
52
|
+
* `status`/`body` for a non-2xx; `data` kept), 'idle' (back to falsy).
|
|
53
|
+
* `ok` / `error` on the request also dispatch those actions after the RESOURCE write.
|
|
54
|
+
* G-177 / D85: a name left out of the declaration (a hidden Switchable page) is paused: its
|
|
55
|
+
* request is aborted and it keeps its last result (`refreshing` off; 'idle' if it had none);
|
|
56
|
+
* declared again, the same request is a refetch that keeps data, another one a key change.
|
|
57
|
+
* - Cache (D79; D88: opt-in, `cache: queryCache({ staleTime, gcTime, refetchOnFocus,
|
|
58
|
+
* refetchOnReconnect, initial })`, ./queryCache.ts owns the entries, keys, gc, de-duplication,
|
|
59
|
+
* the triggers and SSR seeding; this driver only calls its hooks). Cached: resources' GET/HEAD
|
|
60
|
+
* requests, and any request with `cache: true` or `staleTime`. A resource with a cached entry
|
|
61
|
+
* shows it at once ('success', `refreshing` while it refetches); an entry younger than
|
|
62
|
+
* `staleTime` (default 0) is served without a fetch. Identical cacheable requests in flight
|
|
63
|
+
* share one fetch (each sender gets its own reply); the fetch aborts only when no request
|
|
64
|
+
* still wants it. Focus / reconnect refetch stale mounted (not paused) resources. Without a
|
|
65
|
+
* queryCache, `cache: true` / `staleTime` / `{ prefetch }` do nothing (dev: SYG635).
|
|
66
|
+
* - `{ prefetch: request }` (H-7): fetched into the cache with no reply (a fresh entry or a
|
|
67
|
+
* fetch in flight: nothing new).
|
|
68
|
+
* - `refetchEvery: ms` on a resource polls (after each result; skipped while the document is hidden).
|
|
69
|
+
* - `{ invalidate: tag | tags | '/url-prefix' | (request) => boolean }` (D80): matching cache
|
|
70
|
+
* entries go stale and matching mounted resources refetch (keeping data). Tags are explicit
|
|
71
|
+
* (`tags: ['quotes']` on the request); a string starting with '/' is a URL prefix.
|
|
72
|
+
* `invalidates: …` on any request does the same after its success. G-184: a refetch aborts
|
|
73
|
+
* the resource's older read; a shared fetch in flight for a matching entry still answers its
|
|
74
|
+
* waiters but never writes the cache.
|
|
75
|
+
* - `updates: name | names | { name: true | (data, reply) => data }` (G-184, setQueryData): on
|
|
76
|
+
* a 2xx, before the `ok` action and `invalidates`, the reply becomes the data of the sender's
|
|
77
|
+
* named resources that have a request, and of their cache entries; their reads in flight
|
|
78
|
+
* are aborted and the other mounted resources on those entries show it too.
|
|
79
|
+
* - `retry: n | { count, delayMs, maxDelayMs, jitter }` (default 0; the driver option applies
|
|
80
|
+
* to GET/HEAD only): network errors, 408, 429 (Retry-After honoured) and 5xx are retried with
|
|
81
|
+
* the socket driver's backoff; the failure (with `attempts`) is delivered once, at the end.
|
|
82
|
+
* - `validate: schema` (a Standard Schema): the parsed body is validated (and transformed); a
|
|
83
|
+
* failure is an error with `issues`.
|
|
84
|
+
* - dispose / sink completion aborts everything in flight.
|
|
85
|
+
* - `fetch` is read at request time (options.fetch, else globalThis.fetch), so test stubs work.
|
|
86
|
+
* - PLAN-3 5-1 (H-9): renderComponent's HTTP fake runs this driver over an in-memory fetch;
|
|
87
|
+
* the internal `_tap(request, resourceName?)` option is called right before each fetch so the
|
|
88
|
+
* fake can name what is pending. There is no other copy of these rules.
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
const EARLY_LIMIT = 100;
|
|
92
|
+
// the fetch() init keys a request's `init` may set (the driver owns `signal`)
|
|
93
|
+
const INIT_KEYS = ['method', 'headers', 'body', 'mode', 'credentials', 'cache', 'redirect', 'referrer',
|
|
94
|
+
'referrerPolicy', 'integrity', 'keepalive', 'priority', 'window', 'duplex'];
|
|
95
|
+
|
|
96
|
+
// R4-2: the isolation scope path of a request, outermost first (non-enumerable, so a request
|
|
97
|
+
// still toEqual()s what the model returned)
|
|
98
|
+
const SCOPE = '__sygnalScope';
|
|
99
|
+
export const scopeOfRequest = (req: any): any[] => (req && typeof req == 'object' && req[SCOPE]) || [];
|
|
100
|
+
/** a copy of the request tagged with one more (outer) scope */
|
|
101
|
+
export const tagRequest = (req: any, scope: any) => {
|
|
102
|
+
const r = typeof req == 'string' ? {url: req} : req && typeof req == 'object' ? keepSender(req, {...req}) : req;
|
|
103
|
+
if (r && typeof r == 'object') Object.defineProperty(r, SCOPE, {value: [scope, ...scopeOfRequest(req)]});
|
|
104
|
+
return r;
|
|
105
|
+
};
|
|
106
|
+
/** a source at scope path `ns` sees replies to requests made at or under it */
|
|
107
|
+
export const inScope = (ns: any[], req: any) => {
|
|
108
|
+
const rs = scopeOfRequest(req);
|
|
109
|
+
return ns.every((s, i) => rs[i] === s);
|
|
110
|
+
};
|
|
111
|
+
export const scopeKey = (req: any) => scopeOfRequest(req).join('\u0001');
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* a URL with the query appended (arrays repeat the key) before any #fragment (R4-6); `sort`:
|
|
115
|
+
* sorted by name (the cache key)
|
|
116
|
+
*/
|
|
117
|
+
export const withQuery = (href: string, query: any, sort?: any) => {
|
|
118
|
+
if (!query) return href;
|
|
119
|
+
const qs = new URLSearchParams();
|
|
120
|
+
Object.keys(query).forEach(k => [].concat(query[k]).forEach((v: any) => v != null && qs.append(k, String(v))));
|
|
121
|
+
if (sort) qs.sort();
|
|
122
|
+
const s = qs.toString();
|
|
123
|
+
if (!s) return href;
|
|
124
|
+
const hash = href.indexOf('#');
|
|
125
|
+
const base = hash < 0 ? href : href.slice(0, hash);
|
|
126
|
+
return base + (base.includes('?') ? (/[?&]$/.test(base) ? '' : '&') : '?') + s + (hash < 0 ? '' : href.slice(hash));
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const autoParse = async (res: any) => {
|
|
130
|
+
if (res.status == 204) return null;
|
|
131
|
+
const h = res.headers;
|
|
132
|
+
const ct = h && typeof h.get == 'function' ? h.get('content-type') || '' : null;
|
|
133
|
+
// a non-standard fetch stub without headers: prefer json() when it has one
|
|
134
|
+
if (ct === null) return typeof res.json == 'function' ? res.json() : res.text();
|
|
135
|
+
if (!/[/+]json\b/i.test(ct)) return res.text();
|
|
136
|
+
const text = await res.text();
|
|
137
|
+
return text ? JSON.parse(text) : null;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const PARSERS: Record<string, (res: any) => any> = {
|
|
141
|
+
auto: autoParse,
|
|
142
|
+
json: res => res.json(),
|
|
143
|
+
text: res => res.text(),
|
|
144
|
+
response: res => res,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const pickInit = (init: any) => {
|
|
148
|
+
const out: any = {};
|
|
149
|
+
if (init && typeof init == 'object') INIT_KEYS.forEach(k => { if (init[k] !== undefined) out[k] = init[k]; });
|
|
150
|
+
return out;
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** headers merged case-insensitively (R4-5); plain objects and Headers instances */
|
|
154
|
+
const mergeHeaders = (...all: any[]) => {
|
|
155
|
+
if (typeof Headers != 'function') return Object.assign({}, ...all);
|
|
156
|
+
const h = new Headers();
|
|
157
|
+
all.forEach(x => x && new Headers(x).forEach((v, k) => h.set(k, v)));
|
|
158
|
+
const out: Record<string, string> = {};
|
|
159
|
+
h.forEach((v, k) => { out[k] = v; });
|
|
160
|
+
return out;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const IDEMPOTENT = /^(GET|HEAD)$/i;
|
|
164
|
+
// D80: an invalidate value matches a request by tag, URL prefix ('/…') or predicate
|
|
165
|
+
export const hit = (x: any, q: any) => {
|
|
166
|
+
try {
|
|
167
|
+
return typeof x == 'function' ? !!x(q) : [].concat(x).some((t: any) =>
|
|
168
|
+
t[0] == '/' ? String(q.url).startsWith(t) : [].concat(q.tags).includes(t as never));
|
|
169
|
+
} catch (_) { return false; }
|
|
170
|
+
};
|
|
171
|
+
// a settled resource status ('success' / 'error')
|
|
172
|
+
const SETTLED = /^[se]/;
|
|
173
|
+
|
|
174
|
+
export function makeFetchDriver(options: any = {}) {
|
|
175
|
+
// D88: the query cache (queryCache()), or none
|
|
176
|
+
const C = options.cache;
|
|
177
|
+
if (C && !C.key) throw new Error('[Sygnal] makeFetchDriver: cache takes queryCache(...) from sygnal');
|
|
178
|
+
const driver = (request$: Stream<any>) => {
|
|
179
|
+
let seq = 0;
|
|
180
|
+
let disposed = false;
|
|
181
|
+
// requests in flight: id → { category, scope (key), sender, rk (reply key), rn (resource),
|
|
182
|
+
// q (the request), timer, f (the fetch it waits on), ok / ko (its outcomes) }
|
|
183
|
+
const inflight = new Map<number, any>();
|
|
184
|
+
const subs = new Set<{l: any; sel: any; err: boolean; ns: any[]}>();
|
|
185
|
+
// R4-4: replies held until the first select() listener, failures until the first errors()
|
|
186
|
+
// listener (or the next macrotask, when they're logged if nothing listens)
|
|
187
|
+
const started = [false, false];
|
|
188
|
+
const early: any[][] = [[], []];
|
|
189
|
+
|
|
190
|
+
const matches = (s: any, v: any) =>
|
|
191
|
+
inScope(s.ns, v.request) && (s.sel === undefined || (typeof s.sel == 'function' ? s.sel(v) : v.category === s.sel));
|
|
192
|
+
const dispatch = (err: boolean, v: any) => {
|
|
193
|
+
let heard = false;
|
|
194
|
+
subs.forEach(s => {
|
|
195
|
+
let hit = false;
|
|
196
|
+
try { hit = s.err === err && matches(s, v); } catch (_) {}
|
|
197
|
+
if (!hit) return;
|
|
198
|
+
heard = true;
|
|
199
|
+
// R4-3: a throwing listener doesn't stop the others
|
|
200
|
+
try { s.l.next(v); } catch (e) { console.error('[Sygnal] makeFetchDriver: a listener threw', e); }
|
|
201
|
+
});
|
|
202
|
+
if (err && !heard) console.error(`[Sygnal] makeFetchDriver: request${v.category === undefined ? '' : ` '${v.category}'`} failed (nothing listens to errors()):`, v.error);
|
|
203
|
+
};
|
|
204
|
+
const flush = (k: number) => {
|
|
205
|
+
started[k] = true;
|
|
206
|
+
const q = early[k];
|
|
207
|
+
early[k] = [];
|
|
208
|
+
q.forEach(v => dispatch(!!k, v));
|
|
209
|
+
};
|
|
210
|
+
const emit = (err: boolean, v: any) => {
|
|
211
|
+
if (disposed) return;
|
|
212
|
+
const k = +err;
|
|
213
|
+
if (started[k]) return dispatch(err, v);
|
|
214
|
+
if (early[k].push(v) > EARLY_LIMIT) early[k].shift();
|
|
215
|
+
if (err && early[k].length == 1) setTimeout(() => started[k] || flush(k));
|
|
216
|
+
};
|
|
217
|
+
const source = (ns: any[]): any => {
|
|
218
|
+
const make = (err: boolean) => (sel?: any) => {
|
|
219
|
+
let sub: any;
|
|
220
|
+
return xs.create<any>({
|
|
221
|
+
start: l => {
|
|
222
|
+
subs.add((sub = {l, sel, err, ns}));
|
|
223
|
+
const k = +err;
|
|
224
|
+
if (!started[k]) { started[k] = true; queueMicrotask(() => flush(k)); }
|
|
225
|
+
},
|
|
226
|
+
stop: () => { subs.delete(sub); },
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
select: make(false),
|
|
231
|
+
errors: make(true),
|
|
232
|
+
isolateSource: (_: any, scope: any) => source(ns.concat(scope)),
|
|
233
|
+
isolateSink: (sink$: any, scope: any) => sink$.map((req: any) => tagRequest(req, scope)),
|
|
234
|
+
// PLAN-4.6: the same per value (the core scopes a request without a stream per instance)
|
|
235
|
+
isolateValue: tagRequest,
|
|
236
|
+
// the core sends a component's `resources` static here (PLAN-3 3-A)
|
|
237
|
+
__sygnalStatic: 'resources',
|
|
238
|
+
// 5-3: what is in flight, cached and declared (t.cache, inspect()); how many cache
|
|
239
|
+
// entries / mounted resources an `invalidate` value matches (dev diagnostics)
|
|
240
|
+
__inspect: inspect,
|
|
241
|
+
__matches: (x: any) => inval(x, 1),
|
|
242
|
+
...replies,
|
|
243
|
+
};
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// marks matching cache entries stale and refetches matching mounted resources (keeping
|
|
247
|
+
// data); `dry`: only counts them
|
|
248
|
+
const inval = (x: any, dry?: any) => {
|
|
249
|
+
let n = C ? C.inval(x, dry) : 0;
|
|
250
|
+
// G-184: a shared fetch already in flight for a matching entry never writes the cache
|
|
251
|
+
// (and nothing new joins it)
|
|
252
|
+
dry || C?.f.forEach((F: any) => hit(x, F.q) && stale(F));
|
|
253
|
+
const m = seq;
|
|
254
|
+
rsrc.forEach((cur, s) => cur.forEach((R, name) => { if (!R.p && R.q && hit(x, R.q)) { n++; dry || load(s, name, R, 1, m); } }));
|
|
255
|
+
return n;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
// ends a request: true if it was still live (so its result may be delivered). D79: its fetch
|
|
259
|
+
// is aborted when no request still wants it
|
|
260
|
+
const finish = (id: number) => {
|
|
261
|
+
const r = inflight.get(id);
|
|
262
|
+
if (!r) return false;
|
|
263
|
+
inflight.delete(id);
|
|
264
|
+
clearTimeout(r.timer);
|
|
265
|
+
const F = r.f;
|
|
266
|
+
if (F && !F.x && F.s.delete(r) && !F.s.size) {
|
|
267
|
+
done(F);
|
|
268
|
+
try { F.c.abort?.(); } catch (_) {}
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
};
|
|
272
|
+
const done = (F: any) => {
|
|
273
|
+
F.x = 1;
|
|
274
|
+
clearTimeout(F.t);
|
|
275
|
+
if (C?.f.get(F.k) === F) C.f.delete(F.k);
|
|
276
|
+
};
|
|
277
|
+
const stale = (F: any) => { F.z = 1; C.f.delete(F.k); };
|
|
278
|
+
const cancel = (which: (r: any) => boolean) =>
|
|
279
|
+
inflight.forEach((r, id) => { if (which(r)) finish(id); });
|
|
280
|
+
|
|
281
|
+
// 3-A: sender → resource name → R { j (JSON of its request), q (the request), k (cache key),
|
|
282
|
+
// last (the last write), p (paused), t (poll timer), i (its request id) }
|
|
283
|
+
const rsrc = new Map<any, Map<string, any>>();
|
|
284
|
+
const stop = (s: any, n: string, R: any) => {
|
|
285
|
+
clearTimeout(R.t);
|
|
286
|
+
cancel(r => r.sender === s && r.rk === '\0' + n);
|
|
287
|
+
};
|
|
288
|
+
// a disposed sender's requests with reply actions (and resources) are aborted
|
|
289
|
+
const {replies, reply} = makeReplies(sender => {
|
|
290
|
+
rsrc.get(sender)?.forEach(R => { clearTimeout(R.t); C?.use(R); });
|
|
291
|
+
rsrc.delete(sender);
|
|
292
|
+
cancel(r => r.rk !== undefined && r.sender === sender);
|
|
293
|
+
});
|
|
294
|
+
// G-184: `updates` (setQueryData): the reply (or fn(current data, reply)) becomes the data
|
|
295
|
+
// of the sender's named resources that have a request, and of their cache entries; a read
|
|
296
|
+
// of them in flight is aborted, and every mounted resource on that entry shows it at once
|
|
297
|
+
const upd = (s: any, u: any, v: any) => {
|
|
298
|
+
const cur = rsrc.get(s);
|
|
299
|
+
(typeof u == 'object' && !Array.isArray(u) ? Object.keys(u) : [].concat(u)).forEach((n: string) => {
|
|
300
|
+
const R = cur?.get(n), q = R?.q, f = u[n];
|
|
301
|
+
if (!q) return;
|
|
302
|
+
const k = C?.key(q, 1), d = typeof f == 'function' ? f(R.last?.data ?? C?.get(k)?.v, v) : v;
|
|
303
|
+
if (k !== undefined) {
|
|
304
|
+
C.put(k, q, d);
|
|
305
|
+
const F = C.f.get(k);
|
|
306
|
+
F && stale(F);
|
|
307
|
+
}
|
|
308
|
+
rsrc.forEach((c, s2) => c.forEach((R2, n2) => {
|
|
309
|
+
if (R2 === R || (k !== undefined && !R2.p && R2.k === k)) { stop(s2, n2, R2); write(s2, n2, R2, 'success', d); }
|
|
310
|
+
}));
|
|
311
|
+
});
|
|
312
|
+
};
|
|
313
|
+
const write = (s: any, n: string, R: any, status: string, data?: any, error?: any, refreshing?: any) => {
|
|
314
|
+
reply(s, 'RESOURCE', {name: n, ...(R.last = {status, data, error, refreshing: refreshing || undefined})});
|
|
315
|
+
// refetchEvery: polls after each result, skipping while the document is hidden
|
|
316
|
+
clearTimeout(R.t);
|
|
317
|
+
const ms = R.q?.refetchEvery;
|
|
318
|
+
if (ms > 0 && !R.p && !refreshing && SETTLED.test(status))
|
|
319
|
+
R.t = setTimeout(function tick() { isHidden() ? (R.t = setTimeout(tick, ms)) : load(s, n, R, 1, seq); }, ms);
|
|
320
|
+
};
|
|
321
|
+
// D78: (re)loads resource R. `same`: the same request again (keeps data); `after` (a
|
|
322
|
+
// refetch: refresh, invalidation, focus, polling) skips the cache and fetches anew
|
|
323
|
+
const load = (s: any, n: string, R: any, same?: any, after?: number) => {
|
|
324
|
+
const q = R.q, k = C?.key(q, 1), E = C?.get(k), L = R.last, go = after !== undefined || !C?.fresh(k, q);
|
|
325
|
+
stop(s, n, R);
|
|
326
|
+
C?.use(R, k);
|
|
327
|
+
if (E && 'v' in E && !q.validate) write(s, n, R, 'success', E.v, undefined, go);
|
|
328
|
+
else if ((same || q.keepPrevious) && L && SETTLED.test(L.status)) write(s, n, R, L.status, L.data, L.error, true);
|
|
329
|
+
else write(s, n, R, 'loading');
|
|
330
|
+
if (go || q.validate) R.i = send(q, s, n, after, R);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
// `pf`: a prefetch (into the cache, no reply)
|
|
334
|
+
const send = (req: any, sender = senderOf(req), rn?: string, after = -1, R?: any, pf?: any): any => {
|
|
335
|
+
if (typeof req == 'string') req = {url: req};
|
|
336
|
+
if (!req || typeof req != 'object' || disposed || !allowed(req, 'makeFetchDriver')) return;
|
|
337
|
+
const {url, category, method, headers, query, json, body, latest, timeoutMs, parse, abort, init, ok, error, key, resources, refresh, invalidates, updates, retry, validate, prefetch} = req;
|
|
338
|
+
if ('invalidate' in req) return inval(req.invalidate);
|
|
339
|
+
if (prefetch) return C && send(prefetch, 0, rn, -1, R, 1);
|
|
340
|
+
if (resources || refresh) {
|
|
341
|
+
if (sender === undefined) return;
|
|
342
|
+
const cur = rsrc.get(sender) || new Map();
|
|
343
|
+
rsrc.set(sender, cur);
|
|
344
|
+
if (refresh) return [].concat(refresh).forEach((n: string) => { const R = cur.get(n); R?.q && !R.p && load(sender, n, R, 1, seq); });
|
|
345
|
+
new Set([...cur.keys(), ...Object.keys(resources)]).forEach(n => {
|
|
346
|
+
let R = cur.get(n);
|
|
347
|
+
// D85 / G-177: left out (a hidden Switchable page): paused, keeping its last result
|
|
348
|
+
if (!(n in resources)) {
|
|
349
|
+
if (R && !R.p) {
|
|
350
|
+
R.p = 1;
|
|
351
|
+
stop(sender, n, R);
|
|
352
|
+
C?.use(R);
|
|
353
|
+
const L = R.last;
|
|
354
|
+
if (!L || !SETTLED.test(L.status)) write(sender, n, R, 'idle');
|
|
355
|
+
else if (L.refreshing) write(sender, n, R, L.status, L.data, L.error);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
let q = resources[n];
|
|
360
|
+
if (q && typeof q == 'string') q = {url: q};
|
|
361
|
+
const j = q ? JSON.stringify(q) : '', paused = R?.p;
|
|
362
|
+
if (!R) cur.set(n, (R = {j: ''}));
|
|
363
|
+
R.p = 0;
|
|
364
|
+
if (R.j === j && !paused) return;
|
|
365
|
+
const same = R.j === j;
|
|
366
|
+
R.j = j;
|
|
367
|
+
R.q = q;
|
|
368
|
+
if (q) load(sender, n, R, same);
|
|
369
|
+
else { stop(sender, n, R); C?.use(R); write(sender, n, R, 'idle'); }
|
|
370
|
+
});
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const scope = scopeKey(req);
|
|
374
|
+
// reply actions: keyed per (sender, key ?? ok ?? error); a resource per (sender, '\0' + name)
|
|
375
|
+
const rk = rn !== undefined ? '\0' + rn : !pf && sender !== undefined && (ok || error) ? key ?? ok ?? error : undefined;
|
|
376
|
+
if (sender !== undefined && (typeof abort == 'string' || (abort && key !== undefined)))
|
|
377
|
+
return cancel(r => r.sender === sender && r.rk === (key ?? abort));
|
|
378
|
+
// per (scope, category): another component's requests are never cancelled
|
|
379
|
+
if (abort) return cancel(r => r.scope === scope && (!('category' in req) || r.category === category));
|
|
380
|
+
if (rn !== undefined || (!pf && (latest ?? options.latest))) cancel(rk === undefined ? r => r.rk === undefined && r.scope === scope && r.category === category : r => r.sender === sender && r.rk === rk);
|
|
381
|
+
|
|
382
|
+
const href = withQuery((options.baseUrl || '') + (url ?? ''), query);
|
|
383
|
+
const base = options.init, own = init;
|
|
384
|
+
const h = mergeHeaders(options.headers, base?.headers, own?.headers, headers);
|
|
385
|
+
let b = body ?? own?.body;
|
|
386
|
+
if (json !== undefined) {
|
|
387
|
+
b = JSON.stringify(json);
|
|
388
|
+
if (!Object.keys(h).some(k => k.toLowerCase() == 'content-type')) h['content-type'] = 'application/json';
|
|
389
|
+
}
|
|
390
|
+
const m = method || own?.method || base?.method || (b !== undefined ? 'POST' : 'GET');
|
|
391
|
+
const k = C?.key(req, rn !== undefined || pf);
|
|
392
|
+
const id = ++seq;
|
|
393
|
+
const r: any = {category, scope, sender, rk};
|
|
394
|
+
inflight.set(id, r);
|
|
395
|
+
// an outcome with a reply action goes to the sender's actions, the other to errors()/select()
|
|
396
|
+
const fail = r.ko = (e: any, extra?: any) => finish(id) && (R && write(sender, rn!, R, 'error', R.last?.data, e),
|
|
397
|
+
rk !== undefined && error ? reply(sender, error, {error: e, request: req, ...extra}) : rn === undefined && !pf && emit(true, {error: e, category, request: req, ...extra}));
|
|
398
|
+
r.ok = async (v: any, status: number) => {
|
|
399
|
+
if (validate) try { v = await validateWith(validate, v); } catch (e: any) { return fail(e, {status, issues: e.issues}); }
|
|
400
|
+
if (!finish(id)) return;
|
|
401
|
+
if (R) write(sender, rn!, R, 'success', v);
|
|
402
|
+
if (updates) upd(sender, updates, v);
|
|
403
|
+
rk !== undefined && ok ? reply(sender, ok, v) : rn === undefined && !pf && emit(false, {category, value: v, status, request: req});
|
|
404
|
+
if (invalidates) inval(invalidates);
|
|
405
|
+
};
|
|
406
|
+
const ms = timeoutMs ?? options.timeoutMs;
|
|
407
|
+
if (ms > 0) {
|
|
408
|
+
r.timer = setTimeout(() => {
|
|
409
|
+
const e: any = new Error(`Request timed out after ${ms}ms: ${href}`);
|
|
410
|
+
e.name = 'TimeoutError';
|
|
411
|
+
fail(e);
|
|
412
|
+
}, ms);
|
|
413
|
+
}
|
|
414
|
+
// D79: a fresh cache entry answers without a fetch
|
|
415
|
+
if (after < 0 && C?.fresh(k, req)) return queueMicrotask(() => r.ok(C.get(k).v, 200)), id;
|
|
416
|
+
// D79: identical cacheable requests share a fetch (a refetch only one started after it asked)
|
|
417
|
+
let F = k !== undefined && C.f.get(k);
|
|
418
|
+
if (!F || F.id <= after) {
|
|
419
|
+
const rp = retry ?? (IDEMPOTENT.test(m) ? options.retry : 0);
|
|
420
|
+
const pol = rp && typeof rp == 'object' ? rp : {count: +rp || 0};
|
|
421
|
+
const tries = pol.count ?? 3;
|
|
422
|
+
F = {id, k, q: req, s: new Set(), n: 0};
|
|
423
|
+
if (k !== undefined) C.f.set(k, F);
|
|
424
|
+
const pz = parse || options.parse || 'auto';
|
|
425
|
+
const parser = typeof pz == 'function' ? pz : PARSERS[pz] || autoParse;
|
|
426
|
+
// the global fetch is called as a method: a detached window.fetch throws "Illegal invocation"
|
|
427
|
+
const g: any = globalThis;
|
|
428
|
+
const doFetch = options.fetch || (typeof g.fetch == 'function' && ((u: string, i: any) => g.fetch(u, i)));
|
|
429
|
+
const fetchInit = {
|
|
430
|
+
...pickInit(base),
|
|
431
|
+
...pickInit(own),
|
|
432
|
+
method: m,
|
|
433
|
+
headers: h,
|
|
434
|
+
body: b,
|
|
435
|
+
};
|
|
436
|
+
// the outcome, to every request waiting on this fetch (the failure once, after the last attempt)
|
|
437
|
+
const end = (e: any, x?: any, status?: number) => {
|
|
438
|
+
done(F);
|
|
439
|
+
F.s.forEach((w: any) => e ? w.ko(e, tries ? {...x, attempts: F.n} : x) : w.ok(x, status));
|
|
440
|
+
};
|
|
441
|
+
const go = () => {
|
|
442
|
+
const ctl: any = F.c = typeof AbortController == "function" ? new AbortController() : {};
|
|
443
|
+
const live = () => F.c === ctl && !F.x;
|
|
444
|
+
// D80: retries left: the next attempt is scheduled (a Retry-After in seconds wins)
|
|
445
|
+
const again = (res?: any) => {
|
|
446
|
+
if (F.n > tries) return false;
|
|
447
|
+
F.t = setTimeout(go, res?.headers?.get?.('retry-after') * 1e3 || backoff(pol, F.n - 1));
|
|
448
|
+
return true;
|
|
449
|
+
};
|
|
450
|
+
F.n++;
|
|
451
|
+
let p: Promise<any>;
|
|
452
|
+
try {
|
|
453
|
+
if (typeof doFetch != 'function') throw new Error('fetch is not available in this environment (pass makeFetchDriver({ fetch }))');
|
|
454
|
+
// 5-1 (H-9): renderComponent's fake learns which request (and resource) the next fetch is for
|
|
455
|
+
options._tap?.(req, rn, pf);
|
|
456
|
+
// called synchronously, so a test sees the call right after the sink emitted
|
|
457
|
+
p = Promise.resolve(doFetch(href, {...fetchInit, signal: ctl.signal}));
|
|
458
|
+
} catch (e) {
|
|
459
|
+
p = Promise.reject(e);
|
|
460
|
+
}
|
|
461
|
+
p.then(async (res: any) => {
|
|
462
|
+
if (!live()) return;
|
|
463
|
+
// R4-3: anything that throws here (a non-Response, a parser) fails the request
|
|
464
|
+
try {
|
|
465
|
+
const st = res.status;
|
|
466
|
+
if (res.ok === false || st < 200 || st > 299) {
|
|
467
|
+
let errBody: any;
|
|
468
|
+
try { errBody = await autoParse(res); } catch (_) {}
|
|
469
|
+
if (!live() || ((st == 408 || st == 429 || st > 499) && again(res))) return;
|
|
470
|
+
const e: any = new Error(`HTTP ${st}${res.statusText ? ' ' + res.statusText : ''}: ${href}`);
|
|
471
|
+
e.status = st;
|
|
472
|
+
e.body = errBody;
|
|
473
|
+
return end(e, {status: st, body: errBody});
|
|
474
|
+
}
|
|
475
|
+
let v: any;
|
|
476
|
+
try { v = await parser(res); } catch (e) { return live() && end(e, {status: st}); }
|
|
477
|
+
if (!live()) return;
|
|
478
|
+
if (k !== undefined && !F.z) C.put(k, req, v);
|
|
479
|
+
end(0, v, st);
|
|
480
|
+
} catch (e) {
|
|
481
|
+
live() && end(e);
|
|
482
|
+
}
|
|
483
|
+
}, (e: any) => { live() && !again() && end(e); });
|
|
484
|
+
};
|
|
485
|
+
go();
|
|
486
|
+
}
|
|
487
|
+
F.s.add(r);
|
|
488
|
+
r.f = F;
|
|
489
|
+
return id;
|
|
490
|
+
};
|
|
491
|
+
|
|
492
|
+
// D79: focus / reconnect refetch the stale mounted resources (the fake passes `_on`)
|
|
493
|
+
const off = C?.on(options._on, () => {
|
|
494
|
+
const m = seq;
|
|
495
|
+
rsrc.forEach((cur, s) => cur.forEach((R, n) => {
|
|
496
|
+
if (!R.p && R.q && R.k !== undefined && !inflight.has(R.i) && !C.fresh(R.k, R.q)) load(s, n, R, 1, m);
|
|
497
|
+
}));
|
|
498
|
+
});
|
|
499
|
+
if (C) C.d = send;
|
|
500
|
+
// the cache listing (t.cache, inspect()); undefined without a queryCache (dev: SYG635)
|
|
501
|
+
const inspect = () => ({cache: C?.list()});
|
|
502
|
+
const shut = () => {
|
|
503
|
+
cancel(() => true);
|
|
504
|
+
off?.();
|
|
505
|
+
rsrc.forEach(cur => cur.forEach(R => clearTimeout(R.t)));
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
request$.addListener({
|
|
509
|
+
next: (req: any) => { try { send(req); } catch (e) { console.error('[Sygnal] makeFetchDriver: invalid request', req, e); } },
|
|
510
|
+
error: (e: any) => console.error('[Sygnal] makeFetchDriver: the request stream errored', e),
|
|
511
|
+
complete: shut,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
return {...source([]), dispose: () => { shut(); disposed = true; }};
|
|
515
|
+
};
|
|
516
|
+
// the Vike glue hydrates the cache of the drivers it is given (pageContext.queryCache)
|
|
517
|
+
driver.cache = C;
|
|
518
|
+
return driver;
|
|
519
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-5 D194: focusWithin(selector), an ELEMENT target for focusing an element inside the
|
|
3
|
+
* sender's children (a child component's field, an input in a Collection item):
|
|
4
|
+
*
|
|
5
|
+
* ADD: { STATE: addRow, ELEMENT: (s) => ({ focus: focusWithin(`[data-id="${s.next}"] .title`) }) },
|
|
6
|
+
* SUBMIT: { ELEMENT: { focus: focusWithin('[name="email"]'), preventScroll: true } },
|
|
7
|
+
*
|
|
8
|
+
* A plain `{ focus: '.x' }` is resolved in the sender's isolated scope, so it can't reach into
|
|
9
|
+
* a child. This target selects the sender's root element (its selector is '') and declares a
|
|
10
|
+
* `focus` spec command (D102, elementCommands.ts) that focuses the first element under the root
|
|
11
|
+
* matching `selector` (the root itself if it matches), with the command's options. 0 B core.
|
|
12
|
+
*
|
|
13
|
+
* It runs after the next patch, as every element command: a Collection item added by the same
|
|
14
|
+
* action is on the page by then. No match: nothing happens (renderComponent's mock DOM reports
|
|
15
|
+
* SYG640 when nothing in the view, children included, matches). A fragment-rooted sender
|
|
16
|
+
* searches its first root element.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** An ELEMENT target: `{ focus: focusWithin('.title') }` focuses `.title` anywhere under the sender's root, children included. */
|
|
20
|
+
export const focusWithin = (selector: string): any => ({
|
|
21
|
+
within: selector,
|
|
22
|
+
toString: () => '',
|
|
23
|
+
spec: {commands: {focus: (root: any, o: any) => {
|
|
24
|
+
const el = root.matches?.(selector) ? root : root.querySelector?.(selector)
|
|
25
|
+
el?.focus(o)
|
|
26
|
+
}}},
|
|
27
|
+
})
|