sygnal 5.3.7 → 5.4.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/README.md +53 -38
- package/dist/astro/client.cjs.js +18 -8106
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +17 -8105
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +768 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +768 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +4 -1
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +4 -1
- package/dist/astro/server.mjs.map +1 -0
- package/dist/diagnostics.cjs.js +1297 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +1267 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/index.cjs.js +2981 -1386
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +848 -111
- package/dist/index.esm.js +2997 -1411
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +15 -9
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +15 -9
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +15 -9
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +15 -9
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +15 -9
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +15 -9
- package/dist/jsx.esm.js.map +1 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/vike/+config.cjs.js +1 -0
- package/dist/vike/+config.cjs.js.map +1 -0
- package/dist/vike/+config.js +1 -0
- package/dist/vike/+config.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +1 -0
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +1 -0
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/onRenderClient.cjs.js +1 -0
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +1 -0
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +1 -0
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +1 -0
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +670 -44
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +670 -44
- package/dist/vite/plugin.mjs.map +1 -0
- package/llms.txt +250 -0
- package/package.json +15 -6
- package/src/astro/client.ts +13 -2
- package/src/astro/index.d.ts +24 -1
- package/src/astro/index.ts +40 -9
- package/src/astro/server.ts +3 -1
- package/src/collection.ts +5 -2
- package/src/component.ts +375 -330
- package/src/cycle/dom/MainDOMSource.ts +4 -0
- package/src/cycle/dom/classNameModule.ts +74 -0
- package/src/cycle/dom/controlledInputModule.ts +33 -0
- package/src/cycle/dom/enrichEventStream.ts +9 -3
- package/src/cycle/dom/makeDOMDriver.ts +1 -2
- package/src/cycle/dom/mockDOMSource.ts +62 -8
- package/src/cycle/dom/modules.ts +5 -1
- package/src/cycle/state/Collection.ts +13 -3
- package/src/cycle/state/StateSource.ts +1 -1
- package/src/cycle/state/pickCombine.ts +35 -1
- package/src/cycle/state/withState.ts +1 -1
- package/src/extra/devtools.ts +8 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/dom.ts +241 -0
- package/src/extra/diagnostics/checks/events.ts +68 -0
- package/src/extra/diagnostics/checks/index.ts +109 -0
- package/src/extra/diagnostics/checks/inspect.ts +319 -0
- package/src/extra/diagnostics/checks/props.ts +51 -0
- package/src/extra/diagnostics/checks/public.d.ts +211 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +119 -0
- package/src/extra/diagnostics/checks/state.ts +55 -0
- package/src/extra/diagnostics/checks/strict.ts +112 -0
- package/src/extra/diagnostics/checks/wiring.ts +65 -0
- package/src/extra/diagnostics/codes.ts +232 -0
- package/src/extra/diagnostics/index.ts +328 -0
- package/src/extra/diagnostics/legacy.ts +59 -0
- package/src/extra/driverFactories.ts +106 -58
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/reducers.ts +12 -5
- package/src/extra/run.ts +11 -0
- package/src/extra/testing.ts +890 -180
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +428 -17
- package/src/index.ts +7 -6
- package/src/pragma/index.ts +14 -9
- package/src/switchable.ts +10 -8
- package/src/vike/types.ts +21 -4
- package/src/vite/plugin.d.ts +95 -2
- package/src/vite/plugin.ts +720 -54
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
package/src/component.ts
CHANGED
|
@@ -6,17 +6,12 @@ import {init as snabbdomInit} from './cycle/dom/snabbdom';
|
|
|
6
6
|
import defaultModules from './cycle/dom/modules';
|
|
7
7
|
import {makeCommandSource} from './extra/command';
|
|
8
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';
|
|
9
12
|
|
|
10
|
-
import xs, {Stream
|
|
11
|
-
import
|
|
12
|
-
import * as concatModule from 'xstream/extra/concat.js';
|
|
13
|
-
import * as debounceModule from 'xstream/extra/debounce.js';
|
|
14
|
-
import * as dropRepeatsModule from 'xstream/extra/dropRepeats.js';
|
|
15
|
-
|
|
16
|
-
const delay = resolveInteropDefault(delayModule);
|
|
17
|
-
const concat = resolveInteropDefault(concatModule);
|
|
18
|
-
const debounce = resolveInteropDefault(debounceModule);
|
|
19
|
-
const dropRepeats = resolveInteropDefault(dropRepeatsModule);
|
|
13
|
+
import xs, {Stream} from './extra/xstreamCompat';
|
|
14
|
+
import {delay, concat, debounce, dropRepeats} from './extra/xstreamExtras';
|
|
20
15
|
|
|
21
16
|
declare var process: { env: Record<string, any> };
|
|
22
17
|
|
|
@@ -31,8 +26,14 @@ const PARENT_SINK_NAME = 'PARENT';
|
|
|
31
26
|
const CHILD_SOURCE_NAME = 'CHILD';
|
|
32
27
|
const READY_SINK_NAME = 'READY';
|
|
33
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');
|
|
34
33
|
|
|
35
34
|
let COMPONENT_COUNT = 0;
|
|
35
|
+
// 1H-1: STATE reducers emitted but not applied yet (in any component)
|
|
36
|
+
let pendingReducers = 0;
|
|
36
37
|
|
|
37
38
|
function wrapDOMSource(domSource: any): any {
|
|
38
39
|
return new Proxy(domSource, {
|
|
@@ -68,9 +69,7 @@ function normalizeCalculatedEntry(field: string, entry: any): {fn: (...args: any
|
|
|
68
69
|
&& Array.isArray(entry[0]) && typeof entry[1] === 'function') {
|
|
69
70
|
return { fn: entry[1], deps: entry[0] }
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
-
`Invalid calculated field '${field}': expected a function or [depsArray, function]`
|
|
73
|
-
)
|
|
72
|
+
fail('SYG206', undefined, `Invalid calculated field '${field}'`, 'Use fn or [deps, fn]')
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
export interface ComponentOptions {
|
|
@@ -100,7 +99,7 @@ export default function component(opts: ComponentOptions): any {
|
|
|
100
99
|
const { name, sources, isolateOpts, stateSourceName='STATE' } = opts
|
|
101
100
|
|
|
102
101
|
if (sources && !isObj(sources)) {
|
|
103
|
-
|
|
102
|
+
fail('SYG601', name, 'Invalid sources', 'Pass sources from run()')
|
|
104
103
|
}
|
|
105
104
|
|
|
106
105
|
let fixedIsolateOpts
|
|
@@ -197,15 +196,17 @@ class Component {
|
|
|
197
196
|
_calculatedOrder: Array<[string, {fn: (...args: any[]) => any; deps: string[] | null}]> | null;
|
|
198
197
|
_calculatedFieldCache: Record<string, {lastDepValues: any; lastResult: any}> | null;
|
|
199
198
|
_subscriptions: any[];
|
|
199
|
+
_processedChildren$: any;
|
|
200
200
|
_disposeListener: any;
|
|
201
201
|
_dispose$: any;
|
|
202
|
+
_disposed?: boolean;
|
|
202
203
|
_activeSubComponents: Map<string, any>;
|
|
203
204
|
_childReadyState: Record<string, boolean>;
|
|
204
205
|
_readyChanged$: any;
|
|
205
206
|
_readyChangedListener: any;
|
|
206
207
|
|
|
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) {
|
|
208
|
-
if (!sources || !isObj(sources))
|
|
209
|
+
if (!sources || !isObj(sources)) fail('SYG601', name, 'Missing or invalid sources', 'Pass sources from run()')
|
|
209
210
|
|
|
210
211
|
this._componentNumber = COMPONENT_COUNT++
|
|
211
212
|
|
|
@@ -235,10 +236,7 @@ class Component {
|
|
|
235
236
|
&& isObj(this.calculated) && isObj(this.initialState)) {
|
|
236
237
|
for (const key of Object.keys(this.calculated)) {
|
|
237
238
|
if (key in this.initialState) {
|
|
238
|
-
|
|
239
|
-
`[${name}] Calculated field '${key}' shadows a key in initialState. ` +
|
|
240
|
-
`The initialState value will be overwritten on every state update.`
|
|
241
|
-
)
|
|
239
|
+
warn('SYG207', name, `Calculated field '${key}' overwrites the initialState key of the same name`, 'Rename one of them')
|
|
242
240
|
}
|
|
243
241
|
}
|
|
244
242
|
}
|
|
@@ -261,10 +259,7 @@ class Component {
|
|
|
261
259
|
for (const dep of deps) {
|
|
262
260
|
if (!this._calculatedFieldNames.has(dep)
|
|
263
261
|
&& this.initialState && !(dep in this.initialState)) {
|
|
264
|
-
|
|
265
|
-
`[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
|
|
266
|
-
`which is not in initialState or calculated fields`
|
|
267
|
-
)
|
|
262
|
+
warn('SYG208', name, `Calculated field '${field}' depends on unknown key '${dep}'`, 'Add it to initialState')
|
|
268
263
|
}
|
|
269
264
|
}
|
|
270
265
|
}
|
|
@@ -328,7 +323,7 @@ class Component {
|
|
|
328
323
|
traceCycle(inCycle[0])
|
|
329
324
|
const start = path[path.length - 1]
|
|
330
325
|
const cycle = path.slice(path.indexOf(start))
|
|
331
|
-
|
|
326
|
+
fail('SYG209', name, `Circular calculated dependency: ${cycle.join(' \u2192 ')}`, 'Break the cycle')
|
|
332
327
|
}
|
|
333
328
|
|
|
334
329
|
this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized![f]])
|
|
@@ -378,17 +373,20 @@ class Component {
|
|
|
378
373
|
|
|
379
374
|
const children$ = sources.children$
|
|
380
375
|
if (children$) {
|
|
381
|
-
|
|
376
|
+
// Process children and extract slots once, share across subscribers
|
|
377
|
+
this._processedChildren$ = children$.map((val: any) => {
|
|
382
378
|
if (Array.isArray(val)) {
|
|
383
379
|
const { slots, defaultChildren } = extractSlots(val)
|
|
384
380
|
this.currentSlots = slots
|
|
385
381
|
this.currentChildren = defaultChildren
|
|
382
|
+
return { children: defaultChildren, slots }
|
|
386
383
|
} else {
|
|
387
384
|
this.currentSlots = {}
|
|
388
385
|
this.currentChildren = val
|
|
386
|
+
return { children: val, slots: {} }
|
|
389
387
|
}
|
|
390
|
-
return val
|
|
391
388
|
})
|
|
389
|
+
this.sources.children$ = this._processedChildren$.map((p: any) => p.children)
|
|
392
390
|
}
|
|
393
391
|
|
|
394
392
|
if (this.sources[DOMSourceName]) {
|
|
@@ -453,6 +451,10 @@ class Component {
|
|
|
453
451
|
}
|
|
454
452
|
|
|
455
453
|
dispose(): void {
|
|
454
|
+
if (this._disposed) return
|
|
455
|
+
this._disposed = true
|
|
456
|
+
// [diagnostics hook]
|
|
457
|
+
diag.onDispose(this)
|
|
456
458
|
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
457
459
|
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name)
|
|
458
460
|
}
|
|
@@ -471,7 +473,11 @@ class Component {
|
|
|
471
473
|
} catch (_) {}
|
|
472
474
|
this._disposeListener = null
|
|
473
475
|
}
|
|
474
|
-
//
|
|
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
|
|
475
481
|
setTimeout(() => {
|
|
476
482
|
// Complete the action$ stream to stop the entire component cycle
|
|
477
483
|
if (this.action$ && typeof this.action$.shamefullySendComplete === 'function') {
|
|
@@ -488,11 +494,6 @@ class Component {
|
|
|
488
494
|
}
|
|
489
495
|
}
|
|
490
496
|
this._subscriptions = []
|
|
491
|
-
// Dispose any active sub-components
|
|
492
|
-
this._activeSubComponents.forEach((entry) => {
|
|
493
|
-
if (entry?.sink$?.__dispose) entry.sink$.__dispose()
|
|
494
|
-
})
|
|
495
|
-
this._activeSubComponents.clear()
|
|
496
497
|
}, 0)
|
|
497
498
|
}
|
|
498
499
|
|
|
@@ -502,17 +503,22 @@ class Component {
|
|
|
502
503
|
|
|
503
504
|
initIntent$(): void {
|
|
504
505
|
if (!this.intent) {
|
|
506
|
+
// [diagnostics hook]
|
|
507
|
+
diag.onIntent(this, [], undefined)
|
|
505
508
|
return
|
|
506
509
|
}
|
|
507
510
|
if (typeof this.intent != 'function') {
|
|
508
|
-
|
|
511
|
+
fail('SYG602', this, 'intent must be a function', 'Use intent = sources => ({ ACTION: stream$ })')
|
|
509
512
|
}
|
|
510
513
|
|
|
511
514
|
this.intent$ = this.intent(this.sources)
|
|
512
515
|
|
|
513
516
|
if (!(this.intent$ instanceof Stream) && (!isObj(this.intent$))) {
|
|
514
|
-
|
|
517
|
+
fail('SYG603', this, 'intent must return a stream or an object of streams', 'Return { ACTION: stream$ }')
|
|
515
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)
|
|
516
522
|
}
|
|
517
523
|
|
|
518
524
|
initHmrActions(): void {
|
|
@@ -523,11 +529,8 @@ class Component {
|
|
|
523
529
|
if (typeof this.hmrActions === 'string') {
|
|
524
530
|
this.hmrActions = [this.hmrActions]
|
|
525
531
|
}
|
|
526
|
-
if (!Array.isArray(this.hmrActions)) {
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
if (this.hmrActions.some(action => typeof action !== 'string')) {
|
|
530
|
-
throw new Error(`[${this.name}] hmrActions must be the name of an action or an array of names of actions to run when a component is hot-reloaded`)
|
|
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']")
|
|
531
534
|
}
|
|
532
535
|
this.hmrAction$ = xs.fromArray(this.hmrActions.map(action => ({ type: action })))
|
|
533
536
|
}
|
|
@@ -547,12 +550,12 @@ class Component {
|
|
|
547
550
|
// Validate that no intent action names contain '|' (reserved for model shorthand)
|
|
548
551
|
for (const key of Object.keys(this.intent$)) {
|
|
549
552
|
if (key.includes('|')) {
|
|
550
|
-
|
|
553
|
+
fail('SYG605', this, `Intent action '${key}' contains '|', which is reserved for model shorthand`, 'Rename the action')
|
|
551
554
|
}
|
|
552
555
|
}
|
|
553
556
|
const mapped = Object.entries(this.intent$)
|
|
554
557
|
.map(([type, data$]: [string, any]) => data$.map((data: any) => ({type, data})))
|
|
555
|
-
runner = xs.merge(xs.never()
|
|
558
|
+
runner = mapped.length > 0 ? xs.merge(...mapped) : xs.never()
|
|
556
559
|
}
|
|
557
560
|
|
|
558
561
|
const action$ = ((runner instanceof Stream) ? runner : (runner.apply && runner(this.sources) || xs.never()))
|
|
@@ -593,7 +596,7 @@ class Component {
|
|
|
593
596
|
} else if (isObj(this.model[INITIALIZE_ACTION])) {
|
|
594
597
|
Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
|
|
595
598
|
if (name !== this.stateSourceName) {
|
|
596
|
-
|
|
599
|
+
warn('SYG210', this, `${INITIALIZE_ACTION} only supports the ${this.stateSourceName} sink; ignoring '${name}'`, 'Use another action')
|
|
597
600
|
delete this.model[INITIALIZE_ACTION][name]
|
|
598
601
|
}
|
|
599
602
|
})
|
|
@@ -610,7 +613,7 @@ class Component {
|
|
|
610
613
|
const state$ = this.sources[this.stateSourceName]?.stream.startWith({}).compose(dropRepeats(objIsEqual)) || xs.never()
|
|
611
614
|
const parentContext$ = this.sources.__parentContext$?.startWith({}).compose(dropRepeats(objIsEqual)) || xs.of({})
|
|
612
615
|
if (this.context && !isObj(this.context)) {
|
|
613
|
-
|
|
616
|
+
logError('SYG402', this, `context must be an object, got ${typeof this.context}; ignoring it`, 'Use context = { name: state => value }')
|
|
614
617
|
}
|
|
615
618
|
this.context$ = xs.combine(state$, parentContext$)
|
|
616
619
|
.map(([_, parent]: [any, any]) => {
|
|
@@ -628,7 +631,7 @@ class Component {
|
|
|
628
631
|
} else if (valueType === 'function') {
|
|
629
632
|
_value = value(state)
|
|
630
633
|
} else {
|
|
631
|
-
|
|
634
|
+
logError('SYG403', this, `Invalid context entry '${name}'; skipping it`, 'Use a state key or state => value')
|
|
632
635
|
return acc
|
|
633
636
|
}
|
|
634
637
|
acc[name] = _value
|
|
@@ -643,7 +646,7 @@ class Component {
|
|
|
643
646
|
})
|
|
644
647
|
.compose(dropRepeats(objIsEqual))
|
|
645
648
|
.startWith({})
|
|
646
|
-
this._subscriptions.push(this.context$.subscribe({ next: (_: any) => _, error: (err: any) =>
|
|
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) }))
|
|
647
650
|
}
|
|
648
651
|
|
|
649
652
|
initModel$(): void {
|
|
@@ -652,6 +655,8 @@ class Component {
|
|
|
652
655
|
a[s] = xs.never()
|
|
653
656
|
return a
|
|
654
657
|
}, {} as Record<string, any>)
|
|
658
|
+
// [diagnostics hook]
|
|
659
|
+
diag.onModel(this, {})
|
|
655
660
|
return
|
|
656
661
|
}
|
|
657
662
|
|
|
@@ -659,19 +664,39 @@ class Component {
|
|
|
659
664
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
|
|
660
665
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
|
|
661
666
|
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
662
|
-
|
|
667
|
+
warn('SYG405', this, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
|
|
663
668
|
}
|
|
664
669
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined')
|
|
665
670
|
const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined')
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
const
|
|
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$)
|
|
669
693
|
|
|
670
694
|
|
|
671
695
|
const modelEntries = Object.entries(this.model)
|
|
672
696
|
|
|
673
697
|
const reducers: Record<string, any[]> = {}
|
|
674
698
|
const seenActionSinks = new Set<string>()
|
|
699
|
+
const modelMap: Record<string, string[]> = {} // [diagnostics hook]
|
|
675
700
|
|
|
676
701
|
modelEntries.forEach((entry) => {
|
|
677
702
|
let [action, sinks]: [string, any] = entry
|
|
@@ -680,7 +705,7 @@ class Component {
|
|
|
680
705
|
if (action.includes('|')) {
|
|
681
706
|
const parts = action.split('|').map((s: string) => s.trim())
|
|
682
707
|
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
683
|
-
|
|
708
|
+
fail('SYG211', this, `Invalid shorthand model entry '${action}'`, "Use 'ACTION | SINK'")
|
|
684
709
|
}
|
|
685
710
|
action = parts[0]
|
|
686
711
|
sinks = { [parts[1]]: sinks }
|
|
@@ -691,7 +716,7 @@ class Component {
|
|
|
691
716
|
}
|
|
692
717
|
|
|
693
718
|
if (!isObj(sinks)) {
|
|
694
|
-
|
|
719
|
+
fail('SYG212', this, `Model entry '${action}' must be a function or an object`, 'Use { STATE: reducer }')
|
|
695
720
|
}
|
|
696
721
|
|
|
697
722
|
const sinkEntries = Object.entries(sinks)
|
|
@@ -701,13 +726,14 @@ class Component {
|
|
|
701
726
|
|
|
702
727
|
const actionSinkKey = `${action}::${sink}`
|
|
703
728
|
if (seenActionSinks.has(actionSinkKey)) {
|
|
704
|
-
|
|
729
|
+
warn('SYG213', this, `Duplicate model entry for action '${action}' on sink '${sink}'; both run`, 'Remove the duplicate')
|
|
705
730
|
}
|
|
706
731
|
seenActionSinks.add(actionSinkKey)
|
|
732
|
+
;(modelMap[action] ||= []).push(sink) // [diagnostics hook]
|
|
707
733
|
|
|
708
734
|
// EFFECT sink: run the reducer for side effects only, no state change or sink output
|
|
709
735
|
if (sink === EFFECT_SINK_NAME) {
|
|
710
|
-
const effect$ = this.makeEffectHandler(
|
|
736
|
+
const effect$ = this.makeEffectHandler(snapshotted$, action, reducer, this.action$)
|
|
711
737
|
if (Array.isArray(reducers[sink])) {
|
|
712
738
|
reducers[sink].push(effect$)
|
|
713
739
|
} else {
|
|
@@ -744,11 +770,14 @@ class Component {
|
|
|
744
770
|
|
|
745
771
|
const model$ = Object.entries(reducers).reduce((acc: Record<string, any>, entry: [string, any]) => {
|
|
746
772
|
const [sink, streams] = entry
|
|
747
|
-
acc[sink] = xs.merge(
|
|
773
|
+
acc[sink] = streams.length === 1 ? streams[0] : xs.merge(...streams)
|
|
748
774
|
return acc
|
|
749
775
|
}, {} as Record<string, any>)
|
|
750
776
|
|
|
751
777
|
this.model$ = model$
|
|
778
|
+
|
|
779
|
+
// [diagnostics hook]
|
|
780
|
+
diag.onModel(this, modelMap)
|
|
752
781
|
}
|
|
753
782
|
|
|
754
783
|
initPeers$(): void {
|
|
@@ -812,7 +841,7 @@ class Component {
|
|
|
812
841
|
|
|
813
842
|
}
|
|
814
843
|
})
|
|
815
|
-
this._subscriptions.push(subComponentSink$.subscribe({ next: (_: any) => _, error: (err: any) =>
|
|
844
|
+
this._subscriptions.push(subComponentSink$.subscribe({ next: (_: any) => _, error: (err: any) => logError('SYG901', this, 'Sub-component sink stream errored', ERR_FIX, err) }))
|
|
816
845
|
this.subComponentSink$ = subComponentSink$.filter((sinks: any) => Object.keys(sinks).length > 0)
|
|
817
846
|
}
|
|
818
847
|
|
|
@@ -844,23 +873,23 @@ class Component {
|
|
|
844
873
|
return this.view({ ...sanitizedProps, state, children, slots: slots || {}, context, peers }, state, context, peers)
|
|
845
874
|
} catch (err) {
|
|
846
875
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
847
|
-
|
|
876
|
+
// B-022: an error handled by .onError is a warning, without the "add .onError" hint
|
|
848
877
|
if (typeof this.onError === 'function') {
|
|
849
878
|
try {
|
|
850
|
-
|
|
879
|
+
const fallback = this.onError(error, { componentName: this.name })
|
|
880
|
+
warn('SYG406', this, 'View threw; rendered the onError fallback', undefined, error)
|
|
881
|
+
return fallback
|
|
851
882
|
} catch (fallbackErr) {
|
|
852
|
-
|
|
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)
|
|
853
885
|
}
|
|
854
|
-
}
|
|
886
|
+
} else logError('SYG406', this, 'View threw; rendering the error fallback', 'Add .onError for a custom fallback', error)
|
|
855
887
|
return { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
|
|
856
888
|
}
|
|
857
889
|
})
|
|
858
890
|
.compose(this.log('View rendered'))
|
|
859
891
|
.map((vDom: any) => vDom || { sel: 'div', data: {}, children: [] })
|
|
860
|
-
.map((vdom: any) =>
|
|
861
|
-
.map(processPortals)
|
|
862
|
-
.map(processTransitions)
|
|
863
|
-
.map(processClientOnly)
|
|
892
|
+
.map((vdom: any) => preprocessVdom(vdom, this))
|
|
864
893
|
.compose(this.instantiateSubComponents.bind(this))
|
|
865
894
|
.filter((val: any) => val !== undefined)
|
|
866
895
|
.compose(this.renderVdom.bind(this))
|
|
@@ -868,6 +897,15 @@ class Component {
|
|
|
868
897
|
}
|
|
869
898
|
|
|
870
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
|
+
}))
|
|
871
909
|
this.sinks = this.sourceNames.reduce((acc: Record<string, any>, name) => {
|
|
872
910
|
if (name == this.DOMSourceName) return acc
|
|
873
911
|
const subComponentSink$ = (this.subComponentSink$ && name !== PARENT_SINK_NAME) ? this.subComponentSink$.map((sinks: any) => sinks[name]).filter((sink: any) => !!sink).flatten() : xs.never()
|
|
@@ -876,12 +914,6 @@ class Component {
|
|
|
876
914
|
} else {
|
|
877
915
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []))
|
|
878
916
|
}
|
|
879
|
-
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
880
|
-
if (name === 'EVENTS' && acc[name]) {
|
|
881
|
-
const _componentNumber = this._componentNumber
|
|
882
|
-
const _name = this.name
|
|
883
|
-
acc[name] = acc[name].map((ev: any) => ({...ev, __emitterId: _componentNumber, __emitterName: _name}))
|
|
884
|
-
}
|
|
885
917
|
return acc
|
|
886
918
|
}, {} as Record<string, any>)
|
|
887
919
|
|
|
@@ -892,7 +924,7 @@ class Component {
|
|
|
892
924
|
if (this.model$[EFFECT_SINK_NAME]) {
|
|
893
925
|
const effectSub = this.model$[EFFECT_SINK_NAME].subscribe({
|
|
894
926
|
next: () => {},
|
|
895
|
-
error: (err: any) =>
|
|
927
|
+
error: (err: any) => logError('SYG902', this, 'EFFECT stream errored; effects stop running', ERR_FIX, err),
|
|
896
928
|
})
|
|
897
929
|
this._subscriptions.push(effectSub)
|
|
898
930
|
delete this.sinks[EFFECT_SINK_NAME]
|
|
@@ -922,7 +954,7 @@ class Component {
|
|
|
922
954
|
if (typeof reducer === 'function') {
|
|
923
955
|
returnStream$ = filtered$.map((action: any) => {
|
|
924
956
|
const next = (type: any, data: any, delay=10) => {
|
|
925
|
-
if (typeof delay !== 'number')
|
|
957
|
+
if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
|
|
926
958
|
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
927
959
|
setTimeout(() => {
|
|
928
960
|
// push the "next" action request into the action$ stream
|
|
@@ -935,33 +967,50 @@ class Component {
|
|
|
935
967
|
|
|
936
968
|
let data = action.data
|
|
937
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
|
|
938
974
|
return (state: any) => {
|
|
939
|
-
|
|
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
|
|
940
982
|
try {
|
|
941
983
|
const enhancedState = this.addCalculated(_state)
|
|
942
984
|
props.state = enhancedState
|
|
943
985
|
const newState = reducer(enhancedState, data, next, props)
|
|
944
986
|
if (isAbort(newState)) return _state
|
|
945
|
-
|
|
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
|
|
946
993
|
} catch (err) {
|
|
947
|
-
|
|
994
|
+
logError('SYG216', this, `Reducer for '${name}' threw; state unchanged`, ERR_FIX, err)
|
|
948
995
|
return _state
|
|
949
996
|
}
|
|
950
997
|
}
|
|
951
998
|
} else {
|
|
952
999
|
try {
|
|
953
|
-
const enhancedState = this.addCalculated(this.currentState)
|
|
1000
|
+
const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
|
|
954
1001
|
props.state = enhancedState
|
|
955
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
|
|
956
1005
|
const type = typeof reduced
|
|
957
1006
|
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
958
1007
|
if (type === 'undefined') {
|
|
959
|
-
|
|
1008
|
+
warn('SYG217', this, `Reducer for '${name}' sent undefined to the driver`, 'Return a value, or ABORT to send nothing')
|
|
960
1009
|
return reduced
|
|
961
1010
|
}
|
|
962
|
-
|
|
1011
|
+
fail('SYG218', this, `Reducer for '${name}' returned a ${type}`, 'Return a value, or ABORT to send nothing')
|
|
963
1012
|
} catch (err) {
|
|
964
|
-
|
|
1013
|
+
logError('SYG216', this, `Reducer for '${name}' threw; nothing sent`, ERR_FIX, err)
|
|
965
1014
|
return ABORT
|
|
966
1015
|
}
|
|
967
1016
|
}
|
|
@@ -977,28 +1026,28 @@ class Component {
|
|
|
977
1026
|
}
|
|
978
1027
|
}
|
|
979
1028
|
|
|
980
|
-
makeEffectHandler(action$: any, name: string, reducer: any): any {
|
|
1029
|
+
makeEffectHandler(action$: any, name: string, reducer: any, rootAction$: any = action$): any {
|
|
981
1030
|
const filtered$ = action$.filter(({type}: any) => type == name)
|
|
982
1031
|
|
|
983
1032
|
return filtered$.map((action: any) => {
|
|
984
1033
|
if (typeof reducer === 'function') {
|
|
985
1034
|
const next = (type: any, data: any, delay=10) => {
|
|
986
|
-
if (typeof delay !== 'number')
|
|
1035
|
+
if (typeof delay !== 'number') fail('SYG215', this, `next() delay in '${name}' must be a number`, "Use next('ACTION', data, ms)")
|
|
987
1036
|
setTimeout(() => {
|
|
988
|
-
|
|
1037
|
+
rootAction$.shamefullySendNext({ type, data })
|
|
989
1038
|
}, delay)
|
|
990
1039
|
this.log(`<${name}> EFFECT triggered a next() action: <${type}> ${delay}ms delay`, true)
|
|
991
1040
|
}
|
|
992
1041
|
|
|
993
1042
|
try {
|
|
994
|
-
const enhancedState = this.addCalculated(this.currentState)
|
|
1043
|
+
const enhancedState = this.addCalculated(STATE_SNAPSHOT in action ? action[STATE_SNAPSHOT] : this.currentState)
|
|
995
1044
|
const props = { ...this.currentProps, children: this.currentChildren, slots: this.currentSlots || {}, context: this.currentContext, state: enhancedState }
|
|
996
1045
|
const result = reducer(enhancedState, action.data, next, props)
|
|
997
|
-
if (result !== undefined) {
|
|
998
|
-
|
|
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')
|
|
999
1048
|
}
|
|
1000
1049
|
} catch (err) {
|
|
1001
|
-
|
|
1050
|
+
logError('SYG214', this, `EFFECT handler '${name}' threw`, ERR_FIX, err)
|
|
1002
1051
|
}
|
|
1003
1052
|
}
|
|
1004
1053
|
return null
|
|
@@ -1014,7 +1063,7 @@ class Component {
|
|
|
1014
1063
|
if (state === lastState) {
|
|
1015
1064
|
return lastResult
|
|
1016
1065
|
}
|
|
1017
|
-
if (!isObj(this.calculated))
|
|
1066
|
+
if (!isObj(this.calculated)) fail('SYG606', this, 'calculated must be an object', 'Use calculated = { field: state => value }')
|
|
1018
1067
|
|
|
1019
1068
|
const calculated = this.getCalculatedValues(state)
|
|
1020
1069
|
if (!calculated) {
|
|
@@ -1067,7 +1116,7 @@ class Component {
|
|
|
1067
1116
|
computedSoFar[field] = result
|
|
1068
1117
|
mergedState[field] = result
|
|
1069
1118
|
} catch (e: unknown) {
|
|
1070
|
-
|
|
1119
|
+
warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
|
|
1071
1120
|
}
|
|
1072
1121
|
} else {
|
|
1073
1122
|
// No deps declared — always recompute
|
|
@@ -1076,7 +1125,7 @@ class Component {
|
|
|
1076
1125
|
computedSoFar[field] = result
|
|
1077
1126
|
mergedState[field] = result
|
|
1078
1127
|
} catch (e: unknown) {
|
|
1079
|
-
|
|
1128
|
+
warn('SYG220', this, `Calculated field '${field}' threw (${e instanceof Error ? e.message : e}); skipped this update`, 'Guard against missing data')
|
|
1080
1129
|
}
|
|
1081
1130
|
}
|
|
1082
1131
|
}
|
|
@@ -1114,14 +1163,9 @@ class Component {
|
|
|
1114
1163
|
renderParams.props = this.sources.props$.compose(dropRepeats(propsIsEqual))
|
|
1115
1164
|
}
|
|
1116
1165
|
|
|
1117
|
-
if (this.
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
const { slots, defaultChildren } = extractSlots(val)
|
|
1121
|
-
return { children: defaultChildren, slots }
|
|
1122
|
-
})
|
|
1123
|
-
renderParams.children = processedChildren$.map((p: any) => p.children).compose(dropRepeats(objIsEqual))
|
|
1124
|
-
renderParams.slots = processedChildren$.map((p: any) => p.slots).compose(dropRepeats(objIsEqual))
|
|
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))
|
|
1125
1169
|
}
|
|
1126
1170
|
|
|
1127
1171
|
if (this.context$) {
|
|
@@ -1155,9 +1199,11 @@ class Component {
|
|
|
1155
1199
|
}
|
|
1156
1200
|
|
|
1157
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
|
+
|
|
1158
1205
|
return vDom$.fold((previousComponents: any, vDom: any) => {
|
|
1159
|
-
const
|
|
1160
|
-
const foundComponents = getComponents(vDom, componentNames)
|
|
1206
|
+
const foundComponents = getComponents(vDom, componentNameSet)
|
|
1161
1207
|
const entries = Object.entries(foundComponents)
|
|
1162
1208
|
|
|
1163
1209
|
const rootEntry: Record<string, any> = { '::ROOT::': vDom }
|
|
@@ -1178,7 +1224,8 @@ class Component {
|
|
|
1178
1224
|
const newComponents = entries.reduce((acc, [id, el]) => {
|
|
1179
1225
|
const data = el.data
|
|
1180
1226
|
const props = data.props || {}
|
|
1181
|
-
|
|
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 }] : [])
|
|
1182
1229
|
|
|
1183
1230
|
const isCollection = data.isCollection || false
|
|
1184
1231
|
const isSwitchable = data.isSwitchable || false
|
|
@@ -1224,13 +1271,13 @@ class Component {
|
|
|
1224
1271
|
sink$ = instantiator(el, props$, children$)
|
|
1225
1272
|
} catch (err) {
|
|
1226
1273
|
const error = err instanceof Error ? err : new Error(String(err))
|
|
1227
|
-
|
|
1274
|
+
logError('SYG408', this, 'Sub-component threw; rendering the error fallback', ERR_FIX, error)
|
|
1228
1275
|
let fallbackVNode = { sel: 'div', data: { attrs: { 'data-sygnal-error': this.name } }, children: [] }
|
|
1229
1276
|
if (typeof this.onError === 'function') {
|
|
1230
1277
|
try {
|
|
1231
1278
|
fallbackVNode = this.onError(error, { componentName: this.name }) || fallbackVNode
|
|
1232
1279
|
} catch (fallbackErr) {
|
|
1233
|
-
|
|
1280
|
+
logError('SYG407', this, 'onError threw; rendering an empty error <div>', 'Make onError return a vnode', fallbackErr)
|
|
1234
1281
|
}
|
|
1235
1282
|
}
|
|
1236
1283
|
sink$ = { [this.DOMSourceName]: xs.of(fallbackVNode) }
|
|
@@ -1273,16 +1320,52 @@ class Component {
|
|
|
1273
1320
|
}
|
|
1274
1321
|
|
|
1275
1322
|
makeCoordinatedSubComponentDomSink(domSink$: any): any {
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
const coordinated = this.sources[this.stateSourceName].stream
|
|
1279
|
-
.compose(dropRepeats(objIsEqual))
|
|
1280
|
-
.map((state: any) => remembered$)
|
|
1281
|
-
.flatten()
|
|
1323
|
+
return domSink$
|
|
1282
1324
|
.debug((_: any) => this.triggerSubComponentsRendered())
|
|
1283
1325
|
.remember()
|
|
1326
|
+
}
|
|
1284
1327
|
|
|
1285
|
-
|
|
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
|
|
1286
1369
|
}
|
|
1287
1370
|
|
|
1288
1371
|
instantiateCollection(el: any, props$: any, children$: any): any {
|
|
@@ -1307,8 +1390,8 @@ class Component {
|
|
|
1307
1390
|
if (props.sort !== arrayOperators.sort) {
|
|
1308
1391
|
arrayOperators.sort = sortFunctionFromProp(props.sort)
|
|
1309
1392
|
}
|
|
1310
|
-
|
|
1311
|
-
return
|
|
1393
|
+
// calculated fields are added by the lens (B-013)
|
|
1394
|
+
return state
|
|
1312
1395
|
})
|
|
1313
1396
|
|
|
1314
1397
|
const stateSource = new StateSource(state$, this.stateSourceName)
|
|
@@ -1332,7 +1415,7 @@ class Component {
|
|
|
1332
1415
|
} else if (this.components[collectionOf]) {
|
|
1333
1416
|
factory = this.components[collectionOf]
|
|
1334
1417
|
} else {
|
|
1335
|
-
|
|
1418
|
+
fail('SYG411', this, `Collection 'of' is not a component: ${collectionOf}`, 'Use of={ItemComponent}')
|
|
1336
1419
|
}
|
|
1337
1420
|
|
|
1338
1421
|
const fieldLense = {
|
|
@@ -1349,7 +1432,7 @@ class Component {
|
|
|
1349
1432
|
},
|
|
1350
1433
|
set: (oldState: any, newState: any) => {
|
|
1351
1434
|
if (this.calculated && stateField in this.calculated) {
|
|
1352
|
-
|
|
1435
|
+
warn('SYG409', this, `Collection tried to update calculated field '${stateField}'; ignored`, 'Bind it to a non-calculated field')
|
|
1353
1436
|
return oldState
|
|
1354
1437
|
}
|
|
1355
1438
|
const updated = []
|
|
@@ -1378,17 +1461,18 @@ class Component {
|
|
|
1378
1461
|
} else if (typeof stateField === 'string') {
|
|
1379
1462
|
if (isObj(this.currentState)) {
|
|
1380
1463
|
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
1381
|
-
|
|
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')
|
|
1382
1466
|
lense = undefined
|
|
1383
1467
|
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
1384
|
-
|
|
1468
|
+
warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
|
|
1385
1469
|
lense = fieldLense
|
|
1386
1470
|
} else {
|
|
1387
1471
|
lense = fieldLense
|
|
1388
1472
|
}
|
|
1389
1473
|
} else {
|
|
1390
1474
|
if (!Array.isArray(this.currentState[stateField])) {
|
|
1391
|
-
|
|
1475
|
+
warn('SYG401', this, `Collection 'from' field '${stateField}' is not an array; it renders nothing`, 'Set it to an array in initialState')
|
|
1392
1476
|
lense = fieldLense
|
|
1393
1477
|
} else {
|
|
1394
1478
|
lense = fieldLense
|
|
@@ -1396,14 +1480,14 @@ class Component {
|
|
|
1396
1480
|
}
|
|
1397
1481
|
} else if (isObj(stateField)) {
|
|
1398
1482
|
if (typeof stateField.get !== 'function') {
|
|
1399
|
-
|
|
1483
|
+
logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
|
|
1400
1484
|
lense = undefined
|
|
1401
1485
|
} else {
|
|
1402
1486
|
lense = {
|
|
1403
1487
|
get: (state: any) => {
|
|
1404
1488
|
const newState = stateField.get(state)
|
|
1405
1489
|
if (!Array.isArray(newState)) {
|
|
1406
|
-
|
|
1490
|
+
warn('SYG401', this, "Collection 'from' getter returned a non-array; it renders nothing", 'Return an array from get()', newState)
|
|
1407
1491
|
return []
|
|
1408
1492
|
}
|
|
1409
1493
|
return newState
|
|
@@ -1412,10 +1496,15 @@ class Component {
|
|
|
1412
1496
|
}
|
|
1413
1497
|
}
|
|
1414
1498
|
} else {
|
|
1415
|
-
|
|
1499
|
+
logError('SYG412', this, "Collection 'from' prop is invalid; it renders nothing", 'Use a state key string or { get, set }', stateField)
|
|
1416
1500
|
lense = undefined
|
|
1417
1501
|
}
|
|
1418
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
|
+
|
|
1419
1508
|
// Strip collection-specific props and forward only user-defined extra props to each item
|
|
1420
1509
|
const collectionKeys = ['of', 'from', 'filter', 'sort', 'idfield', 'className']
|
|
1421
1510
|
const itemProps$ = props$.map((p: any) => {
|
|
@@ -1430,7 +1519,7 @@ class Component {
|
|
|
1430
1519
|
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$: itemProps$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber }
|
|
1431
1520
|
const sink$ = collection(factory, lense as any, { container: null as any })(sources)
|
|
1432
1521
|
if (!isObj(sink$)) {
|
|
1433
|
-
|
|
1522
|
+
fail('SYG903', this, 'Collection factory returned invalid sinks', 'Return a sinks object')
|
|
1434
1523
|
}
|
|
1435
1524
|
|
|
1436
1525
|
// Notify devtools of collection mount
|
|
@@ -1458,40 +1547,7 @@ class Component {
|
|
|
1458
1547
|
|
|
1459
1548
|
const stateSource = new StateSource(state$, this.stateSourceName)
|
|
1460
1549
|
const stateField = props.state
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
const fieldLense = {
|
|
1464
|
-
get: (state: any) => state[stateField],
|
|
1465
|
-
set: (oldState: any, newState: any) => {
|
|
1466
|
-
if (this.calculated && stateField in this.calculated) {
|
|
1467
|
-
console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`)
|
|
1468
|
-
return oldState
|
|
1469
|
-
}
|
|
1470
|
-
if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
|
|
1471
|
-
return { ...oldState, [stateField]: newState }
|
|
1472
|
-
}
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
const baseLense = {
|
|
1476
|
-
get: (state: any) => state,
|
|
1477
|
-
set: (oldState: any, newState: any) => newState
|
|
1478
|
-
}
|
|
1479
|
-
|
|
1480
|
-
if (typeof stateField === 'undefined') {
|
|
1481
|
-
lense = baseLense
|
|
1482
|
-
} else if (typeof stateField === 'string') {
|
|
1483
|
-
lense = fieldLense
|
|
1484
|
-
} else if (isObj(stateField)) {
|
|
1485
|
-
if (typeof stateField.get !== 'function') {
|
|
1486
|
-
console.error(`Switchable component in ${this.name} has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`)
|
|
1487
|
-
lense = baseLense
|
|
1488
|
-
} else {
|
|
1489
|
-
lense = { get: stateField.get, set: stateField.set }
|
|
1490
|
-
}
|
|
1491
|
-
} else {
|
|
1492
|
-
console.error(`Invalid state provided to switchable sub-component of ${this.name}: Expecting string, object, or undefined, but found ${typeof stateField}. Attempting to use parent component state.`)
|
|
1493
|
-
lense = baseLense
|
|
1494
|
-
}
|
|
1550
|
+
const lense = this.withCalculated(this.createSubComponentLense(stateField, 'Switchable sub-component'))
|
|
1495
1551
|
|
|
1496
1552
|
const switchableComponents = props.of
|
|
1497
1553
|
const keys = Object.keys(switchableComponents)
|
|
@@ -1510,7 +1566,7 @@ class Component {
|
|
|
1510
1566
|
const sink$ = isolate(switchable(switchableComponents, props$.map((props: any) => props.current), ''), { [this.stateSourceName]: lense })(sources)
|
|
1511
1567
|
|
|
1512
1568
|
if (!isObj(sink$)) {
|
|
1513
|
-
|
|
1569
|
+
fail('SYG903', this, 'Switchable factory returned invalid sinks', 'Return a sinks object')
|
|
1514
1570
|
}
|
|
1515
1571
|
|
|
1516
1572
|
return sink$
|
|
@@ -1526,7 +1582,7 @@ class Component {
|
|
|
1526
1582
|
return isObj(state) ? this.addCalculated(state) : state
|
|
1527
1583
|
})
|
|
1528
1584
|
|
|
1529
|
-
|
|
1585
|
+
let stateSource = new StateSource(state$, this.stateSourceName)
|
|
1530
1586
|
const stateField = props.state
|
|
1531
1587
|
|
|
1532
1588
|
if (typeof props.sygnalFactory !== 'function' && isObj(props.sygnalOptions)) {
|
|
@@ -1535,8 +1591,8 @@ class Component {
|
|
|
1535
1591
|
|
|
1536
1592
|
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory)
|
|
1537
1593
|
if (!factory) {
|
|
1538
|
-
if (componentName === 'sygnal-factory')
|
|
1539
|
-
|
|
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')
|
|
1540
1596
|
}
|
|
1541
1597
|
|
|
1542
1598
|
// Guard against sub-components accidentally overwriting parent state with .initialState
|
|
@@ -1544,52 +1600,24 @@ class Component {
|
|
|
1544
1600
|
const subIsolatedState = props.sygnalOptions?.isolatedState
|
|
1545
1601
|
if (subInitialState && !subIsolatedState) {
|
|
1546
1602
|
const subName = props.sygnalOptions?.name || componentName
|
|
1547
|
-
|
|
1548
|
-
`[${subName}] Sub-component has .initialState but no .isolatedState = true. ` +
|
|
1549
|
-
`This will overwrite parent state. If this is intentional, add .isolatedState = true to the component.`
|
|
1550
|
-
)
|
|
1603
|
+
fail('SYG405', subName, 'Sub-component initialState replaces the state its parent passes in', 'Remove initialState, or set isolatedState = true')
|
|
1551
1604
|
}
|
|
1552
1605
|
|
|
1553
|
-
let lense
|
|
1554
|
-
|
|
1555
1606
|
const subInitState = subIsolatedState ? subInitialState : undefined
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
}
|
|
1567
|
-
return { ...oldState, [stateField]: newState }
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
|
|
1571
|
-
const baseLense = {
|
|
1572
|
-
get: (state: any) => state,
|
|
1573
|
-
set: (oldState: any, newState: any) => newState
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
if (typeof stateField === 'undefined') {
|
|
1577
|
-
lense = baseLense
|
|
1578
|
-
} else if (typeof stateField === 'string') {
|
|
1579
|
-
lense = fieldLense
|
|
1580
|
-
} else if (isObj(stateField)) {
|
|
1581
|
-
if (typeof stateField.get !== 'function') {
|
|
1582
|
-
console.error(`Sub-component in ${this.name} has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`)
|
|
1583
|
-
lense = baseLense
|
|
1584
|
-
} else {
|
|
1585
|
-
lense = { get: stateField.get, set: stateField.set }
|
|
1586
|
-
}
|
|
1587
|
-
} else {
|
|
1588
|
-
console.error(`Invalid state provided to sub-component of ${this.name}: Expecting string, object, or undefined, but found ${typeof stateField}. Attempting to use parent component state.`)
|
|
1589
|
-
lense = baseLense
|
|
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)
|
|
1590
1617
|
}
|
|
1591
1618
|
|
|
1592
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)
|
|
1593
1621
|
|
|
1594
1622
|
// Detect Command objects in props and expose as commands$ source
|
|
1595
1623
|
for (const key of Object.keys(props)) {
|
|
@@ -1605,18 +1633,24 @@ class Component {
|
|
|
1605
1633
|
|
|
1606
1634
|
if (!isObj(sink$)) {
|
|
1607
1635
|
const name = componentName === 'sygnal-factory' ? 'custom element' : componentName
|
|
1608
|
-
|
|
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)
|
|
1609
1642
|
}
|
|
1610
1643
|
|
|
1611
1644
|
return sink$
|
|
1612
1645
|
}
|
|
1613
1646
|
|
|
1614
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
|
+
|
|
1615
1651
|
return xs.combine(this.subComponentsRendered$, componentInstances$, this._readyChanged$.startWith(null))
|
|
1616
1652
|
.compose(debounce(1))
|
|
1617
1653
|
.map(([_, components]: [any, any]) => {
|
|
1618
|
-
const componentNames = Object.keys(this.components)
|
|
1619
|
-
|
|
1620
1654
|
const root = components['::ROOT::']
|
|
1621
1655
|
const entries = Object.entries(components).filter(([id]) => id !== '::ROOT::')
|
|
1622
1656
|
|
|
@@ -1665,19 +1699,20 @@ class Component {
|
|
|
1665
1699
|
}
|
|
1666
1700
|
|
|
1667
1701
|
return xs.combine(...vdom$)
|
|
1668
|
-
.
|
|
1702
|
+
.filter((vdoms: any) => vdoms.every((v: any) => v !== undefined))
|
|
1669
1703
|
.map((vdoms: any) => {
|
|
1670
1704
|
const withIds = vdoms.reduce((acc: Record<string, any>, vdom: any, index: any) => {
|
|
1671
1705
|
acc[ids[index]] = vdom
|
|
1672
1706
|
return acc
|
|
1673
1707
|
}, {} as Record<string, any>)
|
|
1674
|
-
const
|
|
1675
|
-
const injected = injectComponents(rootCopy, withIds, componentNames, 'r', undefined, this._childReadyState)
|
|
1708
|
+
const injected = injectComponents(root, withIds, componentNameSet, 'r', undefined, this._childReadyState)
|
|
1676
1709
|
return processSuspensePost(injected)
|
|
1677
1710
|
})
|
|
1678
1711
|
})
|
|
1679
1712
|
.flatten()
|
|
1680
1713
|
.filter((val: any) => !!val)
|
|
1714
|
+
// [diagnostics hook]
|
|
1715
|
+
.map((vdom: any) => { diag.onRender(this, vdom); return vdom })
|
|
1681
1716
|
.remember()
|
|
1682
1717
|
}
|
|
1683
1718
|
|
|
@@ -1731,93 +1766,92 @@ class Component {
|
|
|
1731
1766
|
|
|
1732
1767
|
|
|
1733
1768
|
|
|
1734
|
-
function getComponents(currentElement: any,
|
|
1735
|
-
if (!currentElement) return {}
|
|
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 || {}
|
|
1736
1771
|
|
|
1737
|
-
if (currentElement.data?.componentsProcessed) return {}
|
|
1772
|
+
if (currentElement.data?.componentsProcessed) return found || {}
|
|
1738
1773
|
if (path === 'r') currentElement.data.componentsProcessed = true
|
|
1739
1774
|
|
|
1775
|
+
// Allocate the result object only at the root call
|
|
1776
|
+
if (!found) found = {}
|
|
1777
|
+
|
|
1740
1778
|
const sel = currentElement.sel
|
|
1741
1779
|
const isCollection = sel && sel.toLowerCase() === 'collection'
|
|
1742
1780
|
const isSwitchable = sel && sel.toLowerCase() === 'switchable'
|
|
1743
|
-
const isComponent = sel &&
|
|
1781
|
+
const isComponent = (sel && componentNameSet.has(sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
|
|
1744
1782
|
const props = (currentElement.data && currentElement.data.props) || {}
|
|
1745
|
-
const attrs = (currentElement.data && currentElement.data.attrs) || {}
|
|
1746
1783
|
const children = currentElement.children || []
|
|
1747
1784
|
|
|
1748
|
-
let found: Record<string, any> = {}
|
|
1749
|
-
|
|
1750
1785
|
let id = parentId
|
|
1751
1786
|
if (isComponent) {
|
|
1752
1787
|
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1753
1788
|
if (isCollection) {
|
|
1754
|
-
if (!props.of)
|
|
1755
|
-
if (typeof props.of !== 'string' && typeof props.of !== 'function')
|
|
1756
|
-
if (typeof props.of !== 'function' && !
|
|
1757
|
-
|
|
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)
|
|
1758
1793
|
currentElement.data.isCollection = true
|
|
1759
1794
|
currentElement.data.props ||= {}
|
|
1760
1795
|
} else if (isSwitchable) {
|
|
1761
|
-
if (!props.of)
|
|
1762
|
-
if (!isObj(props.of))
|
|
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 }}')
|
|
1763
1798
|
const switchableComponents = Object.values(props.of)
|
|
1764
|
-
if (!switchableComponents.every(comp => typeof comp === 'function'))
|
|
1765
|
-
if (!props.current || (typeof props.current !== 'string' && typeof props.current !== 'function'))
|
|
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'")
|
|
1766
1801
|
const switchableComponentNames = Object.keys(props.of)
|
|
1767
|
-
if (!switchableComponentNames.includes(props.current))
|
|
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'")
|
|
1768
1803
|
currentElement.data.isSwitchable = true
|
|
1769
|
-
} else {
|
|
1770
|
-
|
|
1771
1804
|
}
|
|
1772
|
-
if (typeof props.key === 'undefined') currentElement.data.props.key = id
|
|
1805
|
+
if (typeof props.key === 'undefined') (currentElement.data.props ||= {}).key = id
|
|
1773
1806
|
found[id] = currentElement
|
|
1774
1807
|
}
|
|
1775
1808
|
|
|
1776
|
-
|
|
1777
|
-
children
|
|
1778
|
-
.forEach((child: any) => {
|
|
1779
|
-
Object.entries(child).forEach(([id, el]: [string, any]) => found[id] = el)
|
|
1780
|
-
})
|
|
1809
|
+
for (let i = 0; i < children.length; i++) {
|
|
1810
|
+
getComponents(children[i], componentNameSet, `${path}.${i}`, id, found)
|
|
1781
1811
|
}
|
|
1782
1812
|
|
|
1783
1813
|
return found
|
|
1784
1814
|
}
|
|
1785
1815
|
|
|
1786
|
-
function injectComponents(currentElement: any, components: Record<string, any>,
|
|
1787
|
-
if (!currentElement) return
|
|
1788
|
-
if (currentElement.data?.componentsInjected) return currentElement
|
|
1789
|
-
if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true
|
|
1790
|
-
|
|
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
|
|
1791
1818
|
|
|
1792
1819
|
const sel = currentElement.sel || 'NO SELECTOR'
|
|
1793
|
-
const isComponent =
|
|
1820
|
+
const isComponent = componentNameSet.has(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function' || isObj(currentElement.data?.props?.sygnalOptions)
|
|
1794
1821
|
const isCollection = currentElement?.data?.isCollection
|
|
1795
|
-
const isSwitchable = currentElement?.data?.isSwitchable
|
|
1796
|
-
const props = (currentElement.data && currentElement.data.props) || {}
|
|
1797
1822
|
const children = currentElement.children || []
|
|
1798
1823
|
|
|
1799
1824
|
let id = parentId
|
|
1800
1825
|
if (isComponent) {
|
|
1801
1826
|
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1802
1827
|
let component = components[id]
|
|
1803
|
-
//
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
component
|
|
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
|
+
}
|
|
1808
1842
|
}
|
|
1809
1843
|
if (isCollection) {
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1844
|
+
return {
|
|
1845
|
+
...currentElement,
|
|
1846
|
+
sel: 'div',
|
|
1847
|
+
children: Array.isArray(component) ? component : [component]
|
|
1848
|
+
}
|
|
1815
1849
|
} else {
|
|
1816
1850
|
return component
|
|
1817
1851
|
}
|
|
1818
1852
|
} else if (children.length > 0) {
|
|
1819
|
-
|
|
1820
|
-
return currentElement
|
|
1853
|
+
const newChildren = children.map((child: any, i: any) => injectComponents(child, components, componentNameSet, `${path}.${i}`, id, readyMap)).flat()
|
|
1854
|
+
return { ...currentElement, children: newChildren }
|
|
1821
1855
|
} else {
|
|
1822
1856
|
return currentElement
|
|
1823
1857
|
}
|
|
@@ -1873,66 +1907,14 @@ function processSuspensePost(vnode: any): any {
|
|
|
1873
1907
|
return { sel: 'div', data: { attrs: { 'data-sygnal-suspense': 'resolved' } }, children: children.map((c: any) => processSuspensePost(c)), text: undefined, elm: undefined, key: undefined }
|
|
1874
1908
|
}
|
|
1875
1909
|
if (vnode.children && vnode.children.length > 0) {
|
|
1876
|
-
|
|
1910
|
+
const newChildren = vnode.children.map((c: any) => processSuspensePost(c))
|
|
1911
|
+
return { ...vnode, children: newChildren }
|
|
1877
1912
|
}
|
|
1878
1913
|
return vnode
|
|
1879
1914
|
}
|
|
1880
1915
|
|
|
1881
1916
|
const portalPatch = snabbdomInit(defaultModules);
|
|
1882
1917
|
|
|
1883
|
-
function processPortals(vnode: any): any {
|
|
1884
|
-
if (!vnode || !vnode.sel) return vnode
|
|
1885
|
-
if (vnode.sel === 'portal') {
|
|
1886
|
-
const target = vnode.data?.props?.target
|
|
1887
|
-
const children = vnode.children || []
|
|
1888
|
-
return createPortalPlaceholder(target, children)
|
|
1889
|
-
}
|
|
1890
|
-
if (vnode.children && vnode.children.length > 0) {
|
|
1891
|
-
vnode.children = vnode.children.map(processPortals)
|
|
1892
|
-
}
|
|
1893
|
-
return vnode
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
function processTransitions(vnode: any): any {
|
|
1897
|
-
if (!vnode || !vnode.sel) return vnode
|
|
1898
|
-
if (vnode.sel === 'transition') {
|
|
1899
|
-
const props = vnode.data?.props || {}
|
|
1900
|
-
const name = props.name || 'v'
|
|
1901
|
-
const duration = props.duration
|
|
1902
|
-
const children = vnode.children || []
|
|
1903
|
-
const child = children[0]
|
|
1904
|
-
if (!child || !child.sel) return child || vnode
|
|
1905
|
-
return applyTransitionHooks(processTransitions(child), name, duration)
|
|
1906
|
-
}
|
|
1907
|
-
if (vnode.children && vnode.children.length > 0) {
|
|
1908
|
-
vnode.children = vnode.children.map(processTransitions)
|
|
1909
|
-
}
|
|
1910
|
-
return vnode
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
function processClientOnly(vnode: any): any {
|
|
1914
|
-
if (!vnode || !vnode.sel) return vnode
|
|
1915
|
-
if (vnode.sel === 'clientonly') {
|
|
1916
|
-
// On the client, unwrap to children (render them normally)
|
|
1917
|
-
const children = vnode.children || []
|
|
1918
|
-
if (children.length === 0) return { sel: 'div', data: {}, children: [] }
|
|
1919
|
-
if (children.length === 1) return processClientOnly(children[0])
|
|
1920
|
-
// Multiple children: wrap in a div
|
|
1921
|
-
return {
|
|
1922
|
-
sel: 'div',
|
|
1923
|
-
data: {},
|
|
1924
|
-
children: children.map(processClientOnly),
|
|
1925
|
-
text: undefined,
|
|
1926
|
-
elm: undefined,
|
|
1927
|
-
key: undefined,
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
if (vnode.children && vnode.children.length > 0) {
|
|
1931
|
-
vnode.children = vnode.children.map(processClientOnly)
|
|
1932
|
-
}
|
|
1933
|
-
return vnode
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
1918
|
function applyTransitionHooks(vnode: any, name: string, duration?: number): any {
|
|
1937
1919
|
const existingInsert = vnode.data?.hook?.insert
|
|
1938
1920
|
const existingRemove = vnode.data?.hook?.remove
|
|
@@ -1976,14 +1958,17 @@ function applyTransitionHooks(vnode: any, name: string, duration?: number): any
|
|
|
1976
1958
|
return vnode
|
|
1977
1959
|
}
|
|
1978
1960
|
|
|
1979
|
-
|
|
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 {
|
|
1980
1966
|
if (!vnode || !vnode.sel) return vnode
|
|
1981
1967
|
|
|
1982
|
-
//
|
|
1968
|
+
// Handle lazy components
|
|
1983
1969
|
const view = vnode.data?.props?.sygnalOptions?.view
|
|
1984
1970
|
if (view && view.__sygnalLazy) {
|
|
1985
1971
|
if (view.__sygnalLazyLoaded()) {
|
|
1986
|
-
// Lazy component is loaded — rebuild VNode with loaded component's properties
|
|
1987
1972
|
const loaded = view.__sygnalLazyLoadedComponent
|
|
1988
1973
|
if (loaded) {
|
|
1989
1974
|
const props = vnode.data?.props || {}
|
|
@@ -2000,14 +1985,12 @@ function processLazy(vnode: any, componentInstance: any): any {
|
|
|
2000
1985
|
}
|
|
2001
1986
|
}
|
|
2002
1987
|
} else {
|
|
2003
|
-
// Schedule re-render when lazy promise resolves
|
|
2004
1988
|
if (!view.__sygnalLazyReRenderScheduled && view.__sygnalLazyPromise && componentInstance) {
|
|
2005
1989
|
view.__sygnalLazyReRenderScheduled = true
|
|
2006
1990
|
view.__sygnalLazyPromise.then(() => {
|
|
2007
1991
|
setTimeout(() => {
|
|
2008
1992
|
const stateSource = componentInstance.sources?.[componentInstance.stateSourceName]
|
|
2009
1993
|
if (stateSource && stateSource.stream) {
|
|
2010
|
-
// Add a unique token to bypass dropRepeats deep comparison
|
|
2011
1994
|
const stateCopy = { ...componentInstance.currentState, __sygnalLazyTick: Date.now() }
|
|
2012
1995
|
stateSource.stream.shamefullySendNext(stateCopy)
|
|
2013
1996
|
}
|
|
@@ -2017,8 +2000,42 @@ function processLazy(vnode: any, componentInstance: any): any {
|
|
|
2017
2000
|
}
|
|
2018
2001
|
}
|
|
2019
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
|
|
2020
2037
|
if (vnode.children && vnode.children.length > 0) {
|
|
2021
|
-
vnode.children = vnode.children.map((child: any) =>
|
|
2038
|
+
vnode.children = vnode.children.map((child: any) => preprocessVdom(child, componentInstance))
|
|
2022
2039
|
}
|
|
2023
2040
|
return vnode
|
|
2024
2041
|
}
|
|
@@ -2037,6 +2054,21 @@ function onTransitionEnd(el: any, duration: number | undefined, cb: () => void):
|
|
|
2037
2054
|
}
|
|
2038
2055
|
|
|
2039
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
|
+
|
|
2040
2072
|
function createPortalPlaceholder(target: string, children: any[]): any {
|
|
2041
2073
|
const portalChildren = children || []
|
|
2042
2074
|
|
|
@@ -2048,24 +2080,41 @@ function createPortalPlaceholder(target: string, children: any[]): any {
|
|
|
2048
2080
|
portalChildren,
|
|
2049
2081
|
hook: {
|
|
2050
2082
|
insert: (vnode: any) => {
|
|
2083
|
+
// Try synchronously first (target outside component tree or
|
|
2084
|
+
// plain element in the same patch cycle).
|
|
2051
2085
|
const container = document.querySelector(target)
|
|
2052
|
-
if (
|
|
2053
|
-
|
|
2086
|
+
if (container) {
|
|
2087
|
+
portalMount(vnode, target, portalChildren)
|
|
2054
2088
|
return
|
|
2055
2089
|
}
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
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)
|
|
2063
2105
|
},
|
|
2064
2106
|
postpatch: (oldVnode: any, newVnode: any) => {
|
|
2065
2107
|
const prevPortalVnode = oldVnode.data?._portalVnode
|
|
2066
2108
|
const container = oldVnode.data?._portalContainer
|
|
2067
|
-
if (!prevPortalVnode || !container) return
|
|
2068
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
|
+
}
|
|
2069
2118
|
newVnode.data._portalVnode = portalPatch(prevPortalVnode, {
|
|
2070
2119
|
sel: 'div', data: {}, children: newChildren,
|
|
2071
2120
|
text: undefined, elm: undefined, key: undefined,
|
|
@@ -2088,11 +2137,6 @@ function createPortalPlaceholder(target: string, children: any[]): any {
|
|
|
2088
2137
|
}
|
|
2089
2138
|
|
|
2090
2139
|
|
|
2091
|
-
function deepCopyVdom(obj: any): any {
|
|
2092
|
-
if (typeof obj === 'undefined') return obj
|
|
2093
|
-
return { ...obj, children: Array.isArray(obj.children) ? obj.children.map(deepCopyVdom) : undefined, data: obj.data && { ...obj.data, componentsInjected: false } }
|
|
2094
|
-
}
|
|
2095
|
-
|
|
2096
2140
|
function propsIsEqual(obj1: any, obj2: any): boolean {
|
|
2097
2141
|
return objIsEqual(sanitizeObject(obj1), sanitizeObject(obj2))
|
|
2098
2142
|
}
|
|
@@ -2128,16 +2172,15 @@ function objIsEqual(obj1: any, obj2?: any, maxDepth: number = 5, depth: number =
|
|
|
2128
2172
|
|
|
2129
2173
|
// Get keys of both objects
|
|
2130
2174
|
const keys1 = Object.keys(obj1);
|
|
2131
|
-
const keys2 = Object.keys(obj2);
|
|
2132
2175
|
|
|
2133
2176
|
// Check if the number of properties is different
|
|
2134
|
-
if (keys1.length !==
|
|
2177
|
+
if (keys1.length !== Object.keys(obj2).length) {
|
|
2135
2178
|
return false;
|
|
2136
2179
|
}
|
|
2137
2180
|
|
|
2138
2181
|
// Recursively check each property
|
|
2139
2182
|
for (const key of keys1) {
|
|
2140
|
-
if (!
|
|
2183
|
+
if (!(key in obj2)) {
|
|
2141
2184
|
return false;
|
|
2142
2185
|
}
|
|
2143
2186
|
if (!objIsEqual(obj1[key], obj2[key], maxDepth, depth + 1)) {
|
|
@@ -2158,6 +2201,8 @@ function isObj(obj: any): obj is Record<string, any> {
|
|
|
2158
2201
|
return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
|
|
2159
2202
|
}
|
|
2160
2203
|
|
|
2204
|
+
const SORT_FIX = "Use a field name, { field: 'asc'|'desc'|1|-1 }, or a function"
|
|
2205
|
+
|
|
2161
2206
|
function __baseSort(a: any, b: any, ascending: boolean = true): number {
|
|
2162
2207
|
const direction = ascending ? 1 : -1
|
|
2163
2208
|
switch(true) {
|
|
@@ -2170,26 +2215,26 @@ function __baseSort(a: any, b: any, ascending: boolean = true): number {
|
|
|
2170
2215
|
function __sortFunctionFromObj(item: Record<string, any>): ((a: any, b: any) => number) | undefined {
|
|
2171
2216
|
const entries = Object.entries(item)
|
|
2172
2217
|
if (entries.length > 1) {
|
|
2173
|
-
|
|
2218
|
+
logError('SYG418', 'Collection', 'sort object must have one key; ignored', SORT_FIX, item)
|
|
2174
2219
|
return undefined
|
|
2175
2220
|
}
|
|
2176
2221
|
const entry = entries[0]
|
|
2177
2222
|
const [field, directionRaw] = entry
|
|
2178
2223
|
if (!['string', 'number'].includes(typeof directionRaw)) {
|
|
2179
|
-
|
|
2224
|
+
logError('SYG418', 'Collection', 'sort direction must be a string or number; ignored', SORT_FIX, item)
|
|
2180
2225
|
return undefined
|
|
2181
2226
|
}
|
|
2182
2227
|
let ascending = true
|
|
2183
2228
|
if (typeof directionRaw === 'string') {
|
|
2184
2229
|
if (!['asc', 'desc'].includes(directionRaw.toLowerCase())) {
|
|
2185
|
-
|
|
2230
|
+
logError('SYG418', 'Collection', "sort direction must be 'asc' or 'desc'; ignored", SORT_FIX, item)
|
|
2186
2231
|
return undefined
|
|
2187
2232
|
}
|
|
2188
2233
|
ascending = directionRaw.toLowerCase() !== 'desc'
|
|
2189
2234
|
}
|
|
2190
2235
|
if (typeof directionRaw === 'number') {
|
|
2191
2236
|
if (directionRaw !== 1 && directionRaw !== -1) {
|
|
2192
|
-
|
|
2237
|
+
logError('SYG418', 'Collection', 'sort direction must be 1 or -1; ignored', SORT_FIX, item)
|
|
2193
2238
|
return undefined
|
|
2194
2239
|
}
|
|
2195
2240
|
ascending = directionRaw === 1
|
|
@@ -2227,7 +2272,7 @@ function sortFunctionFromProp(sortProp: any): ((a: any, b: any) => number) | und
|
|
|
2227
2272
|
} else if (isObj(sortProp)) {
|
|
2228
2273
|
return __sortFunctionFromObj(sortProp)
|
|
2229
2274
|
} else {
|
|
2230
|
-
|
|
2275
|
+
logError('SYG418', 'Collection', 'Invalid sort prop; ignored', SORT_FIX, sortProp)
|
|
2231
2276
|
return undefined
|
|
2232
2277
|
}
|
|
2233
2278
|
}
|