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,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-4 GS-1: the first-party `selection` behavior.
|
|
3
|
+
*
|
|
4
|
+
* const { Pick, All } = controls({ Pick: 'input', All: 'input' })
|
|
5
|
+
* Mailbox.uses = { sel: selection({ multi: true, item: Pick, all: All, from: 'mails' }) }
|
|
6
|
+
* <Pick type="checkbox" data-id={mail.id} checked={isSelected(state.sel, mail.id)} aria-label={mail.subject} />
|
|
7
|
+
*
|
|
8
|
+
* Slice: { selected } (ids as strings, in selection order) plus the calculated `count`.
|
|
9
|
+
* Actions: sel.SELECT (an id, or a click on the `item` control: its `attr`, default data-id;
|
|
10
|
+
* single mode replaces the selection, multi mode toggles the id), sel.SELECT_ALL (an id array,
|
|
11
|
+
* or every `idField` of the host's `state[from]` list), sel.TOGGLE_ALL (the `all` control: all
|
|
12
|
+
* selected → none, else all), sel.CLEAR. Each is ABORT when nothing changes.
|
|
13
|
+
*
|
|
14
|
+
* `from` is read from the host state, which a behavior's own reducers don't see, so this
|
|
15
|
+
* factory wraps the merged SELECT_ALL / TOGGLE_ALL entries to pass the host list's ids.
|
|
16
|
+
*/
|
|
17
|
+
import { defineBehavior } from './behaviors'
|
|
18
|
+
import { ABORT } from '../shared'
|
|
19
|
+
|
|
20
|
+
const ids = (d: any) => Array.isArray(d) ? d.map(String) : null
|
|
21
|
+
const set = (s: any, selected: string[]) =>
|
|
22
|
+
selected.join('\0') === s.selected.join('\0') ? ABORT : {...s, selected}
|
|
23
|
+
// an id, or a click event: the item element's attribute (target.closest: a click on an icon inside)
|
|
24
|
+
const idOf = (d: any, attr: string, item: any): any => {
|
|
25
|
+
if (!d || typeof d != 'object') return d
|
|
26
|
+
const el = d.target?.closest?.(String(item)) || d.ownerTarget || d.target
|
|
27
|
+
return el?.getAttribute?.(attr) ?? el?.dataset?.[attr.slice(5).replace(/-(\w)/g, (_: any, c: string) => c.toUpperCase())]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Is `id` selected in a `selection` slice (`isSelected(state.sel, item.id)`)? Ids compare as strings. */
|
|
31
|
+
export const isSelected = (slice: any, id: any): boolean => !!slice?.selected?.includes(String(id))
|
|
32
|
+
|
|
33
|
+
const def = (multi: boolean, attr: string, item: any) => ({
|
|
34
|
+
initialState: {selected: [] as string[]},
|
|
35
|
+
intent: ({DOM}: any, {item, all, clear}: any) => ({
|
|
36
|
+
...(item && {SELECT: DOM.click(item)}),
|
|
37
|
+
...(all && {TOGGLE_ALL: DOM.click(all)}),
|
|
38
|
+
...(clear && {CLEAR: DOM.click(clear)}),
|
|
39
|
+
}),
|
|
40
|
+
model: {
|
|
41
|
+
SELECT: (s: any, d: any) => {
|
|
42
|
+
const id = idOf(d, attr, item)
|
|
43
|
+
if (id == null) return ABORT
|
|
44
|
+
const k = String(id), has = s.selected.includes(k)
|
|
45
|
+
return set(s, !multi ? [k] : has ? s.selected.filter((x: string) => x !== k) : [...s.selected, k])
|
|
46
|
+
},
|
|
47
|
+
SELECT_ALL: (s: any, d: any) => ids(d) ? set(s, ids(d)!) : ABORT,
|
|
48
|
+
TOGGLE_ALL: (s: any, d: any) => {
|
|
49
|
+
const all = ids(d)
|
|
50
|
+
return !all ? ABORT : set(s, all.every(k => s.selected.includes(k)) ? [] : all)
|
|
51
|
+
},
|
|
52
|
+
CLEAR: (s: any) => set(s, []),
|
|
53
|
+
},
|
|
54
|
+
calculated: {count: (s: any) => s.selected.length},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Single or multiple selection over a list (GS-1 behavior). Options: `multi` (false), the
|
|
59
|
+
* controls `item` (on each row, with the id in `attr`, default 'data-id'), `all` (select-all
|
|
60
|
+
* toggle; needs `from`) and `clear`, `from` (the host state key of the list) and `idField`
|
|
61
|
+
* ('id', the id field of its items). Read it with `state.sel.selected`, `state.sel.count` and
|
|
62
|
+
* `isSelected(state.sel, id)`.
|
|
63
|
+
*/
|
|
64
|
+
export const selection = (options: any = {}): any => {
|
|
65
|
+
const {multi = false, attr = 'data-id', from, idField = 'id'} = options
|
|
66
|
+
const b = defineBehavior(def(multi, attr, options.item))(options), merge = b.merge
|
|
67
|
+
b.merge = (c: any, k: string) => {
|
|
68
|
+
merge(c, k)
|
|
69
|
+
if (from) for (const a of ['SELECT_ALL', 'TOGGLE_ALL']) {
|
|
70
|
+
const e = c.model[k + '.' + a], S = c.stateSourceName, f = e?.[S]
|
|
71
|
+
if (f) c.model[k + '.' + a] = {...e, [S]: (st: any, d: any, ...x: any[]) =>
|
|
72
|
+
f(st, Array.isArray(d) ? d : (st?.[from] || []).map((i: any) => i?.[idField]), ...x)}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return b
|
|
76
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import xs, {Stream} from 'xstream';
|
|
2
|
+
import {senderOf, keepSender, allowed, makeReplies} from './replies';
|
|
3
|
+
import {error as logError} from './diagnostics/legacy';
|
|
4
|
+
import {backoff} from './backoff';
|
|
5
|
+
|
|
6
|
+
/*
|
|
7
|
+
* makeSocketDriver(options?) (PLAN-3 §1.3): WebSocket and server-sent events, declared per
|
|
8
|
+
* component instance and answered with reply actions (./replies.ts). Docs on the declarations
|
|
9
|
+
* in src/index.d.ts. Summary:
|
|
10
|
+
*
|
|
11
|
+
* - Sink values:
|
|
12
|
+
* - `{ connections: { [name]: spec | falsy } }`: the sender's whole set of connections. Diffed
|
|
13
|
+
* per (sender, name): a new name opens, a removed or falsy one closes, a changed URL /
|
|
14
|
+
* protocols / withCredentials / share closes and reopens, anything else (action names,
|
|
15
|
+
* reconnect) only rebinds.
|
|
16
|
+
* - `{ to: name, json? | text? | binary? }`: send on the sender's connection `name` (json is
|
|
17
|
+
* stringified). Queued while (re)connecting, bounded (`queueLimit`, oldest dropped). A send
|
|
18
|
+
* to no such connection, a closed one, or an SSE one is SYG611 (not sent).
|
|
19
|
+
* - A value with a `then` / `catch` key (or a spec with one) is SYG610 (ignored).
|
|
20
|
+
* - Spec: `{ socket: url, protocols?, message?, open?, close?, error?, reconnect?, share? }` or
|
|
21
|
+
* `{ sse: url, withCredentials?, message?, open?, close?, error?, events?, reconnect?, share? }`.
|
|
22
|
+
* URLs are prefixed with `baseUrl` unless absolute; a socket path resolves against `location`
|
|
23
|
+
* with ws:/wss: for http:/https:.
|
|
24
|
+
* - Reply actions (to exactly the sender): `message` (data: the JSON-parsed frame when it parses,
|
|
25
|
+
* else the raw data), `open` ({ reconnected }), `close` ({ code, reason, willReconnect }: only
|
|
26
|
+
* for closes the driver didn't make, G-148), `error` ({ error }); SSE `events: { name: 'ACTION' }`
|
|
27
|
+
* names actions for named events. An event type without an action name goes to `select(name?)` as
|
|
28
|
+
* `{ name, type, data }` (the plain form).
|
|
29
|
+
* - Reconnect (default on): jittered exponential backoff, `{ delayMs: 500, maxDelayMs: 10000,
|
|
30
|
+
* jitter: 0.2 }`, per spec over the driver's `reconnect`; `jitter: false` + equal delays is a
|
|
31
|
+
* fixed delay. A failure to open is a drop. Removal, replacement or dispose cancels a retry.
|
|
32
|
+
* SSE: EventSource retries transient drops itself (close fires with willReconnect: true); the
|
|
33
|
+
* driver's reconnect applies when it gives up (readyState CLOSED).
|
|
34
|
+
* - Sharing (default): connections with the same URL (and protocols / withCredentials) share one
|
|
35
|
+
* socket, ref-counted; every subscriber gets each event. `share: false` opts out.
|
|
36
|
+
* - A disposed sender's connections close (and their retries stop); app dispose closes all.
|
|
37
|
+
* - No WebSocket / EventSource (SSR): nothing opens, nothing is reported.
|
|
38
|
+
* - `WebSocket` / `EventSource` are read at connect time: options.WebSocket ?? globalThis.WebSocket.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
const SCOPE = '__sygnalScope';
|
|
42
|
+
const scopeOf = (v: any): any[] => v[SCOPE] || [];
|
|
43
|
+
/** a copy of a sink value tagged with one more (outer) scope */
|
|
44
|
+
const tag = (v: any, scope: any) => v && typeof v == 'object'
|
|
45
|
+
? Object.defineProperty(keepSender(v, {...v}), SCOPE, {value: [scope, ...scopeOf(v)]}) : v;
|
|
46
|
+
|
|
47
|
+
export function makeSocketDriver(options: any = {}) {
|
|
48
|
+
return (sink$: Stream<any>) => {
|
|
49
|
+
const g: any = globalThis;
|
|
50
|
+
// sender → name → entry { sender, name, spec, scope, key, t (its transport), opened }
|
|
51
|
+
const conns = new Map<any, Map<string, any>>();
|
|
52
|
+
// key → transport { key, url, spec, subs: Set<entry>, s (socket), open, queue, n (attempts), timer, on (SSE names) }
|
|
53
|
+
const transports = new Map<string, any>();
|
|
54
|
+
const subs = new Set<any>();
|
|
55
|
+
let disposed = false;
|
|
56
|
+
let unshared = 0;
|
|
57
|
+
|
|
58
|
+
const bad = (msg: string, v: any) =>
|
|
59
|
+
logError('SYG611', v.__emitterName, `makeSocketDriver: ${msg}`, "Declare { connections: { room: { socket: url } } }, then send { to: 'room', json }", v);
|
|
60
|
+
|
|
61
|
+
const deliver = (e: any, type: string, data: any) => {
|
|
62
|
+
const action = e.spec[type];
|
|
63
|
+
if (typeof action == 'string' && e.sender !== undefined) return reply(e.sender, action, data);
|
|
64
|
+
const ev = {name: e.name, type, data};
|
|
65
|
+
subs.forEach(s => {
|
|
66
|
+
if ((s.name === undefined || s.name === e.name) && s.ns.every((x: any, i: number) => e.scope[i] === x)) {
|
|
67
|
+
try { s.l.next(ev); } catch (err) { console.error(err); }
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
const each = (t: any, f: (e: any) => any) => [...t.subs].forEach(f);
|
|
72
|
+
const parse = (d: any) => {
|
|
73
|
+
if (typeof d == 'string') try { return JSON.parse(d); } catch (_) {}
|
|
74
|
+
return d;
|
|
75
|
+
};
|
|
76
|
+
const resolve = (u: string, sse: boolean) => {
|
|
77
|
+
if (!/^[a-z][a-z\d+.-]*:/i.test(u)) u = (options.baseUrl || '') + u;
|
|
78
|
+
const loc = g.location;
|
|
79
|
+
if (sse || /^wss?:/i.test(u) || !loc) return u;
|
|
80
|
+
try {
|
|
81
|
+
const r = new URL(u, loc.href);
|
|
82
|
+
r.protocol = r.protocol == 'https:' ? 'wss:' : 'ws:';
|
|
83
|
+
return r.href;
|
|
84
|
+
} catch (_) { return u; }
|
|
85
|
+
};
|
|
86
|
+
const policy = (spec: any) => {
|
|
87
|
+
const r = spec.reconnect, d = options.reconnect;
|
|
88
|
+
return r === false || (r == null && d === false) ? null : {...(d && typeof d == 'object' && d), ...(r && typeof r == 'object' && r)};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const out = (t: any, data: any, e: any) => {
|
|
92
|
+
try { t.s.send(data); } catch (error) { deliver(e, 'error', {error}); }
|
|
93
|
+
};
|
|
94
|
+
// SSE named events: one listener per name across the subscribers
|
|
95
|
+
const listen = (t: any) => t.s && t.sse && each(t, e => Object.keys(e.spec.events || {}).forEach(name => {
|
|
96
|
+
if (t.on.has(name)) return;
|
|
97
|
+
t.on.add(name);
|
|
98
|
+
const s = t.s;
|
|
99
|
+
s.addEventListener(name, (m: any) => t.s === s && each(t, x => {
|
|
100
|
+
const a = x.spec.events?.[name];
|
|
101
|
+
if (typeof a == 'string' && x.sender !== undefined) reply(x.sender, a, parse(m.data));
|
|
102
|
+
}));
|
|
103
|
+
}));
|
|
104
|
+
|
|
105
|
+
// the socket closed (or failed to open) without the driver closing it
|
|
106
|
+
const gone = (t: any, info: any) => {
|
|
107
|
+
t.s = null;
|
|
108
|
+
t.open = false;
|
|
109
|
+
let again: any;
|
|
110
|
+
each(t, e => {
|
|
111
|
+
const p = !t.x && policy(e.spec);
|
|
112
|
+
deliver(e, 'close', {...info, willReconnect: !!p});
|
|
113
|
+
if (p) again = again || p;
|
|
114
|
+
else detach(e, true);
|
|
115
|
+
});
|
|
116
|
+
if (again && t.subs.size) t.timer = setTimeout(() => connect(t), backoff(again, t.n++));
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const connect = (t: any) => {
|
|
120
|
+
t.timer = 0;
|
|
121
|
+
const C = t.sse ? options.EventSource ?? g.EventSource : options.WebSocket ?? g.WebSocket;
|
|
122
|
+
if (typeof C != 'function') return;
|
|
123
|
+
let s: any;
|
|
124
|
+
try {
|
|
125
|
+
s = t.sse ? new C(t.url, {withCredentials: !!t.spec.withCredentials}) : new C(t.url, t.spec.protocols);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// a URL the constructor rejects never opens: no retry
|
|
128
|
+
each(t, e => deliver(e, 'error', {error}));
|
|
129
|
+
t.x = 1;
|
|
130
|
+
return gone(t, {code: 1006, reason: '' + error});
|
|
131
|
+
}
|
|
132
|
+
t.s = s;
|
|
133
|
+
t.on = new Set();
|
|
134
|
+
const mine = (f: any) => (ev: any) => { if (t.s === s) f(ev); };
|
|
135
|
+
s.onopen = mine(() => {
|
|
136
|
+
t.n = 0;
|
|
137
|
+
t.open = true;
|
|
138
|
+
each(t, e => { deliver(e, 'open', {reconnected: !!e.opened}); e.opened = true; });
|
|
139
|
+
const q = t.queue;
|
|
140
|
+
t.queue = [];
|
|
141
|
+
q.forEach((x: any) => t.s === s && out(t, x.data, x.e));
|
|
142
|
+
});
|
|
143
|
+
s.onmessage = mine((m: any) => { const d = parse(m.data); each(t, e => deliver(e, 'message', d)); });
|
|
144
|
+
s.onerror = mine((error: any) => {
|
|
145
|
+
each(t, e => deliver(e, 'error', {error}));
|
|
146
|
+
if (t.sse) {
|
|
147
|
+
// CLOSED: EventSource gave up (the driver's reconnect applies); else it retries itself
|
|
148
|
+
if (s.readyState == 2 || ![...t.subs].some(e => policy(e.spec))) { s.close(); gone(t, {}); }
|
|
149
|
+
else if (t.open) { t.open = false; each(t, e => deliver(e, 'close', {willReconnect: true})); }
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (!t.sse) s.onclose = mine((c: any) => gone(t, {code: c.code, reason: c.reason}));
|
|
153
|
+
listen(t);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// closes the driver makes: no `close` action, late events ignored, retry cancelled
|
|
157
|
+
const end = (t: any) => {
|
|
158
|
+
clearTimeout(t.timer);
|
|
159
|
+
transports.delete(t.key);
|
|
160
|
+
const s = t.s;
|
|
161
|
+
t.s = null;
|
|
162
|
+
if (s) try { s.close(); } catch (_) {}
|
|
163
|
+
};
|
|
164
|
+
const detach = (e: any, keep?: boolean) => {
|
|
165
|
+
const t = e.t;
|
|
166
|
+
if (!keep) conns.get(e.sender)?.delete(e.name);
|
|
167
|
+
if (!t) return;
|
|
168
|
+
e.t = null;
|
|
169
|
+
t.subs.delete(e);
|
|
170
|
+
t.queue = t.queue.filter((x: any) => x.e !== e);
|
|
171
|
+
if (!t.subs.size) end(t);
|
|
172
|
+
};
|
|
173
|
+
const attach = (e: any) => {
|
|
174
|
+
const key = e.spec.share === false ? e.key + '#' + ++unshared : e.key;
|
|
175
|
+
let t = transports.get(key);
|
|
176
|
+
const fresh = !t;
|
|
177
|
+
if (fresh) transports.set(key, (t = {key, sse: e.sse, url: e.url, spec: e.spec, subs: new Set(), queue: [], n: 0, on: new Set()}));
|
|
178
|
+
e.t = t;
|
|
179
|
+
t.subs.add(e);
|
|
180
|
+
if (fresh) connect(t);
|
|
181
|
+
else {
|
|
182
|
+
listen(t);
|
|
183
|
+
// joined an open shared socket: it is open for this subscriber too
|
|
184
|
+
if (t.open) queueMicrotask(() => { if (e.t === t && t.open && !e.opened) { e.opened = true; deliver(e, 'open', {reconnected: false}); } });
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const declare = (sender: any, next: any, scope: any[]) => {
|
|
189
|
+
let mine = conns.get(sender);
|
|
190
|
+
if (!mine) conns.set(sender, (mine = new Map()));
|
|
191
|
+
mine.forEach((e, name) => { if (!next[name]) detach(e); });
|
|
192
|
+
Object.keys(next).forEach(name => {
|
|
193
|
+
const spec = next[name];
|
|
194
|
+
if (!spec) return;
|
|
195
|
+
const old = mine!.get(name);
|
|
196
|
+
if (typeof spec != 'object' || (typeof spec.socket != 'string' && typeof spec.sse != 'string') || !allowed(spec, 'makeSocketDriver')) {
|
|
197
|
+
if (old) detach(old);
|
|
198
|
+
if (typeof spec == 'object' && ('then' in spec || 'catch' in spec)) return;
|
|
199
|
+
return bad(`connection '${name}' needs a socket or sse URL`, spec);
|
|
200
|
+
}
|
|
201
|
+
const sse = typeof spec.sse == 'string';
|
|
202
|
+
const url = resolve(sse ? spec.sse : spec.socket, sse);
|
|
203
|
+
// the connection's identity: a change reconnects, anything else only rebinds
|
|
204
|
+
const key = (spec.share === false ? 'u' : '') + (sse ? 'e' + !!spec.withCredentials : 's' + JSON.stringify(spec.protocols)) + url;
|
|
205
|
+
if (old && old.key === key) {
|
|
206
|
+
old.spec = spec;
|
|
207
|
+
if (old.t) listen(old.t);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (old) detach(old);
|
|
211
|
+
const e = {sender, name, spec, scope, key, sse, url, t: null, opened: false};
|
|
212
|
+
mine!.set(name, e);
|
|
213
|
+
attach(e);
|
|
214
|
+
});
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const send = (sender: any, v: any) => {
|
|
218
|
+
const e = conns.get(sender)?.get(v.to);
|
|
219
|
+
const t = e?.t;
|
|
220
|
+
if (!t || t.sse) return bad(t ? `'${v.to}' is a server-sent events connection (read-only)` : `no connection named '${v.to}' is open`, v);
|
|
221
|
+
const data = v.json !== undefined ? JSON.stringify(v.json) : v.text ?? v.binary;
|
|
222
|
+
if (t.open) out(t, data, e);
|
|
223
|
+
else if (t.queue.push({data, e}) > (options.queueLimit ?? 100)) t.queue.shift();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const closeAll = () => {
|
|
227
|
+
conns.forEach(m => m.forEach(e => detach(e)));
|
|
228
|
+
conns.clear();
|
|
229
|
+
};
|
|
230
|
+
const {replies, reply} = makeReplies(sender => {
|
|
231
|
+
conns.get(sender)?.forEach(e => detach(e));
|
|
232
|
+
conns.delete(sender);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
sink$.addListener({
|
|
236
|
+
next: (v: any) => {
|
|
237
|
+
if (!v || typeof v != 'object' || disposed || !allowed(v, 'makeSocketDriver')) return;
|
|
238
|
+
try {
|
|
239
|
+
const sender = senderOf(v);
|
|
240
|
+
if (v.connections !== undefined) declare(sender, v.connections || {}, scopeOf(v));
|
|
241
|
+
else if (v.to !== undefined) send(sender, v);
|
|
242
|
+
else bad('expected { connections } or { to, json | text | binary }', v);
|
|
243
|
+
} catch (e) { console.error('[Sygnal] makeSocketDriver', v, e); }
|
|
244
|
+
},
|
|
245
|
+
error: (e: any) => console.error('[Sygnal] makeSocketDriver', e),
|
|
246
|
+
complete: closeAll,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const source = (ns: any[]): any => ({
|
|
250
|
+
select: (name?: string) => {
|
|
251
|
+
let sub: any;
|
|
252
|
+
return xs.create<any>({start: l => { subs.add((sub = {l, name, ns})); }, stop: () => { subs.delete(sub); }});
|
|
253
|
+
},
|
|
254
|
+
isolateSource: (_: any, scope: any) => source(ns.concat(scope)),
|
|
255
|
+
isolateSink: (sink: any, scope: any) => sink.map((v: any) => tag(v, scope)),
|
|
256
|
+
// PLAN-4.6: the same per value (the core scopes a value without a stream per instance)
|
|
257
|
+
isolateValue: tag,
|
|
258
|
+
// the core sends a component's `connections` static here (PLAN-3 2-B)
|
|
259
|
+
__sygnalStatic: 'connections',
|
|
260
|
+
...replies,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
return {...source([]), dispose: () => { closeAll(); disposed = true; }};
|
|
264
|
+
};
|
|
265
|
+
}
|