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.mjs
CHANGED
|
@@ -90,7 +90,8 @@ function eventBusDriver(out$) {
|
|
|
90
90
|
const events = new EventTarget();
|
|
91
91
|
|
|
92
92
|
out$.subscribe({
|
|
93
|
-
next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
|
|
93
|
+
next: event => events.dispatchEvent(new CustomEvent('data', { detail: event })),
|
|
94
|
+
error: err => console.error('[EVENTS driver] Error in sink stream:', err)
|
|
94
95
|
});
|
|
95
96
|
|
|
96
97
|
return {
|
|
@@ -118,11 +119,19 @@ function logDriver(out$) {
|
|
|
118
119
|
out$.addListener({
|
|
119
120
|
next: (val) => {
|
|
120
121
|
console.log(val);
|
|
122
|
+
},
|
|
123
|
+
error: (err) => {
|
|
124
|
+
console.error('[LOG driver] Error in sink stream:', err);
|
|
121
125
|
}
|
|
122
126
|
});
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
let COLLECTION_COUNT = 0;
|
|
130
|
+
|
|
125
131
|
function collection(component, stateLense, opts={}) {
|
|
132
|
+
if (typeof component !== 'function') {
|
|
133
|
+
throw new Error('collection: first argument (component) must be a function')
|
|
134
|
+
}
|
|
126
135
|
const {
|
|
127
136
|
combineList = ['DOM'],
|
|
128
137
|
globalList = ['EVENTS'],
|
|
@@ -133,7 +142,7 @@ function collection(component, stateLense, opts={}) {
|
|
|
133
142
|
} = opts;
|
|
134
143
|
|
|
135
144
|
return (sources) => {
|
|
136
|
-
const key =
|
|
145
|
+
const key = `sygnal-collection-${COLLECTION_COUNT++}`;
|
|
137
146
|
const collectionOpts = {
|
|
138
147
|
item: component,
|
|
139
148
|
itemKey: (state, ind) => typeof state.id !== 'undefined' ? state.id : ind,
|
|
@@ -2904,7 +2913,7 @@ function switchable(factories, name$, initial, opts={}) {
|
|
|
2904
2913
|
const mapFunction = (nameType === 'function' && name$) || (state => state[name$]);
|
|
2905
2914
|
return sources => {
|
|
2906
2915
|
const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream;
|
|
2907
|
-
if (!state$ instanceof Stream$1) throw new Error(`Could not find the state source: ${
|
|
2916
|
+
if (!(state$ instanceof Stream$1)) throw new Error(`Could not find the state source: ${stateSourceName}`)
|
|
2908
2917
|
const _name$ = state$
|
|
2909
2918
|
.map(mapFunction)
|
|
2910
2919
|
.filter(name => typeof name === 'string')
|
|
@@ -3311,13 +3320,27 @@ function wrapDOMSource(domSource) {
|
|
|
3311
3320
|
}
|
|
3312
3321
|
|
|
3313
3322
|
|
|
3314
|
-
const ABORT = '
|
|
3323
|
+
const ABORT = Symbol('ABORT');
|
|
3324
|
+
|
|
3325
|
+
|
|
3326
|
+
function normalizeCalculatedEntry(field, entry) {
|
|
3327
|
+
if (typeof entry === 'function') {
|
|
3328
|
+
return { fn: entry, deps: null }
|
|
3329
|
+
}
|
|
3330
|
+
if (Array.isArray(entry) && entry.length === 2
|
|
3331
|
+
&& Array.isArray(entry[0]) && typeof entry[1] === 'function') {
|
|
3332
|
+
return { fn: entry[1], deps: entry[0] }
|
|
3333
|
+
}
|
|
3334
|
+
throw new Error(
|
|
3335
|
+
`Invalid calculated field '${field}': expected a function or [depsArray, function]`
|
|
3336
|
+
)
|
|
3337
|
+
}
|
|
3315
3338
|
|
|
3316
3339
|
function component (opts) {
|
|
3317
3340
|
const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
|
|
3318
3341
|
|
|
3319
3342
|
if (sources && !isObj(sources)) {
|
|
3320
|
-
throw new Error(
|
|
3343
|
+
throw new Error(`[${name}] Sources must be a Cycle.js sources object`)
|
|
3321
3344
|
}
|
|
3322
3345
|
|
|
3323
3346
|
let fixedIsolateOpts;
|
|
@@ -3397,7 +3420,9 @@ class Component {
|
|
|
3397
3420
|
// sinks
|
|
3398
3421
|
|
|
3399
3422
|
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 }) {
|
|
3400
|
-
if (!sources || !isObj(sources)) throw new Error(
|
|
3423
|
+
if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
|
|
3424
|
+
|
|
3425
|
+
this._componentNumber = COMPONENT_COUNT++;
|
|
3401
3426
|
|
|
3402
3427
|
this.name = name;
|
|
3403
3428
|
this.sources = sources;
|
|
@@ -3418,6 +3443,123 @@ class Component {
|
|
|
3418
3443
|
this.sourceNames = Object.keys(sources);
|
|
3419
3444
|
this._debug = debug;
|
|
3420
3445
|
|
|
3446
|
+
// Warn if calculated fields shadow base state keys
|
|
3447
|
+
if (this.calculated && this.initialState
|
|
3448
|
+
&& isObj(this.calculated) && isObj(this.initialState)) {
|
|
3449
|
+
for (const key of Object.keys(this.calculated)) {
|
|
3450
|
+
if (key in this.initialState) {
|
|
3451
|
+
console.warn(
|
|
3452
|
+
`[${name}] Calculated field '${key}' shadows a key in initialState. ` +
|
|
3453
|
+
`The initialState value will be overwritten on every state update.`
|
|
3454
|
+
);
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
|
|
3459
|
+
// Normalize calculated entries, build dependency graph, topological sort
|
|
3460
|
+
if (this.calculated && isObj(this.calculated)) {
|
|
3461
|
+
const calcEntries = Object.entries(this.calculated);
|
|
3462
|
+
|
|
3463
|
+
// Normalize all entries to { fn, deps } shape
|
|
3464
|
+
this._calculatedNormalized = {};
|
|
3465
|
+
for (const [field, entry] of calcEntries) {
|
|
3466
|
+
this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry);
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3469
|
+
this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized));
|
|
3470
|
+
|
|
3471
|
+
// Warn on deps referencing nonexistent keys
|
|
3472
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
3473
|
+
if (deps !== null) {
|
|
3474
|
+
for (const dep of deps) {
|
|
3475
|
+
if (!this._calculatedFieldNames.has(dep)
|
|
3476
|
+
&& this.initialState && !(dep in this.initialState)) {
|
|
3477
|
+
console.warn(
|
|
3478
|
+
`[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
|
|
3479
|
+
`which is not in initialState or calculated fields`
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
// Build adjacency: for each field, which other calculated fields must run first?
|
|
3487
|
+
const calcDeps = {};
|
|
3488
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
3489
|
+
if (deps === null) {
|
|
3490
|
+
calcDeps[field] = [];
|
|
3491
|
+
} else {
|
|
3492
|
+
calcDeps[field] = deps.filter(d => this._calculatedFieldNames.has(d));
|
|
3493
|
+
}
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
// Kahn's algorithm for topological sort
|
|
3497
|
+
const inDegree = {};
|
|
3498
|
+
const reverseGraph = {};
|
|
3499
|
+
for (const field of this._calculatedFieldNames) {
|
|
3500
|
+
inDegree[field] = 0;
|
|
3501
|
+
reverseGraph[field] = [];
|
|
3502
|
+
}
|
|
3503
|
+
for (const [field, depList] of Object.entries(calcDeps)) {
|
|
3504
|
+
inDegree[field] = depList.length;
|
|
3505
|
+
for (const dep of depList) {
|
|
3506
|
+
reverseGraph[dep].push(field);
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
const queue = [];
|
|
3511
|
+
for (const [field, degree] of Object.entries(inDegree)) {
|
|
3512
|
+
if (degree === 0) queue.push(field);
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
const sorted = [];
|
|
3516
|
+
while (queue.length > 0) {
|
|
3517
|
+
const current = queue.shift();
|
|
3518
|
+
sorted.push(current);
|
|
3519
|
+
for (const dependent of reverseGraph[current]) {
|
|
3520
|
+
inDegree[dependent]--;
|
|
3521
|
+
if (inDegree[dependent] === 0) queue.push(dependent);
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
if (sorted.length !== this._calculatedFieldNames.size) {
|
|
3526
|
+
// Cycle detected — build error message with cycle path
|
|
3527
|
+
const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f));
|
|
3528
|
+
const visited = new Set();
|
|
3529
|
+
const path = [];
|
|
3530
|
+
const traceCycle = (node) => {
|
|
3531
|
+
if (visited.has(node)) { path.push(node); return true }
|
|
3532
|
+
visited.add(node);
|
|
3533
|
+
path.push(node);
|
|
3534
|
+
for (const dep of calcDeps[node]) {
|
|
3535
|
+
if (inCycle.includes(dep) && traceCycle(dep)) return true
|
|
3536
|
+
}
|
|
3537
|
+
path.pop();
|
|
3538
|
+
visited.delete(node);
|
|
3539
|
+
return false
|
|
3540
|
+
};
|
|
3541
|
+
traceCycle(inCycle[0]);
|
|
3542
|
+
const start = path[path.length - 1];
|
|
3543
|
+
const cycle = path.slice(path.indexOf(start));
|
|
3544
|
+
throw new Error(`Circular calculated dependency: ${cycle.join(' \u2192 ')}`)
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized[f]]);
|
|
3548
|
+
|
|
3549
|
+
// Initialize per-field memoization caches for fields with declared deps
|
|
3550
|
+
this._calculatedFieldCache = {};
|
|
3551
|
+
for (const [field, { deps }] of this._calculatedOrder) {
|
|
3552
|
+
if (deps !== null) {
|
|
3553
|
+
this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined };
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
} else {
|
|
3557
|
+
this._calculatedOrder = null;
|
|
3558
|
+
this._calculatedNormalized = null;
|
|
3559
|
+
this._calculatedFieldNames = null;
|
|
3560
|
+
this._calculatedFieldCache = null;
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3421
3563
|
this.isSubComponent = this.sourceNames.includes('props$');
|
|
3422
3564
|
|
|
3423
3565
|
const state$ = sources[stateSourceName] && sources[stateSourceName].stream;
|
|
@@ -3426,6 +3568,9 @@ class Component {
|
|
|
3426
3568
|
this.currentState = initialState || {};
|
|
3427
3569
|
this.sources[stateSourceName] = new StateSource(state$.map(val => {
|
|
3428
3570
|
this.currentState = val;
|
|
3571
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3572
|
+
window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val);
|
|
3573
|
+
}
|
|
3429
3574
|
return val
|
|
3430
3575
|
}));
|
|
3431
3576
|
}
|
|
@@ -3461,10 +3606,8 @@ class Component {
|
|
|
3461
3606
|
};
|
|
3462
3607
|
}
|
|
3463
3608
|
|
|
3464
|
-
const componentNumber = COMPONENT_COUNT++;
|
|
3465
|
-
|
|
3466
3609
|
this.addCalculated = this.createMemoizedAddCalculated();
|
|
3467
|
-
this.log = makeLog(`${
|
|
3610
|
+
this.log = makeLog(`${this._componentNumber} | ${name}`);
|
|
3468
3611
|
|
|
3469
3612
|
this.initChildSources$();
|
|
3470
3613
|
this.initIntent$();
|
|
@@ -3479,9 +3622,20 @@ class Component {
|
|
|
3479
3622
|
this.initVdom$();
|
|
3480
3623
|
this.initSinks();
|
|
3481
3624
|
|
|
3482
|
-
this.sinks.__index =
|
|
3625
|
+
this.sinks.__index = this._componentNumber;
|
|
3483
3626
|
|
|
3484
3627
|
this.log(`Instantiated`, true);
|
|
3628
|
+
|
|
3629
|
+
// Hook 1: Register with DevTools
|
|
3630
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
|
|
3631
|
+
window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this);
|
|
3632
|
+
|
|
3633
|
+
// Hook 1b: Register parent-child relationship
|
|
3634
|
+
const parentNum = sources?.__parentComponentNumber;
|
|
3635
|
+
if (typeof parentNum === 'number') {
|
|
3636
|
+
window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber);
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3485
3639
|
}
|
|
3486
3640
|
|
|
3487
3641
|
get debug() {
|
|
@@ -3493,13 +3647,13 @@ class Component {
|
|
|
3493
3647
|
return
|
|
3494
3648
|
}
|
|
3495
3649
|
if (typeof this.intent != 'function') {
|
|
3496
|
-
throw new Error(
|
|
3650
|
+
throw new Error(`[${this.name}] Intent must be a function`)
|
|
3497
3651
|
}
|
|
3498
3652
|
|
|
3499
3653
|
this.intent$ = this.intent(this.sources);
|
|
3500
3654
|
|
|
3501
3655
|
if (!(this.intent$ instanceof Stream$1) && (!isObj(this.intent$))) {
|
|
3502
|
-
throw new Error(
|
|
3656
|
+
throw new Error(`[${this.name}] Intent must return either an action$ stream or map of event streams`)
|
|
3503
3657
|
}
|
|
3504
3658
|
}
|
|
3505
3659
|
|
|
@@ -3512,10 +3666,10 @@ class Component {
|
|
|
3512
3666
|
this.hmrActions = [this.hmrActions];
|
|
3513
3667
|
}
|
|
3514
3668
|
if (!Array.isArray(this.hmrActions)) {
|
|
3515
|
-
throw new Error(`[${
|
|
3669
|
+
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`)
|
|
3516
3670
|
}
|
|
3517
3671
|
if (this.hmrActions.some(action => typeof action !== 'string')) {
|
|
3518
|
-
throw new Error(`[${
|
|
3672
|
+
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`)
|
|
3519
3673
|
}
|
|
3520
3674
|
this.hmrAction$ = xs$1.fromArray(this.hmrActions.map(action => ({ type: action })));
|
|
3521
3675
|
}
|
|
@@ -3554,7 +3708,15 @@ class Component {
|
|
|
3554
3708
|
const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }));
|
|
3555
3709
|
|
|
3556
3710
|
this.action$ = xs$1.merge(wrapped$, hydrate$)
|
|
3557
|
-
.compose(this.log(({ type }) => `<${
|
|
3711
|
+
.compose(this.log(({ type }) => `<${type}> Action triggered`))
|
|
3712
|
+
.map(action => {
|
|
3713
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3714
|
+
window.__SYGNAL_DEVTOOLS__.onActionDispatched(
|
|
3715
|
+
this._componentNumber, this.name, action.type, action.data
|
|
3716
|
+
);
|
|
3717
|
+
}
|
|
3718
|
+
return action
|
|
3719
|
+
});
|
|
3558
3720
|
}
|
|
3559
3721
|
|
|
3560
3722
|
initState() {
|
|
@@ -3566,7 +3728,7 @@ class Component {
|
|
|
3566
3728
|
} else if (isObj(this.model[INITIALIZE_ACTION])) {
|
|
3567
3729
|
Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
|
|
3568
3730
|
if (name !== this.stateSourceName) {
|
|
3569
|
-
console.warn(`${
|
|
3731
|
+
console.warn(`${INITIALIZE_ACTION} can only be used with the ${this.stateSourceName} source... disregarding ${name}`);
|
|
3570
3732
|
delete this.model[INITIALIZE_ACTION][name];
|
|
3571
3733
|
}
|
|
3572
3734
|
});
|
|
@@ -3601,7 +3763,7 @@ class Component {
|
|
|
3601
3763
|
} else if (valueType === 'function') {
|
|
3602
3764
|
_value = value(state);
|
|
3603
3765
|
} else {
|
|
3604
|
-
console.error(`[${
|
|
3766
|
+
console.error(`[${this.name}] Invalid context entry '${name}': must be the name of a state property or a function returning a value to use`);
|
|
3605
3767
|
return acc
|
|
3606
3768
|
}
|
|
3607
3769
|
acc[name] = _value;
|
|
@@ -3609,11 +3771,14 @@ class Component {
|
|
|
3609
3771
|
}, {});
|
|
3610
3772
|
const newContext = { ..._parent, ...values };
|
|
3611
3773
|
this.currentContext = newContext;
|
|
3774
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3775
|
+
window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext);
|
|
3776
|
+
}
|
|
3612
3777
|
return newContext
|
|
3613
3778
|
})
|
|
3614
3779
|
.compose(dropRepeats(objIsEqual))
|
|
3615
3780
|
.startWith({});
|
|
3616
|
-
this.context$.subscribe({ next: _ => _ });
|
|
3781
|
+
this.context$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in context stream:`, err) });
|
|
3617
3782
|
}
|
|
3618
3783
|
|
|
3619
3784
|
initModel$() {
|
|
@@ -3629,7 +3794,7 @@ class Component {
|
|
|
3629
3794
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
|
|
3630
3795
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
|
|
3631
3796
|
if (this.isSubComponent && this.initialState) {
|
|
3632
|
-
console.warn(`[${
|
|
3797
|
+
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
|
|
3633
3798
|
}
|
|
3634
3799
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined');
|
|
3635
3800
|
const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined');
|
|
@@ -3650,7 +3815,7 @@ class Component {
|
|
|
3650
3815
|
}
|
|
3651
3816
|
|
|
3652
3817
|
if (!isObj(sinks)) {
|
|
3653
|
-
throw new Error(`Entry for each action must be an object: ${
|
|
3818
|
+
throw new Error(`[${this.name}] Entry for each action must be an object: ${action}`)
|
|
3654
3819
|
}
|
|
3655
3820
|
|
|
3656
3821
|
const sinkEntries = Object.entries(sinks);
|
|
@@ -3667,12 +3832,12 @@ class Component {
|
|
|
3667
3832
|
const wrapped$ = on$
|
|
3668
3833
|
.compose(this.log(data => {
|
|
3669
3834
|
if (isStateSink) {
|
|
3670
|
-
return `<${
|
|
3835
|
+
return `<${action}> State reducer added`
|
|
3671
3836
|
} else if (isParentSink) {
|
|
3672
|
-
return `<${
|
|
3837
|
+
return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
|
|
3673
3838
|
} else {
|
|
3674
3839
|
const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
|
|
3675
|
-
return `<${
|
|
3840
|
+
return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
|
|
3676
3841
|
}
|
|
3677
3842
|
}));
|
|
3678
3843
|
|
|
@@ -3750,7 +3915,7 @@ class Component {
|
|
|
3750
3915
|
|
|
3751
3916
|
}
|
|
3752
3917
|
});
|
|
3753
|
-
subComponentSink$.subscribe({ next: _ => _ });
|
|
3918
|
+
subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) });
|
|
3754
3919
|
this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
|
|
3755
3920
|
}
|
|
3756
3921
|
|
|
@@ -3813,13 +3978,13 @@ class Component {
|
|
|
3813
3978
|
if (typeof reducer === 'function') {
|
|
3814
3979
|
returnStream$ = filtered$.map(action => {
|
|
3815
3980
|
const next = (type, data, delay=10) => {
|
|
3816
|
-
if (typeof delay !== 'number') throw new Error(`[${
|
|
3981
|
+
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.`)
|
|
3817
3982
|
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
3818
3983
|
setTimeout(() => {
|
|
3819
3984
|
// push the "next" action request into the action$ stream
|
|
3820
3985
|
rootAction$.shamefullySendNext({ type, data });
|
|
3821
3986
|
}, delay);
|
|
3822
|
-
this.log(`<${
|
|
3987
|
+
this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true);
|
|
3823
3988
|
};
|
|
3824
3989
|
|
|
3825
3990
|
const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext };
|
|
@@ -3831,7 +3996,7 @@ class Component {
|
|
|
3831
3996
|
const enhancedState = this.addCalculated(_state);
|
|
3832
3997
|
props.state = enhancedState;
|
|
3833
3998
|
const newState = reducer(enhancedState, data, next, props);
|
|
3834
|
-
if (newState
|
|
3999
|
+
if (newState === ABORT) return _state
|
|
3835
4000
|
return this.cleanupCalculated(newState)
|
|
3836
4001
|
}
|
|
3837
4002
|
} else {
|
|
@@ -3840,13 +4005,13 @@ class Component {
|
|
|
3840
4005
|
const reduced = reducer(enhancedState, data, next, props);
|
|
3841
4006
|
const type = typeof reduced;
|
|
3842
4007
|
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
3843
|
-
if (type
|
|
3844
|
-
console.warn(`'undefined' value sent to ${
|
|
4008
|
+
if (type === 'undefined') {
|
|
4009
|
+
console.warn(`[${this.name}] 'undefined' value sent to ${name}`);
|
|
3845
4010
|
return reduced
|
|
3846
4011
|
}
|
|
3847
|
-
throw new Error(`Invalid reducer type for ${
|
|
4012
|
+
throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
|
|
3848
4013
|
}
|
|
3849
|
-
}).filter(result => result
|
|
4014
|
+
}).filter(result => result !== ABORT);
|
|
3850
4015
|
} else if (reducer === undefined || reducer === true) {
|
|
3851
4016
|
returnStream$ = filtered$.map(({data}) => data);
|
|
3852
4017
|
} else {
|
|
@@ -3867,7 +4032,7 @@ class Component {
|
|
|
3867
4032
|
if (state === lastState) {
|
|
3868
4033
|
return lastResult
|
|
3869
4034
|
}
|
|
3870
|
-
if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field
|
|
4035
|
+
if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
|
|
3871
4036
|
|
|
3872
4037
|
const calculated = this.getCalculatedValues(state);
|
|
3873
4038
|
if (!calculated) {
|
|
@@ -3886,19 +4051,55 @@ class Component {
|
|
|
3886
4051
|
}
|
|
3887
4052
|
|
|
3888
4053
|
getCalculatedValues(state) {
|
|
3889
|
-
|
|
3890
|
-
if (entries.length === 0) {
|
|
4054
|
+
if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
|
|
3891
4055
|
return
|
|
3892
4056
|
}
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
4057
|
+
|
|
4058
|
+
const mergedState = { ...state };
|
|
4059
|
+
const computedSoFar = {};
|
|
4060
|
+
|
|
4061
|
+
for (const [field, { fn, deps }] of this._calculatedOrder) {
|
|
4062
|
+
if (deps !== null && this._calculatedFieldCache) {
|
|
4063
|
+
const cache = this._calculatedFieldCache[field];
|
|
4064
|
+
const currentDepValues = deps.map(d => mergedState[d]);
|
|
4065
|
+
|
|
4066
|
+
if (cache.lastDepValues !== undefined) {
|
|
4067
|
+
let unchanged = true;
|
|
4068
|
+
for (let i = 0; i < currentDepValues.length; i++) {
|
|
4069
|
+
if (currentDepValues[i] !== cache.lastDepValues[i]) {
|
|
4070
|
+
unchanged = false;
|
|
4071
|
+
break
|
|
4072
|
+
}
|
|
4073
|
+
}
|
|
4074
|
+
if (unchanged) {
|
|
4075
|
+
computedSoFar[field] = cache.lastResult;
|
|
4076
|
+
mergedState[field] = cache.lastResult;
|
|
4077
|
+
continue
|
|
4078
|
+
}
|
|
4079
|
+
}
|
|
4080
|
+
|
|
4081
|
+
try {
|
|
4082
|
+
const result = fn(mergedState);
|
|
4083
|
+
cache.lastDepValues = currentDepValues;
|
|
4084
|
+
cache.lastResult = result;
|
|
4085
|
+
computedSoFar[field] = result;
|
|
4086
|
+
mergedState[field] = result;
|
|
4087
|
+
} catch (e) {
|
|
4088
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4089
|
+
}
|
|
4090
|
+
} else {
|
|
4091
|
+
// No deps declared — always recompute
|
|
4092
|
+
try {
|
|
4093
|
+
const result = fn(mergedState);
|
|
4094
|
+
computedSoFar[field] = result;
|
|
4095
|
+
mergedState[field] = result;
|
|
4096
|
+
} catch (e) {
|
|
4097
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
|
|
4098
|
+
}
|
|
3899
4099
|
}
|
|
3900
|
-
|
|
3901
|
-
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
return computedSoFar
|
|
3902
4103
|
}
|
|
3903
4104
|
|
|
3904
4105
|
cleanupCalculated(incomingState) {
|
|
@@ -4046,7 +4247,7 @@ class Component {
|
|
|
4046
4247
|
this.newChildSources(childSources);
|
|
4047
4248
|
|
|
4048
4249
|
|
|
4049
|
-
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${
|
|
4250
|
+
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true);
|
|
4050
4251
|
|
|
4051
4252
|
return newComponents
|
|
4052
4253
|
}, {})
|
|
@@ -4112,7 +4313,7 @@ class Component {
|
|
|
4112
4313
|
} else if (this.components[collectionOf]) {
|
|
4113
4314
|
factory = this.components[collectionOf];
|
|
4114
4315
|
} else {
|
|
4115
|
-
throw new Error(`[${this.name}] Invalid 'of'
|
|
4316
|
+
throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
|
|
4116
4317
|
}
|
|
4117
4318
|
|
|
4118
4319
|
const fieldLense = {
|
|
@@ -4120,7 +4321,7 @@ class Component {
|
|
|
4120
4321
|
if (!Array.isArray(state[stateField])) return []
|
|
4121
4322
|
const items = state[stateField];
|
|
4122
4323
|
const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
|
|
4123
|
-
const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4324
|
+
const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered;
|
|
4124
4325
|
const mapped = sorted.map((item, index) => {
|
|
4125
4326
|
return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
|
|
4126
4327
|
});
|
|
@@ -4129,7 +4330,7 @@ class Component {
|
|
|
4129
4330
|
},
|
|
4130
4331
|
set: (oldState, newState) => {
|
|
4131
4332
|
if (this.calculated && stateField in this.calculated) {
|
|
4132
|
-
console.warn(`Collection sub-component of ${
|
|
4333
|
+
console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4133
4334
|
return oldState
|
|
4134
4335
|
}
|
|
4135
4336
|
const updated = [];
|
|
@@ -4158,17 +4359,17 @@ class Component {
|
|
|
4158
4359
|
} else if (typeof stateField === 'string') {
|
|
4159
4360
|
if (isObj(this.currentState)) {
|
|
4160
4361
|
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
4161
|
-
console.error(`Collection component in ${
|
|
4362
|
+
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.`);
|
|
4162
4363
|
lense = undefined;
|
|
4163
4364
|
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
4164
|
-
console.warn(`State property '${
|
|
4365
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4165
4366
|
lense = fieldLense;
|
|
4166
4367
|
} else {
|
|
4167
4368
|
lense = fieldLense;
|
|
4168
4369
|
}
|
|
4169
4370
|
} else {
|
|
4170
4371
|
if (!Array.isArray(this.currentState[stateField])) {
|
|
4171
|
-
console.warn(`State property '${
|
|
4372
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
|
|
4172
4373
|
lense = fieldLense;
|
|
4173
4374
|
} else {
|
|
4174
4375
|
lense = fieldLense;
|
|
@@ -4176,14 +4377,14 @@ class Component {
|
|
|
4176
4377
|
}
|
|
4177
4378
|
} else if (isObj(stateField)) {
|
|
4178
4379
|
if (typeof stateField.get !== 'function') {
|
|
4179
|
-
console.error(`Collection component in ${
|
|
4380
|
+
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.`);
|
|
4180
4381
|
lense = undefined;
|
|
4181
4382
|
} else {
|
|
4182
4383
|
lense = {
|
|
4183
4384
|
get: (state) => {
|
|
4184
4385
|
const newState = stateField.get(state);
|
|
4185
4386
|
if (!Array.isArray(newState)) {
|
|
4186
|
-
console.warn(`State getter function in collection component of ${
|
|
4387
|
+
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);
|
|
4187
4388
|
return []
|
|
4188
4389
|
}
|
|
4189
4390
|
return newState
|
|
@@ -4192,14 +4393,14 @@ class Component {
|
|
|
4192
4393
|
};
|
|
4193
4394
|
}
|
|
4194
4395
|
} else {
|
|
4195
|
-
console.error(`Collection component in ${
|
|
4396
|
+
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.`);
|
|
4196
4397
|
lense = undefined;
|
|
4197
4398
|
}
|
|
4198
4399
|
|
|
4199
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
|
|
4400
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber };
|
|
4200
4401
|
const sink$ = collection(factory, lense, { container: null })(sources);
|
|
4201
4402
|
if (!isObj(sink$)) {
|
|
4202
|
-
throw new Error(
|
|
4403
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
4203
4404
|
}
|
|
4204
4405
|
return sink$
|
|
4205
4406
|
}
|
|
@@ -4221,7 +4422,7 @@ class Component {
|
|
|
4221
4422
|
get: state => state[stateField],
|
|
4222
4423
|
set: (oldState, newState) => {
|
|
4223
4424
|
if (this.calculated && stateField in this.calculated) {
|
|
4224
|
-
console.warn(`Switchable sub-component of ${
|
|
4425
|
+
console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4225
4426
|
return oldState
|
|
4226
4427
|
}
|
|
4227
4428
|
if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
|
|
@@ -4240,13 +4441,13 @@ class Component {
|
|
|
4240
4441
|
lense = fieldLense;
|
|
4241
4442
|
} else if (isObj(stateField)) {
|
|
4242
4443
|
if (typeof stateField.get !== 'function') {
|
|
4243
|
-
console.error(`Switchable component in ${
|
|
4444
|
+
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.`);
|
|
4244
4445
|
lense = baseLense;
|
|
4245
4446
|
} else {
|
|
4246
4447
|
lense = { get: stateField.get, set: stateField.set };
|
|
4247
4448
|
}
|
|
4248
4449
|
} else {
|
|
4249
|
-
console.error(`Invalid state provided to switchable sub-component of ${
|
|
4450
|
+
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.`);
|
|
4250
4451
|
lense = baseLense;
|
|
4251
4452
|
}
|
|
4252
4453
|
|
|
@@ -4262,12 +4463,12 @@ class Component {
|
|
|
4262
4463
|
switchableComponents[key] = component(options);
|
|
4263
4464
|
}
|
|
4264
4465
|
});
|
|
4265
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4466
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4266
4467
|
|
|
4267
4468
|
const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
|
|
4268
4469
|
|
|
4269
4470
|
if (!isObj(sink$)) {
|
|
4270
|
-
throw new Error(
|
|
4471
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
|
|
4271
4472
|
}
|
|
4272
4473
|
|
|
4273
4474
|
return sink$
|
|
@@ -4293,7 +4494,7 @@ class Component {
|
|
|
4293
4494
|
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
|
|
4294
4495
|
if (!factory) {
|
|
4295
4496
|
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.`)
|
|
4296
|
-
throw new Error(`Component not found: ${
|
|
4497
|
+
throw new Error(`Component not found: ${componentName}`)
|
|
4297
4498
|
}
|
|
4298
4499
|
|
|
4299
4500
|
let lense;
|
|
@@ -4302,7 +4503,7 @@ class Component {
|
|
|
4302
4503
|
get: state => state[stateField],
|
|
4303
4504
|
set: (oldState, newState) => {
|
|
4304
4505
|
if (this.calculated && stateField in this.calculated) {
|
|
4305
|
-
console.warn(`Sub-component of ${
|
|
4506
|
+
console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
|
|
4306
4507
|
return oldState
|
|
4307
4508
|
}
|
|
4308
4509
|
return { ...oldState, [stateField]: newState }
|
|
@@ -4320,17 +4521,17 @@ class Component {
|
|
|
4320
4521
|
lense = fieldLense;
|
|
4321
4522
|
} else if (isObj(stateField)) {
|
|
4322
4523
|
if (typeof stateField.get !== 'function') {
|
|
4323
|
-
console.error(`Sub-component in ${
|
|
4524
|
+
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.`);
|
|
4324
4525
|
lense = baseLense;
|
|
4325
4526
|
} else {
|
|
4326
4527
|
lense = { get: stateField.get, set: stateField.set };
|
|
4327
4528
|
}
|
|
4328
4529
|
} else {
|
|
4329
|
-
console.error(`Invalid state provided to sub-component of ${
|
|
4530
|
+
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.`);
|
|
4330
4531
|
lense = baseLense;
|
|
4331
4532
|
}
|
|
4332
4533
|
|
|
4333
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
4534
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
|
|
4334
4535
|
const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
|
|
4335
4536
|
|
|
4336
4537
|
if (!isObj(sink$)) {
|
|
@@ -4405,14 +4606,22 @@ class Component {
|
|
|
4405
4606
|
const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
|
|
4406
4607
|
if (immediate) {
|
|
4407
4608
|
if (this.debug) {
|
|
4408
|
-
|
|
4609
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4610
|
+
console.log(text);
|
|
4611
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4612
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4613
|
+
}
|
|
4409
4614
|
}
|
|
4410
4615
|
return
|
|
4411
4616
|
} else {
|
|
4412
4617
|
return stream => {
|
|
4413
4618
|
return stream.debug(msg => {
|
|
4414
4619
|
if (this.debug) {
|
|
4415
|
-
|
|
4620
|
+
const text = `[${context}] ${fixedMsg(msg)}`;
|
|
4621
|
+
console.log(text);
|
|
4622
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4623
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
|
|
4624
|
+
}
|
|
4416
4625
|
}
|
|
4417
4626
|
})
|
|
4418
4627
|
}
|
|
@@ -4422,11 +4631,11 @@ class Component {
|
|
|
4422
4631
|
|
|
4423
4632
|
|
|
4424
4633
|
|
|
4425
|
-
function getComponents(currentElement, componentNames,
|
|
4634
|
+
function getComponents(currentElement, componentNames, path='r', parentId) {
|
|
4426
4635
|
if (!currentElement) return {}
|
|
4427
4636
|
|
|
4428
4637
|
if (currentElement.data?.componentsProcessed) return {}
|
|
4429
|
-
if (
|
|
4638
|
+
if (path === 'r') currentElement.data.componentsProcessed = true;
|
|
4430
4639
|
|
|
4431
4640
|
const sel = currentElement.sel;
|
|
4432
4641
|
const isCollection = sel && sel.toLowerCase() === 'collection';
|
|
@@ -4440,11 +4649,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4440
4649
|
|
|
4441
4650
|
let id = parentId;
|
|
4442
4651
|
if (isComponent) {
|
|
4443
|
-
id = getComponentIdFromElement(currentElement,
|
|
4652
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4444
4653
|
if (isCollection) {
|
|
4445
4654
|
if (!props.of) throw new Error(`Collection element missing required 'component' property`)
|
|
4446
4655
|
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`)
|
|
4447
|
-
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${
|
|
4656
|
+
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
|
|
4448
4657
|
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);
|
|
4449
4658
|
currentElement.data.isCollection = true;
|
|
4450
4659
|
currentElement.data.props ||= {};
|
|
@@ -4455,7 +4664,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4455
4664
|
if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
|
|
4456
4665
|
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`)
|
|
4457
4666
|
const switchableComponentNames = Object.keys(props.of);
|
|
4458
|
-
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${
|
|
4667
|
+
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
|
|
4459
4668
|
currentElement.data.isSwitchable = true;
|
|
4460
4669
|
} else ;
|
|
4461
4670
|
if (typeof props.key === 'undefined') currentElement.data.props.key = id;
|
|
@@ -4463,7 +4672,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4463
4672
|
}
|
|
4464
4673
|
|
|
4465
4674
|
if (children.length > 0) {
|
|
4466
|
-
children.map((child, i) => getComponents(child, componentNames,
|
|
4675
|
+
children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
|
|
4467
4676
|
.forEach((child) => {
|
|
4468
4677
|
Object.entries(child).forEach(([id, el]) => found[id] = el);
|
|
4469
4678
|
});
|
|
@@ -4472,10 +4681,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
4472
4681
|
return found
|
|
4473
4682
|
}
|
|
4474
4683
|
|
|
4475
|
-
function injectComponents(currentElement, components, componentNames,
|
|
4684
|
+
function injectComponents(currentElement, components, componentNames, path='r', parentId) {
|
|
4476
4685
|
if (!currentElement) return
|
|
4477
4686
|
if (currentElement.data?.componentsInjected) return currentElement
|
|
4478
|
-
if (
|
|
4687
|
+
if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true;
|
|
4479
4688
|
|
|
4480
4689
|
|
|
4481
4690
|
const sel = currentElement.sel || 'NO SELECTOR';
|
|
@@ -4487,7 +4696,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4487
4696
|
|
|
4488
4697
|
let id = parentId;
|
|
4489
4698
|
if (isComponent) {
|
|
4490
|
-
id = getComponentIdFromElement(currentElement,
|
|
4699
|
+
id = getComponentIdFromElement(currentElement, path, parentId);
|
|
4491
4700
|
const component = components[id];
|
|
4492
4701
|
if (isCollection) {
|
|
4493
4702
|
currentElement.sel = 'div';
|
|
@@ -4499,21 +4708,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
4499
4708
|
return component
|
|
4500
4709
|
}
|
|
4501
4710
|
} else if (children.length > 0) {
|
|
4502
|
-
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames,
|
|
4711
|
+
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat();
|
|
4503
4712
|
return currentElement
|
|
4504
4713
|
} else {
|
|
4505
4714
|
return currentElement
|
|
4506
4715
|
}
|
|
4507
4716
|
}
|
|
4508
4717
|
|
|
4509
|
-
function getComponentIdFromElement(el,
|
|
4718
|
+
function getComponentIdFromElement(el, path, parentId) {
|
|
4510
4719
|
const sel = el.sel;
|
|
4511
4720
|
const name = typeof sel === 'string' ? sel : 'functionComponent';
|
|
4512
|
-
const uid = `${depth}:${index}`;
|
|
4513
4721
|
const props = el.data?.props || {};
|
|
4514
|
-
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) ||
|
|
4515
|
-
const parentString = parentId ? `${
|
|
4516
|
-
const fullId = `${
|
|
4722
|
+
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path;
|
|
4723
|
+
const parentString = parentId ? `${parentId}|` : '';
|
|
4724
|
+
const fullId = `${parentString}${name}::${id}`;
|
|
4517
4725
|
return fullId
|
|
4518
4726
|
}
|
|
4519
4727
|
|
|
@@ -4657,12 +4865,264 @@ function sortFunctionFromProp(sortProp) {
|
|
|
4657
4865
|
} else if (isObj(sortProp)) {
|
|
4658
4866
|
return __sortFunctionFromObj(sortProp)
|
|
4659
4867
|
} else {
|
|
4660
|
-
console.error('Invalid sort option (ignoring):',
|
|
4868
|
+
console.error('Invalid sort option (ignoring):', sortProp);
|
|
4661
4869
|
return undefined
|
|
4662
4870
|
}
|
|
4663
4871
|
}
|
|
4664
4872
|
|
|
4873
|
+
const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
|
|
4874
|
+
const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
|
|
4875
|
+
const DEFAULT_MAX_HISTORY = 200;
|
|
4876
|
+
|
|
4877
|
+
class SygnalDevTools {
|
|
4878
|
+
constructor() {
|
|
4879
|
+
this._connected = false;
|
|
4880
|
+
this._components = new Map();
|
|
4881
|
+
this._stateHistory = [];
|
|
4882
|
+
this._maxHistory = DEFAULT_MAX_HISTORY;
|
|
4883
|
+
}
|
|
4884
|
+
|
|
4885
|
+
get connected() {
|
|
4886
|
+
return this._connected && typeof window !== 'undefined'
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
// ─── Initialization ─────────────────────────────────────────────────────────
|
|
4890
|
+
|
|
4891
|
+
init() {
|
|
4892
|
+
if (typeof window === 'undefined') return
|
|
4893
|
+
|
|
4894
|
+
window.__SYGNAL_DEVTOOLS__ = this;
|
|
4895
|
+
|
|
4896
|
+
window.addEventListener('message', (event) => {
|
|
4897
|
+
if (event.source !== window) return
|
|
4898
|
+
if (event.data?.source === EXTENSION_SOURCE) {
|
|
4899
|
+
this._handleExtensionMessage(event.data);
|
|
4900
|
+
}
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
_handleExtensionMessage(msg) {
|
|
4905
|
+
switch (msg.type) {
|
|
4906
|
+
case 'CONNECT':
|
|
4907
|
+
this._connected = true;
|
|
4908
|
+
if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
|
|
4909
|
+
this._sendFullTree();
|
|
4910
|
+
break
|
|
4911
|
+
case 'DISCONNECT':
|
|
4912
|
+
this._connected = false;
|
|
4913
|
+
break
|
|
4914
|
+
case 'SET_DEBUG':
|
|
4915
|
+
this._setDebug(msg.payload);
|
|
4916
|
+
break
|
|
4917
|
+
case 'TIME_TRAVEL':
|
|
4918
|
+
this._timeTravel(msg.payload);
|
|
4919
|
+
break
|
|
4920
|
+
case 'GET_STATE':
|
|
4921
|
+
this._sendComponentState(msg.payload.componentId);
|
|
4922
|
+
break
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
// ─── Hooks (called from component.js) ────────────────────────────────────────
|
|
4927
|
+
|
|
4928
|
+
onComponentCreated(componentNumber, name, instance) {
|
|
4929
|
+
const meta = {
|
|
4930
|
+
id: componentNumber,
|
|
4931
|
+
name: name,
|
|
4932
|
+
isSubComponent: instance.isSubComponent,
|
|
4933
|
+
hasModel: !!instance.model,
|
|
4934
|
+
hasIntent: !!instance.intent,
|
|
4935
|
+
hasContext: !!instance.context,
|
|
4936
|
+
hasCalculated: !!instance.calculated,
|
|
4937
|
+
components: Object.keys(instance.components || {}),
|
|
4938
|
+
parentId: null,
|
|
4939
|
+
children: [],
|
|
4940
|
+
debug: instance._debug,
|
|
4941
|
+
createdAt: Date.now(),
|
|
4942
|
+
_instanceRef: new WeakRef(instance),
|
|
4943
|
+
};
|
|
4944
|
+
this._components.set(componentNumber, meta);
|
|
4945
|
+
|
|
4946
|
+
if (!this.connected) return
|
|
4947
|
+
this._post('COMPONENT_CREATED', this._serializeMeta(meta));
|
|
4948
|
+
}
|
|
4949
|
+
|
|
4950
|
+
onStateChanged(componentNumber, name, state) {
|
|
4951
|
+
if (!this.connected) return
|
|
4952
|
+
|
|
4953
|
+
const entry = {
|
|
4954
|
+
componentId: componentNumber,
|
|
4955
|
+
componentName: name,
|
|
4956
|
+
timestamp: Date.now(),
|
|
4957
|
+
state: this._safeClone(state),
|
|
4958
|
+
};
|
|
4959
|
+
|
|
4960
|
+
this._stateHistory.push(entry);
|
|
4961
|
+
if (this._stateHistory.length > this._maxHistory) {
|
|
4962
|
+
this._stateHistory.shift();
|
|
4963
|
+
}
|
|
4964
|
+
|
|
4965
|
+
this._post('STATE_CHANGED', {
|
|
4966
|
+
componentId: componentNumber,
|
|
4967
|
+
componentName: name,
|
|
4968
|
+
state: entry.state,
|
|
4969
|
+
historyIndex: this._stateHistory.length - 1,
|
|
4970
|
+
});
|
|
4971
|
+
}
|
|
4972
|
+
|
|
4973
|
+
onActionDispatched(componentNumber, name, actionType, data) {
|
|
4974
|
+
if (!this.connected) return
|
|
4975
|
+
this._post('ACTION_DISPATCHED', {
|
|
4976
|
+
componentId: componentNumber,
|
|
4977
|
+
componentName: name,
|
|
4978
|
+
actionType: actionType,
|
|
4979
|
+
data: this._safeClone(data),
|
|
4980
|
+
timestamp: Date.now(),
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4984
|
+
onSubComponentRegistered(parentNumber, childNumber) {
|
|
4985
|
+
const parent = this._components.get(parentNumber);
|
|
4986
|
+
const child = this._components.get(childNumber);
|
|
4987
|
+
if (parent && child) {
|
|
4988
|
+
child.parentId = parentNumber;
|
|
4989
|
+
if (!parent.children.includes(childNumber)) {
|
|
4990
|
+
parent.children.push(childNumber);
|
|
4991
|
+
}
|
|
4992
|
+
}
|
|
4993
|
+
|
|
4994
|
+
if (!this.connected) return
|
|
4995
|
+
this._post('TREE_UPDATED', {
|
|
4996
|
+
parentId: parentNumber,
|
|
4997
|
+
childId: childNumber,
|
|
4998
|
+
});
|
|
4999
|
+
}
|
|
5000
|
+
|
|
5001
|
+
onContextChanged(componentNumber, name, context) {
|
|
5002
|
+
if (!this.connected) return
|
|
5003
|
+
this._post('CONTEXT_CHANGED', {
|
|
5004
|
+
componentId: componentNumber,
|
|
5005
|
+
componentName: name,
|
|
5006
|
+
context: this._safeClone(context),
|
|
5007
|
+
});
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
onDebugLog(componentNumber, message) {
|
|
5011
|
+
if (!this.connected) return
|
|
5012
|
+
this._post('DEBUG_LOG', {
|
|
5013
|
+
componentId: componentNumber,
|
|
5014
|
+
message: message,
|
|
5015
|
+
timestamp: Date.now(),
|
|
5016
|
+
});
|
|
5017
|
+
}
|
|
5018
|
+
|
|
5019
|
+
// ─── Commands (from extension to page) ───────────────────────────────────────
|
|
5020
|
+
|
|
5021
|
+
_setDebug({ componentId, enabled }) {
|
|
5022
|
+
if (typeof componentId === 'undefined' || componentId === null) {
|
|
5023
|
+
if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
|
|
5024
|
+
this._post('DEBUG_TOGGLED', { global: true, enabled });
|
|
5025
|
+
return
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
const meta = this._components.get(componentId);
|
|
5029
|
+
if (meta && meta._instanceRef) {
|
|
5030
|
+
const instance = meta._instanceRef.deref();
|
|
5031
|
+
if (instance) {
|
|
5032
|
+
instance._debug = enabled;
|
|
5033
|
+
meta.debug = enabled;
|
|
5034
|
+
this._post('DEBUG_TOGGLED', { componentId, enabled });
|
|
5035
|
+
}
|
|
5036
|
+
}
|
|
5037
|
+
}
|
|
5038
|
+
|
|
5039
|
+
_timeTravel({ historyIndex }) {
|
|
5040
|
+
const entry = this._stateHistory[historyIndex];
|
|
5041
|
+
if (!entry) return
|
|
5042
|
+
|
|
5043
|
+
const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__;
|
|
5044
|
+
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
5045
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
|
|
5046
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5047
|
+
historyIndex,
|
|
5048
|
+
state: entry.state,
|
|
5049
|
+
});
|
|
5050
|
+
}
|
|
5051
|
+
}
|
|
5052
|
+
|
|
5053
|
+
_sendComponentState(componentId) {
|
|
5054
|
+
const meta = this._components.get(componentId);
|
|
5055
|
+
if (meta && meta._instanceRef) {
|
|
5056
|
+
const instance = meta._instanceRef.deref();
|
|
5057
|
+
if (instance) {
|
|
5058
|
+
this._post('COMPONENT_STATE', {
|
|
5059
|
+
componentId,
|
|
5060
|
+
state: this._safeClone(instance.currentState),
|
|
5061
|
+
context: this._safeClone(instance.currentContext),
|
|
5062
|
+
props: this._safeClone(instance.currentProps),
|
|
5063
|
+
});
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
_sendFullTree() {
|
|
5069
|
+
const tree = [];
|
|
5070
|
+
for (const [id, meta] of this._components) {
|
|
5071
|
+
const instance = meta._instanceRef?.deref();
|
|
5072
|
+
tree.push({
|
|
5073
|
+
...this._serializeMeta(meta),
|
|
5074
|
+
state: instance ? this._safeClone(instance.currentState) : null,
|
|
5075
|
+
context: instance ? this._safeClone(instance.currentContext) : null,
|
|
5076
|
+
});
|
|
5077
|
+
}
|
|
5078
|
+
this._post('FULL_TREE', {
|
|
5079
|
+
components: tree,
|
|
5080
|
+
history: this._stateHistory,
|
|
5081
|
+
});
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
// ─── Transport ───────────────────────────────────────────────────────────────
|
|
5085
|
+
|
|
5086
|
+
_post(type, payload) {
|
|
5087
|
+
if (typeof window === 'undefined') return
|
|
5088
|
+
window.postMessage({
|
|
5089
|
+
source: DEVTOOLS_SOURCE,
|
|
5090
|
+
type,
|
|
5091
|
+
payload,
|
|
5092
|
+
}, '*');
|
|
5093
|
+
}
|
|
5094
|
+
|
|
5095
|
+
_safeClone(obj) {
|
|
5096
|
+
if (obj === undefined || obj === null) return obj
|
|
5097
|
+
try {
|
|
5098
|
+
return JSON.parse(JSON.stringify(obj))
|
|
5099
|
+
} catch (e) {
|
|
5100
|
+
return '[unserializable]'
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
|
|
5104
|
+
_serializeMeta(meta) {
|
|
5105
|
+
const { _instanceRef, ...rest } = meta;
|
|
5106
|
+
return rest
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
|
|
5110
|
+
// ─── Singleton ────────────────────────────────────────────────────────────────
|
|
5111
|
+
|
|
5112
|
+
let instance = null;
|
|
5113
|
+
|
|
5114
|
+
function getDevTools() {
|
|
5115
|
+
if (!instance) instance = new SygnalDevTools();
|
|
5116
|
+
return instance
|
|
5117
|
+
}
|
|
5118
|
+
|
|
4665
5119
|
function run(app, drivers={}, options={}) {
|
|
5120
|
+
// Initialize DevTools instrumentation bridge early (before component creation)
|
|
5121
|
+
if (typeof window !== 'undefined') {
|
|
5122
|
+
const dt = getDevTools();
|
|
5123
|
+
dt.init();
|
|
5124
|
+
}
|
|
5125
|
+
|
|
4666
5126
|
const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options;
|
|
4667
5127
|
if (!app.isSygnalComponent) {
|
|
4668
5128
|
const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
|
|
@@ -4712,6 +5172,11 @@ function run(app, drivers={}, options={}) {
|
|
|
4712
5172
|
|
|
4713
5173
|
const exposed = { sources, sinks, dispose };
|
|
4714
5174
|
|
|
5175
|
+
// Store app reference for time-travel
|
|
5176
|
+
if (typeof window !== 'undefined') {
|
|
5177
|
+
window.__SYGNAL_DEVTOOLS_APP__ = exposed;
|
|
5178
|
+
}
|
|
5179
|
+
|
|
4715
5180
|
const swapToComponent = (newComponent, state) => {
|
|
4716
5181
|
const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
|
|
4717
5182
|
const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
|