sygnal 5.4.0 → 6.1.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 +716 -0
- package/README.md +24 -9
- package/dist/ai.cjs.js +115 -0
- package/dist/ai.cjs.js.map +1 -0
- package/dist/ai.esm.js +2 -0
- package/dist/ai.esm.js.map +1 -0
- 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 +958 -92
- package/dist/astro/index.cjs.js.map +1 -1
- package/dist/astro/index.mjs +958 -93
- 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 +1840 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1827 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +3190 -296
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +3190 -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 +310 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/agent.md +588 -0
- package/dist/guide/ai-chat.md +693 -0
- package/dist/guide/ai-decisions.md +330 -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 +409 -0
- package/dist/guide/forms.md +251 -0
- package/dist/guide/http.md +301 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/mcp-apps.md +272 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/ai-form-fill.md +163 -0
- package/dist/guide/recipes/ai-summarize.md +155 -0
- package/dist/guide/recipes/ai-support-inbox.md +190 -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 +43 -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/webmcp.md +226 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +19288 -7408
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +4466 -552
- package/dist/index.esm.js +19274 -7461
- 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 +919 -89
- package/dist/vite/plugin.cjs.js.map +1 -1
- package/dist/vite/plugin.mjs +919 -90
- 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 +172 -103
- package/package.json +112 -14
- package/src/ai.d.ts +1186 -0
- package/src/ai.ts +23 -0
- 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 +170 -0
- package/src/devtools.ts +37 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/ai/agent/index.ts +484 -0
- package/src/extra/ai/answers.ts +115 -0
- package/src/extra/ai/chat/behavior.ts +336 -0
- package/src/extra/ai/chat/driver.ts +271 -0
- package/src/extra/ai/chat/memoryTransport.ts +61 -0
- package/src/extra/ai/chat/output.ts +37 -0
- package/src/extra/ai/commandBar.ts +281 -0
- package/src/extra/ai/decide.ts +104 -0
- package/src/extra/ai/index.ts +42 -0
- package/src/extra/ai/link.ts +90 -0
- package/src/extra/ai/mcpApp.ts +314 -0
- package/src/extra/ai/messages.ts +51 -0
- package/src/extra/ai/schema/index.ts +166 -0
- package/src/extra/ai/schema/jsonSchema.ts +72 -0
- package/src/extra/ai/schema/strict.ts +152 -0
- package/src/extra/ai/transports/agui.ts +177 -0
- package/src/extra/ai/transports/anthropicMessages.ts +175 -0
- package/src/extra/ai/transports/chatCompletions.ts +105 -0
- package/src/extra/ai/transports/chromePrompt.ts +77 -0
- package/src/extra/ai/transports/encodeOpenResponses.ts +87 -0
- package/src/extra/ai/transports/fromAISDK.ts +126 -0
- package/src/extra/ai/transports/openResponses.ts +105 -0
- package/src/extra/ai/transports/shared.ts +125 -0
- package/src/extra/ai/transports/tools.ts +59 -0
- package/src/extra/ai/transports/uiMessageStream.ts +107 -0
- package/src/extra/ai/webmcp.ts +260 -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/devMcp.ts +386 -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/chat.ts +71 -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 +171 -0
- package/src/extra/diagnostics/checks/index.ts +103 -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 +115 -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 +69 -10
- package/src/extra/diagnostics/codes.ts +335 -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 +284 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/formTool.ts +150 -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 +2550 -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 +2754 -127
- package/src/index.ts +45 -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 +57 -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/mcp.ts +568 -0
- package/src/vite/plugin.d.ts +47 -3
- package/src/vite/plugin.ts +262 -25
- 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
package/src/component.ts
DELETED
|
@@ -1,2301 +0,0 @@
|
|
|
1
|
-
import isolate from './cycle/isolate/index';
|
|
2
|
-
import collection from './collection';
|
|
3
|
-
import switchable from './switchable';
|
|
4
|
-
import {StateSource} from './cycle/state/index';
|
|
5
|
-
import {init as snabbdomInit} from './cycle/dom/snabbdom';
|
|
6
|
-
import defaultModules from './cycle/dom/modules';
|
|
7
|
-
import {makeCommandSource} from './extra/command';
|
|
8
|
-
import type {Command} from './extra/command';
|
|
9
|
-
// [diagnostics hook] shared diagnostics core — hooks are no-ops when diagnostics are off
|
|
10
|
-
import * as diag from './extra/diagnostics/index';
|
|
11
|
-
import {warn, error as logError, fail} from './extra/diagnostics/legacy';
|
|
12
|
-
|
|
13
|
-
import xs, {Stream} from './extra/xstreamCompat';
|
|
14
|
-
import {delay, concat, debounce, dropRepeats} from './extra/xstreamExtras';
|
|
15
|
-
|
|
16
|
-
declare var process: { env: Record<string, any> };
|
|
17
|
-
|
|
18
|
-
const ENVIRONMENT: any =
|
|
19
|
-
(typeof window != 'undefined' && window) || (typeof process !== 'undefined' && process.env) || {};
|
|
20
|
-
|
|
21
|
-
const BOOTSTRAP_ACTION = 'BOOTSTRAP';
|
|
22
|
-
const INITIALIZE_ACTION = 'INITIALIZE';
|
|
23
|
-
const HYDRATE_ACTION = 'HYDRATE';
|
|
24
|
-
const DISPOSE_ACTION = 'DISPOSE';
|
|
25
|
-
const PARENT_SINK_NAME = 'PARENT';
|
|
26
|
-
const CHILD_SOURCE_NAME = 'CHILD';
|
|
27
|
-
const READY_SINK_NAME = 'READY';
|
|
28
|
-
const EFFECT_SINK_NAME = 'EFFECT';
|
|
29
|
-
// fix hint for coded messages that carry the caught error (printed after the text, or diagnostic.data)
|
|
30
|
-
const ERR_FIX = 'See the attached error'
|
|
31
|
-
// B-003: the state an action's non-STATE sinks see (stamped by initModel$)
|
|
32
|
-
const STATE_SNAPSHOT = Symbol('sygnal.stateSnapshot');
|
|
33
|
-
|
|
34
|
-
let COMPONENT_COUNT = 0;
|
|
35
|
-
// 1H-1: STATE reducers emitted but not applied yet (in any component)
|
|
36
|
-
let pendingReducers = 0;
|
|
37
|
-
|
|
38
|
-
function wrapDOMSource(domSource: any): any {
|
|
39
|
-
return new Proxy(domSource, {
|
|
40
|
-
get(target, prop, receiver) {
|
|
41
|
-
if (typeof prop === 'symbol' || prop in target) {
|
|
42
|
-
return Reflect.get(target, prop, receiver)
|
|
43
|
-
}
|
|
44
|
-
return (selector: any) => target.select(selector).events(prop)
|
|
45
|
-
}
|
|
46
|
-
})
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export const ABORT = Symbol.for('sygnal.ABORT')
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Check if a value is the ABORT sentinel.
|
|
54
|
-
* Uses Symbol.for() identity first, then falls back to description check
|
|
55
|
-
* in case bundlers (e.g. Vite) create duplicate module instances with
|
|
56
|
-
* separate Symbol.for() registries.
|
|
57
|
-
*/
|
|
58
|
-
function isAbort(value: any): boolean {
|
|
59
|
-
if (value === ABORT) return true
|
|
60
|
-
return typeof value === 'symbol' && value.description === 'sygnal.ABORT'
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
function normalizeCalculatedEntry(field: string, entry: any): {fn: (...args: any[]) => any; deps: string[] | null} {
|
|
65
|
-
if (typeof entry === 'function') {
|
|
66
|
-
return { fn: entry, deps: null }
|
|
67
|
-
}
|
|
68
|
-
if (Array.isArray(entry) && entry.length === 2
|
|
69
|
-
&& Array.isArray(entry[0]) && typeof entry[1] === 'function') {
|
|
70
|
-
return { fn: entry[1], deps: entry[0] }
|
|
71
|
-
}
|
|
72
|
-
fail('SYG206', undefined, `Invalid calculated field '${field}'`, 'Use fn or [deps, fn]')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface ComponentOptions {
|
|
76
|
-
name?: string;
|
|
77
|
-
sources?: Record<string, any>;
|
|
78
|
-
intent?: ((sources: Record<string, any>) => any) | Record<string, any>;
|
|
79
|
-
model?: Record<string, any>;
|
|
80
|
-
hmrActions?: string | string[];
|
|
81
|
-
context?: Record<string, boolean | ((state: any) => any)>;
|
|
82
|
-
response?: Record<string, any>;
|
|
83
|
-
view?: (...args: any[]) => any;
|
|
84
|
-
peers?: Record<string, (...args: any[]) => any>;
|
|
85
|
-
components?: Record<string, any>;
|
|
86
|
-
initialState?: Record<string, any>;
|
|
87
|
-
calculated?: Record<string, ((state: any) => any) | [string[], (...args: any[]) => any]>;
|
|
88
|
-
storeCalculatedInState?: boolean;
|
|
89
|
-
DOMSourceName?: string;
|
|
90
|
-
stateSourceName?: string;
|
|
91
|
-
requestSourceName?: string;
|
|
92
|
-
isolateOpts?: string | boolean | Record<string, any>;
|
|
93
|
-
isolatedState?: boolean;
|
|
94
|
-
onError?: (error: Error, info: { componentName: string }) => any;
|
|
95
|
-
debug?: boolean;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export default function component(opts: ComponentOptions): any {
|
|
99
|
-
const { name, sources, isolateOpts, stateSourceName='STATE' } = opts
|
|
100
|
-
|
|
101
|
-
if (sources && !isObj(sources)) {
|
|
102
|
-
fail('SYG601', name, 'Invalid sources', 'Pass sources from run()')
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let fixedIsolateOpts
|
|
106
|
-
if (typeof isolateOpts == 'string') {
|
|
107
|
-
fixedIsolateOpts = { [stateSourceName]: isolateOpts }
|
|
108
|
-
} else {
|
|
109
|
-
if (isolateOpts === true) {
|
|
110
|
-
fixedIsolateOpts = {}
|
|
111
|
-
} else {
|
|
112
|
-
fixedIsolateOpts = isolateOpts
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const currySources = typeof sources === 'undefined'
|
|
117
|
-
let returnFunction
|
|
118
|
-
|
|
119
|
-
if (isObj(fixedIsolateOpts)) {
|
|
120
|
-
const wrapped = (sources: any) => {
|
|
121
|
-
const fixedOpts = { ...opts, sources }
|
|
122
|
-
const instance = new Component(fixedOpts)
|
|
123
|
-
instance.sinks.__dispose = () => instance.dispose()
|
|
124
|
-
return instance.sinks
|
|
125
|
-
}
|
|
126
|
-
returnFunction = currySources ? isolate(wrapped, fixedIsolateOpts) : isolate(wrapped, fixedIsolateOpts)(sources)
|
|
127
|
-
} else {
|
|
128
|
-
if (currySources) {
|
|
129
|
-
returnFunction = (sources: any) => {
|
|
130
|
-
const instance = new Component({ ...opts, sources })
|
|
131
|
-
instance.sinks.__dispose = () => instance.dispose()
|
|
132
|
-
return instance.sinks
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
const instance = new Component(opts)
|
|
136
|
-
instance.sinks.__dispose = () => instance.dispose()
|
|
137
|
-
returnFunction = instance.sinks
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
returnFunction.componentName = name
|
|
142
|
-
returnFunction.isSygnalComponent = true
|
|
143
|
-
|
|
144
|
-
return returnFunction
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
class Component {
|
|
152
|
-
_componentNumber: number;
|
|
153
|
-
name: string;
|
|
154
|
-
sources: any;
|
|
155
|
-
intent: any;
|
|
156
|
-
model: any;
|
|
157
|
-
hmrActions: any;
|
|
158
|
-
context: any;
|
|
159
|
-
response: any;
|
|
160
|
-
view: any;
|
|
161
|
-
peers: Record<string, any>;
|
|
162
|
-
components: Record<string, any>;
|
|
163
|
-
initialState: any;
|
|
164
|
-
calculated: any;
|
|
165
|
-
storeCalculatedInState: boolean;
|
|
166
|
-
DOMSourceName: string;
|
|
167
|
-
stateSourceName: string;
|
|
168
|
-
requestSourceName: string;
|
|
169
|
-
sourceNames: string[];
|
|
170
|
-
_debug: boolean;
|
|
171
|
-
onError: ((error: Error, info: { componentName: string }) => any) | undefined;
|
|
172
|
-
isolatedState: boolean;
|
|
173
|
-
isSubComponent: boolean;
|
|
174
|
-
currentState: any;
|
|
175
|
-
currentProps: any;
|
|
176
|
-
currentChildren: any;
|
|
177
|
-
currentSlots: Record<string, any[]>;
|
|
178
|
-
currentContext: any;
|
|
179
|
-
intent$: any;
|
|
180
|
-
hmrAction$: any;
|
|
181
|
-
action$: any;
|
|
182
|
-
model$: any;
|
|
183
|
-
context$: any;
|
|
184
|
-
peers$: any;
|
|
185
|
-
subComponentSink$: any;
|
|
186
|
-
subComponentsRendered$: any;
|
|
187
|
-
vdom$: any;
|
|
188
|
-
sinks: any;
|
|
189
|
-
log: any;
|
|
190
|
-
addCalculated: (state: any) => any;
|
|
191
|
-
newChildSources!: (sources: any) => void;
|
|
192
|
-
newSubComponentSinks!: (sinks: any) => void;
|
|
193
|
-
triggerSubComponentsRendered!: (() => void);
|
|
194
|
-
_calculatedNormalized: Record<string, {fn: (...args: any[]) => any; deps: string[] | null}> | null;
|
|
195
|
-
_calculatedFieldNames: Set<string> | null;
|
|
196
|
-
_calculatedOrder: Array<[string, {fn: (...args: any[]) => any; deps: string[] | null}]> | null;
|
|
197
|
-
_calculatedFieldCache: Record<string, {lastDepValues: any; lastResult: any}> | null;
|
|
198
|
-
_subscriptions: any[];
|
|
199
|
-
_processedChildren$: any;
|
|
200
|
-
_disposeListener: any;
|
|
201
|
-
_dispose$: any;
|
|
202
|
-
_disposed?: boolean;
|
|
203
|
-
_activeSubComponents: Map<string, any>;
|
|
204
|
-
_childReadyState: Record<string, boolean>;
|
|
205
|
-
_readyChanged$: any;
|
|
206
|
-
_readyChangedListener: any;
|
|
207
|
-
|
|
208
|
-
constructor({name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false}: ComponentOptions) {
|
|
209
|
-
if (!sources || !isObj(sources)) fail('SYG601', name, 'Missing or invalid sources', 'Pass sources from run()')
|
|
210
|
-
|
|
211
|
-
this._componentNumber = COMPONENT_COUNT++
|
|
212
|
-
|
|
213
|
-
this.name = name
|
|
214
|
-
this.sources = sources
|
|
215
|
-
this.intent = intent
|
|
216
|
-
this.model = model
|
|
217
|
-
this.hmrActions = hmrActions
|
|
218
|
-
this.context = context
|
|
219
|
-
this.response = response
|
|
220
|
-
this.view = view
|
|
221
|
-
this.peers = peers
|
|
222
|
-
this.components = components
|
|
223
|
-
this.initialState = initialState
|
|
224
|
-
this.calculated = calculated
|
|
225
|
-
this.storeCalculatedInState = storeCalculatedInState
|
|
226
|
-
this.DOMSourceName = DOMSourceName
|
|
227
|
-
this.stateSourceName = stateSourceName
|
|
228
|
-
this.requestSourceName = requestSourceName
|
|
229
|
-
this.sourceNames = Object.keys(sources)
|
|
230
|
-
this.onError = onError
|
|
231
|
-
this.isolatedState = isolatedState
|
|
232
|
-
this._debug = debug
|
|
233
|
-
|
|
234
|
-
// Warn if calculated fields shadow base state keys
|
|
235
|
-
if (this.calculated && this.initialState
|
|
236
|
-
&& isObj(this.calculated) && isObj(this.initialState)) {
|
|
237
|
-
for (const key of Object.keys(this.calculated)) {
|
|
238
|
-
if (key in this.initialState) {
|
|
239
|
-
warn('SYG207', name, `Calculated field '${key}' overwrites the initialState key of the same name`, 'Rename one of them')
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Normalize calculated entries, build dependency graph, topological sort
|
|
245
|
-
if (this.calculated && isObj(this.calculated)) {
|
|
246
|
-
const calcEntries = Object.entries(this.calculated)
|
|
247
|
-
|
|
248
|
-
// Normalize all entries to { fn, deps } shape
|
|
249
|
-
this._calculatedNormalized = {}
|
|
250
|
-
for (const [field, entry] of calcEntries) {
|
|
251
|
-
this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized))
|
|
255
|
-
|
|
256
|
-
// Warn on deps referencing nonexistent keys
|
|
257
|
-
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
258
|
-
if (deps !== null) {
|
|
259
|
-
for (const dep of deps) {
|
|
260
|
-
if (!this._calculatedFieldNames.has(dep)
|
|
261
|
-
&& this.initialState && !(dep in this.initialState)) {
|
|
262
|
-
warn('SYG208', name, `Calculated field '${field}' depends on unknown key '${dep}'`, 'Add it to initialState')
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Build adjacency: for each field, which other calculated fields must run first?
|
|
269
|
-
const calcDeps: Record<string, string[]> = {}
|
|
270
|
-
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
271
|
-
if (deps === null) {
|
|
272
|
-
calcDeps[field] = []
|
|
273
|
-
} else {
|
|
274
|
-
calcDeps[field] = deps.filter(d => this._calculatedFieldNames!.has(d))
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Kahn's algorithm for topological sort
|
|
279
|
-
const inDegree: Record<string, number> = {}
|
|
280
|
-
const reverseGraph: Record<string, string[]> = {}
|
|
281
|
-
for (const field of this._calculatedFieldNames) {
|
|
282
|
-
inDegree[field] = 0
|
|
283
|
-
reverseGraph[field] = []
|
|
284
|
-
}
|
|
285
|
-
for (const [field, depList] of Object.entries(calcDeps)) {
|
|
286
|
-
inDegree[field] = depList.length
|
|
287
|
-
for (const dep of depList) {
|
|
288
|
-
reverseGraph[dep].push(field)
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
const queue = []
|
|
293
|
-
for (const [field, degree] of Object.entries(inDegree)) {
|
|
294
|
-
if (degree === 0) queue.push(field)
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const sorted: string[] = []
|
|
298
|
-
while (queue.length > 0) {
|
|
299
|
-
const current: string = queue.shift()!
|
|
300
|
-
sorted.push(current)
|
|
301
|
-
for (const dependent of reverseGraph[current]) {
|
|
302
|
-
inDegree[dependent]--
|
|
303
|
-
if (inDegree[dependent] === 0) queue.push(dependent)
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
if (sorted.length !== this._calculatedFieldNames.size) {
|
|
308
|
-
// Cycle detected — build error message with cycle path
|
|
309
|
-
const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f))
|
|
310
|
-
const visited = new Set()
|
|
311
|
-
const path: string[] = []
|
|
312
|
-
const traceCycle = (node: any) => {
|
|
313
|
-
if (visited.has(node)) { path.push(node); return true }
|
|
314
|
-
visited.add(node)
|
|
315
|
-
path.push(node)
|
|
316
|
-
for (const dep of calcDeps[node]) {
|
|
317
|
-
if (inCycle.includes(dep) && traceCycle(dep)) return true
|
|
318
|
-
}
|
|
319
|
-
path.pop()
|
|
320
|
-
visited.delete(node)
|
|
321
|
-
return false
|
|
322
|
-
}
|
|
323
|
-
traceCycle(inCycle[0])
|
|
324
|
-
const start = path[path.length - 1]
|
|
325
|
-
const cycle = path.slice(path.indexOf(start))
|
|
326
|
-
fail('SYG209', name, `Circular calculated dependency: ${cycle.join(' \u2192 ')}`, 'Break the cycle')
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized![f]])
|
|
330
|
-
|
|
331
|
-
// Initialize per-field memoization caches for fields with declared deps
|
|
332
|
-
this._calculatedFieldCache = {}
|
|
333
|
-
for (const [field, { deps }] of this._calculatedOrder) {
|
|
334
|
-
if (deps !== null) {
|
|
335
|
-
this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined }
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
} else {
|
|
339
|
-
this._calculatedOrder = null
|
|
340
|
-
this._calculatedNormalized = null
|
|
341
|
-
this._calculatedFieldNames = null
|
|
342
|
-
this._calculatedFieldCache = null
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
this.isSubComponent = this.sourceNames.includes('props$')
|
|
346
|
-
|
|
347
|
-
const state$ = sources[stateSourceName] && sources[stateSourceName].stream
|
|
348
|
-
|
|
349
|
-
this.currentSlots = {}
|
|
350
|
-
|
|
351
|
-
if (state$) {
|
|
352
|
-
this.currentState = initialState || {}
|
|
353
|
-
this.sources[stateSourceName] = new StateSource(state$.map((val: any) => {
|
|
354
|
-
this.currentState = val
|
|
355
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
356
|
-
window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val)
|
|
357
|
-
}
|
|
358
|
-
return val
|
|
359
|
-
}), stateSourceName)
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
const props$ = sources.props$
|
|
363
|
-
if (props$) {
|
|
364
|
-
this.sources.props$ = props$.map((val: any) => {
|
|
365
|
-
const { sygnalFactory, sygnalOptions, ...sanitizedProps }: any = val
|
|
366
|
-
this.currentProps = sanitizedProps
|
|
367
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
368
|
-
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps)
|
|
369
|
-
}
|
|
370
|
-
return val
|
|
371
|
-
})
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const children$ = sources.children$
|
|
375
|
-
if (children$) {
|
|
376
|
-
// Process children and extract slots once, share across subscribers
|
|
377
|
-
this._processedChildren$ = children$.map((val: any) => {
|
|
378
|
-
if (Array.isArray(val)) {
|
|
379
|
-
const { slots, defaultChildren } = extractSlots(val)
|
|
380
|
-
this.currentSlots = slots
|
|
381
|
-
this.currentChildren = defaultChildren
|
|
382
|
-
return { children: defaultChildren, slots }
|
|
383
|
-
} else {
|
|
384
|
-
this.currentSlots = {}
|
|
385
|
-
this.currentChildren = val
|
|
386
|
-
return { children: val, slots: {} }
|
|
387
|
-
}
|
|
388
|
-
})
|
|
389
|
-
this.sources.children$ = this._processedChildren$.map((p: any) => p.children)
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
if (this.sources[DOMSourceName]) {
|
|
393
|
-
this.sources[DOMSourceName] = wrapDOMSource(this.sources[DOMSourceName])
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Ensure that the root component has an intent and model
|
|
397
|
-
// This is necessary to ensure that the component tree's state sink is subscribed to
|
|
398
|
-
if (!this.isSubComponent && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
|
|
399
|
-
this.initialState = initialState || true
|
|
400
|
-
this.intent = (_: any) => ({__NOOP_ACTION__:xs.never()})
|
|
401
|
-
this.model = {
|
|
402
|
-
__NOOP_ACTION__: (state: any) => state
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
this._subscriptions = []
|
|
407
|
-
this._activeSubComponents = new Map()
|
|
408
|
-
this._childReadyState = {}
|
|
409
|
-
this._readyChangedListener = null
|
|
410
|
-
this._readyChanged$ = xs.create({
|
|
411
|
-
start: (listener: any) => { this._readyChangedListener = listener },
|
|
412
|
-
stop: () => {},
|
|
413
|
-
})
|
|
414
|
-
this._disposeListener = null
|
|
415
|
-
this._dispose$ = xs.create({
|
|
416
|
-
start: (listener: any) => { this._disposeListener = listener },
|
|
417
|
-
stop: () => {},
|
|
418
|
-
})
|
|
419
|
-
this.sources.dispose$ = this._dispose$
|
|
420
|
-
|
|
421
|
-
this.addCalculated = this.createMemoizedAddCalculated()
|
|
422
|
-
this.log = makeLog(`${this._componentNumber} | ${name}`)
|
|
423
|
-
|
|
424
|
-
this.initChildSources$()
|
|
425
|
-
this.initIntent$()
|
|
426
|
-
this.initHmrActions()
|
|
427
|
-
this.initAction$()
|
|
428
|
-
this.initState()
|
|
429
|
-
this.initContext()
|
|
430
|
-
this.initModel$()
|
|
431
|
-
this.initPeers$()
|
|
432
|
-
this.initSubComponentSink$()
|
|
433
|
-
this.initSubComponentsRendered$()
|
|
434
|
-
this.initVdom$()
|
|
435
|
-
this.initSinks()
|
|
436
|
-
|
|
437
|
-
this.sinks.__index = this._componentNumber
|
|
438
|
-
|
|
439
|
-
this.log(`Instantiated`, true)
|
|
440
|
-
|
|
441
|
-
// Hook 1: Register with DevTools
|
|
442
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
|
|
443
|
-
window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this)
|
|
444
|
-
|
|
445
|
-
// Hook 1b: Register parent-child relationship
|
|
446
|
-
const parentNum = sources?.__parentComponentNumber
|
|
447
|
-
if (typeof parentNum === 'number') {
|
|
448
|
-
window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber)
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
dispose(): void {
|
|
454
|
-
if (this._disposed) return
|
|
455
|
-
this._disposed = true
|
|
456
|
-
// [diagnostics hook]
|
|
457
|
-
diag.onDispose(this)
|
|
458
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
459
|
-
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name)
|
|
460
|
-
}
|
|
461
|
-
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
462
|
-
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION))
|
|
463
|
-
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
464
|
-
try {
|
|
465
|
-
this.action$.shamefullySendNext({ type: DISPOSE_ACTION })
|
|
466
|
-
} catch (_) {}
|
|
467
|
-
}
|
|
468
|
-
// Signal disposal to the component via dispose$ stream (for advanced use cases)
|
|
469
|
-
if (this._disposeListener) {
|
|
470
|
-
try {
|
|
471
|
-
this._disposeListener.next(true)
|
|
472
|
-
this._disposeListener.complete()
|
|
473
|
-
} catch (_) {}
|
|
474
|
-
this._disposeListener = null
|
|
475
|
-
}
|
|
476
|
-
// Dispose the sub-components now (R3), so the whole subtree's DISPOSE actions and
|
|
477
|
-
// onDispose hooks run within this call (e.g. before renderComponent restores diagnostics)
|
|
478
|
-
this._activeSubComponents.forEach((entry) => entry?.sink$?.__dispose?.())
|
|
479
|
-
this._activeSubComponents.clear()
|
|
480
|
-
// Tear down streams on next macrotask to allow DISPOSE/cleanup actions to process
|
|
481
|
-
setTimeout(() => {
|
|
482
|
-
// Complete the action$ stream to stop the entire component cycle
|
|
483
|
-
if (this.action$ && typeof this.action$.shamefullySendComplete === 'function') {
|
|
484
|
-
try { this.action$.shamefullySendComplete() } catch (_) {}
|
|
485
|
-
}
|
|
486
|
-
// Complete the vdom$ stream to stop rendering
|
|
487
|
-
if (this.vdom$ && typeof this.vdom$.shamefullySendComplete === 'function') {
|
|
488
|
-
try { this.vdom$.shamefullySendComplete() } catch (_) {}
|
|
489
|
-
}
|
|
490
|
-
// Unsubscribe tracked internal subscriptions
|
|
491
|
-
for (const sub of this._subscriptions) {
|
|
492
|
-
if (sub && typeof sub.unsubscribe === 'function') {
|
|
493
|
-
try { sub.unsubscribe() } catch (_) {}
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
this._subscriptions = []
|
|
497
|
-
}, 0)
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
get debug(): boolean {
|
|
501
|
-
return this._debug || (ENVIRONMENT.SYGNAL_DEBUG === 'true' || ENVIRONMENT.SYGNAL_DEBUG === true)
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
initIntent$(): void {
|
|
505
|
-
if (!this.intent) {
|
|
506
|
-
// [diagnostics hook]
|
|
507
|
-
diag.onIntent(this, [], undefined)
|
|
508
|
-
return
|
|
509
|
-
}
|
|
510
|
-
if (typeof this.intent != 'function') {
|
|
511
|
-
fail('SYG602', this, 'intent must be a function', 'Use intent = sources => ({ ACTION: stream$ })')
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
this.intent$ = this.intent(this.sources)
|
|
515
|
-
|
|
516
|
-
if (!(this.intent$ instanceof Stream) && (!isObj(this.intent$))) {
|
|
517
|
-
fail('SYG603', this, 'intent must return a stream or an object of streams', 'Return { ACTION: stream$ }')
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
// [diagnostics hook] TODO(1A): selectorsUsed via MainDOMSource.select instrumentation
|
|
521
|
-
diag.onIntent(this, this.intent$ instanceof Stream ? [] : Object.keys(this.intent$), undefined)
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
initHmrActions(): void {
|
|
525
|
-
if (typeof this.hmrActions === 'undefined') {
|
|
526
|
-
this.hmrAction$ = xs.of().filter((_: any) => false)
|
|
527
|
-
return
|
|
528
|
-
}
|
|
529
|
-
if (typeof this.hmrActions === 'string') {
|
|
530
|
-
this.hmrActions = [this.hmrActions]
|
|
531
|
-
}
|
|
532
|
-
if (!Array.isArray(this.hmrActions) || this.hmrActions.some(action => typeof action !== 'string')) {
|
|
533
|
-
fail('SYG604', this, 'hmrActions must be an action name or an array of action names', "Use hmrActions = ['ACTION']")
|
|
534
|
-
}
|
|
535
|
-
this.hmrAction$ = xs.fromArray(this.hmrActions.map(action => ({ type: action })))
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
initAction$(): void {
|
|
539
|
-
const requestSource = (this.sources && this.sources[this.requestSourceName]) || null
|
|
540
|
-
|
|
541
|
-
if (!this.intent$) {
|
|
542
|
-
this.action$ = xs.never()
|
|
543
|
-
return
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
let runner
|
|
547
|
-
if (this.intent$ instanceof Stream) {
|
|
548
|
-
runner = this.intent$
|
|
549
|
-
} else {
|
|
550
|
-
// Validate that no intent action names contain '|' (reserved for model shorthand)
|
|
551
|
-
for (const key of Object.keys(this.intent$)) {
|
|
552
|
-
if (key.includes('|')) {
|
|
553
|
-
fail('SYG605', this, `Intent action '${key}' contains '|', which is reserved for model shorthand`, 'Rename the action')
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
const mapped = Object.entries(this.intent$)
|
|
557
|
-
.map(([type, data$]: [string, any]) => data$.map((data: any) => ({type, data})))
|
|
558
|
-
runner = mapped.length > 0 ? xs.merge(...mapped) : xs.never()
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
const action$ = ((runner instanceof Stream) ? runner : (runner.apply && runner(this.sources) || xs.never()))
|
|
562
|
-
const bootstrap$ = xs.of({ type: BOOTSTRAP_ACTION }).compose(delay(10))
|
|
563
|
-
const _hmrUpdating = typeof window !== 'undefined' && window.__SYGNAL_HMR_UPDATING === true
|
|
564
|
-
const hmrAction$ = _hmrUpdating ? this.hmrAction$ : xs.of().filter((_: any) => false)
|
|
565
|
-
const wrapped$ = (this.model[BOOTSTRAP_ACTION] && !_hmrUpdating) ? concat(bootstrap$, action$) : concat(xs.of().compose(delay(1)).filter((_: any) => false), hmrAction$, action$)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
let initialApiData
|
|
569
|
-
if (!_hmrUpdating && requestSource && typeof requestSource.select == 'function') {
|
|
570
|
-
initialApiData = requestSource.select('initial')
|
|
571
|
-
.flatten()
|
|
572
|
-
} else {
|
|
573
|
-
initialApiData = xs.never()
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
const hydrate$ = initialApiData.map((data: any) => ({ type: HYDRATE_ACTION, data }))
|
|
577
|
-
|
|
578
|
-
this.action$ = xs.merge(wrapped$, hydrate$)
|
|
579
|
-
.compose(this.log(({ type }: any) => `<${type}> Action triggered`))
|
|
580
|
-
.map((action: any) => {
|
|
581
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
582
|
-
window.__SYGNAL_DEVTOOLS__.onActionDispatched(
|
|
583
|
-
this._componentNumber, this.name, action.type, action.data
|
|
584
|
-
)
|
|
585
|
-
}
|
|
586
|
-
return action
|
|
587
|
-
})
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
initState(): void {
|
|
591
|
-
if (this.model !== undefined) {
|
|
592
|
-
if (this.model[INITIALIZE_ACTION] === undefined) {
|
|
593
|
-
this.model[INITIALIZE_ACTION] = {
|
|
594
|
-
[this.stateSourceName]: (_: any, data: any) => ({ ...this.addCalculated(data) })
|
|
595
|
-
}
|
|
596
|
-
} else if (isObj(this.model[INITIALIZE_ACTION])) {
|
|
597
|
-
Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
|
|
598
|
-
if (name !== this.stateSourceName) {
|
|
599
|
-
warn('SYG210', this, `${INITIALIZE_ACTION} only supports the ${this.stateSourceName} sink; ignoring '${name}'`, 'Use another action')
|
|
600
|
-
delete this.model[INITIALIZE_ACTION][name]
|
|
601
|
-
}
|
|
602
|
-
})
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
initContext(): void {
|
|
608
|
-
if (!this.context && !this.sources.__parentContext$) {
|
|
609
|
-
this.context$ = xs.of({})
|
|
610
|
-
return
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
const state$ = this.sources[this.stateSourceName]?.stream.startWith({}).compose(dropRepeats(objIsEqual)) || xs.never()
|
|
614
|
-
const parentContext$ = this.sources.__parentContext$?.startWith({}).compose(dropRepeats(objIsEqual)) || xs.of({})
|
|
615
|
-
if (this.context && !isObj(this.context)) {
|
|
616
|
-
logError('SYG402', this, `context must be an object, got ${typeof this.context}; ignoring it`, 'Use context = { name: state => value }')
|
|
617
|
-
}
|
|
618
|
-
this.context$ = xs.combine(state$, parentContext$)
|
|
619
|
-
.map(([_, parent]: [any, any]) => {
|
|
620
|
-
const _parent = isObj(parent) ? parent : {}
|
|
621
|
-
const context = isObj(this.context) ? this.context : {}
|
|
622
|
-
const state = this.currentState
|
|
623
|
-
const values = Object.entries(context).reduce((acc, current) => {
|
|
624
|
-
const [name, value] = current
|
|
625
|
-
let _value
|
|
626
|
-
const valueType = typeof value
|
|
627
|
-
if (valueType === 'string') {
|
|
628
|
-
_value = state[value]
|
|
629
|
-
} else if (valueType === 'boolean') {
|
|
630
|
-
_value = state[name]
|
|
631
|
-
} else if (valueType === 'function') {
|
|
632
|
-
_value = value(state)
|
|
633
|
-
} else {
|
|
634
|
-
logError('SYG403', this, `Invalid context entry '${name}'; skipping it`, 'Use a state key or state => value')
|
|
635
|
-
return acc
|
|
636
|
-
}
|
|
637
|
-
acc[name] = _value
|
|
638
|
-
return acc
|
|
639
|
-
}, {} as Record<string, any>)
|
|
640
|
-
const newContext = { ..._parent, ...values }
|
|
641
|
-
this.currentContext = newContext
|
|
642
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
643
|
-
window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext)
|
|
644
|
-
}
|
|
645
|
-
return newContext
|
|
646
|
-
})
|
|
647
|
-
.compose(dropRepeats(objIsEqual))
|
|
648
|
-
.startWith({})
|
|
649
|
-
this._subscriptions.push(this.context$.subscribe({ next: (_: any) => _, error: (err: any) => logError('SYG404', this, 'Context stream errored; context stops updating', 'Check the context functions', err) }))
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
initModel$(): void {
|
|
653
|
-
if (typeof this.model == 'undefined') {
|
|
654
|
-
this.model$ = this.sourceNames.reduce((a: Record<string, any>, s) => {
|
|
655
|
-
a[s] = xs.never()
|
|
656
|
-
return a
|
|
657
|
-
}, {} as Record<string, any>)
|
|
658
|
-
// [diagnostics hook]
|
|
659
|
-
diag.onModel(this, {})
|
|
660
|
-
return
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE
|
|
664
|
-
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
|
|
665
|
-
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
|
|
666
|
-
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
667
|
-
warn('SYG405', this, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
|
|
668
|
-
}
|
|
669
|
-
const hasInitialState = (typeof effectiveInitialState !== 'undefined')
|
|
670
|
-
const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined')
|
|
671
|
-
// Only INITIALIZE is delayed (user actions start >= 1ms later), so the other actions, and
|
|
672
|
-
// their non-STATE sinks, stay synchronous with the event that caused them (1H-1).
|
|
673
|
-
const shimmed$ = shouldInjectInitialState ? xs.merge(xs.of(initial).compose(delay(0)), this.action$) : this.action$
|
|
674
|
-
// B-003: STATE reducers are applied later (withState queues each one in a microtask), so
|
|
675
|
-
// while one is pending a non-STATE sink must not read this.currentState. Then, before the
|
|
676
|
-
// action reaches any reducer, queue a microtask (ahead of this action's own STATE reducer,
|
|
677
|
-
// behind every earlier one) that snapshots the state and runs the non-STATE sinks.
|
|
678
|
-
// Otherwise run them now, so EFFECT can still preventDefault() the live event (1H-1).
|
|
679
|
-
let snapListener: any = null
|
|
680
|
-
const sequenced$ = shimmed$.map((action: any) => {
|
|
681
|
-
const l = snapListener
|
|
682
|
-
const run = () => l.next({ ...action, [STATE_SNAPSHOT]: this.currentState })
|
|
683
|
-
if (l) pendingReducers ? queueMicrotask(run) : run()
|
|
684
|
-
return action
|
|
685
|
-
})
|
|
686
|
-
let snapSub: any
|
|
687
|
-
const snapshotted$ = xs.create({
|
|
688
|
-
start: (l: any) => { snapListener = l; snapSub = sequenced$.subscribe({}) },
|
|
689
|
-
stop: () => { snapListener = null; snapSub?.unsubscribe() },
|
|
690
|
-
})
|
|
691
|
-
const onState = () => this.makeOnAction(sequenced$, true, this.action$)
|
|
692
|
-
const onNormal = () => this.makeOnAction(snapshotted$, false, this.action$)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
const modelEntries = Object.entries(this.model)
|
|
696
|
-
|
|
697
|
-
const reducers: Record<string, any[]> = {}
|
|
698
|
-
const seenActionSinks = new Set<string>()
|
|
699
|
-
const modelMap: Record<string, string[]> = {} // [diagnostics hook]
|
|
700
|
-
|
|
701
|
-
modelEntries.forEach((entry) => {
|
|
702
|
-
let [action, sinks]: [string, any] = entry
|
|
703
|
-
|
|
704
|
-
// ACTION | DRIVER shorthand: 'MY_ACTION | EVENTS': (state) => ({ type: 'foo', data: 'bar' })
|
|
705
|
-
if (action.includes('|')) {
|
|
706
|
-
const parts = action.split('|').map((s: string) => s.trim())
|
|
707
|
-
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
708
|
-
fail('SYG211', this, `Invalid shorthand model entry '${action}'`, "Use 'ACTION | SINK'")
|
|
709
|
-
}
|
|
710
|
-
action = parts[0]
|
|
711
|
-
sinks = { [parts[1]]: sinks }
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
if (typeof sinks === 'function') {
|
|
715
|
-
sinks = { [this.stateSourceName]: sinks }
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (!isObj(sinks)) {
|
|
719
|
-
fail('SYG212', this, `Model entry '${action}' must be a function or an object`, 'Use { STATE: reducer }')
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
const sinkEntries = Object.entries(sinks)
|
|
723
|
-
|
|
724
|
-
sinkEntries.forEach((entry) => {
|
|
725
|
-
const [sink, reducer] = entry
|
|
726
|
-
|
|
727
|
-
const actionSinkKey = `${action}::${sink}`
|
|
728
|
-
if (seenActionSinks.has(actionSinkKey)) {
|
|
729
|
-
warn('SYG213', this, `Duplicate model entry for action '${action}' on sink '${sink}'; both run`, 'Remove the duplicate')
|
|
730
|
-
}
|
|
731
|
-
seenActionSinks.add(actionSinkKey)
|
|
732
|
-
;(modelMap[action] ||= []).push(sink) // [diagnostics hook]
|
|
733
|
-
|
|
734
|
-
// EFFECT sink: run the reducer for side effects only, no state change or sink output
|
|
735
|
-
if (sink === EFFECT_SINK_NAME) {
|
|
736
|
-
const effect$ = this.makeEffectHandler(snapshotted$, action, reducer, this.action$)
|
|
737
|
-
if (Array.isArray(reducers[sink])) {
|
|
738
|
-
reducers[sink].push(effect$)
|
|
739
|
-
} else {
|
|
740
|
-
reducers[sink] = [effect$]
|
|
741
|
-
}
|
|
742
|
-
return
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
const isStateSink = (sink === this.stateSourceName)
|
|
746
|
-
const isParentSink = (sink === PARENT_SINK_NAME)
|
|
747
|
-
|
|
748
|
-
const on = isStateSink ? onState() : onNormal()
|
|
749
|
-
const on$ = isParentSink ? on(action, reducer).map((value: any) => ({ name: this.name, component: this.view, value })) : on(action, reducer)
|
|
750
|
-
|
|
751
|
-
const wrapped$ = on$
|
|
752
|
-
.compose(this.log((data: any) => {
|
|
753
|
-
if (isStateSink) {
|
|
754
|
-
return `<${action}> State reducer added`
|
|
755
|
-
} else if (isParentSink) {
|
|
756
|
-
return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
|
|
757
|
-
} else {
|
|
758
|
-
const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data)
|
|
759
|
-
return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
|
|
760
|
-
}
|
|
761
|
-
}))
|
|
762
|
-
|
|
763
|
-
if (Array.isArray(reducers[sink])) {
|
|
764
|
-
reducers[sink].push(wrapped$)
|
|
765
|
-
} else {
|
|
766
|
-
reducers[sink] = [wrapped$]
|
|
767
|
-
}
|
|
768
|
-
})
|
|
769
|
-
})
|
|
770
|
-
|
|
771
|
-
const model$ = Object.entries(reducers).reduce((acc: Record<string, any>, entry: [string, any]) => {
|
|
772
|
-
const [sink, streams] = entry
|
|
773
|
-
acc[sink] = streams.length === 1 ? streams[0] : xs.merge(...streams)
|
|
774
|
-
return acc
|
|
775
|
-
}, {} as Record<string, any>)
|
|
776
|
-
|
|
777
|
-
this.model$ = model$
|
|
778
|
-
|
|
779
|
-
// [diagnostics hook]
|
|
780
|
-
diag.onModel(this, modelMap)
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
initPeers$(): void {
|
|
784
|
-
const initial: Record<string, any> = this.sourceNames.reduce((acc: Record<string, any>, name) => {
|
|
785
|
-
if (name == this.DOMSourceName) {
|
|
786
|
-
acc[name] = {}
|
|
787
|
-
} else {
|
|
788
|
-
acc[name] = []
|
|
789
|
-
}
|
|
790
|
-
return acc
|
|
791
|
-
}, {} as Record<string, any>)
|
|
792
|
-
|
|
793
|
-
this.peers$ = Object.entries(this.peers).reduce((acc: Record<string, any>, [peerName, peerFactory]) => {
|
|
794
|
-
const peer$ = peerFactory(this.sources)
|
|
795
|
-
this.sourceNames.forEach(source => {
|
|
796
|
-
if (source == this.DOMSourceName) {
|
|
797
|
-
acc[source][peerName] = peer$[source]
|
|
798
|
-
} else {
|
|
799
|
-
acc[source].push(peer$[source])
|
|
800
|
-
}
|
|
801
|
-
})
|
|
802
|
-
return acc
|
|
803
|
-
}, initial)
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
initChildSources$(): void {
|
|
807
|
-
let newSourcesNext: any
|
|
808
|
-
const childSources$ = xs.create({
|
|
809
|
-
start: (listener: any) => {
|
|
810
|
-
newSourcesNext = listener.next.bind(listener)
|
|
811
|
-
},
|
|
812
|
-
stop: (_: any) => {
|
|
813
|
-
|
|
814
|
-
}
|
|
815
|
-
}).map((sources: any) => xs.merge(...sources)).flatten()
|
|
816
|
-
|
|
817
|
-
this.sources[CHILD_SOURCE_NAME] = {
|
|
818
|
-
select: (nameOrComponent: any) => {
|
|
819
|
-
const all$ = childSources$
|
|
820
|
-
const filtered$ = typeof nameOrComponent === 'function'
|
|
821
|
-
? all$.filter((entry: any) => entry.component === nameOrComponent)
|
|
822
|
-
: nameOrComponent
|
|
823
|
-
? all$.filter((entry: any) => entry.name === nameOrComponent)
|
|
824
|
-
: all$
|
|
825
|
-
const unwrapped$ = filtered$.map((entry: any) => entry.value)
|
|
826
|
-
return unwrapped$
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
this.newChildSources = (sources: any) => {
|
|
831
|
-
if (typeof newSourcesNext === 'function') newSourcesNext(sources)
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
initSubComponentSink$(): void {
|
|
836
|
-
const subComponentSink$ = xs.create({
|
|
837
|
-
start: (listener: any) => {
|
|
838
|
-
this.newSubComponentSinks = listener.next.bind(listener)
|
|
839
|
-
},
|
|
840
|
-
stop: (_: any) => {
|
|
841
|
-
|
|
842
|
-
}
|
|
843
|
-
})
|
|
844
|
-
this._subscriptions.push(subComponentSink$.subscribe({ next: (_: any) => _, error: (err: any) => logError('SYG901', this, 'Sub-component sink stream errored', ERR_FIX, err) }))
|
|
845
|
-
this.subComponentSink$ = subComponentSink$.filter((sinks: any) => Object.keys(sinks).length > 0)
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
initSubComponentsRendered$(): void {
|
|
849
|
-
const stream = xs.create({
|
|
850
|
-
start: (listener: any) => {
|
|
851
|
-
this.triggerSubComponentsRendered = listener.next.bind(listener)
|
|
852
|
-
},
|
|
853
|
-
stop: (_: any) => {
|
|
854
|
-
|
|
855
|
-
}
|
|
856
|
-
})
|
|
857
|
-
this.subComponentsRendered$ = stream.startWith(null)
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
initVdom$(): void {
|
|
861
|
-
if (typeof this.view != 'function') {
|
|
862
|
-
this.vdom$ = xs.of(null)
|
|
863
|
-
return
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
const renderParameters$ = this.collectRenderParameters()
|
|
867
|
-
|
|
868
|
-
this.vdom$ = renderParameters$
|
|
869
|
-
.map((params: any) => {
|
|
870
|
-
const { props, state, children, slots, context, ...peers }: any = params
|
|
871
|
-
const { sygnalFactory, sygnalOptions, ...sanitizedProps}: any = props || {}
|
|
872
|
-
try {
|
|
873
|
-
return this.view({ ...sanitizedProps, state, children, slots: slots || {}, context, peers }, state, context, peers)
|
|
874
|
-
} catch (err) {
|
|
875
|
-
const error = err instanceof Error ? err : new Error(String(err))
|
|
876
|
-
// B-022: an error handled by .onError is a warning, without the "add .onError" hint
|
|
877
|
-
if (typeof this.onError === 'function') {
|
|
878
|
-
try {
|
|
879
|
-
const fallback = this.onError(error, { componentName: this.name })
|
|
880
|
-
warn('SYG406', this, 'View threw; rendered the onError fallback', undefined, error)
|
|
881
|
-
return fallback
|
|
882
|
-
} catch (fallbackErr) {
|
|
883
|
-
logError('SYG406', this, 'View threw; rendering the error fallback', undefined, error)
|
|
884
|
-
logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fallbackErr)
|
|
885
|
-
}
|
|
886
|
-
} else logError('SYG406', this, 'View threw; rendering the error fallback', 'Add .onError for a custom fallback', error)
|
|
887
|
-
return { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
|
|
888
|
-
}
|
|
889
|
-
})
|
|
890
|
-
.compose(this.log('View rendered'))
|
|
891
|
-
.map((vDom: any) => vDom || { sel: 'div', data: {}, children: [] })
|
|
892
|
-
.map((vdom: any) => preprocessVdom(vdom, this))
|
|
893
|
-
.compose(this.instantiateSubComponents.bind(this))
|
|
894
|
-
.filter((val: any) => val !== undefined)
|
|
895
|
-
.compose(this.renderVdom.bind(this))
|
|
896
|
-
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
initSinks(): void {
|
|
900
|
-
// Stamp this component's own EVENTS emissions with its info for devtools, before they are
|
|
901
|
-
// merged with the sub-components' (B-023: stamping the merged sink made every ancestor
|
|
902
|
-
// re-stamp, so the emitter was always the root). Non-enumerable (G-020), so sink values
|
|
903
|
-
// still toEqual what the model returned.
|
|
904
|
-
const ev$ = this.model$.EVENTS
|
|
905
|
-
if (ev$) this.model$.EVENTS = ev$.map((ev: any) => Object.defineProperties({...ev}, {
|
|
906
|
-
__emitterId: { value: this._componentNumber, configurable: true },
|
|
907
|
-
__emitterName: { value: this.name, configurable: true },
|
|
908
|
-
}))
|
|
909
|
-
this.sinks = this.sourceNames.reduce((acc: Record<string, any>, name) => {
|
|
910
|
-
if (name == this.DOMSourceName) return acc
|
|
911
|
-
const subComponentSink$ = (this.subComponentSink$ && name !== PARENT_SINK_NAME) ? this.subComponentSink$.map((sinks: any) => sinks[name]).filter((sink: any) => !!sink).flatten() : xs.never()
|
|
912
|
-
if (name === this.stateSourceName) {
|
|
913
|
-
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter((_: any) => false), ...(this.peers$[name] || []))
|
|
914
|
-
} else {
|
|
915
|
-
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []))
|
|
916
|
-
}
|
|
917
|
-
return acc
|
|
918
|
-
}, {} as Record<string, any>)
|
|
919
|
-
|
|
920
|
-
this.sinks[this.DOMSourceName] = this.vdom$
|
|
921
|
-
this.sinks[PARENT_SINK_NAME] = this.model$[PARENT_SINK_NAME] || xs.never()
|
|
922
|
-
|
|
923
|
-
// EFFECT sink: subscribe to trigger side effects but don't expose as a driver sink
|
|
924
|
-
if (this.model$[EFFECT_SINK_NAME]) {
|
|
925
|
-
const effectSub = this.model$[EFFECT_SINK_NAME].subscribe({
|
|
926
|
-
next: () => {},
|
|
927
|
-
error: (err: any) => logError('SYG902', this, 'EFFECT stream errored; effects stop running', ERR_FIX, err),
|
|
928
|
-
})
|
|
929
|
-
this._subscriptions.push(effectSub)
|
|
930
|
-
delete this.sinks[EFFECT_SINK_NAME]
|
|
931
|
-
}
|
|
932
|
-
// READY sink: if the component explicitly defined READY model entries, use them;
|
|
933
|
-
// otherwise auto-emit true. Check the raw model object, not model$ (which always has keys for all sources).
|
|
934
|
-
const hasExplicitReady = this.model && isObj(this.model) && Object.values(this.model).some(
|
|
935
|
-
(sinks: any) => {
|
|
936
|
-
if (isObj(sinks) && READY_SINK_NAME in sinks) return true
|
|
937
|
-
return false
|
|
938
|
-
}
|
|
939
|
-
)
|
|
940
|
-
if (hasExplicitReady) {
|
|
941
|
-
this.sinks[READY_SINK_NAME] = this.model$[READY_SINK_NAME]
|
|
942
|
-
this.sinks[READY_SINK_NAME].__explicitReady = true
|
|
943
|
-
} else {
|
|
944
|
-
this.sinks[READY_SINK_NAME] = xs.of(true)
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
makeOnAction(action$: any, isStateSink: boolean = true, rootAction$?: any): (name: string, reducer: any) => any {
|
|
949
|
-
rootAction$ = rootAction$ || action$
|
|
950
|
-
return (name, reducer) => {
|
|
951
|
-
const filtered$ = action$.filter(({type}: any) => type == name)
|
|
952
|
-
|
|
953
|
-
let returnStream$
|
|
954
|
-
if (typeof reducer === 'function') {
|
|
955
|
-
returnStream$ = filtered$.map((action: any) => {
|
|
956
|
-
const next = (type: any, data: any, delay=10) => {
|
|
957
|
-
if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
|
|
958
|
-
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
959
|
-
setTimeout(() => {
|
|
960
|
-
// push the "next" action request into the action$ stream
|
|
961
|
-
rootAction$.shamefullySendNext({ type, data })
|
|
962
|
-
}, delay)
|
|
963
|
-
this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true)
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const props = { ...this.currentProps, children: this.currentChildren, slots: this.currentSlots || {}, context: this.currentContext }
|
|
967
|
-
|
|
968
|
-
let data = action.data
|
|
969
|
-
if (isStateSink) {
|
|
970
|
-
// withState applies reducers in microtasks, so none is legitimately pending at the
|
|
971
|
-
// next macrotask: reset then, in case one was never applied (e.g. app disposed)
|
|
972
|
-
if (!pendingReducers++) setTimeout(() => { pendingReducers = 0 })
|
|
973
|
-
let applied = false
|
|
974
|
-
return (state: any) => {
|
|
975
|
-
if (!applied && pendingReducers) { applied = true; pendingReducers-- }
|
|
976
|
-
// Reduce from the fresh argument, not currentState (B-013, 1H-3): below a
|
|
977
|
-
// Collection, currentState lags behind instantiateCollection's debounce, so a
|
|
978
|
-
// second same-tick action would start from the pre-update state. The parents'
|
|
979
|
-
// lenses add their calculated fields, so it matches what the view gets.
|
|
980
|
-
const fresh = typeof state !== 'undefined'
|
|
981
|
-
const _state = this.isSubComponent && !fresh ? this.currentState : state
|
|
982
|
-
try {
|
|
983
|
-
const enhancedState = this.addCalculated(_state)
|
|
984
|
-
props.state = enhancedState
|
|
985
|
-
const newState = reducer(enhancedState, data, next, props)
|
|
986
|
-
if (isAbort(newState)) return _state
|
|
987
|
-
// [diagnostics hook]
|
|
988
|
-
diag.onReducer(this, name, _state, newState, this.stateSourceName)
|
|
989
|
-
const result = this.cleanupCalculated(newState)
|
|
990
|
-
// B-013: later same-tick actions' non-STATE sinks snapshot currentState (B-003)
|
|
991
|
-
if (fresh) this.currentState = result
|
|
992
|
-
return result
|
|
993
|
-
} catch (err) {
|
|
994
|
-
logError('SYG216', this, `Reducer for '${name}' threw; state unchanged`, ERR_FIX, err)
|
|
995
|
-
return _state
|
|
996
|
-
}
|
|
997
|
-
}
|
|
998
|
-
} else {
|
|
999
|
-
try {
|
|
1000
|
-
const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
|
|
1001
|
-
props.state = enhancedState
|
|
1002
|
-
const reduced = reducer(enhancedState, data, next, props)
|
|
1003
|
-
// B-029: ABORT = send nothing (filtered below), before the type checks
|
|
1004
|
-
if (isAbort(reduced)) return reduced
|
|
1005
|
-
const type = typeof reduced
|
|
1006
|
-
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
1007
|
-
if (type === 'undefined') {
|
|
1008
|
-
warn('SYG217', this, `Reducer for '${name}' sent undefined to the driver`, 'Return a value, or ABORT to send nothing')
|
|
1009
|
-
return reduced
|
|
1010
|
-
}
|
|
1011
|
-
fail('SYG218', this, `Reducer for '${name}' returned a ${type}`, 'Return a value, or ABORT to send nothing')
|
|
1012
|
-
} catch (err) {
|
|
1013
|
-
logError('SYG216', this, `Reducer for '${name}' threw; nothing sent`, ERR_FIX, err)
|
|
1014
|
-
return ABORT
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
}).filter((result: any) => !isAbort(result))
|
|
1018
|
-
} else if (reducer === undefined || reducer === true) {
|
|
1019
|
-
returnStream$ = filtered$.map(({data}: any) => data)
|
|
1020
|
-
} else {
|
|
1021
|
-
const value = reducer
|
|
1022
|
-
returnStream$ = filtered$.mapTo(value)
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
return returnStream$
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
|
|
1029
|
-
makeEffectHandler(action$: any, name: string, reducer: any, rootAction$: any = action$): any {
|
|
1030
|
-
const filtered$ = action$.filter(({type}: any) => type == name)
|
|
1031
|
-
|
|
1032
|
-
return filtered$.map((action: any) => {
|
|
1033
|
-
if (typeof reducer === 'function') {
|
|
1034
|
-
const next = (type: any, data: any, delay=10) => {
|
|
1035
|
-
if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
|
|
1036
|
-
setTimeout(() => {
|
|
1037
|
-
rootAction$.shamefullySendNext({ type, data })
|
|
1038
|
-
}, delay)
|
|
1039
|
-
this.log(`<${name}> EFFECT triggered a next() action: <${type}> ${delay}ms delay`, true)
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
try {
|
|
1043
|
-
const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
|
|
1044
|
-
const props = { ...this.currentProps, children: this.currentChildren, slots: this.currentSlots || {}, context: this.currentContext, state: enhancedState }
|
|
1045
|
-
const result = reducer(enhancedState, action.data, next, props)
|
|
1046
|
-
if (result !== undefined && !isAbort(result)) {
|
|
1047
|
-
warn('SYG219', this, `EFFECT handler '${name}' returned a value, which is ignored`, 'Use a STATE or driver sink')
|
|
1048
|
-
}
|
|
1049
|
-
} catch (err) {
|
|
1050
|
-
logError('SYG214', this, `EFFECT handler '${name}' threw`, ERR_FIX, err)
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
return null
|
|
1054
|
-
}).filter((_: any) => false) // EFFECT never emits — stream is consumed but produces no output
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
createMemoizedAddCalculated(): (state: any) => any {
|
|
1058
|
-
let lastState: any
|
|
1059
|
-
let lastResult: any
|
|
1060
|
-
|
|
1061
|
-
return function(this: Component, state: any) {
|
|
1062
|
-
if (!this.calculated || !isObj(state) || Array.isArray(state)) return state
|
|
1063
|
-
if (state === lastState) {
|
|
1064
|
-
return lastResult
|
|
1065
|
-
}
|
|
1066
|
-
if (!isObj(this.calculated)) fail('SYG606', this, 'calculated must be an object', 'Use calculated = { field: state => value }')
|
|
1067
|
-
|
|
1068
|
-
const calculated = this.getCalculatedValues(state)
|
|
1069
|
-
if (!calculated) {
|
|
1070
|
-
lastState = state
|
|
1071
|
-
lastResult = state
|
|
1072
|
-
return state
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
const newState = { ...state, ...calculated }
|
|
1076
|
-
|
|
1077
|
-
lastState = state
|
|
1078
|
-
lastResult = newState
|
|
1079
|
-
|
|
1080
|
-
return newState
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
getCalculatedValues(state: any): Record<string, any> | undefined {
|
|
1085
|
-
if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
|
|
1086
|
-
return
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
const mergedState: Record<string, any> = { ...state }
|
|
1090
|
-
const computedSoFar: Record<string, any> = {}
|
|
1091
|
-
|
|
1092
|
-
for (const [field, { fn, deps }] of this._calculatedOrder) {
|
|
1093
|
-
if (deps !== null && this._calculatedFieldCache) {
|
|
1094
|
-
const cache = this._calculatedFieldCache[field]
|
|
1095
|
-
const currentDepValues = deps.map(d => mergedState[d])
|
|
1096
|
-
|
|
1097
|
-
if (cache.lastDepValues !== undefined) {
|
|
1098
|
-
let unchanged = true
|
|
1099
|
-
for (let i = 0; i < currentDepValues.length; i++) {
|
|
1100
|
-
if (currentDepValues[i] !== cache.lastDepValues[i]) {
|
|
1101
|
-
unchanged = false
|
|
1102
|
-
break
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
if (unchanged) {
|
|
1106
|
-
computedSoFar[field] = cache.lastResult
|
|
1107
|
-
mergedState[field] = cache.lastResult
|
|
1108
|
-
continue
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
try {
|
|
1113
|
-
const result = fn(mergedState)
|
|
1114
|
-
cache.lastDepValues = currentDepValues
|
|
1115
|
-
cache.lastResult = result
|
|
1116
|
-
computedSoFar[field] = result
|
|
1117
|
-
mergedState[field] = result
|
|
1118
|
-
} catch (e: unknown) {
|
|
1119
|
-
warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
|
|
1120
|
-
}
|
|
1121
|
-
} else {
|
|
1122
|
-
// No deps declared — always recompute
|
|
1123
|
-
try {
|
|
1124
|
-
const result = fn(mergedState)
|
|
1125
|
-
computedSoFar[field] = result
|
|
1126
|
-
mergedState[field] = result
|
|
1127
|
-
} catch (e: unknown) {
|
|
1128
|
-
warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
return computedSoFar
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
cleanupCalculated(incomingState: any): any {
|
|
1137
|
-
if (!incomingState || !isObj(incomingState) || Array.isArray(incomingState)) return incomingState
|
|
1138
|
-
const state = this.storeCalculatedInState ? this.addCalculated(incomingState) : incomingState
|
|
1139
|
-
const copy = { ...state }
|
|
1140
|
-
if (!this.calculated || this.storeCalculatedInState) return copy
|
|
1141
|
-
const keys = Object.keys(this.calculated)
|
|
1142
|
-
keys.forEach(key => {
|
|
1143
|
-
if (this.initialState && typeof this.initialState[key] !== 'undefined') {
|
|
1144
|
-
copy[key] = this.initialState[key]
|
|
1145
|
-
} else {
|
|
1146
|
-
delete copy[key]
|
|
1147
|
-
}
|
|
1148
|
-
})
|
|
1149
|
-
return copy
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
collectRenderParameters(): any {
|
|
1153
|
-
const state = this.sources[this.stateSourceName]
|
|
1154
|
-
const renderParams = { ...this.peers$[this.DOMSourceName] }
|
|
1155
|
-
|
|
1156
|
-
const enhancedState = state && state.isolateSource(state, { get: (state: any) => this.addCalculated(state) })
|
|
1157
|
-
const stateStream = (enhancedState && enhancedState.stream) || xs.never()
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
renderParams.state = stateStream.compose(dropRepeats(objIsEqual))
|
|
1161
|
-
|
|
1162
|
-
if (this.sources.props$) {
|
|
1163
|
-
renderParams.props = this.sources.props$.compose(dropRepeats(propsIsEqual))
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
if (this._processedChildren$) {
|
|
1167
|
-
renderParams.children = this._processedChildren$.map((p: any) => p.children).compose(dropRepeats(objIsEqual))
|
|
1168
|
-
renderParams.slots = this._processedChildren$.map((p: any) => p.slots).compose(dropRepeats(objIsEqual))
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (this.context$) {
|
|
1172
|
-
renderParams.context = this.context$.compose(dropRepeats(objIsEqual))
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
const names: string[] = []
|
|
1176
|
-
const streams: any[] = []
|
|
1177
|
-
|
|
1178
|
-
Object.entries(renderParams).forEach(([name, stream]: [string, any]) => {
|
|
1179
|
-
names.push(name)
|
|
1180
|
-
streams.push(stream)
|
|
1181
|
-
})
|
|
1182
|
-
|
|
1183
|
-
const combined = xs.combine(...streams)
|
|
1184
|
-
.compose(debounce(1))
|
|
1185
|
-
// map the streams from an array back to an object with the render parameter names as the keys
|
|
1186
|
-
.map((arr: any) => {
|
|
1187
|
-
const params = names.reduce((acc: Record<string, any>, name, index) => {
|
|
1188
|
-
acc[name] = arr[index]
|
|
1189
|
-
if (name === 'state') {
|
|
1190
|
-
acc[this.stateSourceName] = arr[index]
|
|
1191
|
-
acc.calculated = (arr[index] && this.getCalculatedValues(arr[index])) || {}
|
|
1192
|
-
}
|
|
1193
|
-
return acc
|
|
1194
|
-
}, {} as Record<string, any>)
|
|
1195
|
-
return params
|
|
1196
|
-
})
|
|
1197
|
-
|
|
1198
|
-
return combined
|
|
1199
|
-
}
|
|
1200
|
-
|
|
1201
|
-
instantiateSubComponents(vDom$: any): any {
|
|
1202
|
-
// Build the component name Set once outside the fold — avoids rebuilding per render
|
|
1203
|
-
const componentNameSet = new Set(['collection', 'switchable', 'sygnal-factory', ...Object.keys(this.components)])
|
|
1204
|
-
|
|
1205
|
-
return vDom$.fold((previousComponents: any, vDom: any) => {
|
|
1206
|
-
const foundComponents = getComponents(vDom, componentNameSet)
|
|
1207
|
-
const entries = Object.entries(foundComponents)
|
|
1208
|
-
|
|
1209
|
-
const rootEntry: Record<string, any> = { '::ROOT::': vDom }
|
|
1210
|
-
|
|
1211
|
-
if (entries.length === 0) {
|
|
1212
|
-
// Dispose any previously active sub-components
|
|
1213
|
-
this._activeSubComponents.forEach((entry) => {
|
|
1214
|
-
if (entry?.sink$?.__dispose) entry.sink$.__dispose()
|
|
1215
|
-
})
|
|
1216
|
-
this._activeSubComponents.clear()
|
|
1217
|
-
return rootEntry
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
const sinkArrsByType: Record<string, any[]> = {}
|
|
1221
|
-
const childSources: any[] = []
|
|
1222
|
-
let newInstanceCount = 0
|
|
1223
|
-
|
|
1224
|
-
const newComponents = entries.reduce((acc, [id, el]) => {
|
|
1225
|
-
const data = el.data
|
|
1226
|
-
const props = data.props || {}
|
|
1227
|
-
// a string-tag component with a single text child is a text-only vnode (B-011)
|
|
1228
|
-
const children = el.children || (el.text != null ? [{ text: el.text }] : [])
|
|
1229
|
-
|
|
1230
|
-
const isCollection = data.isCollection || false
|
|
1231
|
-
const isSwitchable = data.isSwitchable || false
|
|
1232
|
-
|
|
1233
|
-
const addSinks = (sinks: any) => {
|
|
1234
|
-
Object.entries(sinks).forEach(([name, stream]: [string, any]) => {
|
|
1235
|
-
sinkArrsByType[name] ||= []
|
|
1236
|
-
if (name === PARENT_SINK_NAME) {
|
|
1237
|
-
childSources.push(stream)
|
|
1238
|
-
} else if (name !== this.DOMSourceName && name !== READY_SINK_NAME) {
|
|
1239
|
-
sinkArrsByType[name].push(stream)
|
|
1240
|
-
}
|
|
1241
|
-
})
|
|
1242
|
-
}
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
if (previousComponents[id]) {
|
|
1246
|
-
const entry = previousComponents[id]
|
|
1247
|
-
acc[id] = entry
|
|
1248
|
-
entry.props$.shamefullySendNext(props)
|
|
1249
|
-
entry.children$.shamefullySendNext(children)
|
|
1250
|
-
addSinks(entry.sink$)
|
|
1251
|
-
return acc
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
const props$ = xs.create().startWith(props)
|
|
1255
|
-
const children$ = xs.create().startWith(children)
|
|
1256
|
-
|
|
1257
|
-
let instantiator
|
|
1258
|
-
|
|
1259
|
-
if (isCollection) {
|
|
1260
|
-
instantiator = this.instantiateCollection.bind(this)
|
|
1261
|
-
} else if (isSwitchable) {
|
|
1262
|
-
instantiator = this.instantiateSwitchable.bind(this)
|
|
1263
|
-
} else {
|
|
1264
|
-
instantiator = this.instantiateCustomComponent.bind(this)
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
newInstanceCount++
|
|
1268
|
-
|
|
1269
|
-
let sink$
|
|
1270
|
-
try {
|
|
1271
|
-
sink$ = instantiator(el, props$, children$)
|
|
1272
|
-
} catch (err) {
|
|
1273
|
-
const error = err instanceof Error ? err : new Error(String(err))
|
|
1274
|
-
logError('SYG408', this, 'Sub-component threw; rendering the error fallback', ERR_FIX, error)
|
|
1275
|
-
let fallbackVNode = { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
|
|
1276
|
-
if (typeof this.onError === 'function') {
|
|
1277
|
-
try {
|
|
1278
|
-
fallbackVNode = this.onError(error, { componentName: this.name }) || fallbackVNode
|
|
1279
|
-
} catch (fallbackErr) {
|
|
1280
|
-
logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fallbackErr)
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
sink$ = { [this.DOMSourceName]: xs.of(fallbackVNode) }
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
sink$[this.DOMSourceName] = sink$[this.DOMSourceName] ? this.makeCoordinatedSubComponentDomSink(sink$[this.DOMSourceName]) : xs.never()
|
|
1287
|
-
|
|
1288
|
-
acc[id] = { sink$, props$, children$ }
|
|
1289
|
-
this._activeSubComponents.set(id, acc[id])
|
|
1290
|
-
|
|
1291
|
-
addSinks(sink$)
|
|
1292
|
-
|
|
1293
|
-
return acc
|
|
1294
|
-
}, rootEntry)
|
|
1295
|
-
|
|
1296
|
-
const mergedSinksByType = Object.entries(sinkArrsByType).reduce((acc: Record<string, any>, [name, streamArr]: [string, any]) => {
|
|
1297
|
-
if (streamArr.length === 0) return acc
|
|
1298
|
-
acc[name] = streamArr.length === 1 ? streamArr[0] : xs.merge(...streamArr)
|
|
1299
|
-
return acc
|
|
1300
|
-
}, {} as Record<string, any>)
|
|
1301
|
-
|
|
1302
|
-
// Dispose removed sub-components
|
|
1303
|
-
const currentIds = new Set(Object.keys(newComponents))
|
|
1304
|
-
this._activeSubComponents.forEach((entry, id) => {
|
|
1305
|
-
if (!currentIds.has(id)) {
|
|
1306
|
-
if (entry?.sink$?.__dispose) entry.sink$.__dispose()
|
|
1307
|
-
this._activeSubComponents.delete(id)
|
|
1308
|
-
delete this._childReadyState[id]
|
|
1309
|
-
}
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
this.newSubComponentSinks(mergedSinksByType)
|
|
1313
|
-
this.newChildSources(childSources)
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true)
|
|
1317
|
-
|
|
1318
|
-
return newComponents
|
|
1319
|
-
}, {})
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
makeCoordinatedSubComponentDomSink(domSink$: any): any {
|
|
1323
|
-
return domSink$
|
|
1324
|
-
.debug((_: any) => this.triggerSubComponentsRendered())
|
|
1325
|
-
.remember()
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
// A child's reducers get their state through its lens from this component's raw reducer
|
|
1329
|
-
// state; adding the calculated fields here gives them what the child's view gets (1H-3).
|
|
1330
|
-
withCalculated(lense: any): any {
|
|
1331
|
-
return this.calculated ? { get: (state: any) => lense.get(isObj(state) ? this.addCalculated(state) : state), set: lense.set } : lense
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
createSubComponentLense(stateField: any, componentType: string, defaultState?: any): any {
|
|
1335
|
-
const baseLense = {
|
|
1336
|
-
get: (state: any) => state,
|
|
1337
|
-
set: (_oldState: any, newState: any) => newState
|
|
1338
|
-
}
|
|
1339
|
-
|
|
1340
|
-
if (typeof stateField === 'undefined') return baseLense
|
|
1341
|
-
|
|
1342
|
-
if (typeof stateField === 'string') {
|
|
1343
|
-
return {
|
|
1344
|
-
get: (state: any) => {
|
|
1345
|
-
const slice = state[stateField]
|
|
1346
|
-
if (typeof slice === 'undefined' && defaultState) return defaultState
|
|
1347
|
-
return slice
|
|
1348
|
-
},
|
|
1349
|
-
set: (oldState: any, newState: any) => {
|
|
1350
|
-
if (this.calculated && stateField in this.calculated) {
|
|
1351
|
-
warn('SYG409', this, `${componentType} tried to update calculated field '${stateField}'; ignored`, 'Bind it to a non-calculated field')
|
|
1352
|
-
return oldState
|
|
1353
|
-
}
|
|
1354
|
-
return { ...oldState, [stateField]: newState }
|
|
1355
|
-
}
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
|
|
1359
|
-
if (isObj(stateField)) {
|
|
1360
|
-
if (typeof stateField.get !== 'function') {
|
|
1361
|
-
logError('SYG410', this, `${componentType} 'state' prop has no get(); it gets the parent's whole state`, 'Use a state key string or { get, set }')
|
|
1362
|
-
return baseLense
|
|
1363
|
-
}
|
|
1364
|
-
return { get: stateField.get, set: stateField.set }
|
|
1365
|
-
}
|
|
1366
|
-
|
|
1367
|
-
logError('SYG410', this, `${componentType} 'state' prop is a ${typeof stateField}; it gets the parent's whole state`, 'Use a state key string or { get, set }')
|
|
1368
|
-
return baseLense
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
instantiateCollection(el: any, props$: any, children$: any): any {
|
|
1372
|
-
const data = el.data
|
|
1373
|
-
const props = data.props || {}
|
|
1374
|
-
let filter = typeof props.filter === 'function' ? props.filter : undefined
|
|
1375
|
-
let sort = sortFunctionFromProp(props.sort)
|
|
1376
|
-
|
|
1377
|
-
const arrayOperators = {
|
|
1378
|
-
filter,
|
|
1379
|
-
sort
|
|
1380
|
-
}
|
|
1381
|
-
|
|
1382
|
-
const state$ = xs.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$.startWith(props))
|
|
1383
|
-
// this debounce is important. it forces state and prop updates to happen at the same time
|
|
1384
|
-
// without this, changes to sort or filter won't happen properly
|
|
1385
|
-
.compose(debounce(1))
|
|
1386
|
-
.map(([state, props]: [any, any]) => {
|
|
1387
|
-
if (props.filter !== arrayOperators.filter) {
|
|
1388
|
-
arrayOperators.filter = typeof props.filter === 'function' ? props.filter : undefined
|
|
1389
|
-
}
|
|
1390
|
-
if (props.sort !== arrayOperators.sort) {
|
|
1391
|
-
arrayOperators.sort = sortFunctionFromProp(props.sort)
|
|
1392
|
-
}
|
|
1393
|
-
// calculated fields are added by the lens (B-013)
|
|
1394
|
-
return state
|
|
1395
|
-
})
|
|
1396
|
-
|
|
1397
|
-
const stateSource = new StateSource(state$, this.stateSourceName)
|
|
1398
|
-
const stateField = props.from
|
|
1399
|
-
const collectionOf = props.of
|
|
1400
|
-
const idField = props.idfield || 'id'
|
|
1401
|
-
|
|
1402
|
-
let lense
|
|
1403
|
-
let factory
|
|
1404
|
-
|
|
1405
|
-
if (typeof collectionOf === 'function') {
|
|
1406
|
-
if (collectionOf.isSygnalComponent) {
|
|
1407
|
-
factory = collectionOf
|
|
1408
|
-
} else {
|
|
1409
|
-
const name = collectionOf.componentName || collectionOf.label || collectionOf.name || 'FUNCTION_COMPONENT'
|
|
1410
|
-
const view = collectionOf
|
|
1411
|
-
const { model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = collectionOf
|
|
1412
|
-
const options = { name, view, model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
|
|
1413
|
-
factory = component(options)
|
|
1414
|
-
}
|
|
1415
|
-
} else if (this.components[collectionOf]) {
|
|
1416
|
-
factory = this.components[collectionOf]
|
|
1417
|
-
} else {
|
|
1418
|
-
fail('SYG411', this, `Collection 'of' is not a component: ${collectionOf}`, 'Use of={ItemComponent}')
|
|
1419
|
-
}
|
|
1420
|
-
|
|
1421
|
-
const fieldLense = {
|
|
1422
|
-
get: (state: any) => {
|
|
1423
|
-
if (!Array.isArray(state[stateField])) return []
|
|
1424
|
-
const items = state[stateField]
|
|
1425
|
-
const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items
|
|
1426
|
-
const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered
|
|
1427
|
-
const mapped = sorted.map((item: any, index: any) => {
|
|
1428
|
-
return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
|
|
1429
|
-
})
|
|
1430
|
-
|
|
1431
|
-
return mapped
|
|
1432
|
-
},
|
|
1433
|
-
set: (oldState: any, newState: any) => {
|
|
1434
|
-
if (this.calculated && stateField in this.calculated) {
|
|
1435
|
-
warn('SYG409', this, `Collection tried to update calculated field '${stateField}'; ignored`, 'Bind it to a non-calculated field')
|
|
1436
|
-
return oldState
|
|
1437
|
-
}
|
|
1438
|
-
const updated = []
|
|
1439
|
-
for (const oldItem of oldState[stateField].map((item: any, index: any) => (isObj(item) ? { ...item, [idField]: item[idField] || index } : { __primitive: true, value: item, [idField]: index }))) {
|
|
1440
|
-
if (typeof arrayOperators.filter === 'function' && !arrayOperators.filter(oldItem)) {
|
|
1441
|
-
updated.push(oldItem.__primitive ? oldItem.value : oldItem)
|
|
1442
|
-
} else {
|
|
1443
|
-
const newItem = newState.find((item: any) => item[idField] === oldItem[idField])
|
|
1444
|
-
if (typeof newItem !== 'undefined') updated.push(oldItem.__primitive ? newItem.value : newItem)
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
return { ...oldState, [stateField]: updated }
|
|
1448
|
-
}
|
|
1449
|
-
}
|
|
1450
|
-
|
|
1451
|
-
if (stateField === undefined) {
|
|
1452
|
-
lense = {
|
|
1453
|
-
get: (state: any) => {
|
|
1454
|
-
if (!(state instanceof Array) && state.value && state.value instanceof Array) return state.value
|
|
1455
|
-
return state
|
|
1456
|
-
},
|
|
1457
|
-
set: (oldState: any, newState: any) => {
|
|
1458
|
-
return newState
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
} else if (typeof stateField === 'string') {
|
|
1462
|
-
if (isObj(this.currentState)) {
|
|
1463
|
-
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
1464
|
-
const arrayFields = Object.keys(this.currentState).filter(k => Array.isArray(this.currentState[k]))
|
|
1465
|
-
logError('SYG401', this, `Collection from="${stateField}" is not in state${arrayFields.length ? ` (array fields: '${arrayFields.join("', '")}')` : ''}; it renders nothing`, 'Set it to an array in initialState')
|
|
1466
|
-
lense = undefined
|
|
1467
|
-
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
1468
|
-
warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
|
|
1469
|
-
lense = fieldLense
|
|
1470
|
-
} else {
|
|
1471
|
-
lense = fieldLense
|
|
1472
|
-
}
|
|
1473
|
-
} else {
|
|
1474
|
-
if (!Array.isArray(this.currentState[stateField])) {
|
|
1475
|
-
warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
|
|
1476
|
-
lense = fieldLense
|
|
1477
|
-
} else {
|
|
1478
|
-
lense = fieldLense
|
|
1479
|
-
}
|
|
1480
|
-
}
|
|
1481
|
-
} else if (isObj(stateField)) {
|
|
1482
|
-
if (typeof stateField.get !== 'function') {
|
|
1483
|
-
logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
|
|
1484
|
-
lense = undefined
|
|
1485
|
-
} else {
|
|
1486
|
-
lense = {
|
|
1487
|
-
get: (state: any) => {
|
|
1488
|
-
const newState = stateField.get(state)
|
|
1489
|
-
if (!Array.isArray(newState)) {
|
|
1490
|
-
warn('SYG401', this, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', newState)
|
|
1491
|
-
return []
|
|
1492
|
-
}
|
|
1493
|
-
return newState
|
|
1494
|
-
},
|
|
1495
|
-
set: stateField.set
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
} else {
|
|
1499
|
-
logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
|
|
1500
|
-
lense = undefined
|
|
1501
|
-
}
|
|
1502
|
-
|
|
1503
|
-
// B-013: add this component's calculated fields inside the lens, not on state$, so the
|
|
1504
|
-
// items' reducers (which read through the lens from the raw state) see the same item
|
|
1505
|
-
// array as their views, e.g. for from={calculatedField} or a custom get().
|
|
1506
|
-
if (lense) lense = this.withCalculated(lense)
|
|
1507
|
-
|
|
1508
|
-
// Strip collection-specific props and forward only user-defined extra props to each item
|
|
1509
|
-
const collectionKeys = ['of', 'from', 'filter', 'sort', 'idfield', 'className']
|
|
1510
|
-
const itemProps$ = props$.map((p: any) => {
|
|
1511
|
-
if (!p || typeof p !== 'object') return {}
|
|
1512
|
-
const itemProps: Record<string, any> = {}
|
|
1513
|
-
for (const key in p) {
|
|
1514
|
-
if (!collectionKeys.includes(key)) itemProps[key] = p[key]
|
|
1515
|
-
}
|
|
1516
|
-
return itemProps
|
|
1517
|
-
})
|
|
1518
|
-
|
|
1519
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$: itemProps$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber }
|
|
1520
|
-
const sink$ = collection(factory, lense as any, { container: null as any })(sources)
|
|
1521
|
-
if (!isObj(sink$)) {
|
|
1522
|
-
fail('SYG903', this, 'Collection factory returned invalid sinks', 'Return a sinks object')
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
// Notify devtools of collection mount
|
|
1526
|
-
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1527
|
-
const itemName = typeof collectionOf === 'function'
|
|
1528
|
-
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
1529
|
-
: String(collectionOf)
|
|
1530
|
-
;(window as any).__SYGNAL_DEVTOOLS__.onCollectionMounted(
|
|
1531
|
-
this._componentNumber, this.name, itemName,
|
|
1532
|
-
typeof stateField === 'string' ? stateField : null
|
|
1533
|
-
)
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
return sink$
|
|
1537
|
-
}
|
|
1538
|
-
|
|
1539
|
-
instantiateSwitchable(el: any, props$: any, children$: any): any {
|
|
1540
|
-
const data = el.data
|
|
1541
|
-
const props = data.props || {}
|
|
1542
|
-
|
|
1543
|
-
const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
|
|
1544
|
-
.map((state: any) => {
|
|
1545
|
-
return isObj(state) ? this.addCalculated(state) : state
|
|
1546
|
-
})
|
|
1547
|
-
|
|
1548
|
-
const stateSource = new StateSource(state$, this.stateSourceName)
|
|
1549
|
-
const stateField = props.state
|
|
1550
|
-
const lense = this.withCalculated(this.createSubComponentLense(stateField, 'Switchable sub-component'))
|
|
1551
|
-
|
|
1552
|
-
const switchableComponents = props.of
|
|
1553
|
-
const keys = Object.keys(switchableComponents)
|
|
1554
|
-
keys.forEach(key => {
|
|
1555
|
-
const current = switchableComponents[key]
|
|
1556
|
-
if (!current.isSygnalComponent) {
|
|
1557
|
-
const name = current.componentName || current.label || current.name || 'FUNCTION_COMPONENT'
|
|
1558
|
-
const view = current
|
|
1559
|
-
const { model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = current
|
|
1560
|
-
const options = { name, view, model, intent, hmrActions, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
|
|
1561
|
-
switchableComponents[key] = component(options)
|
|
1562
|
-
}
|
|
1563
|
-
})
|
|
1564
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
|
|
1565
|
-
|
|
1566
|
-
const sink$ = isolate(switchable(switchableComponents, props$.map((props: any) => props.current), ''), { [this.stateSourceName]: lense })(sources)
|
|
1567
|
-
|
|
1568
|
-
if (!isObj(sink$)) {
|
|
1569
|
-
fail('SYG903', this, 'Switchable factory returned invalid sinks', 'Return a sinks object')
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
return sink$
|
|
1573
|
-
}
|
|
1574
|
-
|
|
1575
|
-
instantiateCustomComponent(el: any, props$: any, children$: any): any {
|
|
1576
|
-
const componentName = el.sel
|
|
1577
|
-
const data = el.data
|
|
1578
|
-
const props = data.props || {}
|
|
1579
|
-
|
|
1580
|
-
const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
|
|
1581
|
-
.map((state: any) => {
|
|
1582
|
-
return isObj(state) ? this.addCalculated(state) : state
|
|
1583
|
-
})
|
|
1584
|
-
|
|
1585
|
-
let stateSource = new StateSource(state$, this.stateSourceName)
|
|
1586
|
-
const stateField = props.state
|
|
1587
|
-
|
|
1588
|
-
if (typeof props.sygnalFactory !== 'function' && isObj(props.sygnalOptions)) {
|
|
1589
|
-
props.sygnalFactory = component(props.sygnalOptions)
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory)
|
|
1593
|
-
if (!factory) {
|
|
1594
|
-
if (componentName === 'sygnal-factory') fail('SYG413', this, 'Unnamed component has no factory', 'Name the function, or set Comp.componentName')
|
|
1595
|
-
fail('SYG414', this, `Component '${componentName}' not found`, 'Import it, or add it to .components')
|
|
1596
|
-
}
|
|
1597
|
-
|
|
1598
|
-
// Guard against sub-components accidentally overwriting parent state with .initialState
|
|
1599
|
-
const subInitialState = props.sygnalOptions?.initialState
|
|
1600
|
-
const subIsolatedState = props.sygnalOptions?.isolatedState
|
|
1601
|
-
if (subInitialState && !subIsolatedState) {
|
|
1602
|
-
const subName = props.sygnalOptions?.name || componentName
|
|
1603
|
-
fail('SYG405', subName, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
|
|
1604
|
-
}
|
|
1605
|
-
|
|
1606
|
-
const subInitState = subIsolatedState ? subInitialState : undefined
|
|
1607
|
-
let lense = this.createSubComponentLense(stateField, 'Sub-component', subInitState)
|
|
1608
|
-
// B-008: isolatedState without a `state` prop = state local to this instance; the
|
|
1609
|
-
// parent's state is never replaced. It's kept off the parent's state stream (1H-9): the
|
|
1610
|
-
// child's reducers are applied below and feed its own state source, so a child write
|
|
1611
|
-
// doesn't produce a new parent state. Until the child first writes (INITIALIZE; never,
|
|
1612
|
-
// without a model) it reads the parent's state, as before.
|
|
1613
|
-
let local: any, local$: any
|
|
1614
|
-
if (subIsolatedState && typeof stateField === 'undefined') {
|
|
1615
|
-
local$ = xs.create()
|
|
1616
|
-
stateSource = new StateSource(xs.merge(state$.filter(() => local === undefined), local$), this.stateSourceName)
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
const sources: Record<string, any> = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
|
|
1620
|
-
lense = local$ ? null : this.withCalculated(lense)
|
|
1621
|
-
|
|
1622
|
-
// Detect Command objects in props and expose as commands$ source
|
|
1623
|
-
for (const key of Object.keys(props)) {
|
|
1624
|
-
const val = props[key]
|
|
1625
|
-
if (val && val.__sygnalCommand) {
|
|
1626
|
-
val._targetComponentName = componentName
|
|
1627
|
-
sources.commands$ = makeCommandSource(val as Command)
|
|
1628
|
-
break
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources)
|
|
1633
|
-
|
|
1634
|
-
if (!isObj(sink$)) {
|
|
1635
|
-
const name = componentName === 'sygnal-factory' ? 'custom element' : componentName
|
|
1636
|
-
fail('SYG903', this, `Factory for ${name} returned invalid sinks`, 'Return a sinks object')
|
|
1637
|
-
}
|
|
1638
|
-
if (local$ && sink$[this.stateSourceName]) {
|
|
1639
|
-
sink$[this.stateSourceName] = sink$[this.stateSourceName]
|
|
1640
|
-
.map((reducer: any) => local$.shamefullySendNext(local = reducer(local === undefined ? this.addCalculated(this.currentState) : local)))
|
|
1641
|
-
.filter(() => false)
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
return sink$
|
|
1645
|
-
}
|
|
1646
|
-
|
|
1647
|
-
renderVdom(componentInstances$: any): any {
|
|
1648
|
-
// Build the component name Set once — avoids rebuilding per render cycle
|
|
1649
|
-
const componentNameSet = new Set(['collection', 'switchable', 'sygnal-factory', ...Object.keys(this.components)])
|
|
1650
|
-
|
|
1651
|
-
return xs.combine(this.subComponentsRendered$, componentInstances$, this._readyChanged$.startWith(null))
|
|
1652
|
-
.compose(debounce(1))
|
|
1653
|
-
.map(([_, components]: [any, any]) => {
|
|
1654
|
-
const root = components['::ROOT::']
|
|
1655
|
-
const entries = Object.entries(components).filter(([id]) => id !== '::ROOT::')
|
|
1656
|
-
|
|
1657
|
-
if (entries.length === 0) {
|
|
1658
|
-
return xs.of(processSuspensePost(root))
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
const ids: string[] = []
|
|
1662
|
-
const vdom$ = entries
|
|
1663
|
-
.map(([id, val]: [string, any]) => {
|
|
1664
|
-
ids.push(id)
|
|
1665
|
-
return val.sink$[this.DOMSourceName].startWith(undefined)
|
|
1666
|
-
})
|
|
1667
|
-
|
|
1668
|
-
if (vdom$.length === 0) return xs.of(root)
|
|
1669
|
-
|
|
1670
|
-
// Track READY state on the component instance (persists across folds)
|
|
1671
|
-
for (const [id, val] of entries as [string, any]) {
|
|
1672
|
-
if (this._childReadyState[id] !== undefined) continue // already tracking
|
|
1673
|
-
const readySink = val.sink$[READY_SINK_NAME]
|
|
1674
|
-
if (readySink) {
|
|
1675
|
-
const isExplicit = readySink.__explicitReady
|
|
1676
|
-
this._childReadyState[id] = isExplicit ? false : true
|
|
1677
|
-
readySink.addListener({
|
|
1678
|
-
next: (ready: any) => {
|
|
1679
|
-
const wasReady = this._childReadyState[id]
|
|
1680
|
-
this._childReadyState[id] = !!ready
|
|
1681
|
-
// When READY state changes, trigger a re-render
|
|
1682
|
-
if (wasReady !== !!ready) {
|
|
1683
|
-
if (this._readyChangedListener) {
|
|
1684
|
-
setTimeout(() => {
|
|
1685
|
-
this._readyChangedListener?.next(null)
|
|
1686
|
-
}, 0)
|
|
1687
|
-
}
|
|
1688
|
-
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1689
|
-
(window as any).__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready)
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1692
|
-
},
|
|
1693
|
-
error: () => {},
|
|
1694
|
-
complete: () => {},
|
|
1695
|
-
})
|
|
1696
|
-
} else {
|
|
1697
|
-
this._childReadyState[id] = true
|
|
1698
|
-
}
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
return xs.combine(...vdom$)
|
|
1702
|
-
.filter((vdoms: any) => vdoms.every((v: any) => v !== undefined))
|
|
1703
|
-
.map((vdoms: any) => {
|
|
1704
|
-
const withIds = vdoms.reduce((acc: Record<string, any>, vdom: any, index: any) => {
|
|
1705
|
-
acc[ids[index]] = vdom
|
|
1706
|
-
return acc
|
|
1707
|
-
}, {} as Record<string, any>)
|
|
1708
|
-
const injected = injectComponents(root, withIds, componentNameSet, 'r', undefined, this._childReadyState)
|
|
1709
|
-
return processSuspensePost(injected)
|
|
1710
|
-
})
|
|
1711
|
-
})
|
|
1712
|
-
.flatten()
|
|
1713
|
-
.filter((val: any) => !!val)
|
|
1714
|
-
// [diagnostics hook]
|
|
1715
|
-
.map((vdom: any) => { diag.onRender(this, vdom); return vdom })
|
|
1716
|
-
.remember()
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
/**
|
|
1728
|
-
* factory to create a logging function meant to be used inside of an xstream .compose()
|
|
1729
|
-
*
|
|
1730
|
-
* @param {String} context name of the component or file to be prepended to any messages
|
|
1731
|
-
* @return {Function}
|
|
1732
|
-
*
|
|
1733
|
-
* returned function accepts either a `String` of `Function`
|
|
1734
|
-
* `String` values will be logged to `console` as is
|
|
1735
|
-
* `Function` values will be called with the current `stream` value and the result will be logged to `console`
|
|
1736
|
-
* all output will be prepended with the `context` (ex. "[CONTEXT] My output")
|
|
1737
|
-
* ONLY outputs if the global `DEBUG` variable is set to `true`
|
|
1738
|
-
*/
|
|
1739
|
-
function makeLog(context: string): any {
|
|
1740
|
-
return function (this: Component, msg: any, immediate: boolean = false) {
|
|
1741
|
-
const fixedMsg = (typeof msg === 'function') ? msg : (_: any) => msg
|
|
1742
|
-
if (immediate) {
|
|
1743
|
-
if (this.debug) {
|
|
1744
|
-
const text = `[${context}] ${fixedMsg(msg)}`
|
|
1745
|
-
console.log(text)
|
|
1746
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
1747
|
-
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
|
|
1748
|
-
}
|
|
1749
|
-
}
|
|
1750
|
-
return
|
|
1751
|
-
} else {
|
|
1752
|
-
return (stream: any) => {
|
|
1753
|
-
return stream.debug((msg: any) => {
|
|
1754
|
-
if (this.debug) {
|
|
1755
|
-
const text = `[${context}] ${fixedMsg(msg)}`
|
|
1756
|
-
console.log(text)
|
|
1757
|
-
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
1758
|
-
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
})
|
|
1762
|
-
}
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
function getComponents(currentElement: any, componentNameSet: Set<string>, path: string = 'r', parentId?: string, found?: Record<string, any>): Record<string, any> {
|
|
1770
|
-
if (!currentElement) return found || {}
|
|
1771
|
-
|
|
1772
|
-
if (currentElement.data?.componentsProcessed) return found || {}
|
|
1773
|
-
if (path === 'r') currentElement.data.componentsProcessed = true
|
|
1774
|
-
|
|
1775
|
-
// Allocate the result object only at the root call
|
|
1776
|
-
if (!found) found = {}
|
|
1777
|
-
|
|
1778
|
-
const sel = currentElement.sel
|
|
1779
|
-
const isCollection = sel && sel.toLowerCase() === 'collection'
|
|
1780
|
-
const isSwitchable = sel && sel.toLowerCase() === 'switchable'
|
|
1781
|
-
const isComponent = (sel && componentNameSet.has(sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
|
|
1782
|
-
const props = (currentElement.data && currentElement.data.props) || {}
|
|
1783
|
-
const children = currentElement.children || []
|
|
1784
|
-
|
|
1785
|
-
let id = parentId
|
|
1786
|
-
if (isComponent) {
|
|
1787
|
-
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1788
|
-
if (isCollection) {
|
|
1789
|
-
if (!props.of) fail('SYG411', undefined, "Collection is missing 'of'", 'Use of={ItemComponent}')
|
|
1790
|
-
if (typeof props.of !== 'string' && typeof props.of !== 'function') fail('SYG411', undefined, `Collection 'of' is a ${typeof props.of}`, 'Use of={ItemComponent}')
|
|
1791
|
-
if (typeof props.of !== 'function' && !componentNameSet.has(props.of)) fail('SYG411', undefined, `Collection 'of' component not found: ${props.of}`, 'Use of={ItemComponent}')
|
|
1792
|
-
// an invalid 'from' is reported once, with the component name, by instantiateCollection (G-026)
|
|
1793
|
-
currentElement.data.isCollection = true
|
|
1794
|
-
currentElement.data.props ||= {}
|
|
1795
|
-
} else if (isSwitchable) {
|
|
1796
|
-
if (!props.of) fail('SYG415', undefined, "Switchable is missing 'of'", 'Use of={{ name: Component }}')
|
|
1797
|
-
if (!isObj(props.of)) fail('SYG415', undefined, `Switchable 'of' is a ${typeof props.of}`, 'Use of={{ name: Component }}')
|
|
1798
|
-
const switchableComponents = Object.values(props.of)
|
|
1799
|
-
if (!switchableComponents.every(comp => typeof comp === 'function')) fail('SYG415', undefined, "Switchable 'of' has a value that is not a component", 'Use of={{ name: Component }}')
|
|
1800
|
-
if (!props.current || (typeof props.current !== 'string' && typeof props.current !== 'function')) fail('SYG416', undefined, `Switchable 'current' is missing or a ${typeof props.current}`, "Set current to a key of 'of'")
|
|
1801
|
-
const switchableComponentNames = Object.keys(props.of)
|
|
1802
|
-
if (!switchableComponentNames.includes(props.current)) fail('SYG416', undefined, `Switchable 'current' '${props.current}' is not a key of 'of'`, "Set current to a key of 'of'")
|
|
1803
|
-
currentElement.data.isSwitchable = true
|
|
1804
|
-
}
|
|
1805
|
-
if (typeof props.key === 'undefined') (currentElement.data.props ||= {}).key = id
|
|
1806
|
-
found[id] = currentElement
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
for (let i = 0; i < children.length; i++) {
|
|
1810
|
-
getComponents(children[i], componentNameSet, `${path}.${i}`, id, found)
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
return found
|
|
1814
|
-
}
|
|
1815
|
-
|
|
1816
|
-
function injectComponents(currentElement: any, components: Record<string, any>, componentNameSet: Set<string>, path: string = 'r', parentId?: string, readyMap?: Record<string, boolean>): any {
|
|
1817
|
-
if (!currentElement) return undefined
|
|
1818
|
-
|
|
1819
|
-
const sel = currentElement.sel || 'NO SELECTOR'
|
|
1820
|
-
const isComponent = componentNameSet.has(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
|
|
1821
|
-
const isCollection = currentElement?.data?.isCollection
|
|
1822
|
-
const children = currentElement.children || []
|
|
1823
|
-
|
|
1824
|
-
let id = parentId
|
|
1825
|
-
if (isComponent) {
|
|
1826
|
-
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1827
|
-
let component = components[id]
|
|
1828
|
-
// Mark an injected VNode that is NOT ready (non-mutating) so Suspense can find it.
|
|
1829
|
-
// Ready children get no attribute (G-018): moving markup into a child component
|
|
1830
|
-
// must not change the DOM.
|
|
1831
|
-
if (readyMap && id && readyMap[id] === false && component && typeof component === 'object' && component.sel) {
|
|
1832
|
-
component = {
|
|
1833
|
-
...component,
|
|
1834
|
-
data: {
|
|
1835
|
-
...(component.data || {}),
|
|
1836
|
-
attrs: {
|
|
1837
|
-
...(component.data?.attrs || {}),
|
|
1838
|
-
'data-sygnal-ready': 'false'
|
|
1839
|
-
}
|
|
1840
|
-
}
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
if (isCollection) {
|
|
1844
|
-
return {
|
|
1845
|
-
...currentElement,
|
|
1846
|
-
sel: 'div',
|
|
1847
|
-
children: Array.isArray(component) ? component : [component]
|
|
1848
|
-
}
|
|
1849
|
-
} else {
|
|
1850
|
-
return component
|
|
1851
|
-
}
|
|
1852
|
-
} else if (children.length > 0) {
|
|
1853
|
-
const newChildren = children.map((child: any, i: any) => injectComponents(child, components, componentNameSet, `${path}.${i}`, id, readyMap)).flat()
|
|
1854
|
-
return { ...currentElement, children: newChildren }
|
|
1855
|
-
} else {
|
|
1856
|
-
return currentElement
|
|
1857
|
-
}
|
|
1858
|
-
}
|
|
1859
|
-
|
|
1860
|
-
function getComponentIdFromElement(el: any, path: string, parentId?: string): string {
|
|
1861
|
-
const sel = el.sel
|
|
1862
|
-
const name = typeof sel === 'string' ? sel : 'functionComponent'
|
|
1863
|
-
const props = el.data?.props || {}
|
|
1864
|
-
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path
|
|
1865
|
-
const parentString = parentId ? `${parentId}|` : ''
|
|
1866
|
-
const fullId = `${parentString}${name}::${id}`
|
|
1867
|
-
return fullId
|
|
1868
|
-
}
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
function hasNotReadyChild(vnode: any): boolean {
|
|
1872
|
-
if (!vnode || !vnode.sel) return false
|
|
1873
|
-
// Check for data-sygnal-ready="false" on injected sub-components
|
|
1874
|
-
if (vnode.data?.attrs?.['data-sygnal-ready'] === 'false') return true
|
|
1875
|
-
// Check for lazy-loading placeholder (not yet instantiated as a component)
|
|
1876
|
-
if (vnode.data?.attrs?.['data-sygnal-lazy'] === 'loading') return true
|
|
1877
|
-
// Stop at inner Suspense boundaries — they handle their own children
|
|
1878
|
-
if (vnode.sel === 'suspense') return false
|
|
1879
|
-
if (Array.isArray(vnode.children)) {
|
|
1880
|
-
for (const child of vnode.children) {
|
|
1881
|
-
if (hasNotReadyChild(child)) return true
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
return false
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function processSuspensePost(vnode: any): any {
|
|
1888
|
-
if (!vnode || !vnode.sel) return vnode
|
|
1889
|
-
if (vnode.sel === 'suspense') {
|
|
1890
|
-
const props = vnode.data?.props || {}
|
|
1891
|
-
const fallback = props.fallback
|
|
1892
|
-
const children = vnode.children || []
|
|
1893
|
-
|
|
1894
|
-
// Check if any child within this boundary is not ready
|
|
1895
|
-
const isPending = children.some(hasNotReadyChild)
|
|
1896
|
-
|
|
1897
|
-
if (isPending && fallback) {
|
|
1898
|
-
// Render fallback
|
|
1899
|
-
if (typeof fallback === 'string') {
|
|
1900
|
-
return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'pending' } }, children: [{ text: fallback }], text: undefined, elm: undefined, key: undefined }
|
|
1901
|
-
}
|
|
1902
|
-
return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'pending' } }, children: [fallback], text: undefined, elm: undefined, key: undefined }
|
|
1903
|
-
}
|
|
1904
|
-
|
|
1905
|
-
// All children ready or no fallback — render children directly
|
|
1906
|
-
if (children.length === 1) return processSuspensePost(children[0])
|
|
1907
|
-
return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'resolved' } }, children: children.map((c: any) => processSuspensePost(c)), text: undefined, elm: undefined, key: undefined }
|
|
1908
|
-
}
|
|
1909
|
-
if (vnode.children && vnode.children.length > 0) {
|
|
1910
|
-
const newChildren = vnode.children.map((c: any) => processSuspensePost(c))
|
|
1911
|
-
return { ...vnode, children: newChildren }
|
|
1912
|
-
}
|
|
1913
|
-
return vnode
|
|
1914
|
-
}
|
|
1915
|
-
|
|
1916
|
-
const portalPatch = snabbdomInit(defaultModules);
|
|
1917
|
-
|
|
1918
|
-
function applyTransitionHooks(vnode: any, name: string, duration?: number): any {
|
|
1919
|
-
const existingInsert = vnode.data?.hook?.insert
|
|
1920
|
-
const existingRemove = vnode.data?.hook?.remove
|
|
1921
|
-
|
|
1922
|
-
vnode.data = vnode.data || {}
|
|
1923
|
-
vnode.data.hook = vnode.data.hook || {}
|
|
1924
|
-
|
|
1925
|
-
vnode.data.hook.insert = (vn: any) => {
|
|
1926
|
-
if (existingInsert) existingInsert(vn)
|
|
1927
|
-
const el = vn.elm
|
|
1928
|
-
if (!el || !el.classList) return
|
|
1929
|
-
el.classList.add(`${name}-enter-from`, `${name}-enter-active`)
|
|
1930
|
-
requestAnimationFrame(() => {
|
|
1931
|
-
requestAnimationFrame(() => {
|
|
1932
|
-
el.classList.remove(`${name}-enter-from`)
|
|
1933
|
-
el.classList.add(`${name}-enter-to`)
|
|
1934
|
-
onTransitionEnd(el, duration, () => {
|
|
1935
|
-
el.classList.remove(`${name}-enter-active`, `${name}-enter-to`)
|
|
1936
|
-
})
|
|
1937
|
-
})
|
|
1938
|
-
})
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
vnode.data.hook.remove = (vn: any, rm: () => void) => {
|
|
1942
|
-
if (existingRemove) existingRemove(vn, () => {})
|
|
1943
|
-
const el = vn.elm
|
|
1944
|
-
if (!el || !el.classList) { rm(); return }
|
|
1945
|
-
el.classList.add(`${name}-leave-from`, `${name}-leave-active`)
|
|
1946
|
-
requestAnimationFrame(() => {
|
|
1947
|
-
requestAnimationFrame(() => {
|
|
1948
|
-
el.classList.remove(`${name}-leave-from`)
|
|
1949
|
-
el.classList.add(`${name}-leave-to`)
|
|
1950
|
-
onTransitionEnd(el, duration, () => {
|
|
1951
|
-
el.classList.remove(`${name}-leave-active`, `${name}-leave-to`)
|
|
1952
|
-
rm()
|
|
1953
|
-
})
|
|
1954
|
-
})
|
|
1955
|
-
})
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
|
-
return vnode
|
|
1959
|
-
}
|
|
1960
|
-
|
|
1961
|
-
/**
|
|
1962
|
-
* Single-pass VNode preprocessor that handles lazy, portal, transition, and clientonly
|
|
1963
|
-
* nodes in one recursive walk instead of four separate passes.
|
|
1964
|
-
*/
|
|
1965
|
-
function preprocessVdom(vnode: any, componentInstance: any): any {
|
|
1966
|
-
if (!vnode || !vnode.sel) return vnode
|
|
1967
|
-
|
|
1968
|
-
// Handle lazy components
|
|
1969
|
-
const view = vnode.data?.props?.sygnalOptions?.view
|
|
1970
|
-
if (view && view.__sygnalLazy) {
|
|
1971
|
-
if (view.__sygnalLazyLoaded()) {
|
|
1972
|
-
const loaded = view.__sygnalLazyLoadedComponent
|
|
1973
|
-
if (loaded) {
|
|
1974
|
-
const props = vnode.data?.props || {}
|
|
1975
|
-
const name = loaded.componentName || loaded.label || loaded.name || 'LazyLoaded'
|
|
1976
|
-
const { model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug } = loaded
|
|
1977
|
-
const options = { name, view: loaded, model, intent, hmrActions, context, peers, components, initialState, isolatedState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, onError, debug }
|
|
1978
|
-
const cleanProps = { ...props }
|
|
1979
|
-
delete cleanProps.sygnalOptions
|
|
1980
|
-
return {
|
|
1981
|
-
sel: name,
|
|
1982
|
-
data: { props: { ...cleanProps, sygnalOptions: options } },
|
|
1983
|
-
children: vnode.children || [],
|
|
1984
|
-
text: undefined, elm: undefined, key: undefined,
|
|
1985
|
-
}
|
|
1986
|
-
}
|
|
1987
|
-
} else {
|
|
1988
|
-
if (!view.__sygnalLazyReRenderScheduled && view.__sygnalLazyPromise && componentInstance) {
|
|
1989
|
-
view.__sygnalLazyReRenderScheduled = true
|
|
1990
|
-
view.__sygnalLazyPromise.then(() => {
|
|
1991
|
-
setTimeout(() => {
|
|
1992
|
-
const stateSource = componentInstance.sources?.[componentInstance.stateSourceName]
|
|
1993
|
-
if (stateSource && stateSource.stream) {
|
|
1994
|
-
const stateCopy = { ...componentInstance.currentState, __sygnalLazyTick: Date.now() }
|
|
1995
|
-
stateSource.stream.shamefullySendNext(stateCopy)
|
|
1996
|
-
}
|
|
1997
|
-
}, 0)
|
|
1998
|
-
})
|
|
1999
|
-
}
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
|
|
2003
|
-
// Handle portals
|
|
2004
|
-
if (vnode.sel === 'portal') {
|
|
2005
|
-
const target = vnode.data?.props?.target
|
|
2006
|
-
const children = vnode.children || []
|
|
2007
|
-
return createPortalPlaceholder(target, children)
|
|
2008
|
-
}
|
|
2009
|
-
|
|
2010
|
-
// Handle transitions — recursively preprocess the child before applying hooks
|
|
2011
|
-
if (vnode.sel === 'transition') {
|
|
2012
|
-
const props = vnode.data?.props || {}
|
|
2013
|
-
const name = props.name || 'v'
|
|
2014
|
-
const duration = props.duration
|
|
2015
|
-
const children = vnode.children || []
|
|
2016
|
-
const child = children[0]
|
|
2017
|
-
if (!child || !child.sel) return child || vnode
|
|
2018
|
-
return applyTransitionHooks(preprocessVdom(child, componentInstance), name, duration)
|
|
2019
|
-
}
|
|
2020
|
-
|
|
2021
|
-
// Handle clientonly — unwrap to children on client
|
|
2022
|
-
if (vnode.sel === 'clientonly') {
|
|
2023
|
-
const children = vnode.children || []
|
|
2024
|
-
if (children.length === 0) return { sel: 'div', data: {}, children: [] }
|
|
2025
|
-
if (children.length === 1) return preprocessVdom(children[0], componentInstance)
|
|
2026
|
-
return {
|
|
2027
|
-
sel: 'div',
|
|
2028
|
-
data: {},
|
|
2029
|
-
children: children.map((c: any) => preprocessVdom(c, componentInstance)),
|
|
2030
|
-
text: undefined,
|
|
2031
|
-
elm: undefined,
|
|
2032
|
-
key: undefined,
|
|
2033
|
-
}
|
|
2034
|
-
}
|
|
2035
|
-
|
|
2036
|
-
// Recurse into children
|
|
2037
|
-
if (vnode.children && vnode.children.length > 0) {
|
|
2038
|
-
vnode.children = vnode.children.map((child: any) => preprocessVdom(child, componentInstance))
|
|
2039
|
-
}
|
|
2040
|
-
return vnode
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
function onTransitionEnd(el: any, duration: number | undefined, cb: () => void): void {
|
|
2045
|
-
if (typeof duration === 'number') {
|
|
2046
|
-
setTimeout(cb, duration)
|
|
2047
|
-
} else {
|
|
2048
|
-
const handler = () => {
|
|
2049
|
-
el.removeEventListener('transitionend', handler)
|
|
2050
|
-
cb()
|
|
2051
|
-
}
|
|
2052
|
-
el.addEventListener('transitionend', handler)
|
|
2053
|
-
}
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
function portalMount(vnode: any, target: string, children: any[]): void {
|
|
2058
|
-
const container = document.querySelector(target)
|
|
2059
|
-
if (!container) {
|
|
2060
|
-
warn('SYG417', 'Portal', `Target '${target}' not found; content not rendered`, 'Render the target first')
|
|
2061
|
-
return
|
|
2062
|
-
}
|
|
2063
|
-
const anchor = document.createElement('div')
|
|
2064
|
-
container.appendChild(anchor)
|
|
2065
|
-
vnode.data._portalVnode = portalPatch(anchor, {
|
|
2066
|
-
sel: 'div', data: {}, children,
|
|
2067
|
-
text: undefined, elm: undefined, key: undefined,
|
|
2068
|
-
})
|
|
2069
|
-
vnode.data._portalContainer = container
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
|
-
function createPortalPlaceholder(target: string, children: any[]): any {
|
|
2073
|
-
const portalChildren = children || []
|
|
2074
|
-
|
|
2075
|
-
return {
|
|
2076
|
-
sel: 'div',
|
|
2077
|
-
data: {
|
|
2078
|
-
style: { display: 'none' },
|
|
2079
|
-
attrs: { 'data-sygnal-portal': target },
|
|
2080
|
-
portalChildren,
|
|
2081
|
-
hook: {
|
|
2082
|
-
insert: (vnode: any) => {
|
|
2083
|
-
// Try synchronously first (target outside component tree or
|
|
2084
|
-
// plain element in the same patch cycle).
|
|
2085
|
-
const container = document.querySelector(target)
|
|
2086
|
-
if (container) {
|
|
2087
|
-
portalMount(vnode, target, portalChildren)
|
|
2088
|
-
return
|
|
2089
|
-
}
|
|
2090
|
-
// Target may be rendered by a sub-component whose streams
|
|
2091
|
-
// haven't settled yet (debounced). Retry a few times to
|
|
2092
|
-
// allow sub-component rendering to complete.
|
|
2093
|
-
let attempts = 0
|
|
2094
|
-
const tryMount = () => {
|
|
2095
|
-
if (vnode.data._portalVnode) return // already mounted
|
|
2096
|
-
if (document.querySelector(target)) {
|
|
2097
|
-
portalMount(vnode, target, portalChildren)
|
|
2098
|
-
} else if (++attempts < 10) {
|
|
2099
|
-
setTimeout(tryMount, 5)
|
|
2100
|
-
} else {
|
|
2101
|
-
warn('SYG417', 'Portal', `Target '${target}' not found; content not rendered`, 'Render the target first')
|
|
2102
|
-
}
|
|
2103
|
-
}
|
|
2104
|
-
setTimeout(tryMount, 5)
|
|
2105
|
-
},
|
|
2106
|
-
postpatch: (oldVnode: any, newVnode: any) => {
|
|
2107
|
-
const prevPortalVnode = oldVnode.data?._portalVnode
|
|
2108
|
-
const container = oldVnode.data?._portalContainer
|
|
2109
|
-
const newChildren = newVnode.data?.portalChildren || []
|
|
2110
|
-
if (!prevPortalVnode || !container) {
|
|
2111
|
-
// insert was deferred and hasn't fired yet, or target
|
|
2112
|
-
// wasn't found. Try mounting now with updated children.
|
|
2113
|
-
if (!newVnode.data._portalVnode) {
|
|
2114
|
-
portalMount(newVnode, target, newChildren)
|
|
2115
|
-
}
|
|
2116
|
-
return
|
|
2117
|
-
}
|
|
2118
|
-
newVnode.data._portalVnode = portalPatch(prevPortalVnode, {
|
|
2119
|
-
sel: 'div', data: {}, children: newChildren,
|
|
2120
|
-
text: undefined, elm: undefined, key: undefined,
|
|
2121
|
-
})
|
|
2122
|
-
newVnode.data._portalContainer = container
|
|
2123
|
-
},
|
|
2124
|
-
destroy: (vnode: any) => {
|
|
2125
|
-
const pv = vnode.data?._portalVnode
|
|
2126
|
-
if (pv && pv.elm && pv.elm.parentNode) {
|
|
2127
|
-
pv.elm.parentNode.removeChild(pv.elm)
|
|
2128
|
-
}
|
|
2129
|
-
},
|
|
2130
|
-
},
|
|
2131
|
-
},
|
|
2132
|
-
children: [],
|
|
2133
|
-
text: undefined,
|
|
2134
|
-
elm: undefined,
|
|
2135
|
-
key: undefined,
|
|
2136
|
-
}
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
function propsIsEqual(obj1: any, obj2: any): boolean {
|
|
2141
|
-
return objIsEqual(sanitizeObject(obj1), sanitizeObject(obj2))
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
function objIsEqual(obj1: any, obj2?: any, maxDepth: number = 5, depth: number = 0): boolean {
|
|
2145
|
-
// Base case: if the current depth exceeds maxDepth, return true
|
|
2146
|
-
if (depth > maxDepth) {
|
|
2147
|
-
return false;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
// If both are the same object or are both exactly null or undefined
|
|
2151
|
-
if (obj1 === obj2) {
|
|
2152
|
-
return true;
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
// If either is not an object (null, undefined, or primitive), directly compare
|
|
2156
|
-
if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
|
|
2157
|
-
return false;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
|
-
// Special handling for arrays
|
|
2161
|
-
if (Array.isArray(obj1) && Array.isArray(obj2)) {
|
|
2162
|
-
if (obj1.length !== obj2.length) {
|
|
2163
|
-
return false;
|
|
2164
|
-
}
|
|
2165
|
-
for (let i = 0; i < obj1.length; i++) {
|
|
2166
|
-
if (!objIsEqual(obj1[i], obj2[i], maxDepth, depth + 1)) {
|
|
2167
|
-
return false;
|
|
2168
|
-
}
|
|
2169
|
-
}
|
|
2170
|
-
return true;
|
|
2171
|
-
}
|
|
2172
|
-
|
|
2173
|
-
// Get keys of both objects
|
|
2174
|
-
const keys1 = Object.keys(obj1);
|
|
2175
|
-
|
|
2176
|
-
// Check if the number of properties is different
|
|
2177
|
-
if (keys1.length !== Object.keys(obj2).length) {
|
|
2178
|
-
return false;
|
|
2179
|
-
}
|
|
2180
|
-
|
|
2181
|
-
// Recursively check each property
|
|
2182
|
-
for (const key of keys1) {
|
|
2183
|
-
if (!(key in obj2)) {
|
|
2184
|
-
return false;
|
|
2185
|
-
}
|
|
2186
|
-
if (!objIsEqual(obj1[key], obj2[key], maxDepth, depth + 1)) {
|
|
2187
|
-
return false;
|
|
2188
|
-
}
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
return true;
|
|
2192
|
-
}
|
|
2193
|
-
|
|
2194
|
-
function sanitizeObject(obj: any): any {
|
|
2195
|
-
if (!isObj(obj)) return obj
|
|
2196
|
-
const {state, of, from, filter, ...sanitized} = obj
|
|
2197
|
-
return sanitized
|
|
2198
|
-
}
|
|
2199
|
-
|
|
2200
|
-
function isObj(obj: any): obj is Record<string, any> {
|
|
2201
|
-
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
|
2202
|
-
}
|
|
2203
|
-
|
|
2204
|
-
const SORT_FIX = "Use a field name, { field: 'asc'|'desc'|1|-1 }, or a function"
|
|
2205
|
-
|
|
2206
|
-
function __baseSort(a: any, b: any, ascending: boolean = true): number {
|
|
2207
|
-
const direction = ascending ? 1 : -1
|
|
2208
|
-
switch(true) {
|
|
2209
|
-
case a > b: return 1 * direction
|
|
2210
|
-
case a < b: return -1 * direction
|
|
2211
|
-
default: return 0
|
|
2212
|
-
}
|
|
2213
|
-
}
|
|
2214
|
-
|
|
2215
|
-
function __sortFunctionFromObj(item: Record<string, any>): ((a: any, b: any) => number) | undefined {
|
|
2216
|
-
const entries = Object.entries(item)
|
|
2217
|
-
if (entries.length > 1) {
|
|
2218
|
-
logError('SYG418', 'Collection', 'sort object must have one key; ignored', SORT_FIX, item)
|
|
2219
|
-
return undefined
|
|
2220
|
-
}
|
|
2221
|
-
const entry = entries[0]
|
|
2222
|
-
const [field, directionRaw] = entry
|
|
2223
|
-
if (!['string', 'number'].includes(typeof directionRaw)) {
|
|
2224
|
-
logError('SYG418', 'Collection', 'sort direction must be a string or number; ignored', SORT_FIX, item)
|
|
2225
|
-
return undefined
|
|
2226
|
-
}
|
|
2227
|
-
let ascending = true
|
|
2228
|
-
if (typeof directionRaw === 'string') {
|
|
2229
|
-
if (!['asc', 'desc'].includes(directionRaw.toLowerCase())) {
|
|
2230
|
-
logError('SYG418', 'Collection', "sort direction must be 'asc' or 'desc'; ignored", SORT_FIX, item)
|
|
2231
|
-
return undefined
|
|
2232
|
-
}
|
|
2233
|
-
ascending = directionRaw.toLowerCase() !== 'desc'
|
|
2234
|
-
}
|
|
2235
|
-
if (typeof directionRaw === 'number') {
|
|
2236
|
-
if (directionRaw !== 1 && directionRaw !== -1) {
|
|
2237
|
-
logError('SYG418', 'Collection', 'sort direction must be 1 or -1; ignored', SORT_FIX, item)
|
|
2238
|
-
return undefined
|
|
2239
|
-
}
|
|
2240
|
-
ascending = directionRaw === 1
|
|
2241
|
-
}
|
|
2242
|
-
return (a, b) => __baseSort(a[field], b[field], ascending)
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
function sortFunctionFromProp(sortProp: any): ((a: any, b: any) => number) | undefined {
|
|
2246
|
-
if (!sortProp) return undefined
|
|
2247
|
-
const propType = typeof sortProp
|
|
2248
|
-
// if function do nothing
|
|
2249
|
-
if (propType === 'function') return sortProp
|
|
2250
|
-
if (propType === 'string') {
|
|
2251
|
-
// if passed either 'asc' or 'desc' sort on the entire item
|
|
2252
|
-
if (sortProp.toLowerCase() === 'asc' || sortProp.toLowerCase() === 'desc') {
|
|
2253
|
-
const ascending = sortProp.toLowerCase() !== 'desc'
|
|
2254
|
-
return (a, b) => __baseSort(a, b, ascending)
|
|
2255
|
-
}
|
|
2256
|
-
// assume it's a field/property name, and sort it ascending
|
|
2257
|
-
const field = sortProp
|
|
2258
|
-
return (a, b) => __baseSort(a[field], b[field], true)
|
|
2259
|
-
} else if (Array.isArray(sortProp)) {
|
|
2260
|
-
const sorters = sortProp.map(item => {
|
|
2261
|
-
if (typeof item === 'function') return item
|
|
2262
|
-
if (typeof item === 'string' && !['asc', 'desc'].includes(item.toLowerCase())) return (a: any, b: any) => __baseSort(a[item], b[item], true)
|
|
2263
|
-
if (isObj(item)) {
|
|
2264
|
-
return __sortFunctionFromObj(item)
|
|
2265
|
-
}
|
|
2266
|
-
})
|
|
2267
|
-
|
|
2268
|
-
return (a, b) => sorters.filter(sorter => typeof sorter === 'function').reduce((comparisonSoFar, currentSorter) => {
|
|
2269
|
-
if (comparisonSoFar !== 0) return comparisonSoFar
|
|
2270
|
-
return currentSorter(a, b)
|
|
2271
|
-
}, 0)
|
|
2272
|
-
} else if (isObj(sortProp)) {
|
|
2273
|
-
return __sortFunctionFromObj(sortProp)
|
|
2274
|
-
} else {
|
|
2275
|
-
logError('SYG418', 'Collection', 'Invalid sort prop; ignored', SORT_FIX, sortProp)
|
|
2276
|
-
return undefined
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
function extractSlots(children: any[]): { slots: Record<string, any[]>, defaultChildren: any[] } {
|
|
2281
|
-
const slots: Record<string, any[]> = {}
|
|
2282
|
-
const defaultChildren: any[] = []
|
|
2283
|
-
|
|
2284
|
-
for (const child of children) {
|
|
2285
|
-
if (child && child.sel === 'slot') {
|
|
2286
|
-
const name = (child.data?.props?.name) || 'default'
|
|
2287
|
-
if (!slots[name]) slots[name] = []
|
|
2288
|
-
const slotChildren = Array.isArray(child.children) ? child.children : (child.children ? [child.children] : [])
|
|
2289
|
-
slots[name].push(...slotChildren)
|
|
2290
|
-
} else {
|
|
2291
|
-
defaultChildren.push(child)
|
|
2292
|
-
}
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
if (defaultChildren.length > 0) {
|
|
2296
|
-
if (!slots['default']) slots['default'] = []
|
|
2297
|
-
slots['default'].push(...defaultChildren)
|
|
2298
|
-
}
|
|
2299
|
-
|
|
2300
|
-
return { slots, defaultChildren: slots['default'] || [] }
|
|
2301
|
-
}
|