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/index.cjs.js CHANGED
@@ -40,7 +40,12 @@ function _mergeNamespaces(n, m) {
40
40
 
41
41
  var xstreamNamespace__namespace = /*#__PURE__*/_interopNamespaceDefault(xstreamNamespace);
42
42
 
43
+ let COLLECTION_COUNT = 0;
44
+
43
45
  function collection(component, stateLense, opts={}) {
46
+ if (typeof component !== 'function') {
47
+ throw new Error('collection: first argument (component) must be a function')
48
+ }
44
49
  const {
45
50
  combineList = ['DOM'],
46
51
  globalList = ['EVENTS'],
@@ -51,7 +56,7 @@ function collection(component, stateLense, opts={}) {
51
56
  } = opts;
52
57
 
53
58
  return (sources) => {
54
- const key = Date.now();
59
+ const key = `sygnal-collection-${COLLECTION_COUNT++}`;
55
60
  const collectionOpts = {
56
61
  item: component,
57
62
  itemKey: (state, ind) => typeof state.id !== 'undefined' ? state.id : ind,
@@ -2868,7 +2873,7 @@ function switchable(factories, name$, initial, opts={}) {
2868
2873
  const mapFunction = (nameType === 'function' && name$) || (state => state[name$]);
2869
2874
  return sources => {
2870
2875
  const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream;
2871
- if (!state$ instanceof Stream$1) throw new Error(`Could not find the state source: ${ stateSourceName }`)
2876
+ if (!(state$ instanceof Stream$1)) throw new Error(`Could not find the state source: ${stateSourceName}`)
2872
2877
  const _name$ = state$
2873
2878
  .map(mapFunction)
2874
2879
  .filter(name => typeof name === 'string')
@@ -3282,13 +3287,27 @@ function wrapDOMSource(domSource) {
3282
3287
  }
3283
3288
 
3284
3289
 
3285
- const ABORT = '~#~#~ABORT~#~#~';
3290
+ const ABORT = Symbol('ABORT');
3291
+
3292
+
3293
+ function normalizeCalculatedEntry(field, entry) {
3294
+ if (typeof entry === 'function') {
3295
+ return { fn: entry, deps: null }
3296
+ }
3297
+ if (Array.isArray(entry) && entry.length === 2
3298
+ && Array.isArray(entry[0]) && typeof entry[1] === 'function') {
3299
+ return { fn: entry[1], deps: entry[0] }
3300
+ }
3301
+ throw new Error(
3302
+ `Invalid calculated field '${field}': expected a function or [depsArray, function]`
3303
+ )
3304
+ }
3286
3305
 
3287
3306
  function component (opts) {
3288
3307
  const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
3289
3308
 
3290
3309
  if (sources && !isObj(sources)) {
3291
- throw new Error('Sources must be a Cycle.js sources object:', name)
3310
+ throw new Error(`[${name}] Sources must be a Cycle.js sources object`)
3292
3311
  }
3293
3312
 
3294
3313
  let fixedIsolateOpts;
@@ -3368,7 +3387,9 @@ class Component {
3368
3387
  // sinks
3369
3388
 
3370
3389
  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 }) {
3371
- if (!sources || !isObj(sources)) throw new Error('Missing or invalid sources')
3390
+ if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
3391
+
3392
+ this._componentNumber = COMPONENT_COUNT++;
3372
3393
 
3373
3394
  this.name = name;
3374
3395
  this.sources = sources;
@@ -3389,6 +3410,123 @@ class Component {
3389
3410
  this.sourceNames = Object.keys(sources);
3390
3411
  this._debug = debug;
3391
3412
 
3413
+ // Warn if calculated fields shadow base state keys
3414
+ if (this.calculated && this.initialState
3415
+ && isObj(this.calculated) && isObj(this.initialState)) {
3416
+ for (const key of Object.keys(this.calculated)) {
3417
+ if (key in this.initialState) {
3418
+ console.warn(
3419
+ `[${name}] Calculated field '${key}' shadows a key in initialState. ` +
3420
+ `The initialState value will be overwritten on every state update.`
3421
+ );
3422
+ }
3423
+ }
3424
+ }
3425
+
3426
+ // Normalize calculated entries, build dependency graph, topological sort
3427
+ if (this.calculated && isObj(this.calculated)) {
3428
+ const calcEntries = Object.entries(this.calculated);
3429
+
3430
+ // Normalize all entries to { fn, deps } shape
3431
+ this._calculatedNormalized = {};
3432
+ for (const [field, entry] of calcEntries) {
3433
+ this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry);
3434
+ }
3435
+
3436
+ this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized));
3437
+
3438
+ // Warn on deps referencing nonexistent keys
3439
+ for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
3440
+ if (deps !== null) {
3441
+ for (const dep of deps) {
3442
+ if (!this._calculatedFieldNames.has(dep)
3443
+ && this.initialState && !(dep in this.initialState)) {
3444
+ console.warn(
3445
+ `[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
3446
+ `which is not in initialState or calculated fields`
3447
+ );
3448
+ }
3449
+ }
3450
+ }
3451
+ }
3452
+
3453
+ // Build adjacency: for each field, which other calculated fields must run first?
3454
+ const calcDeps = {};
3455
+ for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
3456
+ if (deps === null) {
3457
+ calcDeps[field] = [];
3458
+ } else {
3459
+ calcDeps[field] = deps.filter(d => this._calculatedFieldNames.has(d));
3460
+ }
3461
+ }
3462
+
3463
+ // Kahn's algorithm for topological sort
3464
+ const inDegree = {};
3465
+ const reverseGraph = {};
3466
+ for (const field of this._calculatedFieldNames) {
3467
+ inDegree[field] = 0;
3468
+ reverseGraph[field] = [];
3469
+ }
3470
+ for (const [field, depList] of Object.entries(calcDeps)) {
3471
+ inDegree[field] = depList.length;
3472
+ for (const dep of depList) {
3473
+ reverseGraph[dep].push(field);
3474
+ }
3475
+ }
3476
+
3477
+ const queue = [];
3478
+ for (const [field, degree] of Object.entries(inDegree)) {
3479
+ if (degree === 0) queue.push(field);
3480
+ }
3481
+
3482
+ const sorted = [];
3483
+ while (queue.length > 0) {
3484
+ const current = queue.shift();
3485
+ sorted.push(current);
3486
+ for (const dependent of reverseGraph[current]) {
3487
+ inDegree[dependent]--;
3488
+ if (inDegree[dependent] === 0) queue.push(dependent);
3489
+ }
3490
+ }
3491
+
3492
+ if (sorted.length !== this._calculatedFieldNames.size) {
3493
+ // Cycle detected — build error message with cycle path
3494
+ const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f));
3495
+ const visited = new Set();
3496
+ const path = [];
3497
+ const traceCycle = (node) => {
3498
+ if (visited.has(node)) { path.push(node); return true }
3499
+ visited.add(node);
3500
+ path.push(node);
3501
+ for (const dep of calcDeps[node]) {
3502
+ if (inCycle.includes(dep) && traceCycle(dep)) return true
3503
+ }
3504
+ path.pop();
3505
+ visited.delete(node);
3506
+ return false
3507
+ };
3508
+ traceCycle(inCycle[0]);
3509
+ const start = path[path.length - 1];
3510
+ const cycle = path.slice(path.indexOf(start));
3511
+ throw new Error(`Circular calculated dependency: ${cycle.join(' \u2192 ')}`)
3512
+ }
3513
+
3514
+ this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized[f]]);
3515
+
3516
+ // Initialize per-field memoization caches for fields with declared deps
3517
+ this._calculatedFieldCache = {};
3518
+ for (const [field, { deps }] of this._calculatedOrder) {
3519
+ if (deps !== null) {
3520
+ this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined };
3521
+ }
3522
+ }
3523
+ } else {
3524
+ this._calculatedOrder = null;
3525
+ this._calculatedNormalized = null;
3526
+ this._calculatedFieldNames = null;
3527
+ this._calculatedFieldCache = null;
3528
+ }
3529
+
3392
3530
  this.isSubComponent = this.sourceNames.includes('props$');
3393
3531
 
3394
3532
  const state$ = sources[stateSourceName] && sources[stateSourceName].stream;
@@ -3397,6 +3535,9 @@ class Component {
3397
3535
  this.currentState = initialState || {};
3398
3536
  this.sources[stateSourceName] = new state.StateSource(state$.map(val => {
3399
3537
  this.currentState = val;
3538
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3539
+ window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val);
3540
+ }
3400
3541
  return val
3401
3542
  }));
3402
3543
  }
@@ -3432,10 +3573,8 @@ class Component {
3432
3573
  };
3433
3574
  }
3434
3575
 
3435
- const componentNumber = COMPONENT_COUNT++;
3436
-
3437
3576
  this.addCalculated = this.createMemoizedAddCalculated();
3438
- this.log = makeLog(`${componentNumber} | ${name}`);
3577
+ this.log = makeLog(`${this._componentNumber} | ${name}`);
3439
3578
 
3440
3579
  this.initChildSources$();
3441
3580
  this.initIntent$();
@@ -3450,9 +3589,20 @@ class Component {
3450
3589
  this.initVdom$();
3451
3590
  this.initSinks();
3452
3591
 
3453
- this.sinks.__index = componentNumber;
3592
+ this.sinks.__index = this._componentNumber;
3454
3593
 
3455
3594
  this.log(`Instantiated`, true);
3595
+
3596
+ // Hook 1: Register with DevTools
3597
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
3598
+ window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this);
3599
+
3600
+ // Hook 1b: Register parent-child relationship
3601
+ const parentNum = sources?.__parentComponentNumber;
3602
+ if (typeof parentNum === 'number') {
3603
+ window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber);
3604
+ }
3605
+ }
3456
3606
  }
3457
3607
 
3458
3608
  get debug() {
@@ -3464,13 +3614,13 @@ class Component {
3464
3614
  return
3465
3615
  }
3466
3616
  if (typeof this.intent != 'function') {
3467
- throw new Error('Intent must be a function')
3617
+ throw new Error(`[${this.name}] Intent must be a function`)
3468
3618
  }
3469
3619
 
3470
3620
  this.intent$ = this.intent(this.sources);
3471
3621
 
3472
3622
  if (!(this.intent$ instanceof Stream$1) && (!isObj(this.intent$))) {
3473
- throw new Error('Intent must return either an action$ stream or map of event streams')
3623
+ throw new Error(`[${this.name}] Intent must return either an action$ stream or map of event streams`)
3474
3624
  }
3475
3625
  }
3476
3626
 
@@ -3483,10 +3633,10 @@ class Component {
3483
3633
  this.hmrActions = [this.hmrActions];
3484
3634
  }
3485
3635
  if (!Array.isArray(this.hmrActions)) {
3486
- 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`)
3636
+ 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`)
3487
3637
  }
3488
3638
  if (this.hmrActions.some(action => typeof action !== 'string')) {
3489
- 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`)
3639
+ 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`)
3490
3640
  }
3491
3641
  this.hmrAction$ = xs$1.fromArray(this.hmrActions.map(action => ({ type: action })));
3492
3642
  }
@@ -3525,7 +3675,15 @@ class Component {
3525
3675
  const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }));
3526
3676
 
3527
3677
  this.action$ = xs$1.merge(wrapped$, hydrate$)
3528
- .compose(this.log(({ type }) => `<${ type }> Action triggered`));
3678
+ .compose(this.log(({ type }) => `<${type}> Action triggered`))
3679
+ .map(action => {
3680
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3681
+ window.__SYGNAL_DEVTOOLS__.onActionDispatched(
3682
+ this._componentNumber, this.name, action.type, action.data
3683
+ );
3684
+ }
3685
+ return action
3686
+ });
3529
3687
  }
3530
3688
 
3531
3689
  initState() {
@@ -3537,7 +3695,7 @@ class Component {
3537
3695
  } else if (isObj(this.model[INITIALIZE_ACTION])) {
3538
3696
  Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
3539
3697
  if (name !== this.stateSourceName) {
3540
- console.warn(`${ INITIALIZE_ACTION } can only be used with the ${ this.stateSourceName } source... disregarding ${ name }`);
3698
+ console.warn(`${INITIALIZE_ACTION} can only be used with the ${this.stateSourceName} source... disregarding ${name}`);
3541
3699
  delete this.model[INITIALIZE_ACTION][name];
3542
3700
  }
3543
3701
  });
@@ -3572,7 +3730,7 @@ class Component {
3572
3730
  } else if (valueType === 'function') {
3573
3731
  _value = value(state);
3574
3732
  } else {
3575
- console.error(`[${ this.name }] Invalid context entry '${ name }': must be the name of a state property or a function returning a value to use`);
3733
+ console.error(`[${this.name}] Invalid context entry '${name}': must be the name of a state property or a function returning a value to use`);
3576
3734
  return acc
3577
3735
  }
3578
3736
  acc[name] = _value;
@@ -3580,11 +3738,14 @@ class Component {
3580
3738
  }, {});
3581
3739
  const newContext = { ..._parent, ...values };
3582
3740
  this.currentContext = newContext;
3741
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3742
+ window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext);
3743
+ }
3583
3744
  return newContext
3584
3745
  })
3585
3746
  .compose(dropRepeats(objIsEqual))
3586
3747
  .startWith({});
3587
- this.context$.subscribe({ next: _ => _ });
3748
+ this.context$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in context stream:`, err) });
3588
3749
  }
3589
3750
 
3590
3751
  initModel$() {
@@ -3600,7 +3761,7 @@ class Component {
3600
3761
  const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
3601
3762
  const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
3602
3763
  if (this.isSubComponent && this.initialState) {
3603
- console.warn(`[${ this.name }] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
3764
+ console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
3604
3765
  }
3605
3766
  const hasInitialState = (typeof effectiveInitialState !== 'undefined');
3606
3767
  const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined');
@@ -3621,7 +3782,7 @@ class Component {
3621
3782
  }
3622
3783
 
3623
3784
  if (!isObj(sinks)) {
3624
- throw new Error(`Entry for each action must be an object: ${ this.name } ${ action }`)
3785
+ throw new Error(`[${this.name}] Entry for each action must be an object: ${action}`)
3625
3786
  }
3626
3787
 
3627
3788
  const sinkEntries = Object.entries(sinks);
@@ -3638,12 +3799,12 @@ class Component {
3638
3799
  const wrapped$ = on$
3639
3800
  .compose(this.log(data => {
3640
3801
  if (isStateSink) {
3641
- return `<${ action }> State reducer added`
3802
+ return `<${action}> State reducer added`
3642
3803
  } else if (isParentSink) {
3643
- return `<${ action }> Data sent to parent component: ${ JSON.stringify(data.value).replaceAll('"', '') }`
3804
+ return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
3644
3805
  } else {
3645
3806
  const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
3646
- return `<${ action }> Data sent to [${ sink }]: ${ JSON.stringify(extra).replaceAll('"', '') }`
3807
+ return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
3647
3808
  }
3648
3809
  }));
3649
3810
 
@@ -3721,7 +3882,7 @@ class Component {
3721
3882
 
3722
3883
  }
3723
3884
  });
3724
- subComponentSink$.subscribe({ next: _ => _ });
3885
+ subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) });
3725
3886
  this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
3726
3887
  }
3727
3888
 
@@ -3784,13 +3945,13 @@ class Component {
3784
3945
  if (typeof reducer === 'function') {
3785
3946
  returnStream$ = filtered$.map(action => {
3786
3947
  const next = (type, data, delay=10) => {
3787
- 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.`)
3948
+ 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.`)
3788
3949
  // put the "next" action request at the end of the event loop so the "current" action completes first
3789
3950
  setTimeout(() => {
3790
3951
  // push the "next" action request into the action$ stream
3791
3952
  rootAction$.shamefullySendNext({ type, data });
3792
3953
  }, delay);
3793
- this.log(`<${ name }> Triggered a next() action: <${ type }> ${ delay }ms delay`, true);
3954
+ this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true);
3794
3955
  };
3795
3956
 
3796
3957
  const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext };
@@ -3802,7 +3963,7 @@ class Component {
3802
3963
  const enhancedState = this.addCalculated(_state);
3803
3964
  props.state = enhancedState;
3804
3965
  const newState = reducer(enhancedState, data, next, props);
3805
- if (newState == ABORT) return _state
3966
+ if (newState === ABORT) return _state
3806
3967
  return this.cleanupCalculated(newState)
3807
3968
  }
3808
3969
  } else {
@@ -3811,13 +3972,13 @@ class Component {
3811
3972
  const reduced = reducer(enhancedState, data, next, props);
3812
3973
  const type = typeof reduced;
3813
3974
  if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3814
- if (type == 'undefined') {
3815
- console.warn(`'undefined' value sent to ${ name }`);
3975
+ if (type === 'undefined') {
3976
+ console.warn(`[${this.name}] 'undefined' value sent to ${name}`);
3816
3977
  return reduced
3817
3978
  }
3818
- throw new Error(`Invalid reducer type for ${ name } ${ type }`)
3979
+ throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
3819
3980
  }
3820
- }).filter(result => result != ABORT);
3981
+ }).filter(result => result !== ABORT);
3821
3982
  } else if (reducer === undefined || reducer === true) {
3822
3983
  returnStream$ = filtered$.map(({data}) => data);
3823
3984
  } else {
@@ -3838,7 +3999,7 @@ class Component {
3838
3999
  if (state === lastState) {
3839
4000
  return lastResult
3840
4001
  }
3841
- if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
4002
+ if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
3842
4003
 
3843
4004
  const calculated = this.getCalculatedValues(state);
3844
4005
  if (!calculated) {
@@ -3857,19 +4018,55 @@ class Component {
3857
4018
  }
3858
4019
 
3859
4020
  getCalculatedValues(state) {
3860
- const entries = Object.entries(this.calculated || {});
3861
- if (entries.length === 0) {
4021
+ if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
3862
4022
  return
3863
4023
  }
3864
- return entries.reduce((acc, [field, fn]) => {
3865
- if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3866
- try {
3867
- acc[field] = fn(state);
3868
- } catch(e) {
3869
- console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
4024
+
4025
+ const mergedState = { ...state };
4026
+ const computedSoFar = {};
4027
+
4028
+ for (const [field, { fn, deps }] of this._calculatedOrder) {
4029
+ if (deps !== null && this._calculatedFieldCache) {
4030
+ const cache = this._calculatedFieldCache[field];
4031
+ const currentDepValues = deps.map(d => mergedState[d]);
4032
+
4033
+ if (cache.lastDepValues !== undefined) {
4034
+ let unchanged = true;
4035
+ for (let i = 0; i < currentDepValues.length; i++) {
4036
+ if (currentDepValues[i] !== cache.lastDepValues[i]) {
4037
+ unchanged = false;
4038
+ break
4039
+ }
4040
+ }
4041
+ if (unchanged) {
4042
+ computedSoFar[field] = cache.lastResult;
4043
+ mergedState[field] = cache.lastResult;
4044
+ continue
4045
+ }
4046
+ }
4047
+
4048
+ try {
4049
+ const result = fn(mergedState);
4050
+ cache.lastDepValues = currentDepValues;
4051
+ cache.lastResult = result;
4052
+ computedSoFar[field] = result;
4053
+ mergedState[field] = result;
4054
+ } catch (e) {
4055
+ console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
4056
+ }
4057
+ } else {
4058
+ // No deps declared — always recompute
4059
+ try {
4060
+ const result = fn(mergedState);
4061
+ computedSoFar[field] = result;
4062
+ mergedState[field] = result;
4063
+ } catch (e) {
4064
+ console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
4065
+ }
3870
4066
  }
3871
- return acc
3872
- }, {})
4067
+ }
4068
+
4069
+ return computedSoFar
3873
4070
  }
3874
4071
 
3875
4072
  cleanupCalculated(incomingState) {
@@ -4017,7 +4214,7 @@ class Component {
4017
4214
  this.newChildSources(childSources);
4018
4215
 
4019
4216
 
4020
- if (newInstanceCount > 0) this.log(`New sub components instantiated: ${ newInstanceCount }`, true);
4217
+ if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true);
4021
4218
 
4022
4219
  return newComponents
4023
4220
  }, {})
@@ -4083,7 +4280,7 @@ class Component {
4083
4280
  } else if (this.components[collectionOf]) {
4084
4281
  factory = this.components[collectionOf];
4085
4282
  } else {
4086
- throw new Error(`[${this.name}] Invalid 'of' propery in collection: ${ collectionOf }`)
4283
+ throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
4087
4284
  }
4088
4285
 
4089
4286
  const fieldLense = {
@@ -4091,7 +4288,7 @@ class Component {
4091
4288
  if (!Array.isArray(state[stateField])) return []
4092
4289
  const items = state[stateField];
4093
4290
  const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
4094
- const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
4291
+ const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered;
4095
4292
  const mapped = sorted.map((item, index) => {
4096
4293
  return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
4097
4294
  });
@@ -4100,7 +4297,7 @@ class Component {
4100
4297
  },
4101
4298
  set: (oldState, newState) => {
4102
4299
  if (this.calculated && stateField in this.calculated) {
4103
- console.warn(`Collection sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4300
+ console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4104
4301
  return oldState
4105
4302
  }
4106
4303
  const updated = [];
@@ -4129,17 +4326,17 @@ class Component {
4129
4326
  } else if (typeof stateField === 'string') {
4130
4327
  if (isObj(this.currentState)) {
4131
4328
  if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
4132
- 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.`);
4329
+ 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.`);
4133
4330
  lense = undefined;
4134
4331
  } else if (!Array.isArray(this.currentState[stateField])) {
4135
- console.warn(`State property '${ stateField }' in collection comopnent of ${ this.name } is not an array: No components will be instantiated in the collection.`);
4332
+ console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
4136
4333
  lense = fieldLense;
4137
4334
  } else {
4138
4335
  lense = fieldLense;
4139
4336
  }
4140
4337
  } else {
4141
4338
  if (!Array.isArray(this.currentState[stateField])) {
4142
- console.warn(`State property '${ stateField }' in collection component of ${ this.name } is not an array: No components will be instantiated in the collection.`);
4339
+ console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
4143
4340
  lense = fieldLense;
4144
4341
  } else {
4145
4342
  lense = fieldLense;
@@ -4147,14 +4344,14 @@ class Component {
4147
4344
  }
4148
4345
  } else if (isObj(stateField)) {
4149
4346
  if (typeof stateField.get !== 'function') {
4150
- 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.`);
4347
+ 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.`);
4151
4348
  lense = undefined;
4152
4349
  } else {
4153
4350
  lense = {
4154
4351
  get: (state) => {
4155
4352
  const newState = stateField.get(state);
4156
4353
  if (!Array.isArray(newState)) {
4157
- 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);
4354
+ 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);
4158
4355
  return []
4159
4356
  }
4160
4357
  return newState
@@ -4163,14 +4360,14 @@ class Component {
4163
4360
  };
4164
4361
  }
4165
4362
  } else {
4166
- 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.`);
4363
+ 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.`);
4167
4364
  lense = undefined;
4168
4365
  }
4169
4366
 
4170
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
4367
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber };
4171
4368
  const sink$ = collection(factory, lense, { container: null })(sources);
4172
4369
  if (!isObj(sink$)) {
4173
- throw new Error('Invalid sinks returned from component factory of collection element')
4370
+ throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
4174
4371
  }
4175
4372
  return sink$
4176
4373
  }
@@ -4192,7 +4389,7 @@ class Component {
4192
4389
  get: state => state[stateField],
4193
4390
  set: (oldState, newState) => {
4194
4391
  if (this.calculated && stateField in this.calculated) {
4195
- console.warn(`Switchable sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4392
+ console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4196
4393
  return oldState
4197
4394
  }
4198
4395
  if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
@@ -4211,13 +4408,13 @@ class Component {
4211
4408
  lense = fieldLense;
4212
4409
  } else if (isObj(stateField)) {
4213
4410
  if (typeof stateField.get !== 'function') {
4214
- 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.`);
4411
+ 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.`);
4215
4412
  lense = baseLense;
4216
4413
  } else {
4217
4414
  lense = { get: stateField.get, set: stateField.set };
4218
4415
  }
4219
4416
  } else {
4220
- 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.`);
4417
+ 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.`);
4221
4418
  lense = baseLense;
4222
4419
  }
4223
4420
 
@@ -4233,12 +4430,12 @@ class Component {
4233
4430
  switchableComponents[key] = component(options);
4234
4431
  }
4235
4432
  });
4236
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4433
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
4237
4434
 
4238
4435
  const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
4239
4436
 
4240
4437
  if (!isObj(sink$)) {
4241
- throw new Error('Invalid sinks returned from component factory of switchable element')
4438
+ throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
4242
4439
  }
4243
4440
 
4244
4441
  return sink$
@@ -4264,7 +4461,7 @@ class Component {
4264
4461
  const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
4265
4462
  if (!factory) {
4266
4463
  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.`)
4267
- throw new Error(`Component not found: ${ componentName }`)
4464
+ throw new Error(`Component not found: ${componentName}`)
4268
4465
  }
4269
4466
 
4270
4467
  let lense;
@@ -4273,7 +4470,7 @@ class Component {
4273
4470
  get: state => state[stateField],
4274
4471
  set: (oldState, newState) => {
4275
4472
  if (this.calculated && stateField in this.calculated) {
4276
- console.warn(`Sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4473
+ console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4277
4474
  return oldState
4278
4475
  }
4279
4476
  return { ...oldState, [stateField]: newState }
@@ -4291,17 +4488,17 @@ class Component {
4291
4488
  lense = fieldLense;
4292
4489
  } else if (isObj(stateField)) {
4293
4490
  if (typeof stateField.get !== 'function') {
4294
- 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.`);
4491
+ 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.`);
4295
4492
  lense = baseLense;
4296
4493
  } else {
4297
4494
  lense = { get: stateField.get, set: stateField.set };
4298
4495
  }
4299
4496
  } else {
4300
- 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.`);
4497
+ 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.`);
4301
4498
  lense = baseLense;
4302
4499
  }
4303
4500
 
4304
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4501
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
4305
4502
  const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
4306
4503
 
4307
4504
  if (!isObj(sink$)) {
@@ -4376,14 +4573,22 @@ class Component {
4376
4573
  const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
4377
4574
  if (immediate) {
4378
4575
  if (this.debug) {
4379
- console.log(`[${context}] ${fixedMsg(msg)}`);
4576
+ const text = `[${context}] ${fixedMsg(msg)}`;
4577
+ console.log(text);
4578
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
4579
+ window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
4580
+ }
4380
4581
  }
4381
4582
  return
4382
4583
  } else {
4383
4584
  return stream => {
4384
4585
  return stream.debug(msg => {
4385
4586
  if (this.debug) {
4386
- console.log(`[${context}] ${fixedMsg(msg)}`);
4587
+ const text = `[${context}] ${fixedMsg(msg)}`;
4588
+ console.log(text);
4589
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
4590
+ window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
4591
+ }
4387
4592
  }
4388
4593
  })
4389
4594
  }
@@ -4393,11 +4598,11 @@ class Component {
4393
4598
 
4394
4599
 
4395
4600
 
4396
- function getComponents(currentElement, componentNames, depth=0, index=0, parentId) {
4601
+ function getComponents(currentElement, componentNames, path='r', parentId) {
4397
4602
  if (!currentElement) return {}
4398
4603
 
4399
4604
  if (currentElement.data?.componentsProcessed) return {}
4400
- if (depth === 0) currentElement.data.componentsProcessed = true;
4605
+ if (path === 'r') currentElement.data.componentsProcessed = true;
4401
4606
 
4402
4607
  const sel = currentElement.sel;
4403
4608
  const isCollection = sel && sel.toLowerCase() === 'collection';
@@ -4411,11 +4616,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4411
4616
 
4412
4617
  let id = parentId;
4413
4618
  if (isComponent) {
4414
- id = getComponentIdFromElement(currentElement, depth, index, parentId);
4619
+ id = getComponentIdFromElement(currentElement, path, parentId);
4415
4620
  if (isCollection) {
4416
4621
  if (!props.of) throw new Error(`Collection element missing required 'component' property`)
4417
4622
  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`)
4418
- if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${ props.of }`)
4623
+ if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
4419
4624
  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);
4420
4625
  currentElement.data.isCollection = true;
4421
4626
  currentElement.data.props ||= {};
@@ -4426,7 +4631,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4426
4631
  if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
4427
4632
  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`)
4428
4633
  const switchableComponentNames = Object.keys(props.of);
4429
- if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${ props.current }' not found in switchable element`)
4634
+ if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
4430
4635
  currentElement.data.isSwitchable = true;
4431
4636
  } else ;
4432
4637
  if (typeof props.key === 'undefined') currentElement.data.props.key = id;
@@ -4434,7 +4639,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4434
4639
  }
4435
4640
 
4436
4641
  if (children.length > 0) {
4437
- children.map((child, i) => getComponents(child, componentNames, depth + 1, index + i, id))
4642
+ children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
4438
4643
  .forEach((child) => {
4439
4644
  Object.entries(child).forEach(([id, el]) => found[id] = el);
4440
4645
  });
@@ -4443,10 +4648,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4443
4648
  return found
4444
4649
  }
4445
4650
 
4446
- function injectComponents(currentElement, components, componentNames, depth=0, index=0, parentId) {
4651
+ function injectComponents(currentElement, components, componentNames, path='r', parentId) {
4447
4652
  if (!currentElement) return
4448
4653
  if (currentElement.data?.componentsInjected) return currentElement
4449
- if (depth === 0 && currentElement.data) currentElement.data.componentsInjected = true;
4654
+ if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true;
4450
4655
 
4451
4656
 
4452
4657
  const sel = currentElement.sel || 'NO SELECTOR';
@@ -4458,7 +4663,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
4458
4663
 
4459
4664
  let id = parentId;
4460
4665
  if (isComponent) {
4461
- id = getComponentIdFromElement(currentElement, depth, index, parentId);
4666
+ id = getComponentIdFromElement(currentElement, path, parentId);
4462
4667
  const component = components[id];
4463
4668
  if (isCollection) {
4464
4669
  currentElement.sel = 'div';
@@ -4470,21 +4675,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
4470
4675
  return component
4471
4676
  }
4472
4677
  } else if (children.length > 0) {
4473
- currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, depth + 1, index + i, id)).flat();
4678
+ currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat();
4474
4679
  return currentElement
4475
4680
  } else {
4476
4681
  return currentElement
4477
4682
  }
4478
4683
  }
4479
4684
 
4480
- function getComponentIdFromElement(el, depth, index, parentId) {
4685
+ function getComponentIdFromElement(el, path, parentId) {
4481
4686
  const sel = el.sel;
4482
4687
  const name = typeof sel === 'string' ? sel : 'functionComponent';
4483
- const uid = `${depth}:${index}`;
4484
4688
  const props = el.data?.props || {};
4485
- const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || uid;
4486
- const parentString = parentId ? `${ parentId }|` : '';
4487
- const fullId = `${ parentString }${ name }::${ id }`;
4689
+ const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path;
4690
+ const parentString = parentId ? `${parentId}|` : '';
4691
+ const fullId = `${parentString}${name}::${id}`;
4488
4692
  return fullId
4489
4693
  }
4490
4694
 
@@ -4628,7 +4832,7 @@ function sortFunctionFromProp(sortProp) {
4628
4832
  } else if (isObj(sortProp)) {
4629
4833
  return __sortFunctionFromObj(sortProp)
4630
4834
  } else {
4631
- console.error('Invalid sort option (ignoring):', item);
4835
+ console.error('Invalid sort option (ignoring):', sortProp);
4632
4836
  return undefined
4633
4837
  }
4634
4838
  }
@@ -4645,11 +4849,11 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4645
4849
  const functionName = promiseReturningFunction.name || '[anonymous function]';
4646
4850
  const functionArgsType = typeof functionArgs;
4647
4851
  if (functionArgsType !== 'string' && functionArgsType !== 'function' && !(Array.isArray(functionArgs) && functionArgs.every((arg) => typeof arg === 'string'))) {
4648
- throw new Error(`The 'args' option for driverFromAsync(${ functionName }) must be a string, array of strings, or a function. Received ${functionArgsType}`)
4852
+ throw new Error(`The 'args' option for driverFromAsync(${functionName}) must be a string, array of strings, or a function. Received ${functionArgsType}`)
4649
4853
  }
4650
4854
 
4651
4855
  if (typeof selectorProperty !== 'string') {
4652
- throw new Error(`The 'selector' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof selectorProperty}`)
4856
+ throw new Error(`The 'selector' option for driverFromAsync(${functionName}) must be a string. Received ${typeof selectorProperty}`)
4653
4857
  }
4654
4858
 
4655
4859
  return (fromApp$) => {
@@ -4678,7 +4882,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4678
4882
  argArr = functionArgs.map((arg) => preProcessed[arg]);
4679
4883
  }
4680
4884
  }
4681
- const errMsg = `Error in driver created using driverFromAsync(${ functionName })`;
4885
+ const errMsg = `Error in driver created using driverFromAsync(${functionName})`;
4682
4886
  promiseReturningFunction(...argArr)
4683
4887
  .then((innerVal) => {
4684
4888
  const constructReply = (rawVal) => {
@@ -4688,7 +4892,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4688
4892
  if (typeof outgoing === 'object' && outgoing !== null) {
4689
4893
  outgoing[selectorProperty] = incoming[selectorProperty];
4690
4894
  } else {
4691
- console.warn(`The 'return' option for driverFromAsync(${ functionName }) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`);
4895
+ console.warn(`The 'return' option for driverFromAsync(${functionName}) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`);
4692
4896
  }
4693
4897
  } else if (typeof returnProperty === 'string') {
4694
4898
  outgoing = {
@@ -4696,7 +4900,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4696
4900
  [selectorProperty]: incoming[selectorProperty]
4697
4901
  };
4698
4902
  } else {
4699
- throw new Error(`The 'return' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof returnProperty}`)
4903
+ throw new Error(`The 'return' option for driverFromAsync(${functionName}) must be a string. Received ${typeof returnProperty}`)
4700
4904
  }
4701
4905
  return outgoing
4702
4906
  };
@@ -4712,12 +4916,12 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4712
4916
  .then((innerProcessedOutgoing) => {
4713
4917
  sendFn(constructReply(innerProcessedOutgoing));
4714
4918
  })
4715
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4919
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4716
4920
  } else {
4717
- sendFn(constructReply(rocessedOutgoing));
4921
+ sendFn(constructReply(processedOutgoing));
4718
4922
  }
4719
4923
  })
4720
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4924
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4721
4925
  } else {
4722
4926
  const processedOutgoing = postFunction(innerVal, incoming);
4723
4927
  if (typeof processedOutgoing.then === 'function') {
@@ -4725,19 +4929,19 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4725
4929
  .then((innerProcessedOutgoing) => {
4726
4930
  sendFn(constructReply(innerProcessedOutgoing));
4727
4931
  })
4728
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4932
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4729
4933
  } else {
4730
4934
  sendFn(constructReply(processedOutgoing));
4731
4935
  }
4732
4936
  }
4733
4937
  })
4734
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4938
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4735
4939
  },
4736
4940
  error: (err) => {
4737
- console.error(`Error recieved from sink stream in driver created using driverFromAsync(${ functionName }): ${ err }`);
4941
+ console.error(`Error received from sink stream in driver created using driverFromAsync(${functionName}):`, err);
4738
4942
  },
4739
4943
  complete: () => {
4740
- console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${ functionName })`);
4944
+ console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${functionName})`);
4741
4945
  }
4742
4946
  });
4743
4947
 
@@ -4753,6 +4957,9 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4753
4957
  }
4754
4958
 
4755
4959
  function processForm(form, options={}) {
4960
+ if (!form || typeof form.events !== 'function') {
4961
+ throw new Error('processForm: first argument must have an .events() method (e.g. DOM.select(...))')
4962
+ }
4756
4963
  let { events = ['input', 'submit'], preventDefault = true } = options;
4757
4964
  if (typeof events === 'string') events = [events];
4758
4965
 
@@ -4780,6 +4987,12 @@ function processForm(form, options={}) {
4780
4987
  }
4781
4988
 
4782
4989
  function processDrag({ draggable, dropZone } = {}, options = {}) {
4990
+ if (draggable && typeof draggable.events !== 'function') {
4991
+ throw new Error('processDrag: draggable must have an .events() method (e.g. DOM.select(...))')
4992
+ }
4993
+ if (dropZone && typeof dropZone.events !== 'function') {
4994
+ throw new Error('processDrag: dropZone must have an .events() method (e.g. DOM.select(...))')
4995
+ }
4783
4996
  const { effectAllowed = 'move' } = options;
4784
4997
 
4785
4998
  const dragStart$ = draggable
@@ -5008,7 +5221,8 @@ function eventBusDriver(out$) {
5008
5221
  const events = new EventTarget();
5009
5222
 
5010
5223
  out$.subscribe({
5011
- next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
5224
+ next: event => events.dispatchEvent(new CustomEvent('data', { detail: event })),
5225
+ error: err => console.error('[EVENTS driver] Error in sink stream:', err)
5012
5226
  });
5013
5227
 
5014
5228
  return {
@@ -5036,11 +5250,266 @@ function logDriver(out$) {
5036
5250
  out$.addListener({
5037
5251
  next: (val) => {
5038
5252
  console.log(val);
5253
+ },
5254
+ error: (err) => {
5255
+ console.error('[LOG driver] Error in sink stream:', err);
5039
5256
  }
5040
5257
  });
5041
5258
  }
5042
5259
 
5260
+ const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
5261
+ const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
5262
+ const DEFAULT_MAX_HISTORY = 200;
5263
+
5264
+ class SygnalDevTools {
5265
+ constructor() {
5266
+ this._connected = false;
5267
+ this._components = new Map();
5268
+ this._stateHistory = [];
5269
+ this._maxHistory = DEFAULT_MAX_HISTORY;
5270
+ }
5271
+
5272
+ get connected() {
5273
+ return this._connected && typeof window !== 'undefined'
5274
+ }
5275
+
5276
+ // ─── Initialization ─────────────────────────────────────────────────────────
5277
+
5278
+ init() {
5279
+ if (typeof window === 'undefined') return
5280
+
5281
+ window.__SYGNAL_DEVTOOLS__ = this;
5282
+
5283
+ window.addEventListener('message', (event) => {
5284
+ if (event.source !== window) return
5285
+ if (event.data?.source === EXTENSION_SOURCE) {
5286
+ this._handleExtensionMessage(event.data);
5287
+ }
5288
+ });
5289
+ }
5290
+
5291
+ _handleExtensionMessage(msg) {
5292
+ switch (msg.type) {
5293
+ case 'CONNECT':
5294
+ this._connected = true;
5295
+ if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
5296
+ this._sendFullTree();
5297
+ break
5298
+ case 'DISCONNECT':
5299
+ this._connected = false;
5300
+ break
5301
+ case 'SET_DEBUG':
5302
+ this._setDebug(msg.payload);
5303
+ break
5304
+ case 'TIME_TRAVEL':
5305
+ this._timeTravel(msg.payload);
5306
+ break
5307
+ case 'GET_STATE':
5308
+ this._sendComponentState(msg.payload.componentId);
5309
+ break
5310
+ }
5311
+ }
5312
+
5313
+ // ─── Hooks (called from component.js) ────────────────────────────────────────
5314
+
5315
+ onComponentCreated(componentNumber, name, instance) {
5316
+ const meta = {
5317
+ id: componentNumber,
5318
+ name: name,
5319
+ isSubComponent: instance.isSubComponent,
5320
+ hasModel: !!instance.model,
5321
+ hasIntent: !!instance.intent,
5322
+ hasContext: !!instance.context,
5323
+ hasCalculated: !!instance.calculated,
5324
+ components: Object.keys(instance.components || {}),
5325
+ parentId: null,
5326
+ children: [],
5327
+ debug: instance._debug,
5328
+ createdAt: Date.now(),
5329
+ _instanceRef: new WeakRef(instance),
5330
+ };
5331
+ this._components.set(componentNumber, meta);
5332
+
5333
+ if (!this.connected) return
5334
+ this._post('COMPONENT_CREATED', this._serializeMeta(meta));
5335
+ }
5336
+
5337
+ onStateChanged(componentNumber, name, state) {
5338
+ if (!this.connected) return
5339
+
5340
+ const entry = {
5341
+ componentId: componentNumber,
5342
+ componentName: name,
5343
+ timestamp: Date.now(),
5344
+ state: this._safeClone(state),
5345
+ };
5346
+
5347
+ this._stateHistory.push(entry);
5348
+ if (this._stateHistory.length > this._maxHistory) {
5349
+ this._stateHistory.shift();
5350
+ }
5351
+
5352
+ this._post('STATE_CHANGED', {
5353
+ componentId: componentNumber,
5354
+ componentName: name,
5355
+ state: entry.state,
5356
+ historyIndex: this._stateHistory.length - 1,
5357
+ });
5358
+ }
5359
+
5360
+ onActionDispatched(componentNumber, name, actionType, data) {
5361
+ if (!this.connected) return
5362
+ this._post('ACTION_DISPATCHED', {
5363
+ componentId: componentNumber,
5364
+ componentName: name,
5365
+ actionType: actionType,
5366
+ data: this._safeClone(data),
5367
+ timestamp: Date.now(),
5368
+ });
5369
+ }
5370
+
5371
+ onSubComponentRegistered(parentNumber, childNumber) {
5372
+ const parent = this._components.get(parentNumber);
5373
+ const child = this._components.get(childNumber);
5374
+ if (parent && child) {
5375
+ child.parentId = parentNumber;
5376
+ if (!parent.children.includes(childNumber)) {
5377
+ parent.children.push(childNumber);
5378
+ }
5379
+ }
5380
+
5381
+ if (!this.connected) return
5382
+ this._post('TREE_UPDATED', {
5383
+ parentId: parentNumber,
5384
+ childId: childNumber,
5385
+ });
5386
+ }
5387
+
5388
+ onContextChanged(componentNumber, name, context) {
5389
+ if (!this.connected) return
5390
+ this._post('CONTEXT_CHANGED', {
5391
+ componentId: componentNumber,
5392
+ componentName: name,
5393
+ context: this._safeClone(context),
5394
+ });
5395
+ }
5396
+
5397
+ onDebugLog(componentNumber, message) {
5398
+ if (!this.connected) return
5399
+ this._post('DEBUG_LOG', {
5400
+ componentId: componentNumber,
5401
+ message: message,
5402
+ timestamp: Date.now(),
5403
+ });
5404
+ }
5405
+
5406
+ // ─── Commands (from extension to page) ───────────────────────────────────────
5407
+
5408
+ _setDebug({ componentId, enabled }) {
5409
+ if (typeof componentId === 'undefined' || componentId === null) {
5410
+ if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
5411
+ this._post('DEBUG_TOGGLED', { global: true, enabled });
5412
+ return
5413
+ }
5414
+
5415
+ const meta = this._components.get(componentId);
5416
+ if (meta && meta._instanceRef) {
5417
+ const instance = meta._instanceRef.deref();
5418
+ if (instance) {
5419
+ instance._debug = enabled;
5420
+ meta.debug = enabled;
5421
+ this._post('DEBUG_TOGGLED', { componentId, enabled });
5422
+ }
5423
+ }
5424
+ }
5425
+
5426
+ _timeTravel({ historyIndex }) {
5427
+ const entry = this._stateHistory[historyIndex];
5428
+ if (!entry) return
5429
+
5430
+ const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__;
5431
+ if (app?.sinks?.STATE?.shamefullySendNext) {
5432
+ app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
5433
+ this._post('TIME_TRAVEL_APPLIED', {
5434
+ historyIndex,
5435
+ state: entry.state,
5436
+ });
5437
+ }
5438
+ }
5439
+
5440
+ _sendComponentState(componentId) {
5441
+ const meta = this._components.get(componentId);
5442
+ if (meta && meta._instanceRef) {
5443
+ const instance = meta._instanceRef.deref();
5444
+ if (instance) {
5445
+ this._post('COMPONENT_STATE', {
5446
+ componentId,
5447
+ state: this._safeClone(instance.currentState),
5448
+ context: this._safeClone(instance.currentContext),
5449
+ props: this._safeClone(instance.currentProps),
5450
+ });
5451
+ }
5452
+ }
5453
+ }
5454
+
5455
+ _sendFullTree() {
5456
+ const tree = [];
5457
+ for (const [id, meta] of this._components) {
5458
+ const instance = meta._instanceRef?.deref();
5459
+ tree.push({
5460
+ ...this._serializeMeta(meta),
5461
+ state: instance ? this._safeClone(instance.currentState) : null,
5462
+ context: instance ? this._safeClone(instance.currentContext) : null,
5463
+ });
5464
+ }
5465
+ this._post('FULL_TREE', {
5466
+ components: tree,
5467
+ history: this._stateHistory,
5468
+ });
5469
+ }
5470
+
5471
+ // ─── Transport ───────────────────────────────────────────────────────────────
5472
+
5473
+ _post(type, payload) {
5474
+ if (typeof window === 'undefined') return
5475
+ window.postMessage({
5476
+ source: DEVTOOLS_SOURCE,
5477
+ type,
5478
+ payload,
5479
+ }, '*');
5480
+ }
5481
+
5482
+ _safeClone(obj) {
5483
+ if (obj === undefined || obj === null) return obj
5484
+ try {
5485
+ return JSON.parse(JSON.stringify(obj))
5486
+ } catch (e) {
5487
+ return '[unserializable]'
5488
+ }
5489
+ }
5490
+
5491
+ _serializeMeta(meta) {
5492
+ const { _instanceRef, ...rest } = meta;
5493
+ return rest
5494
+ }
5495
+ }
5496
+
5497
+ // ─── Singleton ────────────────────────────────────────────────────────────────
5498
+
5499
+ let instance = null;
5500
+
5501
+ function getDevTools() {
5502
+ if (!instance) instance = new SygnalDevTools();
5503
+ return instance
5504
+ }
5505
+
5043
5506
  function run(app, drivers={}, options={}) {
5507
+ // Initialize DevTools instrumentation bridge early (before component creation)
5508
+ if (typeof window !== 'undefined') {
5509
+ const dt = getDevTools();
5510
+ dt.init();
5511
+ }
5512
+
5044
5513
  const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options;
5045
5514
  if (!app.isSygnalComponent) {
5046
5515
  const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
@@ -5090,6 +5559,11 @@ function run(app, drivers={}, options={}) {
5090
5559
 
5091
5560
  const exposed = { sources, sinks, dispose };
5092
5561
 
5562
+ // Store app reference for time-travel
5563
+ if (typeof window !== 'undefined') {
5564
+ window.__SYGNAL_DEVTOOLS_APP__ = exposed;
5565
+ }
5566
+
5093
5567
  const swapToComponent = (newComponent, state) => {
5094
5568
  const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
5095
5569
  const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
@@ -5604,6 +6078,7 @@ exports.driverFromAsync = driverFromAsync;
5604
6078
  exports.dropRepeats = _default$5;
5605
6079
  exports.enableHMR = enableHMR;
5606
6080
  exports.exactState = exactState;
6081
+ exports.getDevTools = getDevTools;
5607
6082
  exports.makeDragDriver = makeDragDriver;
5608
6083
  exports.processDrag = processDrag;
5609
6084
  exports.processForm = processForm;