sygnal 4.2.1 → 4.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/dist/astro/client.cjs.js +554 -85
- package/dist/astro/client.mjs +554 -85
- package/dist/index.cjs.js +576 -97
- package/dist/index.esm.js +576 -98
- package/dist/sygnal.min.js +1 -1
- package/package.json +6 -3
- package/src/collection.js +5 -2
- package/src/component.js +285 -82
- package/src/extra/devtools.js +249 -0
- package/src/extra/driverFactories.js +12 -12
- package/src/extra/eventDriver.js +2 -1
- package/src/extra/logDriver.js +3 -0
- package/src/extra/processDrag.js +6 -0
- package/src/extra/processForm.js +3 -0
- package/src/extra/run.js +12 -0
- package/src/index.d.ts +9 -5
- package/src/index.js +1 -0
- package/src/switchable.js +1 -1
package/dist/astro/client.cjs.js
CHANGED
|
@@ -111,7 +111,8 @@ function eventBusDriver(out$) {
|
|
|
111
111
|
const events = new EventTarget();
|
|
112
112
|
|
|
113
113
|
out$.subscribe({
|
|
114
|
-
next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
|
|
114
|
+
next: event => events.dispatchEvent(new CustomEvent('data', { detail: event })),
|
|
115
|
+
error: err => console.error('[EVENTS driver] Error in sink stream:', err)
|
|
115
116
|
});
|
|
116
117
|
|
|
117
118
|
return {
|
|
@@ -139,11 +140,19 @@ function logDriver(out$) {
|
|
|
139
140
|
out$.addListener({
|
|
140
141
|
next: (val) => {
|
|
141
142
|
console.log(val);
|
|
143
|
+
},
|
|
144
|
+
error: (err) => {
|
|
145
|
+
console.error('[LOG driver] Error in sink stream:', err);
|
|
142
146
|
}
|
|
143
147
|
});
|
|
144
148
|
}
|
|
145
149
|
|
|
150
|
+
let COLLECTION_COUNT = 0;
|
|
151
|
+
|
|
146
152
|
function collection(component, stateLense, opts={}) {
|
|
153
|
+
if (typeof component !== 'function') {
|
|
154
|
+
throw new Error('collection: first argument (component) must be a function')
|
|
155
|
+
}
|
|
147
156
|
const {
|
|
148
157
|
combineList = ['DOM'],
|
|
149
158
|
globalList = ['EVENTS'],
|
|
@@ -154,7 +163,7 @@ function collection(component, stateLense, opts={}) {
|
|
|
154
163
|
} = opts;
|
|
155
164
|
|
|
156
165
|
return (sources) => {
|
|
157
|
-
const key =
|
|
166
|
+
const key = `sygnal-collection-${COLLECTION_COUNT++}`;
|
|
158
167
|
const collectionOpts = {
|
|
159
168
|
item: component,
|
|
160
169
|
itemKey: (state, ind) => typeof state.id !== 'undefined' ? state.id : ind,
|
|
@@ -2925,7 +2934,7 @@ function switchable(factories, name$, initial, opts={}) {
|
|
|
2925
2934
|
const mapFunction = (nameType === 'function' && name$) || (state => state[name$]);
|
|
2926
2935
|
return sources => {
|
|
2927
2936
|
const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream;
|
|
2928
|
-
if (!state$ instanceof Stream$1) throw new Error(`Could not find the state source: ${
|
|
2937
|
+
if (!(state$ instanceof Stream$1)) throw new Error(`Could not find the state source: ${stateSourceName}`)
|
|
2929
2938
|
const _name$ = state$
|
|
2930
2939
|
.map(mapFunction)
|
|
2931
2940
|
.filter(name => typeof name === 'string')
|
|
@@ -3332,13 +3341,27 @@ function wrapDOMSource(domSource) {
|
|
|
3332
3341
|
}
|
|
3333
3342
|
|
|
3334
3343
|
|
|
3335
|
-
const ABORT = '
|
|
3344
|
+
const ABORT = Symbol('ABORT');
|
|
3345
|
+
|
|
3346
|
+
|
|
3347
|
+
function normalizeCalculatedEntry(field, entry) {
|
|
3348
|
+
if (typeof entry === 'function') {
|
|
3349
|
+
return { fn: entry, deps: null }
|
|
3350
|
+
}
|
|
3351
|
+
if (Array.isArray(entry) && entry.length === 2
|
|
3352
|
+
&& Array.isArray(entry[0]) && typeof entry[1] === 'function') {
|
|
3353
|
+
return { fn: entry[1], deps: entry[0] }
|
|
3354
|
+
}
|
|
3355
|
+
throw new Error(
|
|
3356
|
+
`Invalid calculated field '${field}': expected a function or [depsArray, function]`
|
|
3357
|
+
)
|
|
3358
|
+
}
|
|
3336
3359
|
|
|
3337
3360
|
function component (opts) {
|
|
3338
3361
|
const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
|
|
3339
3362
|
|
|
3340
3363
|
if (sources && !isObj(sources)) {
|
|
3341
|
-
throw new Error(
|
|
3364
|
+
throw new Error(`[${name}] Sources must be a Cycle.js sources object`)
|
|
3342
3365
|
}
|
|
3343
3366
|
|
|
3344
3367
|
let fixedIsolateOpts;
|
|
@@ -3418,7 +3441,9 @@ class Component {
|
|
|
3418
3441
|
// sinks
|
|
3419
3442
|
|
|
3420
3443
|
constructor({ name='NO NAME', sources, intent, model, hmrActions, context, response, view, peers={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
|
|
3421
|
-
if (!sources || !isObj(sources)) throw new Error(
|
|
3444
|
+
if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
|
|
3445
|
+
|
|
3446
|
+
this._componentNumber = COMPONENT_COUNT++;
|
|
3422
3447
|
|
|
3423
3448
|
this.name = name;
|
|
3424
3449
|
this.sources = sources;
|
|
@@ -3439,6 +3464,123 @@ class Component {
|
|
|
3439
3464
|
this.sourceNames = Object.keys(sources);
|
|
3440
3465
|
this._debug = debug;
|
|
3441
3466
|
|
|
3467
|
+
// Warn if calculated fields shadow base state keys
|
|
3468
|
+
if (this.calculated && this.initialState
|
|
3469
|
+
&& isObj(this.calculated) && isObj(this.initialState)) {
|
|
3470
|
+
for (const key of Object.keys(this.calculated)) {
|
|
3471
|
+
if (key in this.initialState) {
|
|
3472
|
+
console.warn(
|
|
3473
|
+
`[${name}] Calculated field '${key}' shadows a key in initialState. ` +
|
|
3474
|
+
`The initialState value will be overwritten on every state update.`
|
|
3475
|
+
);
|
|
3476
|
+
}
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
|
|
3480
|
+
// Normalize calculated entries, build dependency graph, topological sort
|
|
3481
|
+
if (this.calculated && isObj(this.calculated)) {
|
|
3482
|
+
const calcEntries = Object.entries(this.calculated);
|
|
3483
|
+
|
|
3484
|
+
// Normalize all entries to { fn, deps } shape
|
|
3485
|
+
this._calculatedNormalized = {};
|
|
3486
|
+
for (const [field, entry] of calcEntries) {
|
|
3487
|
+
this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry);
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized));
|
|
3491
|
+
|
|
3492
|
+
// Warn on deps referencing nonexistent keys
|
|
3493
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
3494
|
+
if (deps !== null) {
|
|
3495
|
+
for (const dep of deps) {
|
|
3496
|
+
if (!this._calculatedFieldNames.has(dep)
|
|
3497
|
+
&& this.initialState && !(dep in this.initialState)) {
|
|
3498
|
+
console.warn(
|
|
3499
|
+
`[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
|
|
3500
|
+
`which is not in initialState or calculated fields`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
// Build adjacency: for each field, which other calculated fields must run first?
|
|
3508
|
+
const calcDeps = {};
|
|
3509
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
3510
|
+
if (deps === null) {
|
|
3511
|
+
calcDeps[field] = [];
|
|
3512
|
+
} else {
|
|
3513
|
+
calcDeps[field] = deps.filter(d => this._calculatedFieldNames.has(d));
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
// Kahn's algorithm for topological sort
|
|
3518
|
+
const inDegree = {};
|
|
3519
|
+
const reverseGraph = {};
|
|
3520
|
+
for (const field of this._calculatedFieldNames) {
|
|
3521
|
+
inDegree[field] = 0;
|
|
3522
|
+
reverseGraph[field] = [];
|
|
3523
|
+
}
|
|
3524
|
+
for (const [field, depList] of Object.entries(calcDeps)) {
|
|
3525
|
+
inDegree[field] = depList.length;
|
|
3526
|
+
for (const dep of depList) {
|
|
3527
|
+
reverseGraph[dep].push(field);
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
|
|
3531
|
+
const queue = [];
|
|
3532
|
+
for (const [field, degree] of Object.entries(inDegree)) {
|
|
3533
|
+
if (degree === 0) queue.push(field);
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
const sorted = [];
|
|
3537
|
+
while (queue.length > 0) {
|
|
3538
|
+
const current = queue.shift();
|
|
3539
|
+
sorted.push(current);
|
|
3540
|
+
for (const dependent of reverseGraph[current]) {
|
|
3541
|
+
inDegree[dependent]--;
|
|
3542
|
+
if (inDegree[dependent] === 0) queue.push(dependent);
|
|
3543
|
+
}
|
|
3544
|
+
}
|
|
3545
|
+
|
|
3546
|
+
if (sorted.length !== this._calculatedFieldNames.size) {
|
|
3547
|
+
// Cycle detected — build error message with cycle path
|
|
3548
|
+
const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f));
|
|
3549
|
+
const visited = new Set();
|
|
3550
|
+
const path = [];
|
|
3551
|
+
const traceCycle = (node) => {
|
|
3552
|
+
if (visited.has(node)) { path.push(node); return true }
|
|
3553
|
+
visited.add(node);
|
|
3554
|
+
path.push(node);
|
|
3555
|
+
for (const dep of calcDeps[node]) {
|
|
3556
|
+
if (inCycle.includes(dep) && traceCycle(dep)) return true
|
|
3557
|
+
}
|
|
3558
|
+
path.pop();
|
|
3559
|
+
visited.delete(node);
|
|
3560
|
+
return false
|
|
3561
|
+
};
|
|
3562
|
+
traceCycle(inCycle[0]);
|
|
3563
|
+
const start = path[path.length - 1];
|
|
3564
|
+
const cycle = path.slice(path.indexOf(start));
|
|
3565
|
+
throw new Error(`Circular calculated dependency: ${cycle.join(' \u2192 ')}`)
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized[f]]);
|
|
3569
|
+
|
|
3570
|
+
// Initialize per-field memoization caches for fields with declared deps
|
|
3571
|
+
this._calculatedFieldCache = {};
|
|
3572
|
+
for (const [field, { deps }] of this._calculatedOrder) {
|
|
3573
|
+
if (deps !== null) {
|
|
3574
|
+
this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined };
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
} else {
|
|
3578
|
+
this._calculatedOrder = null;
|
|
3579
|
+
this._calculatedNormalized = null;
|
|
3580
|
+
this._calculatedFieldNames = null;
|
|
3581
|
+
this._calculatedFieldCache = null;
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3442
3584
|
this.isSubComponent = this.sourceNames.includes('props$');
|
|
3443
3585
|
|
|
3444
3586
|
const state$ = sources[stateSourceName] && sources[stateSourceName].stream;
|
|
@@ -3447,6 +3589,9 @@ class Component {
|
|
|
3447
3589
|
this.currentState = initialState || {};
|
|
3448
3590
|
this.sources[stateSourceName] = new state.StateSource(state$.map(val => {
|
|
3449
3591
|
this.currentState = val;
|
|
3592
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3593
|
+
window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val);
|
|
3594
|
+
}
|
|
3450
3595
|
return val
|
|
3451
3596
|
}));
|
|
3452
3597
|
}
|
|
@@ -3482,10 +3627,8 @@ class Component {
|
|
|
3482
3627
|
};
|
|
3483
3628
|
}
|
|
3484
3629
|
|
|
3485
|
-
const componentNumber = COMPONENT_COUNT++;
|
|
3486
|
-
|
|
3487
3630
|
this.addCalculated = this.createMemoizedAddCalculated();
|
|
3488
|
-
this.log = makeLog(`${
|
|
3631
|
+
this.log = makeLog(`${this._componentNumber} | ${name}`);
|
|
3489
3632
|
|
|
3490
3633
|
this.initChildSources$();
|
|
3491
3634
|
this.initIntent$();
|
|
@@ -3500,9 +3643,20 @@ class Component {
|
|
|
3500
3643
|
this.initVdom$();
|
|
3501
3644
|
this.initSinks();
|
|
3502
3645
|
|
|
3503
|
-
this.sinks.__index =
|
|
3646
|
+
this.sinks.__index = this._componentNumber;
|
|
3504
3647
|
|
|
3505
3648
|
this.log(`Instantiated`, true);
|
|
3649
|
+
|
|
3650
|
+
// Hook 1: Register with DevTools
|
|
3651
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
|
|
3652
|
+
window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this);
|
|
3653
|
+
|
|
3654
|
+
// Hook 1b: Register parent-child relationship
|
|
3655
|
+
const parentNum = sources?.__parentComponentNumber;
|
|
3656
|
+
if (typeof parentNum === 'number') {
|
|
3657
|
+
window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber);
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3506
3660
|
}
|
|
3507
3661
|
|
|
3508
3662
|
get debug() {
|
|
@@ -3514,13 +3668,13 @@ class Component {
|
|
|
3514
3668
|
return
|
|
3515
3669
|
}
|
|
3516
3670
|
if (typeof this.intent != 'function') {
|
|
3517
|
-
throw new Error(
|
|
3671
|
+
throw new Error(`[${this.name}] Intent must be a function`)
|
|
3518
3672
|
}
|
|
3519
3673
|
|
|
3520
3674
|
this.intent$ = this.intent(this.sources);
|
|
3521
3675
|
|
|
3522
3676
|
if (!(this.intent$ instanceof Stream$1) && (!isObj(this.intent$))) {
|
|
3523
|
-
throw new Error(
|
|
3677
|
+
throw new Error(`[${this.name}] Intent must return either an action$ stream or map of event streams`)
|
|
3524
3678
|
}
|
|
3525
3679
|
}
|
|
3526
3680
|
|
|
@@ -3533,10 +3687,10 @@ class Component {
|
|
|
3533
3687
|
this.hmrActions = [this.hmrActions];
|
|
3534
3688
|
}
|
|
3535
3689
|
if (!Array.isArray(this.hmrActions)) {
|
|
3536
|
-
throw new Error(`[${
|
|
3690
|
+
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`)
|
|
3537
3691
|
}
|
|
3538
3692
|
if (this.hmrActions.some(action => typeof action !== 'string')) {
|
|
3539
|
-
throw new Error(`[${
|
|
3693
|
+
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`)
|
|
3540
3694
|
}
|
|
3541
3695
|
this.hmrAction$ = xs$1.fromArray(this.hmrActions.map(action => ({ type: action })));
|
|
3542
3696
|
}
|
|
@@ -3575,7 +3729,15 @@ class Component {
|
|
|
3575
3729
|
const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }));
|
|
3576
3730
|
|
|
3577
3731
|
this.action$ = xs$1.merge(wrapped$, hydrate$)
|
|
3578
|
-
.compose(this.log(({ type }) => `<${
|
|
3732
|
+
.compose(this.log(({ type }) => `<${type}> Action triggered`))
|
|
3733
|
+
.map(action => {
|
|
3734
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3735
|
+
window.__SYGNAL_DEVTOOLS__.onActionDispatched(
|
|
3736
|
+
this._componentNumber, this.name, action.type, action.data
|
|
3737
|
+
);
|
|
3738
|
+
}
|
|
3739
|
+
return action
|
|
3740
|
+
});
|
|
3579
3741
|
}
|
|
3580
3742
|
|
|
3581
3743
|
initState() {
|
|
@@ -3587,7 +3749,7 @@ class Component {
|
|
|
3587
3749
|
} else if (isObj(this.model[INITIALIZE_ACTION])) {
|
|
3588
3750
|
Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
|
|
3589
3751
|
if (name !== this.stateSourceName) {
|
|
3590
|
-
console.warn(`${
|
|
3752
|
+
console.warn(`${INITIALIZE_ACTION} can only be used with the ${this.stateSourceName} source... disregarding ${name}`);
|
|
3591
3753
|
delete this.model[INITIALIZE_ACTION][name];
|
|
3592
3754
|
}
|
|
3593
3755
|
});
|
|
@@ -3622,7 +3784,7 @@ class Component {
|
|
|
3622
3784
|
} else if (valueType === 'function') {
|
|
3623
3785
|
_value = value(state);
|
|
3624
3786
|
} else {
|
|
3625
|
-
console.error(`[${
|
|
3787
|
+
console.error(`[${this.name}] Invalid context entry '${name}': must be the name of a state property or a function returning a value to use`);
|
|
3626
3788
|
return acc
|
|
3627
3789
|
}
|
|
3628
3790
|
acc[name] = _value;
|
|
@@ -3630,11 +3792,14 @@ class Component {
|
|
|
3630
3792
|
}, {});
|
|
3631
3793
|
const newContext = { ..._parent, ...values };
|
|
3632
3794
|
this.currentContext = newContext;
|
|
3795
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3796
|
+
window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext);
|
|
3797
|
+
}
|
|
3633
3798
|
return newContext
|
|
3634
3799
|
})
|
|
3635
3800
|
.compose(dropRepeats(objIsEqual))
|
|
3636
3801
|
.startWith({});
|
|
3637
|
-
this.context$.subscribe({ next: _ => _ });
|
|
3802
|
+
this.context$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in context stream:`, err) });
|
|
3638
3803
|
}
|
|
3639
3804
|
|
|
3640
3805
|
initModel$() {
|
|
@@ -3650,7 +3815,7 @@ class Component {
|
|
|
3650
3815
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
3651
3816
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
3652
3817
|
if (this.isSubComponent && this.initialState) {
|
|
3653
|
-
console.warn(`[${
|
|
3818
|
+
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
3654
3819
|
}
|
|
3655
3820
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
3656
3821
|
const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined');
|
|
@@ -3671,7 +3836,7 @@ class Component {
|
|
|
3671
3836
|
}
|
|
3672
3837
|
|
|
3673
3838
|
if (!isObj(sinks)) {
|
|
3674
|
-
throw new Error(`Entry for each action must be an object: ${
|
|
3839
|
+
throw new Error(`[${this.name}] Entry for each action must be an object: ${action}`)
|
|
3675
3840
|
}
|
|
3676
3841
|
|
|
3677
3842
|
const sinkEntries = Object.entries(sinks);
|
|
@@ -3683,17 +3848,17 @@ class Component {
|
|
|
3683
3848
|
const isParentSink = (sink === PARENT_SINK_NAME);
|
|
3684
3849
|
|
|
3685
3850
|
const on = isStateSink ? onState() : onNormal();
|
|
3686
|
-
const on$ = isParentSink ? on(action, reducer).map(value => ({ name: this.name, value })) : on(action, reducer);
|
|
3851
|
+
const on$ = isParentSink ? on(action, reducer).map(value => ({ name: this.name, component: this.view, value })) : on(action, reducer);
|
|
3687
3852
|
|
|
3688
3853
|
const wrapped$ = on$
|
|
3689
3854
|
.compose(this.log(data => {
|
|
3690
3855
|
if (isStateSink) {
|
|
3691
|
-
return `<${
|
|
3856
|
+
return `<${action}> State reducer added`
|
|
3692
3857
|
} else if (isParentSink) {
|
|
3693
|
-
return `<${
|
|
3858
|
+
return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
|
|
3694
3859
|
} else {
|
|
3695
3860
|
const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
|
|
3696
|
-
return `<${
|
|
3861
|
+
return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
|
|
3697
3862
|
}
|
|
3698
3863
|
}));
|
|
3699
3864
|
|
|
@@ -3749,9 +3914,13 @@ class Component {
|
|
|
3749
3914
|
}).map(sources => xs$1.merge(...sources)).flatten();
|
|
3750
3915
|
|
|
3751
3916
|
this.sources[CHILD_SOURCE_NAME] = {
|
|
3752
|
-
select: (
|
|
3917
|
+
select: (nameOrComponent) => {
|
|
3753
3918
|
const all$ = childSources$;
|
|
3754
|
-
const filtered$ =
|
|
3919
|
+
const filtered$ = typeof nameOrComponent === 'function'
|
|
3920
|
+
? all$.filter(entry => entry.component === nameOrComponent)
|
|
3921
|
+
: nameOrComponent
|
|
3922
|
+
? all$.filter(entry => entry.name === nameOrComponent)
|
|
3923
|
+
: all$;
|
|
3755
3924
|
const unwrapped$ = filtered$.map(entry => entry.value);
|
|
3756
3925
|
return unwrapped$
|
|
3757
3926
|
}
|
|
@@ -3771,7 +3940,7 @@ class Component {
|
|
|
3771
3940
|
|
|
3772
3941
|
}
|
|
3773
3942
|
});
|
|
3774
|
-
subComponentSink$.subscribe({ next: _ => _ });
|
|
3943
|
+
subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) });
|
|
3775
3944
|
this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
|
|
3776
3945
|
}
|
|
3777
3946
|
|
|
@@ -3834,13 +4003,13 @@ class Component {
|
|
|
3834
4003
|
if (typeof reducer === 'function') {
|
|
3835
4004
|
returnStream$ = filtered$.map(action => {
|
|
3836
4005
|
const next = (type, data, delay=10) => {
|
|
3837
|
-
if (typeof delay !== 'number') throw new Error(`[${
|
|
4006
|
+
if (typeof delay !== 'number') throw new Error(`[${this.name}] Invalid delay value provided to next() function in model action '${name}'. Must be a number in ms.`)
|
|
3838
4007
|
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
3839
4008
|
setTimeout(() => {
|
|
3840
4009
|
// push the "next" action request into the action$ stream
|
|
3841
4010
|
rootAction$.shamefullySendNext({ type, data });
|
|
3842
4011
|
}, delay);
|
|
3843
|
-
this.log(`<${
|
|
4012
|
+
this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true);
|
|
3844
4013
|
};
|
|
3845
4014
|
|
|
3846
4015
|
const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext };
|
|
@@ -3852,7 +4021,7 @@ class Component {
|
|
|
3852
4021
|
const enhancedState = this.addCalculated(_state);
|
|
3853
4022
|
props.state = enhancedState;
|
|
3854
4023
|
const newState = reducer(enhancedState, data, next, props);
|
|
3855
|
-
if (newState
|
|
4024
|
+
if (newState === ABORT) return _state
|
|
3856
4025
|
return this.cleanupCalculated(newState)
|
|
3857
4026
|
}
|
|
3858
4027
|
} else {
|
|
@@ -3861,13 +4030,13 @@ class Component {
|
|
|
3861
4030
|
const reduced = reducer(enhancedState, data, next, props);
|
|
3862
4031
|
const type = typeof reduced;
|
|
3863
4032
|
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
3864
|
-
if (type
|
|
3865
|
-
console.warn(`'undefined' value sent to ${
|
|
4033
|
+
if (type === 'undefined') {
|
|
4034
|
+
console.warn(`[${this.name}] 'undefined' value sent to ${name}`);
|
|
3866
4035
|
return reduced
|
|
3867
4036
|
}
|
|
3868
|
-
throw new Error(`Invalid reducer type for ${
|
|
4037
|
+
throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
|
|
3869
4038
|
}
|
|
3870
|
-
}).filter(result => result
|
|
4039
|
+
}).filter(result => result !== ABORT);
|
|
3871
4040
|
} else if (reducer === undefined || reducer === true) {
|
|
3872
4041
|
returnStream$ = filtered$.map(({data}) => data);
|
|
3873
4042
|
} else {
|
|
@@ -3888,7 +4057,7 @@ class Component {
|
|
|
3888
4057
|
if (state === lastState) {
|
|
3889
4058
|
return lastResult
|
|
3890
4059
|
}
|
|
3891
|
-
if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field
|
|
4060
|
+
if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
|
|
3892
4061
|
|
|
3893
4062
|
const calculated = this.getCalculatedValues(state);
|
|
3894
4063
|
if (!calculated) {
|
|
@@ -3907,19 +4076,55 @@ class Component {
|
|
|
3907
4076
|
}
|
|
3908
4077
|
|
|
3909
4078
|
getCalculatedValues(state) {
|
|
3910
|
-
|
|
3911
|
-
if (entries.length === 0) {
|
|
4079
|
+
if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
|
|
3912
4080
|
return
|
|
3913
4081
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4082
|
+
|
|
4083
|
+
const mergedState = { ...state };
|
|
4084
|
+
const computedSoFar = {};
|
|
4085
|
+
|
|
4086
|
+
for (const [field, { fn, deps }] of this._calculatedOrder) {
|
|
4087
|
+
if (deps !== null && this._calculatedFieldCache) {
|
|
4088
|
+
const cache = this._calculatedFieldCache[field];
|
|
4089
|
+
const currentDepValues = deps.map(d => mergedState[d]);
|
|
4090
|
+
|
|
4091
|
+
if (cache.lastDepValues !== undefined) {
|
|
4092
|
+
let unchanged = true;
|
|
4093
|
+
for (let i = 0; i < currentDepValues.length; i++) {
|
|
4094
|
+
if (currentDepValues[i] !== cache.lastDepValues[i]) {
|
|
4095
|
+
unchanged = false;
|
|
4096
|
+
break
|
|
4097
|
+
}
|
|
4098
|
+
}
|
|
4099
|
+
if (unchanged) {
|
|
4100
|
+
computedSoFar[field] = cache.lastResult;
|
|
4101
|
+
mergedState[field] = cache.lastResult;
|
|
4102
|
+
continue
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
|
|
4106
|
+
try {
|
|
4107
|
+
const result = fn(mergedState);
|
|
4108
|
+
cache.lastDepValues = currentDepValues;
|
|
4109
|
+
cache.lastResult = result;
|
|
4110
|
+
computedSoFar[field] = result;
|
|
4111
|
+
mergedState[field] = result;
|
|
4112
|
+
} catch (e) {
|
|
4113
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4114
|
+
}
|
|
4115
|
+
} else {
|
|
4116
|
+
// No deps declared — always recompute
|
|
4117
|
+
try {
|
|
4118
|
+
const result = fn(mergedState);
|
|
4119
|
+
computedSoFar[field] = result;
|
|
4120
|
+
mergedState[field] = result;
|
|
4121
|
+
} catch (e) {
|
|
4122
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4123
|
+
}
|
|
3920
4124
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
4125
|
+
}
|
|
4126
|
+
|
|
4127
|
+
return computedSoFar
|
|
3923
4128
|
}
|
|
3924
4129
|
|
|
3925
4130
|
cleanupCalculated(incomingState) {
|
|
@@ -4067,7 +4272,7 @@ class Component {
|
|
|
4067
4272
|
this.newChildSources(childSources);
|
|
4068
4273
|
|
|
4069
4274
|
|
|
4070
|
-
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${
|
|
4275
|
+
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true);
|
|
4071
4276
|
|
|
4072
4277
|
return newComponents
|
|
4073
4278
|
}, {})
|
|
@@ -4133,7 +4338,7 @@ class Component {
|
|
|
4133
4338
|
} else if (this.components[collectionOf]) {
|
|
4134
4339
|
factory = this.components[collectionOf];
|
|
4135
4340
|
} else {
|
|
4136
|
-
throw new Error(`[${this.name}] Invalid 'of'
|
|
4341
|
+
throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
|
|
4137
4342
|
}
|
|
4138
4343
|
|
|
4139
4344
|
const fieldLense = {
|
|
@@ -4141,7 +4346,7 @@ class Component {
|
|
|
4141
4346
|
if (!Array.isArray(state[stateField])) return []
|
|
4142
4347
|
const items = state[stateField];
|
|
4143
4348
|
const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
|
|
4144
|
-
const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4349
|
+
const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4145
4350
|
const mapped = sorted.map((item, index) => {
|
|
4146
4351
|
return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
|
|
4147
4352
|
});
|
|
@@ -4150,7 +4355,7 @@ class Component {
|
|
|
4150
4355
|
},
|
|
4151
4356
|
set: (oldState, newState) => {
|
|
4152
4357
|
if (this.calculated && stateField in this.calculated) {
|
|
4153
|
-
console.warn(`Collection sub-component of ${
|
|
4358
|
+
console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4154
4359
|
return oldState
|
|
4155
4360
|
}
|
|
4156
4361
|
const updated = [];
|
|
@@ -4179,17 +4384,17 @@ class Component {
|
|
|
4179
4384
|
} else if (typeof stateField === 'string') {
|
|
4180
4385
|
if (isObj(this.currentState)) {
|
|
4181
4386
|
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
4182
|
-
console.error(`Collection component in ${
|
|
4387
|
+
console.error(`Collection component in ${this.name} is attempting to use non-existent state property '${stateField}': To fix this error, specify a valid array property on the state. Attempting to use parent component state.`);
|
|
4183
4388
|
lense = undefined;
|
|
4184
4389
|
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
4185
|
-
console.warn(`State property '${
|
|
4390
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4186
4391
|
lense = fieldLense;
|
|
4187
4392
|
} else {
|
|
4188
4393
|
lense = fieldLense;
|
|
4189
4394
|
}
|
|
4190
4395
|
} else {
|
|
4191
4396
|
if (!Array.isArray(this.currentState[stateField])) {
|
|
4192
|
-
console.warn(`State property '${
|
|
4397
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4193
4398
|
lense = fieldLense;
|
|
4194
4399
|
} else {
|
|
4195
4400
|
lense = fieldLense;
|
|
@@ -4197,14 +4402,14 @@ class Component {
|
|
|
4197
4402
|
}
|
|
4198
4403
|
} else if (isObj(stateField)) {
|
|
4199
4404
|
if (typeof stateField.get !== 'function') {
|
|
4200
|
-
console.error(`Collection component in ${
|
|
4405
|
+
console.error(`Collection component in ${this.name} has an invalid 'from' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting child state from the current state. Attempting to use parent component state.`);
|
|
4201
4406
|
lense = undefined;
|
|
4202
4407
|
} else {
|
|
4203
4408
|
lense = {
|
|
4204
4409
|
get: (state) => {
|
|
4205
4410
|
const newState = stateField.get(state);
|
|
4206
4411
|
if (!Array.isArray(newState)) {
|
|
4207
|
-
console.warn(`State getter function in collection component of ${
|
|
4412
|
+
console.warn(`State getter function in collection component of ${this.name} did not return an array: No components will be instantiated in the collection. Returned value:`, newState);
|
|
4208
4413
|
return []
|
|
4209
4414
|
}
|
|
4210
4415
|
return newState
|
|
@@ -4213,14 +4418,14 @@ class Component {
|
|
|
4213
4418
|
};
|
|
4214
4419
|
}
|
|
4215
4420
|
} else {
|
|
4216
|
-
console.error(`Collection component in ${
|
|
4421
|
+
console.error(`Collection component in ${this.name} has an invalid 'from' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting child state from the current state. Attempting to use parent component state.`);
|
|
4217
4422
|
lense = undefined;
|
|
4218
4423
|
}
|
|
4219
4424
|
|
|
4220
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
|
|
4425
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber };
|
|
4221
4426
|
const sink$ = collection(factory, lense, { container: null })(sources);
|
|
4222
4427
|
if (!isObj(sink$)) {
|
|
4223
|
-
throw new Error(
|
|
4428
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
4224
4429
|
}
|
|
4225
4430
|
return sink$
|
|
4226
4431
|
}
|
|
@@ -4242,7 +4447,7 @@ class Component {
|
|
|
4242
4447
|
get: state => state[stateField],
|
|
4243
4448
|
set: (oldState, newState) => {
|
|
4244
4449
|
if (this.calculated && stateField in this.calculated) {
|
|
4245
|
-
console.warn(`Switchable sub-component of ${
|
|
4450
|
+
console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4246
4451
|
return oldState
|
|
4247
4452
|
}
|
|
4248
4453
|
if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
|
|
@@ -4261,13 +4466,13 @@ class Component {
|
|
|
4261
4466
|
lense = fieldLense;
|
|
4262
4467
|
} else if (isObj(stateField)) {
|
|
4263
4468
|
if (typeof stateField.get !== 'function') {
|
|
4264
|
-
console.error(`Switchable component in ${
|
|
4469
|
+
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.`);
|
|
4265
4470
|
lense = baseLense;
|
|
4266
4471
|
} else {
|
|
4267
4472
|
lense = { get: stateField.get, set: stateField.set };
|
|
4268
4473
|
}
|
|
4269
4474
|
} else {
|
|
4270
|
-
console.error(`Invalid state provided to switchable sub-component of ${
|
|
4475
|
+
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.`);
|
|
4271
4476
|
lense = baseLense;
|
|
4272
4477
|
}
|
|
4273
4478
|
|
|
@@ -4283,12 +4488,12 @@ class Component {
|
|
|
4283
4488
|
switchableComponents[key] = component(options);
|
|
4284
4489
|
}
|
|
4285
4490
|
});
|
|
4286
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4491
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4287
4492
|
|
|
4288
4493
|
const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
|
|
4289
4494
|
|
|
4290
4495
|
if (!isObj(sink$)) {
|
|
4291
|
-
throw new Error(
|
|
4496
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
|
|
4292
4497
|
}
|
|
4293
4498
|
|
|
4294
4499
|
return sink$
|
|
@@ -4314,7 +4519,7 @@ class Component {
|
|
|
4314
4519
|
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
|
|
4315
4520
|
if (!factory) {
|
|
4316
4521
|
if (componentName === 'sygnal-factory') throw new Error(`Component not found on element with Capitalized selector and nameless function: JSX transpilation replaces selectors starting with upper case letters with functions in-scope with the same name, Sygnal cannot see the name of the resulting component.`)
|
|
4317
|
-
throw new Error(`Component not found: ${
|
|
4522
|
+
throw new Error(`Component not found: ${componentName}`)
|
|
4318
4523
|
}
|
|
4319
4524
|
|
|
4320
4525
|
let lense;
|
|
@@ -4323,7 +4528,7 @@ class Component {
|
|
|
4323
4528
|
get: state => state[stateField],
|
|
4324
4529
|
set: (oldState, newState) => {
|
|
4325
4530
|
if (this.calculated && stateField in this.calculated) {
|
|
4326
|
-
console.warn(`Sub-component of ${
|
|
4531
|
+
console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4327
4532
|
return oldState
|
|
4328
4533
|
}
|
|
4329
4534
|
return { ...oldState, [stateField]: newState }
|
|
@@ -4341,17 +4546,17 @@ class Component {
|
|
|
4341
4546
|
lense = fieldLense;
|
|
4342
4547
|
} else if (isObj(stateField)) {
|
|
4343
4548
|
if (typeof stateField.get !== 'function') {
|
|
4344
|
-
console.error(`Sub-component in ${
|
|
4549
|
+
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.`);
|
|
4345
4550
|
lense = baseLense;
|
|
4346
4551
|
} else {
|
|
4347
4552
|
lense = { get: stateField.get, set: stateField.set };
|
|
4348
4553
|
}
|
|
4349
4554
|
} else {
|
|
4350
|
-
console.error(`Invalid state provided to sub-component of ${
|
|
4555
|
+
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.`);
|
|
4351
4556
|
lense = baseLense;
|
|
4352
4557
|
}
|
|
4353
4558
|
|
|
4354
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4559
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4355
4560
|
const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
|
|
4356
4561
|
|
|
4357
4562
|
if (!isObj(sink$)) {
|
|
@@ -4426,14 +4631,22 @@ class Component {
|
|
|
4426
4631
|
const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
|
|
4427
4632
|
if (immediate) {
|
|
4428
4633
|
if (this.debug) {
|
|
4429
|
-
|
|
4634
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4635
|
+
console.log(text);
|
|
4636
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4637
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4638
|
+
}
|
|
4430
4639
|
}
|
|
4431
4640
|
return
|
|
4432
4641
|
} else {
|
|
4433
4642
|
return stream => {
|
|
4434
4643
|
return stream.debug(msg => {
|
|
4435
4644
|
if (this.debug) {
|
|
4436
|
-
|
|
4645
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4646
|
+
console.log(text);
|
|
4647
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4648
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4649
|
+
}
|
|
4437
4650
|
}
|
|
4438
4651
|
})
|
|
4439
4652
|
}
|
|
@@ -4443,11 +4656,11 @@ class Component {
|
|
|
4443
4656
|
|
|
4444
4657
|
|
|
4445
4658
|
|
|
4446
|
-
function getComponents(currentElement, componentNames,
|
|
4659
|
+
function getComponents(currentElement, componentNames, path='r', parentId) {
|
|
4447
4660
|
if (!currentElement) return {}
|
|
4448
4661
|
|
|
4449
4662
|
if (currentElement.data?.componentsProcessed) return {}
|
|
4450
|
-
if (
|
|
4663
|
+
if (path === 'r') currentElement.data.componentsProcessed = true;
|
|
4451
4664
|
|
|
4452
4665
|
const sel = currentElement.sel;
|
|
4453
4666
|
const isCollection = sel && sel.toLowerCase() === 'collection';
|
|
@@ -4461,11 +4674,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4461
4674
|
|
|
4462
4675
|
let id = parentId;
|
|
4463
4676
|
if (isComponent) {
|
|
4464
|
-
id = getComponentIdFromElement(currentElement,
|
|
4677
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4465
4678
|
if (isCollection) {
|
|
4466
4679
|
if (!props.of) throw new Error(`Collection element missing required 'component' property`)
|
|
4467
4680
|
if (typeof props.of !== 'string' && typeof props.of !== 'function') throw new Error(`Invalid 'component' property of collection element: found ${ typeof props.of } requires string or component factory function`)
|
|
4468
|
-
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${
|
|
4681
|
+
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
|
|
4469
4682
|
if (typeof props.from !== 'undefined' && !(typeof props.from === 'string' || Array.isArray(props.from) || typeof props.from.get === 'function')) console.warn(`No valid array found for collection ${ typeof props.of === 'string' ? props.of : 'function component' }: no collection components will be created`, props.from);
|
|
4470
4683
|
currentElement.data.isCollection = true;
|
|
4471
4684
|
currentElement.data.props ||= {};
|
|
@@ -4476,7 +4689,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4476
4689
|
if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
|
|
4477
4690
|
if (!props.current || (typeof props.current !== 'string' && typeof props.current !== 'function')) throw new Error(`Missing or invalid 'current' property for switchable element: found '${ typeof props.current }' requires string or function`)
|
|
4478
4691
|
const switchableComponentNames = Object.keys(props.of);
|
|
4479
|
-
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${
|
|
4692
|
+
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
|
|
4480
4693
|
currentElement.data.isSwitchable = true;
|
|
4481
4694
|
} else ;
|
|
4482
4695
|
if (typeof props.key === 'undefined') currentElement.data.props.key = id;
|
|
@@ -4484,7 +4697,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4484
4697
|
}
|
|
4485
4698
|
|
|
4486
4699
|
if (children.length > 0) {
|
|
4487
|
-
children.map((child, i) => getComponents(child, componentNames,
|
|
4700
|
+
children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
|
|
4488
4701
|
.forEach((child) => {
|
|
4489
4702
|
Object.entries(child).forEach(([id, el]) => found[id] = el);
|
|
4490
4703
|
});
|
|
@@ -4493,10 +4706,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4493
4706
|
return found
|
|
4494
4707
|
}
|
|
4495
4708
|
|
|
4496
|
-
function injectComponents(currentElement, components, componentNames,
|
|
4709
|
+
function injectComponents(currentElement, components, componentNames, path='r', parentId) {
|
|
4497
4710
|
if (!currentElement) return
|
|
4498
4711
|
if (currentElement.data?.componentsInjected) return currentElement
|
|
4499
|
-
if (
|
|
4712
|
+
if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true;
|
|
4500
4713
|
|
|
4501
4714
|
|
|
4502
4715
|
const sel = currentElement.sel || 'NO SELECTOR';
|
|
@@ -4508,7 +4721,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4508
4721
|
|
|
4509
4722
|
let id = parentId;
|
|
4510
4723
|
if (isComponent) {
|
|
4511
|
-
id = getComponentIdFromElement(currentElement,
|
|
4724
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4512
4725
|
const component = components[id];
|
|
4513
4726
|
if (isCollection) {
|
|
4514
4727
|
currentElement.sel = 'div';
|
|
@@ -4520,21 +4733,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4520
4733
|
return component
|
|
4521
4734
|
}
|
|
4522
4735
|
} else if (children.length > 0) {
|
|
4523
|
-
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames,
|
|
4736
|
+
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat();
|
|
4524
4737
|
return currentElement
|
|
4525
4738
|
} else {
|
|
4526
4739
|
return currentElement
|
|
4527
4740
|
}
|
|
4528
4741
|
}
|
|
4529
4742
|
|
|
4530
|
-
function getComponentIdFromElement(el,
|
|
4743
|
+
function getComponentIdFromElement(el, path, parentId) {
|
|
4531
4744
|
const sel = el.sel;
|
|
4532
4745
|
const name = typeof sel === 'string' ? sel : 'functionComponent';
|
|
4533
|
-
const uid = `${depth}:${index}`;
|
|
4534
4746
|
const props = el.data?.props || {};
|
|
4535
|
-
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) ||
|
|
4536
|
-
const parentString = parentId ? `${
|
|
4537
|
-
const fullId = `${
|
|
4747
|
+
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path;
|
|
4748
|
+
const parentString = parentId ? `${parentId}|` : '';
|
|
4749
|
+
const fullId = `${parentString}${name}::${id}`;
|
|
4538
4750
|
return fullId
|
|
4539
4751
|
}
|
|
4540
4752
|
|
|
@@ -4678,12 +4890,264 @@ function sortFunctionFromProp(sortProp) {
|
|
|
4678
4890
|
} else if (isObj(sortProp)) {
|
|
4679
4891
|
return __sortFunctionFromObj(sortProp)
|
|
4680
4892
|
} else {
|
|
4681
|
-
console.error('Invalid sort option (ignoring):',
|
|
4893
|
+
console.error('Invalid sort option (ignoring):', sortProp);
|
|
4682
4894
|
return undefined
|
|
4683
4895
|
}
|
|
4684
4896
|
}
|
|
4685
4897
|
|
|
4898
|
+
const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
|
|
4899
|
+
const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
|
|
4900
|
+
const DEFAULT_MAX_HISTORY = 200;
|
|
4901
|
+
|
|
4902
|
+
class SygnalDevTools {
|
|
4903
|
+
constructor() {
|
|
4904
|
+
this._connected = false;
|
|
4905
|
+
this._components = new Map();
|
|
4906
|
+
this._stateHistory = [];
|
|
4907
|
+
this._maxHistory = DEFAULT_MAX_HISTORY;
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
get connected() {
|
|
4911
|
+
return this._connected && typeof window !== 'undefined'
|
|
4912
|
+
}
|
|
4913
|
+
|
|
4914
|
+
// ─── Initialization ─────────────────────────────────────────────────────────
|
|
4915
|
+
|
|
4916
|
+
init() {
|
|
4917
|
+
if (typeof window === 'undefined') return
|
|
4918
|
+
|
|
4919
|
+
window.__SYGNAL_DEVTOOLS__ = this;
|
|
4920
|
+
|
|
4921
|
+
window.addEventListener('message', (event) => {
|
|
4922
|
+
if (event.source !== window) return
|
|
4923
|
+
if (event.data?.source === EXTENSION_SOURCE) {
|
|
4924
|
+
this._handleExtensionMessage(event.data);
|
|
4925
|
+
}
|
|
4926
|
+
});
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
_handleExtensionMessage(msg) {
|
|
4930
|
+
switch (msg.type) {
|
|
4931
|
+
case 'CONNECT':
|
|
4932
|
+
this._connected = true;
|
|
4933
|
+
if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
|
|
4934
|
+
this._sendFullTree();
|
|
4935
|
+
break
|
|
4936
|
+
case 'DISCONNECT':
|
|
4937
|
+
this._connected = false;
|
|
4938
|
+
break
|
|
4939
|
+
case 'SET_DEBUG':
|
|
4940
|
+
this._setDebug(msg.payload);
|
|
4941
|
+
break
|
|
4942
|
+
case 'TIME_TRAVEL':
|
|
4943
|
+
this._timeTravel(msg.payload);
|
|
4944
|
+
break
|
|
4945
|
+
case 'GET_STATE':
|
|
4946
|
+
this._sendComponentState(msg.payload.componentId);
|
|
4947
|
+
break
|
|
4948
|
+
}
|
|
4949
|
+
}
|
|
4950
|
+
|
|
4951
|
+
// ─── Hooks (called from component.js) ────────────────────────────────────────
|
|
4952
|
+
|
|
4953
|
+
onComponentCreated(componentNumber, name, instance) {
|
|
4954
|
+
const meta = {
|
|
4955
|
+
id: componentNumber,
|
|
4956
|
+
name: name,
|
|
4957
|
+
isSubComponent: instance.isSubComponent,
|
|
4958
|
+
hasModel: !!instance.model,
|
|
4959
|
+
hasIntent: !!instance.intent,
|
|
4960
|
+
hasContext: !!instance.context,
|
|
4961
|
+
hasCalculated: !!instance.calculated,
|
|
4962
|
+
components: Object.keys(instance.components || {}),
|
|
4963
|
+
parentId: null,
|
|
4964
|
+
children: [],
|
|
4965
|
+
debug: instance._debug,
|
|
4966
|
+
createdAt: Date.now(),
|
|
4967
|
+
_instanceRef: new WeakRef(instance),
|
|
4968
|
+
};
|
|
4969
|
+
this._components.set(componentNumber, meta);
|
|
4970
|
+
|
|
4971
|
+
if (!this.connected) return
|
|
4972
|
+
this._post('COMPONENT_CREATED', this._serializeMeta(meta));
|
|
4973
|
+
}
|
|
4974
|
+
|
|
4975
|
+
onStateChanged(componentNumber, name, state) {
|
|
4976
|
+
if (!this.connected) return
|
|
4977
|
+
|
|
4978
|
+
const entry = {
|
|
4979
|
+
componentId: componentNumber,
|
|
4980
|
+
componentName: name,
|
|
4981
|
+
timestamp: Date.now(),
|
|
4982
|
+
state: this._safeClone(state),
|
|
4983
|
+
};
|
|
4984
|
+
|
|
4985
|
+
this._stateHistory.push(entry);
|
|
4986
|
+
if (this._stateHistory.length > this._maxHistory) {
|
|
4987
|
+
this._stateHistory.shift();
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4990
|
+
this._post('STATE_CHANGED', {
|
|
4991
|
+
componentId: componentNumber,
|
|
4992
|
+
componentName: name,
|
|
4993
|
+
state: entry.state,
|
|
4994
|
+
historyIndex: this._stateHistory.length - 1,
|
|
4995
|
+
});
|
|
4996
|
+
}
|
|
4997
|
+
|
|
4998
|
+
onActionDispatched(componentNumber, name, actionType, data) {
|
|
4999
|
+
if (!this.connected) return
|
|
5000
|
+
this._post('ACTION_DISPATCHED', {
|
|
5001
|
+
componentId: componentNumber,
|
|
5002
|
+
componentName: name,
|
|
5003
|
+
actionType: actionType,
|
|
5004
|
+
data: this._safeClone(data),
|
|
5005
|
+
timestamp: Date.now(),
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
|
|
5009
|
+
onSubComponentRegistered(parentNumber, childNumber) {
|
|
5010
|
+
const parent = this._components.get(parentNumber);
|
|
5011
|
+
const child = this._components.get(childNumber);
|
|
5012
|
+
if (parent && child) {
|
|
5013
|
+
child.parentId = parentNumber;
|
|
5014
|
+
if (!parent.children.includes(childNumber)) {
|
|
5015
|
+
parent.children.push(childNumber);
|
|
5016
|
+
}
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
if (!this.connected) return
|
|
5020
|
+
this._post('TREE_UPDATED', {
|
|
5021
|
+
parentId: parentNumber,
|
|
5022
|
+
childId: childNumber,
|
|
5023
|
+
});
|
|
5024
|
+
}
|
|
5025
|
+
|
|
5026
|
+
onContextChanged(componentNumber, name, context) {
|
|
5027
|
+
if (!this.connected) return
|
|
5028
|
+
this._post('CONTEXT_CHANGED', {
|
|
5029
|
+
componentId: componentNumber,
|
|
5030
|
+
componentName: name,
|
|
5031
|
+
context: this._safeClone(context),
|
|
5032
|
+
});
|
|
5033
|
+
}
|
|
5034
|
+
|
|
5035
|
+
onDebugLog(componentNumber, message) {
|
|
5036
|
+
if (!this.connected) return
|
|
5037
|
+
this._post('DEBUG_LOG', {
|
|
5038
|
+
componentId: componentNumber,
|
|
5039
|
+
message: message,
|
|
5040
|
+
timestamp: Date.now(),
|
|
5041
|
+
});
|
|
5042
|
+
}
|
|
5043
|
+
|
|
5044
|
+
// ─── Commands (from extension to page) ───────────────────────────────────────
|
|
5045
|
+
|
|
5046
|
+
_setDebug({ componentId, enabled }) {
|
|
5047
|
+
if (typeof componentId === 'undefined' || componentId === null) {
|
|
5048
|
+
if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
|
|
5049
|
+
this._post('DEBUG_TOGGLED', { global: true, enabled });
|
|
5050
|
+
return
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
const meta = this._components.get(componentId);
|
|
5054
|
+
if (meta && meta._instanceRef) {
|
|
5055
|
+
const instance = meta._instanceRef.deref();
|
|
5056
|
+
if (instance) {
|
|
5057
|
+
instance._debug = enabled;
|
|
5058
|
+
meta.debug = enabled;
|
|
5059
|
+
this._post('DEBUG_TOGGLED', { componentId, enabled });
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
}
|
|
5063
|
+
|
|
5064
|
+
_timeTravel({ historyIndex }) {
|
|
5065
|
+
const entry = this._stateHistory[historyIndex];
|
|
5066
|
+
if (!entry) return
|
|
5067
|
+
|
|
5068
|
+
const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__;
|
|
5069
|
+
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
5070
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
|
|
5071
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5072
|
+
historyIndex,
|
|
5073
|
+
state: entry.state,
|
|
5074
|
+
});
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
5077
|
+
|
|
5078
|
+
_sendComponentState(componentId) {
|
|
5079
|
+
const meta = this._components.get(componentId);
|
|
5080
|
+
if (meta && meta._instanceRef) {
|
|
5081
|
+
const instance = meta._instanceRef.deref();
|
|
5082
|
+
if (instance) {
|
|
5083
|
+
this._post('COMPONENT_STATE', {
|
|
5084
|
+
componentId,
|
|
5085
|
+
state: this._safeClone(instance.currentState),
|
|
5086
|
+
context: this._safeClone(instance.currentContext),
|
|
5087
|
+
props: this._safeClone(instance.currentProps),
|
|
5088
|
+
});
|
|
5089
|
+
}
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
|
|
5093
|
+
_sendFullTree() {
|
|
5094
|
+
const tree = [];
|
|
5095
|
+
for (const [id, meta] of this._components) {
|
|
5096
|
+
const instance = meta._instanceRef?.deref();
|
|
5097
|
+
tree.push({
|
|
5098
|
+
...this._serializeMeta(meta),
|
|
5099
|
+
state: instance ? this._safeClone(instance.currentState) : null,
|
|
5100
|
+
context: instance ? this._safeClone(instance.currentContext) : null,
|
|
5101
|
+
});
|
|
5102
|
+
}
|
|
5103
|
+
this._post('FULL_TREE', {
|
|
5104
|
+
components: tree,
|
|
5105
|
+
history: this._stateHistory,
|
|
5106
|
+
});
|
|
5107
|
+
}
|
|
5108
|
+
|
|
5109
|
+
// ─── Transport ───────────────────────────────────────────────────────────────
|
|
5110
|
+
|
|
5111
|
+
_post(type, payload) {
|
|
5112
|
+
if (typeof window === 'undefined') return
|
|
5113
|
+
window.postMessage({
|
|
5114
|
+
source: DEVTOOLS_SOURCE,
|
|
5115
|
+
type,
|
|
5116
|
+
payload,
|
|
5117
|
+
}, '*');
|
|
5118
|
+
}
|
|
5119
|
+
|
|
5120
|
+
_safeClone(obj) {
|
|
5121
|
+
if (obj === undefined || obj === null) return obj
|
|
5122
|
+
try {
|
|
5123
|
+
return JSON.parse(JSON.stringify(obj))
|
|
5124
|
+
} catch (e) {
|
|
5125
|
+
return '[unserializable]'
|
|
5126
|
+
}
|
|
5127
|
+
}
|
|
5128
|
+
|
|
5129
|
+
_serializeMeta(meta) {
|
|
5130
|
+
const { _instanceRef, ...rest } = meta;
|
|
5131
|
+
return rest
|
|
5132
|
+
}
|
|
5133
|
+
}
|
|
5134
|
+
|
|
5135
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
5136
|
+
|
|
5137
|
+
let instance = null;
|
|
5138
|
+
|
|
5139
|
+
function getDevTools() {
|
|
5140
|
+
if (!instance) instance = new SygnalDevTools();
|
|
5141
|
+
return instance
|
|
5142
|
+
}
|
|
5143
|
+
|
|
4686
5144
|
function run(app, drivers={}, options={}) {
|
|
5145
|
+
// Initialize DevTools instrumentation bridge early (before component creation)
|
|
5146
|
+
if (typeof window !== 'undefined') {
|
|
5147
|
+
const dt = getDevTools();
|
|
5148
|
+
dt.init();
|
|
5149
|
+
}
|
|
5150
|
+
|
|
4687
5151
|
const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options;
|
|
4688
5152
|
if (!app.isSygnalComponent) {
|
|
4689
5153
|
const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
|
|
@@ -4733,6 +5197,11 @@ function run(app, drivers={}, options={}) {
|
|
|
4733
5197
|
|
|
4734
5198
|
const exposed = { sources, sinks, dispose };
|
|
4735
5199
|
|
|
5200
|
+
// Store app reference for time-travel
|
|
5201
|
+
if (typeof window !== 'undefined') {
|
|
5202
|
+
window.__SYGNAL_DEVTOOLS_APP__ = exposed;
|
|
5203
|
+
}
|
|
5204
|
+
|
|
4736
5205
|
const swapToComponent = (newComponent, state) => {
|
|
4737
5206
|
const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
|
|
4738
5207
|
const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
|