sygnal 4.2.0 → 4.3.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 +547 -82
- package/dist/astro/client.mjs +547 -82
- package/dist/index.cjs.js +569 -94
- package/dist/index.esm.js +569 -95
- package/dist/jsx-dev-runtime.cjs.js +49 -12
- package/dist/jsx-dev-runtime.esm.js +49 -12
- package/dist/jsx-runtime.cjs.js +49 -12
- package/dist/jsx-runtime.esm.js +49 -12
- package/dist/jsx.cjs.js +49 -12
- package/dist/jsx.esm.js +49 -12
- package/dist/sygnal.min.js +1 -1
- package/package.json +6 -3
- package/src/collection.js +5 -2
- package/src/component.js +278 -79
- 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 +8 -4
- package/src/index.js +1 -0
- package/src/pragma/index.js +21 -9
- package/src/pragma/is.js +27 -2
- 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);
|
|
@@ -3688,12 +3853,12 @@ class Component {
|
|
|
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
|
|
|
@@ -3771,7 +3936,7 @@ class Component {
|
|
|
3771
3936
|
|
|
3772
3937
|
}
|
|
3773
3938
|
});
|
|
3774
|
-
subComponentSink$.subscribe({ next: _ => _ });
|
|
3939
|
+
subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) });
|
|
3775
3940
|
this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
|
|
3776
3941
|
}
|
|
3777
3942
|
|
|
@@ -3834,13 +3999,13 @@ class Component {
|
|
|
3834
3999
|
if (typeof reducer === 'function') {
|
|
3835
4000
|
returnStream$ = filtered$.map(action => {
|
|
3836
4001
|
const next = (type, data, delay=10) => {
|
|
3837
|
-
if (typeof delay !== 'number') throw new Error(`[${
|
|
4002
|
+
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
4003
|
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
3839
4004
|
setTimeout(() => {
|
|
3840
4005
|
// push the "next" action request into the action$ stream
|
|
3841
4006
|
rootAction$.shamefullySendNext({ type, data });
|
|
3842
4007
|
}, delay);
|
|
3843
|
-
this.log(`<${
|
|
4008
|
+
this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true);
|
|
3844
4009
|
};
|
|
3845
4010
|
|
|
3846
4011
|
const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext };
|
|
@@ -3852,7 +4017,7 @@ class Component {
|
|
|
3852
4017
|
const enhancedState = this.addCalculated(_state);
|
|
3853
4018
|
props.state = enhancedState;
|
|
3854
4019
|
const newState = reducer(enhancedState, data, next, props);
|
|
3855
|
-
if (newState
|
|
4020
|
+
if (newState === ABORT) return _state
|
|
3856
4021
|
return this.cleanupCalculated(newState)
|
|
3857
4022
|
}
|
|
3858
4023
|
} else {
|
|
@@ -3861,13 +4026,13 @@ class Component {
|
|
|
3861
4026
|
const reduced = reducer(enhancedState, data, next, props);
|
|
3862
4027
|
const type = typeof reduced;
|
|
3863
4028
|
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
3864
|
-
if (type
|
|
3865
|
-
console.warn(`'undefined' value sent to ${
|
|
4029
|
+
if (type === 'undefined') {
|
|
4030
|
+
console.warn(`[${this.name}] 'undefined' value sent to ${name}`);
|
|
3866
4031
|
return reduced
|
|
3867
4032
|
}
|
|
3868
|
-
throw new Error(`Invalid reducer type for ${
|
|
4033
|
+
throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
|
|
3869
4034
|
}
|
|
3870
|
-
}).filter(result => result
|
|
4035
|
+
}).filter(result => result !== ABORT);
|
|
3871
4036
|
} else if (reducer === undefined || reducer === true) {
|
|
3872
4037
|
returnStream$ = filtered$.map(({data}) => data);
|
|
3873
4038
|
} else {
|
|
@@ -3888,7 +4053,7 @@ class Component {
|
|
|
3888
4053
|
if (state === lastState) {
|
|
3889
4054
|
return lastResult
|
|
3890
4055
|
}
|
|
3891
|
-
if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field
|
|
4056
|
+
if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
|
|
3892
4057
|
|
|
3893
4058
|
const calculated = this.getCalculatedValues(state);
|
|
3894
4059
|
if (!calculated) {
|
|
@@ -3907,19 +4072,55 @@ class Component {
|
|
|
3907
4072
|
}
|
|
3908
4073
|
|
|
3909
4074
|
getCalculatedValues(state) {
|
|
3910
|
-
|
|
3911
|
-
if (entries.length === 0) {
|
|
4075
|
+
if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
|
|
3912
4076
|
return
|
|
3913
4077
|
}
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4078
|
+
|
|
4079
|
+
const mergedState = { ...state };
|
|
4080
|
+
const computedSoFar = {};
|
|
4081
|
+
|
|
4082
|
+
for (const [field, { fn, deps }] of this._calculatedOrder) {
|
|
4083
|
+
if (deps !== null && this._calculatedFieldCache) {
|
|
4084
|
+
const cache = this._calculatedFieldCache[field];
|
|
4085
|
+
const currentDepValues = deps.map(d => mergedState[d]);
|
|
4086
|
+
|
|
4087
|
+
if (cache.lastDepValues !== undefined) {
|
|
4088
|
+
let unchanged = true;
|
|
4089
|
+
for (let i = 0; i < currentDepValues.length; i++) {
|
|
4090
|
+
if (currentDepValues[i] !== cache.lastDepValues[i]) {
|
|
4091
|
+
unchanged = false;
|
|
4092
|
+
break
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
if (unchanged) {
|
|
4096
|
+
computedSoFar[field] = cache.lastResult;
|
|
4097
|
+
mergedState[field] = cache.lastResult;
|
|
4098
|
+
continue
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
try {
|
|
4103
|
+
const result = fn(mergedState);
|
|
4104
|
+
cache.lastDepValues = currentDepValues;
|
|
4105
|
+
cache.lastResult = result;
|
|
4106
|
+
computedSoFar[field] = result;
|
|
4107
|
+
mergedState[field] = result;
|
|
4108
|
+
} catch (e) {
|
|
4109
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4110
|
+
}
|
|
4111
|
+
} else {
|
|
4112
|
+
// No deps declared — always recompute
|
|
4113
|
+
try {
|
|
4114
|
+
const result = fn(mergedState);
|
|
4115
|
+
computedSoFar[field] = result;
|
|
4116
|
+
mergedState[field] = result;
|
|
4117
|
+
} catch (e) {
|
|
4118
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4119
|
+
}
|
|
3920
4120
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
4121
|
+
}
|
|
4122
|
+
|
|
4123
|
+
return computedSoFar
|
|
3923
4124
|
}
|
|
3924
4125
|
|
|
3925
4126
|
cleanupCalculated(incomingState) {
|
|
@@ -4067,7 +4268,7 @@ class Component {
|
|
|
4067
4268
|
this.newChildSources(childSources);
|
|
4068
4269
|
|
|
4069
4270
|
|
|
4070
|
-
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${
|
|
4271
|
+
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true);
|
|
4071
4272
|
|
|
4072
4273
|
return newComponents
|
|
4073
4274
|
}, {})
|
|
@@ -4133,7 +4334,7 @@ class Component {
|
|
|
4133
4334
|
} else if (this.components[collectionOf]) {
|
|
4134
4335
|
factory = this.components[collectionOf];
|
|
4135
4336
|
} else {
|
|
4136
|
-
throw new Error(`[${this.name}] Invalid 'of'
|
|
4337
|
+
throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
|
|
4137
4338
|
}
|
|
4138
4339
|
|
|
4139
4340
|
const fieldLense = {
|
|
@@ -4141,7 +4342,7 @@ class Component {
|
|
|
4141
4342
|
if (!Array.isArray(state[stateField])) return []
|
|
4142
4343
|
const items = state[stateField];
|
|
4143
4344
|
const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
|
|
4144
|
-
const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4345
|
+
const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4145
4346
|
const mapped = sorted.map((item, index) => {
|
|
4146
4347
|
return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
|
|
4147
4348
|
});
|
|
@@ -4150,7 +4351,7 @@ class Component {
|
|
|
4150
4351
|
},
|
|
4151
4352
|
set: (oldState, newState) => {
|
|
4152
4353
|
if (this.calculated && stateField in this.calculated) {
|
|
4153
|
-
console.warn(`Collection sub-component of ${
|
|
4354
|
+
console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4154
4355
|
return oldState
|
|
4155
4356
|
}
|
|
4156
4357
|
const updated = [];
|
|
@@ -4179,17 +4380,17 @@ class Component {
|
|
|
4179
4380
|
} else if (typeof stateField === 'string') {
|
|
4180
4381
|
if (isObj(this.currentState)) {
|
|
4181
4382
|
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
4182
|
-
console.error(`Collection component in ${
|
|
4383
|
+
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
4384
|
lense = undefined;
|
|
4184
4385
|
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
4185
|
-
console.warn(`State property '${
|
|
4386
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4186
4387
|
lense = fieldLense;
|
|
4187
4388
|
} else {
|
|
4188
4389
|
lense = fieldLense;
|
|
4189
4390
|
}
|
|
4190
4391
|
} else {
|
|
4191
4392
|
if (!Array.isArray(this.currentState[stateField])) {
|
|
4192
|
-
console.warn(`State property '${
|
|
4393
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4193
4394
|
lense = fieldLense;
|
|
4194
4395
|
} else {
|
|
4195
4396
|
lense = fieldLense;
|
|
@@ -4197,14 +4398,14 @@ class Component {
|
|
|
4197
4398
|
}
|
|
4198
4399
|
} else if (isObj(stateField)) {
|
|
4199
4400
|
if (typeof stateField.get !== 'function') {
|
|
4200
|
-
console.error(`Collection component in ${
|
|
4401
|
+
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
4402
|
lense = undefined;
|
|
4202
4403
|
} else {
|
|
4203
4404
|
lense = {
|
|
4204
4405
|
get: (state) => {
|
|
4205
4406
|
const newState = stateField.get(state);
|
|
4206
4407
|
if (!Array.isArray(newState)) {
|
|
4207
|
-
console.warn(`State getter function in collection component of ${
|
|
4408
|
+
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
4409
|
return []
|
|
4209
4410
|
}
|
|
4210
4411
|
return newState
|
|
@@ -4213,14 +4414,14 @@ class Component {
|
|
|
4213
4414
|
};
|
|
4214
4415
|
}
|
|
4215
4416
|
} else {
|
|
4216
|
-
console.error(`Collection component in ${
|
|
4417
|
+
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
4418
|
lense = undefined;
|
|
4218
4419
|
}
|
|
4219
4420
|
|
|
4220
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
|
|
4421
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber };
|
|
4221
4422
|
const sink$ = collection(factory, lense, { container: null })(sources);
|
|
4222
4423
|
if (!isObj(sink$)) {
|
|
4223
|
-
throw new Error(
|
|
4424
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
4224
4425
|
}
|
|
4225
4426
|
return sink$
|
|
4226
4427
|
}
|
|
@@ -4242,7 +4443,7 @@ class Component {
|
|
|
4242
4443
|
get: state => state[stateField],
|
|
4243
4444
|
set: (oldState, newState) => {
|
|
4244
4445
|
if (this.calculated && stateField in this.calculated) {
|
|
4245
|
-
console.warn(`Switchable sub-component of ${
|
|
4446
|
+
console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4246
4447
|
return oldState
|
|
4247
4448
|
}
|
|
4248
4449
|
if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
|
|
@@ -4261,13 +4462,13 @@ class Component {
|
|
|
4261
4462
|
lense = fieldLense;
|
|
4262
4463
|
} else if (isObj(stateField)) {
|
|
4263
4464
|
if (typeof stateField.get !== 'function') {
|
|
4264
|
-
console.error(`Switchable component in ${
|
|
4465
|
+
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
4466
|
lense = baseLense;
|
|
4266
4467
|
} else {
|
|
4267
4468
|
lense = { get: stateField.get, set: stateField.set };
|
|
4268
4469
|
}
|
|
4269
4470
|
} else {
|
|
4270
|
-
console.error(`Invalid state provided to switchable sub-component of ${
|
|
4471
|
+
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
4472
|
lense = baseLense;
|
|
4272
4473
|
}
|
|
4273
4474
|
|
|
@@ -4283,12 +4484,12 @@ class Component {
|
|
|
4283
4484
|
switchableComponents[key] = component(options);
|
|
4284
4485
|
}
|
|
4285
4486
|
});
|
|
4286
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4487
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4287
4488
|
|
|
4288
4489
|
const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
|
|
4289
4490
|
|
|
4290
4491
|
if (!isObj(sink$)) {
|
|
4291
|
-
throw new Error(
|
|
4492
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
|
|
4292
4493
|
}
|
|
4293
4494
|
|
|
4294
4495
|
return sink$
|
|
@@ -4314,7 +4515,7 @@ class Component {
|
|
|
4314
4515
|
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
|
|
4315
4516
|
if (!factory) {
|
|
4316
4517
|
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: ${
|
|
4518
|
+
throw new Error(`Component not found: ${componentName}`)
|
|
4318
4519
|
}
|
|
4319
4520
|
|
|
4320
4521
|
let lense;
|
|
@@ -4323,7 +4524,7 @@ class Component {
|
|
|
4323
4524
|
get: state => state[stateField],
|
|
4324
4525
|
set: (oldState, newState) => {
|
|
4325
4526
|
if (this.calculated && stateField in this.calculated) {
|
|
4326
|
-
console.warn(`Sub-component of ${
|
|
4527
|
+
console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4327
4528
|
return oldState
|
|
4328
4529
|
}
|
|
4329
4530
|
return { ...oldState, [stateField]: newState }
|
|
@@ -4341,17 +4542,17 @@ class Component {
|
|
|
4341
4542
|
lense = fieldLense;
|
|
4342
4543
|
} else if (isObj(stateField)) {
|
|
4343
4544
|
if (typeof stateField.get !== 'function') {
|
|
4344
|
-
console.error(`Sub-component in ${
|
|
4545
|
+
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
4546
|
lense = baseLense;
|
|
4346
4547
|
} else {
|
|
4347
4548
|
lense = { get: stateField.get, set: stateField.set };
|
|
4348
4549
|
}
|
|
4349
4550
|
} else {
|
|
4350
|
-
console.error(`Invalid state provided to sub-component of ${
|
|
4551
|
+
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
4552
|
lense = baseLense;
|
|
4352
4553
|
}
|
|
4353
4554
|
|
|
4354
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4555
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4355
4556
|
const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
|
|
4356
4557
|
|
|
4357
4558
|
if (!isObj(sink$)) {
|
|
@@ -4426,14 +4627,22 @@ class Component {
|
|
|
4426
4627
|
const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
|
|
4427
4628
|
if (immediate) {
|
|
4428
4629
|
if (this.debug) {
|
|
4429
|
-
|
|
4630
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4631
|
+
console.log(text);
|
|
4632
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4633
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4634
|
+
}
|
|
4430
4635
|
}
|
|
4431
4636
|
return
|
|
4432
4637
|
} else {
|
|
4433
4638
|
return stream => {
|
|
4434
4639
|
return stream.debug(msg => {
|
|
4435
4640
|
if (this.debug) {
|
|
4436
|
-
|
|
4641
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4642
|
+
console.log(text);
|
|
4643
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4644
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4645
|
+
}
|
|
4437
4646
|
}
|
|
4438
4647
|
})
|
|
4439
4648
|
}
|
|
@@ -4443,11 +4652,11 @@ class Component {
|
|
|
4443
4652
|
|
|
4444
4653
|
|
|
4445
4654
|
|
|
4446
|
-
function getComponents(currentElement, componentNames,
|
|
4655
|
+
function getComponents(currentElement, componentNames, path='r', parentId) {
|
|
4447
4656
|
if (!currentElement) return {}
|
|
4448
4657
|
|
|
4449
4658
|
if (currentElement.data?.componentsProcessed) return {}
|
|
4450
|
-
if (
|
|
4659
|
+
if (path === 'r') currentElement.data.componentsProcessed = true;
|
|
4451
4660
|
|
|
4452
4661
|
const sel = currentElement.sel;
|
|
4453
4662
|
const isCollection = sel && sel.toLowerCase() === 'collection';
|
|
@@ -4461,11 +4670,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4461
4670
|
|
|
4462
4671
|
let id = parentId;
|
|
4463
4672
|
if (isComponent) {
|
|
4464
|
-
id = getComponentIdFromElement(currentElement,
|
|
4673
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4465
4674
|
if (isCollection) {
|
|
4466
4675
|
if (!props.of) throw new Error(`Collection element missing required 'component' property`)
|
|
4467
4676
|
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: ${
|
|
4677
|
+
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
|
|
4469
4678
|
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
4679
|
currentElement.data.isCollection = true;
|
|
4471
4680
|
currentElement.data.props ||= {};
|
|
@@ -4476,7 +4685,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4476
4685
|
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
4686
|
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
4687
|
const switchableComponentNames = Object.keys(props.of);
|
|
4479
|
-
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${
|
|
4688
|
+
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
|
|
4480
4689
|
currentElement.data.isSwitchable = true;
|
|
4481
4690
|
} else ;
|
|
4482
4691
|
if (typeof props.key === 'undefined') currentElement.data.props.key = id;
|
|
@@ -4484,7 +4693,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4484
4693
|
}
|
|
4485
4694
|
|
|
4486
4695
|
if (children.length > 0) {
|
|
4487
|
-
children.map((child, i) => getComponents(child, componentNames,
|
|
4696
|
+
children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
|
|
4488
4697
|
.forEach((child) => {
|
|
4489
4698
|
Object.entries(child).forEach(([id, el]) => found[id] = el);
|
|
4490
4699
|
});
|
|
@@ -4493,10 +4702,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4493
4702
|
return found
|
|
4494
4703
|
}
|
|
4495
4704
|
|
|
4496
|
-
function injectComponents(currentElement, components, componentNames,
|
|
4705
|
+
function injectComponents(currentElement, components, componentNames, path='r', parentId) {
|
|
4497
4706
|
if (!currentElement) return
|
|
4498
4707
|
if (currentElement.data?.componentsInjected) return currentElement
|
|
4499
|
-
if (
|
|
4708
|
+
if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true;
|
|
4500
4709
|
|
|
4501
4710
|
|
|
4502
4711
|
const sel = currentElement.sel || 'NO SELECTOR';
|
|
@@ -4508,7 +4717,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4508
4717
|
|
|
4509
4718
|
let id = parentId;
|
|
4510
4719
|
if (isComponent) {
|
|
4511
|
-
id = getComponentIdFromElement(currentElement,
|
|
4720
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4512
4721
|
const component = components[id];
|
|
4513
4722
|
if (isCollection) {
|
|
4514
4723
|
currentElement.sel = 'div';
|
|
@@ -4520,21 +4729,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4520
4729
|
return component
|
|
4521
4730
|
}
|
|
4522
4731
|
} else if (children.length > 0) {
|
|
4523
|
-
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames,
|
|
4732
|
+
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat();
|
|
4524
4733
|
return currentElement
|
|
4525
4734
|
} else {
|
|
4526
4735
|
return currentElement
|
|
4527
4736
|
}
|
|
4528
4737
|
}
|
|
4529
4738
|
|
|
4530
|
-
function getComponentIdFromElement(el,
|
|
4739
|
+
function getComponentIdFromElement(el, path, parentId) {
|
|
4531
4740
|
const sel = el.sel;
|
|
4532
4741
|
const name = typeof sel === 'string' ? sel : 'functionComponent';
|
|
4533
|
-
const uid = `${depth}:${index}`;
|
|
4534
4742
|
const props = el.data?.props || {};
|
|
4535
|
-
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) ||
|
|
4536
|
-
const parentString = parentId ? `${
|
|
4537
|
-
const fullId = `${
|
|
4743
|
+
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path;
|
|
4744
|
+
const parentString = parentId ? `${parentId}|` : '';
|
|
4745
|
+
const fullId = `${parentString}${name}::${id}`;
|
|
4538
4746
|
return fullId
|
|
4539
4747
|
}
|
|
4540
4748
|
|
|
@@ -4678,12 +4886,264 @@ function sortFunctionFromProp(sortProp) {
|
|
|
4678
4886
|
} else if (isObj(sortProp)) {
|
|
4679
4887
|
return __sortFunctionFromObj(sortProp)
|
|
4680
4888
|
} else {
|
|
4681
|
-
console.error('Invalid sort option (ignoring):',
|
|
4889
|
+
console.error('Invalid sort option (ignoring):', sortProp);
|
|
4682
4890
|
return undefined
|
|
4683
4891
|
}
|
|
4684
4892
|
}
|
|
4685
4893
|
|
|
4894
|
+
const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
|
|
4895
|
+
const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
|
|
4896
|
+
const DEFAULT_MAX_HISTORY = 200;
|
|
4897
|
+
|
|
4898
|
+
class SygnalDevTools {
|
|
4899
|
+
constructor() {
|
|
4900
|
+
this._connected = false;
|
|
4901
|
+
this._components = new Map();
|
|
4902
|
+
this._stateHistory = [];
|
|
4903
|
+
this._maxHistory = DEFAULT_MAX_HISTORY;
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
get connected() {
|
|
4907
|
+
return this._connected && typeof window !== 'undefined'
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
// ─── Initialization ─────────────────────────────────────────────────────────
|
|
4911
|
+
|
|
4912
|
+
init() {
|
|
4913
|
+
if (typeof window === 'undefined') return
|
|
4914
|
+
|
|
4915
|
+
window.__SYGNAL_DEVTOOLS__ = this;
|
|
4916
|
+
|
|
4917
|
+
window.addEventListener('message', (event) => {
|
|
4918
|
+
if (event.source !== window) return
|
|
4919
|
+
if (event.data?.source === EXTENSION_SOURCE) {
|
|
4920
|
+
this._handleExtensionMessage(event.data);
|
|
4921
|
+
}
|
|
4922
|
+
});
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
_handleExtensionMessage(msg) {
|
|
4926
|
+
switch (msg.type) {
|
|
4927
|
+
case 'CONNECT':
|
|
4928
|
+
this._connected = true;
|
|
4929
|
+
if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
|
|
4930
|
+
this._sendFullTree();
|
|
4931
|
+
break
|
|
4932
|
+
case 'DISCONNECT':
|
|
4933
|
+
this._connected = false;
|
|
4934
|
+
break
|
|
4935
|
+
case 'SET_DEBUG':
|
|
4936
|
+
this._setDebug(msg.payload);
|
|
4937
|
+
break
|
|
4938
|
+
case 'TIME_TRAVEL':
|
|
4939
|
+
this._timeTravel(msg.payload);
|
|
4940
|
+
break
|
|
4941
|
+
case 'GET_STATE':
|
|
4942
|
+
this._sendComponentState(msg.payload.componentId);
|
|
4943
|
+
break
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
|
|
4947
|
+
// ─── Hooks (called from component.js) ────────────────────────────────────────
|
|
4948
|
+
|
|
4949
|
+
onComponentCreated(componentNumber, name, instance) {
|
|
4950
|
+
const meta = {
|
|
4951
|
+
id: componentNumber,
|
|
4952
|
+
name: name,
|
|
4953
|
+
isSubComponent: instance.isSubComponent,
|
|
4954
|
+
hasModel: !!instance.model,
|
|
4955
|
+
hasIntent: !!instance.intent,
|
|
4956
|
+
hasContext: !!instance.context,
|
|
4957
|
+
hasCalculated: !!instance.calculated,
|
|
4958
|
+
components: Object.keys(instance.components || {}),
|
|
4959
|
+
parentId: null,
|
|
4960
|
+
children: [],
|
|
4961
|
+
debug: instance._debug,
|
|
4962
|
+
createdAt: Date.now(),
|
|
4963
|
+
_instanceRef: new WeakRef(instance),
|
|
4964
|
+
};
|
|
4965
|
+
this._components.set(componentNumber, meta);
|
|
4966
|
+
|
|
4967
|
+
if (!this.connected) return
|
|
4968
|
+
this._post('COMPONENT_CREATED', this._serializeMeta(meta));
|
|
4969
|
+
}
|
|
4970
|
+
|
|
4971
|
+
onStateChanged(componentNumber, name, state) {
|
|
4972
|
+
if (!this.connected) return
|
|
4973
|
+
|
|
4974
|
+
const entry = {
|
|
4975
|
+
componentId: componentNumber,
|
|
4976
|
+
componentName: name,
|
|
4977
|
+
timestamp: Date.now(),
|
|
4978
|
+
state: this._safeClone(state),
|
|
4979
|
+
};
|
|
4980
|
+
|
|
4981
|
+
this._stateHistory.push(entry);
|
|
4982
|
+
if (this._stateHistory.length > this._maxHistory) {
|
|
4983
|
+
this._stateHistory.shift();
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
this._post('STATE_CHANGED', {
|
|
4987
|
+
componentId: componentNumber,
|
|
4988
|
+
componentName: name,
|
|
4989
|
+
state: entry.state,
|
|
4990
|
+
historyIndex: this._stateHistory.length - 1,
|
|
4991
|
+
});
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
onActionDispatched(componentNumber, name, actionType, data) {
|
|
4995
|
+
if (!this.connected) return
|
|
4996
|
+
this._post('ACTION_DISPATCHED', {
|
|
4997
|
+
componentId: componentNumber,
|
|
4998
|
+
componentName: name,
|
|
4999
|
+
actionType: actionType,
|
|
5000
|
+
data: this._safeClone(data),
|
|
5001
|
+
timestamp: Date.now(),
|
|
5002
|
+
});
|
|
5003
|
+
}
|
|
5004
|
+
|
|
5005
|
+
onSubComponentRegistered(parentNumber, childNumber) {
|
|
5006
|
+
const parent = this._components.get(parentNumber);
|
|
5007
|
+
const child = this._components.get(childNumber);
|
|
5008
|
+
if (parent && child) {
|
|
5009
|
+
child.parentId = parentNumber;
|
|
5010
|
+
if (!parent.children.includes(childNumber)) {
|
|
5011
|
+
parent.children.push(childNumber);
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
|
|
5015
|
+
if (!this.connected) return
|
|
5016
|
+
this._post('TREE_UPDATED', {
|
|
5017
|
+
parentId: parentNumber,
|
|
5018
|
+
childId: childNumber,
|
|
5019
|
+
});
|
|
5020
|
+
}
|
|
5021
|
+
|
|
5022
|
+
onContextChanged(componentNumber, name, context) {
|
|
5023
|
+
if (!this.connected) return
|
|
5024
|
+
this._post('CONTEXT_CHANGED', {
|
|
5025
|
+
componentId: componentNumber,
|
|
5026
|
+
componentName: name,
|
|
5027
|
+
context: this._safeClone(context),
|
|
5028
|
+
});
|
|
5029
|
+
}
|
|
5030
|
+
|
|
5031
|
+
onDebugLog(componentNumber, message) {
|
|
5032
|
+
if (!this.connected) return
|
|
5033
|
+
this._post('DEBUG_LOG', {
|
|
5034
|
+
componentId: componentNumber,
|
|
5035
|
+
message: message,
|
|
5036
|
+
timestamp: Date.now(),
|
|
5037
|
+
});
|
|
5038
|
+
}
|
|
5039
|
+
|
|
5040
|
+
// ─── Commands (from extension to page) ───────────────────────────────────────
|
|
5041
|
+
|
|
5042
|
+
_setDebug({ componentId, enabled }) {
|
|
5043
|
+
if (typeof componentId === 'undefined' || componentId === null) {
|
|
5044
|
+
if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
|
|
5045
|
+
this._post('DEBUG_TOGGLED', { global: true, enabled });
|
|
5046
|
+
return
|
|
5047
|
+
}
|
|
5048
|
+
|
|
5049
|
+
const meta = this._components.get(componentId);
|
|
5050
|
+
if (meta && meta._instanceRef) {
|
|
5051
|
+
const instance = meta._instanceRef.deref();
|
|
5052
|
+
if (instance) {
|
|
5053
|
+
instance._debug = enabled;
|
|
5054
|
+
meta.debug = enabled;
|
|
5055
|
+
this._post('DEBUG_TOGGLED', { componentId, enabled });
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
}
|
|
5059
|
+
|
|
5060
|
+
_timeTravel({ historyIndex }) {
|
|
5061
|
+
const entry = this._stateHistory[historyIndex];
|
|
5062
|
+
if (!entry) return
|
|
5063
|
+
|
|
5064
|
+
const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__;
|
|
5065
|
+
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
5066
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
|
|
5067
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5068
|
+
historyIndex,
|
|
5069
|
+
state: entry.state,
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
_sendComponentState(componentId) {
|
|
5075
|
+
const meta = this._components.get(componentId);
|
|
5076
|
+
if (meta && meta._instanceRef) {
|
|
5077
|
+
const instance = meta._instanceRef.deref();
|
|
5078
|
+
if (instance) {
|
|
5079
|
+
this._post('COMPONENT_STATE', {
|
|
5080
|
+
componentId,
|
|
5081
|
+
state: this._safeClone(instance.currentState),
|
|
5082
|
+
context: this._safeClone(instance.currentContext),
|
|
5083
|
+
props: this._safeClone(instance.currentProps),
|
|
5084
|
+
});
|
|
5085
|
+
}
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
|
|
5089
|
+
_sendFullTree() {
|
|
5090
|
+
const tree = [];
|
|
5091
|
+
for (const [id, meta] of this._components) {
|
|
5092
|
+
const instance = meta._instanceRef?.deref();
|
|
5093
|
+
tree.push({
|
|
5094
|
+
...this._serializeMeta(meta),
|
|
5095
|
+
state: instance ? this._safeClone(instance.currentState) : null,
|
|
5096
|
+
context: instance ? this._safeClone(instance.currentContext) : null,
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
this._post('FULL_TREE', {
|
|
5100
|
+
components: tree,
|
|
5101
|
+
history: this._stateHistory,
|
|
5102
|
+
});
|
|
5103
|
+
}
|
|
5104
|
+
|
|
5105
|
+
// ─── Transport ───────────────────────────────────────────────────────────────
|
|
5106
|
+
|
|
5107
|
+
_post(type, payload) {
|
|
5108
|
+
if (typeof window === 'undefined') return
|
|
5109
|
+
window.postMessage({
|
|
5110
|
+
source: DEVTOOLS_SOURCE,
|
|
5111
|
+
type,
|
|
5112
|
+
payload,
|
|
5113
|
+
}, '*');
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
_safeClone(obj) {
|
|
5117
|
+
if (obj === undefined || obj === null) return obj
|
|
5118
|
+
try {
|
|
5119
|
+
return JSON.parse(JSON.stringify(obj))
|
|
5120
|
+
} catch (e) {
|
|
5121
|
+
return '[unserializable]'
|
|
5122
|
+
}
|
|
5123
|
+
}
|
|
5124
|
+
|
|
5125
|
+
_serializeMeta(meta) {
|
|
5126
|
+
const { _instanceRef, ...rest } = meta;
|
|
5127
|
+
return rest
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
5130
|
+
|
|
5131
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
5132
|
+
|
|
5133
|
+
let instance = null;
|
|
5134
|
+
|
|
5135
|
+
function getDevTools() {
|
|
5136
|
+
if (!instance) instance = new SygnalDevTools();
|
|
5137
|
+
return instance
|
|
5138
|
+
}
|
|
5139
|
+
|
|
4686
5140
|
function run(app, drivers={}, options={}) {
|
|
5141
|
+
// Initialize DevTools instrumentation bridge early (before component creation)
|
|
5142
|
+
if (typeof window !== 'undefined') {
|
|
5143
|
+
const dt = getDevTools();
|
|
5144
|
+
dt.init();
|
|
5145
|
+
}
|
|
5146
|
+
|
|
4687
5147
|
const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options;
|
|
4688
5148
|
if (!app.isSygnalComponent) {
|
|
4689
5149
|
const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
|
|
@@ -4733,6 +5193,11 @@ function run(app, drivers={}, options={}) {
|
|
|
4733
5193
|
|
|
4734
5194
|
const exposed = { sources, sinks, dispose };
|
|
4735
5195
|
|
|
5196
|
+
// Store app reference for time-travel
|
|
5197
|
+
if (typeof window !== 'undefined') {
|
|
5198
|
+
window.__SYGNAL_DEVTOOLS_APP__ = exposed;
|
|
5199
|
+
}
|
|
5200
|
+
|
|
4736
5201
|
const swapToComponent = (newComponent, state) => {
|
|
4737
5202
|
const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
|
|
4738
5203
|
const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
|