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.esm.js CHANGED
@@ -20,7 +20,12 @@ function _mergeNamespaces(n, m) {
20
20
  return Object.freeze(n);
21
21
  }
22
22
 
23
+ let COLLECTION_COUNT = 0;
24
+
23
25
  function collection(component, stateLense, opts={}) {
26
+ if (typeof component !== 'function') {
27
+ throw new Error('collection: first argument (component) must be a function')
28
+ }
24
29
  const {
25
30
  combineList = ['DOM'],
26
31
  globalList = ['EVENTS'],
@@ -31,7 +36,7 @@ function collection(component, stateLense, opts={}) {
31
36
  } = opts;
32
37
 
33
38
  return (sources) => {
34
- const key = Date.now();
39
+ const key = `sygnal-collection-${COLLECTION_COUNT++}`;
35
40
  const collectionOpts = {
36
41
  item: component,
37
42
  itemKey: (state, ind) => typeof state.id !== 'undefined' ? state.id : ind,
@@ -2848,7 +2853,7 @@ function switchable(factories, name$, initial, opts={}) {
2848
2853
  const mapFunction = (nameType === 'function' && name$) || (state => state[name$]);
2849
2854
  return sources => {
2850
2855
  const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream;
2851
- if (!state$ instanceof Stream$1) throw new Error(`Could not find the state source: ${ stateSourceName }`)
2856
+ if (!(state$ instanceof Stream$1)) throw new Error(`Could not find the state source: ${stateSourceName}`)
2852
2857
  const _name$ = state$
2853
2858
  .map(mapFunction)
2854
2859
  .filter(name => typeof name === 'string')
@@ -3262,13 +3267,27 @@ function wrapDOMSource(domSource) {
3262
3267
  }
3263
3268
 
3264
3269
 
3265
- const ABORT = '~#~#~ABORT~#~#~';
3270
+ const ABORT = Symbol('ABORT');
3271
+
3272
+
3273
+ function normalizeCalculatedEntry(field, entry) {
3274
+ if (typeof entry === 'function') {
3275
+ return { fn: entry, deps: null }
3276
+ }
3277
+ if (Array.isArray(entry) && entry.length === 2
3278
+ && Array.isArray(entry[0]) && typeof entry[1] === 'function') {
3279
+ return { fn: entry[1], deps: entry[0] }
3280
+ }
3281
+ throw new Error(
3282
+ `Invalid calculated field '${field}': expected a function or [depsArray, function]`
3283
+ )
3284
+ }
3266
3285
 
3267
3286
  function component (opts) {
3268
3287
  const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
3269
3288
 
3270
3289
  if (sources && !isObj(sources)) {
3271
- throw new Error('Sources must be a Cycle.js sources object:', name)
3290
+ throw new Error(`[${name}] Sources must be a Cycle.js sources object`)
3272
3291
  }
3273
3292
 
3274
3293
  let fixedIsolateOpts;
@@ -3348,7 +3367,9 @@ class Component {
3348
3367
  // sinks
3349
3368
 
3350
3369
  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 }) {
3351
- if (!sources || !isObj(sources)) throw new Error('Missing or invalid sources')
3370
+ if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
3371
+
3372
+ this._componentNumber = COMPONENT_COUNT++;
3352
3373
 
3353
3374
  this.name = name;
3354
3375
  this.sources = sources;
@@ -3369,6 +3390,123 @@ class Component {
3369
3390
  this.sourceNames = Object.keys(sources);
3370
3391
  this._debug = debug;
3371
3392
 
3393
+ // Warn if calculated fields shadow base state keys
3394
+ if (this.calculated && this.initialState
3395
+ && isObj(this.calculated) && isObj(this.initialState)) {
3396
+ for (const key of Object.keys(this.calculated)) {
3397
+ if (key in this.initialState) {
3398
+ console.warn(
3399
+ `[${name}] Calculated field '${key}' shadows a key in initialState. ` +
3400
+ `The initialState value will be overwritten on every state update.`
3401
+ );
3402
+ }
3403
+ }
3404
+ }
3405
+
3406
+ // Normalize calculated entries, build dependency graph, topological sort
3407
+ if (this.calculated && isObj(this.calculated)) {
3408
+ const calcEntries = Object.entries(this.calculated);
3409
+
3410
+ // Normalize all entries to { fn, deps } shape
3411
+ this._calculatedNormalized = {};
3412
+ for (const [field, entry] of calcEntries) {
3413
+ this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry);
3414
+ }
3415
+
3416
+ this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized));
3417
+
3418
+ // Warn on deps referencing nonexistent keys
3419
+ for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
3420
+ if (deps !== null) {
3421
+ for (const dep of deps) {
3422
+ if (!this._calculatedFieldNames.has(dep)
3423
+ && this.initialState && !(dep in this.initialState)) {
3424
+ console.warn(
3425
+ `[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
3426
+ `which is not in initialState or calculated fields`
3427
+ );
3428
+ }
3429
+ }
3430
+ }
3431
+ }
3432
+
3433
+ // Build adjacency: for each field, which other calculated fields must run first?
3434
+ const calcDeps = {};
3435
+ for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
3436
+ if (deps === null) {
3437
+ calcDeps[field] = [];
3438
+ } else {
3439
+ calcDeps[field] = deps.filter(d => this._calculatedFieldNames.has(d));
3440
+ }
3441
+ }
3442
+
3443
+ // Kahn's algorithm for topological sort
3444
+ const inDegree = {};
3445
+ const reverseGraph = {};
3446
+ for (const field of this._calculatedFieldNames) {
3447
+ inDegree[field] = 0;
3448
+ reverseGraph[field] = [];
3449
+ }
3450
+ for (const [field, depList] of Object.entries(calcDeps)) {
3451
+ inDegree[field] = depList.length;
3452
+ for (const dep of depList) {
3453
+ reverseGraph[dep].push(field);
3454
+ }
3455
+ }
3456
+
3457
+ const queue = [];
3458
+ for (const [field, degree] of Object.entries(inDegree)) {
3459
+ if (degree === 0) queue.push(field);
3460
+ }
3461
+
3462
+ const sorted = [];
3463
+ while (queue.length > 0) {
3464
+ const current = queue.shift();
3465
+ sorted.push(current);
3466
+ for (const dependent of reverseGraph[current]) {
3467
+ inDegree[dependent]--;
3468
+ if (inDegree[dependent] === 0) queue.push(dependent);
3469
+ }
3470
+ }
3471
+
3472
+ if (sorted.length !== this._calculatedFieldNames.size) {
3473
+ // Cycle detected — build error message with cycle path
3474
+ const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f));
3475
+ const visited = new Set();
3476
+ const path = [];
3477
+ const traceCycle = (node) => {
3478
+ if (visited.has(node)) { path.push(node); return true }
3479
+ visited.add(node);
3480
+ path.push(node);
3481
+ for (const dep of calcDeps[node]) {
3482
+ if (inCycle.includes(dep) && traceCycle(dep)) return true
3483
+ }
3484
+ path.pop();
3485
+ visited.delete(node);
3486
+ return false
3487
+ };
3488
+ traceCycle(inCycle[0]);
3489
+ const start = path[path.length - 1];
3490
+ const cycle = path.slice(path.indexOf(start));
3491
+ throw new Error(`Circular calculated dependency: ${cycle.join(' \u2192 ')}`)
3492
+ }
3493
+
3494
+ this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized[f]]);
3495
+
3496
+ // Initialize per-field memoization caches for fields with declared deps
3497
+ this._calculatedFieldCache = {};
3498
+ for (const [field, { deps }] of this._calculatedOrder) {
3499
+ if (deps !== null) {
3500
+ this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined };
3501
+ }
3502
+ }
3503
+ } else {
3504
+ this._calculatedOrder = null;
3505
+ this._calculatedNormalized = null;
3506
+ this._calculatedFieldNames = null;
3507
+ this._calculatedFieldCache = null;
3508
+ }
3509
+
3372
3510
  this.isSubComponent = this.sourceNames.includes('props$');
3373
3511
 
3374
3512
  const state$ = sources[stateSourceName] && sources[stateSourceName].stream;
@@ -3377,6 +3515,9 @@ class Component {
3377
3515
  this.currentState = initialState || {};
3378
3516
  this.sources[stateSourceName] = new StateSource(state$.map(val => {
3379
3517
  this.currentState = val;
3518
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3519
+ window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val);
3520
+ }
3380
3521
  return val
3381
3522
  }));
3382
3523
  }
@@ -3412,10 +3553,8 @@ class Component {
3412
3553
  };
3413
3554
  }
3414
3555
 
3415
- const componentNumber = COMPONENT_COUNT++;
3416
-
3417
3556
  this.addCalculated = this.createMemoizedAddCalculated();
3418
- this.log = makeLog(`${componentNumber} | ${name}`);
3557
+ this.log = makeLog(`${this._componentNumber} | ${name}`);
3419
3558
 
3420
3559
  this.initChildSources$();
3421
3560
  this.initIntent$();
@@ -3430,9 +3569,20 @@ class Component {
3430
3569
  this.initVdom$();
3431
3570
  this.initSinks();
3432
3571
 
3433
- this.sinks.__index = componentNumber;
3572
+ this.sinks.__index = this._componentNumber;
3434
3573
 
3435
3574
  this.log(`Instantiated`, true);
3575
+
3576
+ // Hook 1: Register with DevTools
3577
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
3578
+ window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this);
3579
+
3580
+ // Hook 1b: Register parent-child relationship
3581
+ const parentNum = sources?.__parentComponentNumber;
3582
+ if (typeof parentNum === 'number') {
3583
+ window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber);
3584
+ }
3585
+ }
3436
3586
  }
3437
3587
 
3438
3588
  get debug() {
@@ -3444,13 +3594,13 @@ class Component {
3444
3594
  return
3445
3595
  }
3446
3596
  if (typeof this.intent != 'function') {
3447
- throw new Error('Intent must be a function')
3597
+ throw new Error(`[${this.name}] Intent must be a function`)
3448
3598
  }
3449
3599
 
3450
3600
  this.intent$ = this.intent(this.sources);
3451
3601
 
3452
3602
  if (!(this.intent$ instanceof Stream$1) && (!isObj(this.intent$))) {
3453
- throw new Error('Intent must return either an action$ stream or map of event streams')
3603
+ throw new Error(`[${this.name}] Intent must return either an action$ stream or map of event streams`)
3454
3604
  }
3455
3605
  }
3456
3606
 
@@ -3463,10 +3613,10 @@ class Component {
3463
3613
  this.hmrActions = [this.hmrActions];
3464
3614
  }
3465
3615
  if (!Array.isArray(this.hmrActions)) {
3466
- 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`)
3616
+ 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`)
3467
3617
  }
3468
3618
  if (this.hmrActions.some(action => typeof action !== 'string')) {
3469
- 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`)
3619
+ 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`)
3470
3620
  }
3471
3621
  this.hmrAction$ = xs$1.fromArray(this.hmrActions.map(action => ({ type: action })));
3472
3622
  }
@@ -3505,7 +3655,15 @@ class Component {
3505
3655
  const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }));
3506
3656
 
3507
3657
  this.action$ = xs$1.merge(wrapped$, hydrate$)
3508
- .compose(this.log(({ type }) => `<${ type }> Action triggered`));
3658
+ .compose(this.log(({ type }) => `<${type}> Action triggered`))
3659
+ .map(action => {
3660
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3661
+ window.__SYGNAL_DEVTOOLS__.onActionDispatched(
3662
+ this._componentNumber, this.name, action.type, action.data
3663
+ );
3664
+ }
3665
+ return action
3666
+ });
3509
3667
  }
3510
3668
 
3511
3669
  initState() {
@@ -3517,7 +3675,7 @@ class Component {
3517
3675
  } else if (isObj(this.model[INITIALIZE_ACTION])) {
3518
3676
  Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
3519
3677
  if (name !== this.stateSourceName) {
3520
- console.warn(`${ INITIALIZE_ACTION } can only be used with the ${ this.stateSourceName } source... disregarding ${ name }`);
3678
+ console.warn(`${INITIALIZE_ACTION} can only be used with the ${this.stateSourceName} source... disregarding ${name}`);
3521
3679
  delete this.model[INITIALIZE_ACTION][name];
3522
3680
  }
3523
3681
  });
@@ -3552,7 +3710,7 @@ class Component {
3552
3710
  } else if (valueType === 'function') {
3553
3711
  _value = value(state);
3554
3712
  } else {
3555
- console.error(`[${ this.name }] Invalid context entry '${ name }': must be the name of a state property or a function returning a value to use`);
3713
+ console.error(`[${this.name}] Invalid context entry '${name}': must be the name of a state property or a function returning a value to use`);
3556
3714
  return acc
3557
3715
  }
3558
3716
  acc[name] = _value;
@@ -3560,11 +3718,14 @@ class Component {
3560
3718
  }, {});
3561
3719
  const newContext = { ..._parent, ...values };
3562
3720
  this.currentContext = newContext;
3721
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
3722
+ window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext);
3723
+ }
3563
3724
  return newContext
3564
3725
  })
3565
3726
  .compose(dropRepeats(objIsEqual))
3566
3727
  .startWith({});
3567
- this.context$.subscribe({ next: _ => _ });
3728
+ this.context$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in context stream:`, err) });
3568
3729
  }
3569
3730
 
3570
3731
  initModel$() {
@@ -3580,7 +3741,7 @@ class Component {
3580
3741
  const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState;
3581
3742
  const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState };
3582
3743
  if (this.isSubComponent && this.initialState) {
3583
- console.warn(`[${ this.name }] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
3744
+ console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`);
3584
3745
  }
3585
3746
  const hasInitialState = (typeof effectiveInitialState !== 'undefined');
3586
3747
  const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined');
@@ -3601,7 +3762,7 @@ class Component {
3601
3762
  }
3602
3763
 
3603
3764
  if (!isObj(sinks)) {
3604
- throw new Error(`Entry for each action must be an object: ${ this.name } ${ action }`)
3765
+ throw new Error(`[${this.name}] Entry for each action must be an object: ${action}`)
3605
3766
  }
3606
3767
 
3607
3768
  const sinkEntries = Object.entries(sinks);
@@ -3618,12 +3779,12 @@ class Component {
3618
3779
  const wrapped$ = on$
3619
3780
  .compose(this.log(data => {
3620
3781
  if (isStateSink) {
3621
- return `<${ action }> State reducer added`
3782
+ return `<${action}> State reducer added`
3622
3783
  } else if (isParentSink) {
3623
- return `<${ action }> Data sent to parent component: ${ JSON.stringify(data.value).replaceAll('"', '') }`
3784
+ return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
3624
3785
  } else {
3625
3786
  const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
3626
- return `<${ action }> Data sent to [${ sink }]: ${ JSON.stringify(extra).replaceAll('"', '') }`
3787
+ return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
3627
3788
  }
3628
3789
  }));
3629
3790
 
@@ -3701,7 +3862,7 @@ class Component {
3701
3862
 
3702
3863
  }
3703
3864
  });
3704
- subComponentSink$.subscribe({ next: _ => _ });
3865
+ subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) });
3705
3866
  this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
3706
3867
  }
3707
3868
 
@@ -3764,13 +3925,13 @@ class Component {
3764
3925
  if (typeof reducer === 'function') {
3765
3926
  returnStream$ = filtered$.map(action => {
3766
3927
  const next = (type, data, delay=10) => {
3767
- 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.`)
3928
+ 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.`)
3768
3929
  // put the "next" action request at the end of the event loop so the "current" action completes first
3769
3930
  setTimeout(() => {
3770
3931
  // push the "next" action request into the action$ stream
3771
3932
  rootAction$.shamefullySendNext({ type, data });
3772
3933
  }, delay);
3773
- this.log(`<${ name }> Triggered a next() action: <${ type }> ${ delay }ms delay`, true);
3934
+ this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true);
3774
3935
  };
3775
3936
 
3776
3937
  const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext };
@@ -3782,7 +3943,7 @@ class Component {
3782
3943
  const enhancedState = this.addCalculated(_state);
3783
3944
  props.state = enhancedState;
3784
3945
  const newState = reducer(enhancedState, data, next, props);
3785
- if (newState == ABORT) return _state
3946
+ if (newState === ABORT) return _state
3786
3947
  return this.cleanupCalculated(newState)
3787
3948
  }
3788
3949
  } else {
@@ -3791,13 +3952,13 @@ class Component {
3791
3952
  const reduced = reducer(enhancedState, data, next, props);
3792
3953
  const type = typeof reduced;
3793
3954
  if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3794
- if (type == 'undefined') {
3795
- console.warn(`'undefined' value sent to ${ name }`);
3955
+ if (type === 'undefined') {
3956
+ console.warn(`[${this.name}] 'undefined' value sent to ${name}`);
3796
3957
  return reduced
3797
3958
  }
3798
- throw new Error(`Invalid reducer type for ${ name } ${ type }`)
3959
+ throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
3799
3960
  }
3800
- }).filter(result => result != ABORT);
3961
+ }).filter(result => result !== ABORT);
3801
3962
  } else if (reducer === undefined || reducer === true) {
3802
3963
  returnStream$ = filtered$.map(({data}) => data);
3803
3964
  } else {
@@ -3818,7 +3979,7 @@ class Component {
3818
3979
  if (state === lastState) {
3819
3980
  return lastResult
3820
3981
  }
3821
- if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3982
+ if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
3822
3983
 
3823
3984
  const calculated = this.getCalculatedValues(state);
3824
3985
  if (!calculated) {
@@ -3837,19 +3998,55 @@ class Component {
3837
3998
  }
3838
3999
 
3839
4000
  getCalculatedValues(state) {
3840
- const entries = Object.entries(this.calculated || {});
3841
- if (entries.length === 0) {
4001
+ if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
3842
4002
  return
3843
4003
  }
3844
- return entries.reduce((acc, [field, fn]) => {
3845
- if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3846
- try {
3847
- acc[field] = fn(state);
3848
- } catch(e) {
3849
- console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
4004
+
4005
+ const mergedState = { ...state };
4006
+ const computedSoFar = {};
4007
+
4008
+ for (const [field, { fn, deps }] of this._calculatedOrder) {
4009
+ if (deps !== null && this._calculatedFieldCache) {
4010
+ const cache = this._calculatedFieldCache[field];
4011
+ const currentDepValues = deps.map(d => mergedState[d]);
4012
+
4013
+ if (cache.lastDepValues !== undefined) {
4014
+ let unchanged = true;
4015
+ for (let i = 0; i < currentDepValues.length; i++) {
4016
+ if (currentDepValues[i] !== cache.lastDepValues[i]) {
4017
+ unchanged = false;
4018
+ break
4019
+ }
4020
+ }
4021
+ if (unchanged) {
4022
+ computedSoFar[field] = cache.lastResult;
4023
+ mergedState[field] = cache.lastResult;
4024
+ continue
4025
+ }
4026
+ }
4027
+
4028
+ try {
4029
+ const result = fn(mergedState);
4030
+ cache.lastDepValues = currentDepValues;
4031
+ cache.lastResult = result;
4032
+ computedSoFar[field] = result;
4033
+ mergedState[field] = result;
4034
+ } catch (e) {
4035
+ console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
4036
+ }
4037
+ } else {
4038
+ // No deps declared — always recompute
4039
+ try {
4040
+ const result = fn(mergedState);
4041
+ computedSoFar[field] = result;
4042
+ mergedState[field] = result;
4043
+ } catch (e) {
4044
+ console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`);
4045
+ }
3850
4046
  }
3851
- return acc
3852
- }, {})
4047
+ }
4048
+
4049
+ return computedSoFar
3853
4050
  }
3854
4051
 
3855
4052
  cleanupCalculated(incomingState) {
@@ -3997,7 +4194,7 @@ class Component {
3997
4194
  this.newChildSources(childSources);
3998
4195
 
3999
4196
 
4000
- if (newInstanceCount > 0) this.log(`New sub components instantiated: ${ newInstanceCount }`, true);
4197
+ if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true);
4001
4198
 
4002
4199
  return newComponents
4003
4200
  }, {})
@@ -4063,7 +4260,7 @@ class Component {
4063
4260
  } else if (this.components[collectionOf]) {
4064
4261
  factory = this.components[collectionOf];
4065
4262
  } else {
4066
- throw new Error(`[${this.name}] Invalid 'of' propery in collection: ${ collectionOf }`)
4263
+ throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
4067
4264
  }
4068
4265
 
4069
4266
  const fieldLense = {
@@ -4071,7 +4268,7 @@ class Component {
4071
4268
  if (!Array.isArray(state[stateField])) return []
4072
4269
  const items = state[stateField];
4073
4270
  const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
4074
- const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
4271
+ const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered;
4075
4272
  const mapped = sorted.map((item, index) => {
4076
4273
  return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
4077
4274
  });
@@ -4080,7 +4277,7 @@ class Component {
4080
4277
  },
4081
4278
  set: (oldState, newState) => {
4082
4279
  if (this.calculated && stateField in this.calculated) {
4083
- console.warn(`Collection sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4280
+ console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4084
4281
  return oldState
4085
4282
  }
4086
4283
  const updated = [];
@@ -4109,17 +4306,17 @@ class Component {
4109
4306
  } else if (typeof stateField === 'string') {
4110
4307
  if (isObj(this.currentState)) {
4111
4308
  if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
4112
- 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.`);
4309
+ 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.`);
4113
4310
  lense = undefined;
4114
4311
  } else if (!Array.isArray(this.currentState[stateField])) {
4115
- console.warn(`State property '${ stateField }' in collection comopnent of ${ this.name } is not an array: No components will be instantiated in the collection.`);
4312
+ console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
4116
4313
  lense = fieldLense;
4117
4314
  } else {
4118
4315
  lense = fieldLense;
4119
4316
  }
4120
4317
  } else {
4121
4318
  if (!Array.isArray(this.currentState[stateField])) {
4122
- console.warn(`State property '${ stateField }' in collection component of ${ this.name } is not an array: No components will be instantiated in the collection.`);
4319
+ console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`);
4123
4320
  lense = fieldLense;
4124
4321
  } else {
4125
4322
  lense = fieldLense;
@@ -4127,14 +4324,14 @@ class Component {
4127
4324
  }
4128
4325
  } else if (isObj(stateField)) {
4129
4326
  if (typeof stateField.get !== 'function') {
4130
- 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.`);
4327
+ 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.`);
4131
4328
  lense = undefined;
4132
4329
  } else {
4133
4330
  lense = {
4134
4331
  get: (state) => {
4135
4332
  const newState = stateField.get(state);
4136
4333
  if (!Array.isArray(newState)) {
4137
- 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);
4334
+ 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);
4138
4335
  return []
4139
4336
  }
4140
4337
  return newState
@@ -4143,14 +4340,14 @@ class Component {
4143
4340
  };
4144
4341
  }
4145
4342
  } else {
4146
- 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.`);
4343
+ 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.`);
4147
4344
  lense = undefined;
4148
4345
  }
4149
4346
 
4150
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
4347
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber };
4151
4348
  const sink$ = collection(factory, lense, { container: null })(sources);
4152
4349
  if (!isObj(sink$)) {
4153
- throw new Error('Invalid sinks returned from component factory of collection element')
4350
+ throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
4154
4351
  }
4155
4352
  return sink$
4156
4353
  }
@@ -4172,7 +4369,7 @@ class Component {
4172
4369
  get: state => state[stateField],
4173
4370
  set: (oldState, newState) => {
4174
4371
  if (this.calculated && stateField in this.calculated) {
4175
- console.warn(`Switchable sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4372
+ console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4176
4373
  return oldState
4177
4374
  }
4178
4375
  if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
@@ -4191,13 +4388,13 @@ class Component {
4191
4388
  lense = fieldLense;
4192
4389
  } else if (isObj(stateField)) {
4193
4390
  if (typeof stateField.get !== 'function') {
4194
- 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.`);
4391
+ 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.`);
4195
4392
  lense = baseLense;
4196
4393
  } else {
4197
4394
  lense = { get: stateField.get, set: stateField.set };
4198
4395
  }
4199
4396
  } else {
4200
- 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.`);
4397
+ 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.`);
4201
4398
  lense = baseLense;
4202
4399
  }
4203
4400
 
@@ -4213,12 +4410,12 @@ class Component {
4213
4410
  switchableComponents[key] = component(options);
4214
4411
  }
4215
4412
  });
4216
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4413
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
4217
4414
 
4218
4415
  const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
4219
4416
 
4220
4417
  if (!isObj(sink$)) {
4221
- throw new Error('Invalid sinks returned from component factory of switchable element')
4418
+ throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
4222
4419
  }
4223
4420
 
4224
4421
  return sink$
@@ -4244,7 +4441,7 @@ class Component {
4244
4441
  const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
4245
4442
  if (!factory) {
4246
4443
  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.`)
4247
- throw new Error(`Component not found: ${ componentName }`)
4444
+ throw new Error(`Component not found: ${componentName}`)
4248
4445
  }
4249
4446
 
4250
4447
  let lense;
@@ -4253,7 +4450,7 @@ class Component {
4253
4450
  get: state => state[stateField],
4254
4451
  set: (oldState, newState) => {
4255
4452
  if (this.calculated && stateField in this.calculated) {
4256
- console.warn(`Sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4453
+ console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`);
4257
4454
  return oldState
4258
4455
  }
4259
4456
  return { ...oldState, [stateField]: newState }
@@ -4271,17 +4468,17 @@ class Component {
4271
4468
  lense = fieldLense;
4272
4469
  } else if (isObj(stateField)) {
4273
4470
  if (typeof stateField.get !== 'function') {
4274
- 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.`);
4471
+ 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.`);
4275
4472
  lense = baseLense;
4276
4473
  } else {
4277
4474
  lense = { get: stateField.get, set: stateField.set };
4278
4475
  }
4279
4476
  } else {
4280
- 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.`);
4477
+ 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.`);
4281
4478
  lense = baseLense;
4282
4479
  }
4283
4480
 
4284
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4481
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber };
4285
4482
  const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources);
4286
4483
 
4287
4484
  if (!isObj(sink$)) {
@@ -4356,14 +4553,22 @@ class Component {
4356
4553
  const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
4357
4554
  if (immediate) {
4358
4555
  if (this.debug) {
4359
- console.log(`[${context}] ${fixedMsg(msg)}`);
4556
+ const text = `[${context}] ${fixedMsg(msg)}`;
4557
+ console.log(text);
4558
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
4559
+ window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
4560
+ }
4360
4561
  }
4361
4562
  return
4362
4563
  } else {
4363
4564
  return stream => {
4364
4565
  return stream.debug(msg => {
4365
4566
  if (this.debug) {
4366
- console.log(`[${context}] ${fixedMsg(msg)}`);
4567
+ const text = `[${context}] ${fixedMsg(msg)}`;
4568
+ console.log(text);
4569
+ if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
4570
+ window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text);
4571
+ }
4367
4572
  }
4368
4573
  })
4369
4574
  }
@@ -4373,11 +4578,11 @@ class Component {
4373
4578
 
4374
4579
 
4375
4580
 
4376
- function getComponents(currentElement, componentNames, depth=0, index=0, parentId) {
4581
+ function getComponents(currentElement, componentNames, path='r', parentId) {
4377
4582
  if (!currentElement) return {}
4378
4583
 
4379
4584
  if (currentElement.data?.componentsProcessed) return {}
4380
- if (depth === 0) currentElement.data.componentsProcessed = true;
4585
+ if (path === 'r') currentElement.data.componentsProcessed = true;
4381
4586
 
4382
4587
  const sel = currentElement.sel;
4383
4588
  const isCollection = sel && sel.toLowerCase() === 'collection';
@@ -4391,11 +4596,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4391
4596
 
4392
4597
  let id = parentId;
4393
4598
  if (isComponent) {
4394
- id = getComponentIdFromElement(currentElement, depth, index, parentId);
4599
+ id = getComponentIdFromElement(currentElement, path, parentId);
4395
4600
  if (isCollection) {
4396
4601
  if (!props.of) throw new Error(`Collection element missing required 'component' property`)
4397
4602
  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`)
4398
- if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${ props.of }`)
4603
+ if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
4399
4604
  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);
4400
4605
  currentElement.data.isCollection = true;
4401
4606
  currentElement.data.props ||= {};
@@ -4406,7 +4611,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4406
4611
  if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
4407
4612
  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`)
4408
4613
  const switchableComponentNames = Object.keys(props.of);
4409
- if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${ props.current }' not found in switchable element`)
4614
+ if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
4410
4615
  currentElement.data.isSwitchable = true;
4411
4616
  } else ;
4412
4617
  if (typeof props.key === 'undefined') currentElement.data.props.key = id;
@@ -4414,7 +4619,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4414
4619
  }
4415
4620
 
4416
4621
  if (children.length > 0) {
4417
- children.map((child, i) => getComponents(child, componentNames, depth + 1, index + i, id))
4622
+ children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
4418
4623
  .forEach((child) => {
4419
4624
  Object.entries(child).forEach(([id, el]) => found[id] = el);
4420
4625
  });
@@ -4423,10 +4628,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
4423
4628
  return found
4424
4629
  }
4425
4630
 
4426
- function injectComponents(currentElement, components, componentNames, depth=0, index=0, parentId) {
4631
+ function injectComponents(currentElement, components, componentNames, path='r', parentId) {
4427
4632
  if (!currentElement) return
4428
4633
  if (currentElement.data?.componentsInjected) return currentElement
4429
- if (depth === 0 && currentElement.data) currentElement.data.componentsInjected = true;
4634
+ if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true;
4430
4635
 
4431
4636
 
4432
4637
  const sel = currentElement.sel || 'NO SELECTOR';
@@ -4438,7 +4643,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
4438
4643
 
4439
4644
  let id = parentId;
4440
4645
  if (isComponent) {
4441
- id = getComponentIdFromElement(currentElement, depth, index, parentId);
4646
+ id = getComponentIdFromElement(currentElement, path, parentId);
4442
4647
  const component = components[id];
4443
4648
  if (isCollection) {
4444
4649
  currentElement.sel = 'div';
@@ -4450,21 +4655,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
4450
4655
  return component
4451
4656
  }
4452
4657
  } else if (children.length > 0) {
4453
- currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, depth + 1, index + i, id)).flat();
4658
+ currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat();
4454
4659
  return currentElement
4455
4660
  } else {
4456
4661
  return currentElement
4457
4662
  }
4458
4663
  }
4459
4664
 
4460
- function getComponentIdFromElement(el, depth, index, parentId) {
4665
+ function getComponentIdFromElement(el, path, parentId) {
4461
4666
  const sel = el.sel;
4462
4667
  const name = typeof sel === 'string' ? sel : 'functionComponent';
4463
- const uid = `${depth}:${index}`;
4464
4668
  const props = el.data?.props || {};
4465
- const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || uid;
4466
- const parentString = parentId ? `${ parentId }|` : '';
4467
- const fullId = `${ parentString }${ name }::${ id }`;
4669
+ const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path;
4670
+ const parentString = parentId ? `${parentId}|` : '';
4671
+ const fullId = `${parentString}${name}::${id}`;
4468
4672
  return fullId
4469
4673
  }
4470
4674
 
@@ -4608,7 +4812,7 @@ function sortFunctionFromProp(sortProp) {
4608
4812
  } else if (isObj(sortProp)) {
4609
4813
  return __sortFunctionFromObj(sortProp)
4610
4814
  } else {
4611
- console.error('Invalid sort option (ignoring):', item);
4815
+ console.error('Invalid sort option (ignoring):', sortProp);
4612
4816
  return undefined
4613
4817
  }
4614
4818
  }
@@ -4625,11 +4829,11 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4625
4829
  const functionName = promiseReturningFunction.name || '[anonymous function]';
4626
4830
  const functionArgsType = typeof functionArgs;
4627
4831
  if (functionArgsType !== 'string' && functionArgsType !== 'function' && !(Array.isArray(functionArgs) && functionArgs.every((arg) => typeof arg === 'string'))) {
4628
- throw new Error(`The 'args' option for driverFromAsync(${ functionName }) must be a string, array of strings, or a function. Received ${functionArgsType}`)
4832
+ throw new Error(`The 'args' option for driverFromAsync(${functionName}) must be a string, array of strings, or a function. Received ${functionArgsType}`)
4629
4833
  }
4630
4834
 
4631
4835
  if (typeof selectorProperty !== 'string') {
4632
- throw new Error(`The 'selector' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof selectorProperty}`)
4836
+ throw new Error(`The 'selector' option for driverFromAsync(${functionName}) must be a string. Received ${typeof selectorProperty}`)
4633
4837
  }
4634
4838
 
4635
4839
  return (fromApp$) => {
@@ -4658,7 +4862,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4658
4862
  argArr = functionArgs.map((arg) => preProcessed[arg]);
4659
4863
  }
4660
4864
  }
4661
- const errMsg = `Error in driver created using driverFromAsync(${ functionName })`;
4865
+ const errMsg = `Error in driver created using driverFromAsync(${functionName})`;
4662
4866
  promiseReturningFunction(...argArr)
4663
4867
  .then((innerVal) => {
4664
4868
  const constructReply = (rawVal) => {
@@ -4668,7 +4872,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4668
4872
  if (typeof outgoing === 'object' && outgoing !== null) {
4669
4873
  outgoing[selectorProperty] = incoming[selectorProperty];
4670
4874
  } else {
4671
- 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.`);
4875
+ 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.`);
4672
4876
  }
4673
4877
  } else if (typeof returnProperty === 'string') {
4674
4878
  outgoing = {
@@ -4676,7 +4880,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4676
4880
  [selectorProperty]: incoming[selectorProperty]
4677
4881
  };
4678
4882
  } else {
4679
- throw new Error(`The 'return' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof returnProperty}`)
4883
+ throw new Error(`The 'return' option for driverFromAsync(${functionName}) must be a string. Received ${typeof returnProperty}`)
4680
4884
  }
4681
4885
  return outgoing
4682
4886
  };
@@ -4692,12 +4896,12 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4692
4896
  .then((innerProcessedOutgoing) => {
4693
4897
  sendFn(constructReply(innerProcessedOutgoing));
4694
4898
  })
4695
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4899
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4696
4900
  } else {
4697
- sendFn(constructReply(rocessedOutgoing));
4901
+ sendFn(constructReply(processedOutgoing));
4698
4902
  }
4699
4903
  })
4700
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4904
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4701
4905
  } else {
4702
4906
  const processedOutgoing = postFunction(innerVal, incoming);
4703
4907
  if (typeof processedOutgoing.then === 'function') {
@@ -4705,19 +4909,19 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4705
4909
  .then((innerProcessedOutgoing) => {
4706
4910
  sendFn(constructReply(innerProcessedOutgoing));
4707
4911
  })
4708
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4912
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4709
4913
  } else {
4710
4914
  sendFn(constructReply(processedOutgoing));
4711
4915
  }
4712
4916
  }
4713
4917
  })
4714
- .catch((err) => console.error(`${ errMsg }: ${ err }`));
4918
+ .catch((err) => console.error(`${errMsg}: ${err}`));
4715
4919
  },
4716
4920
  error: (err) => {
4717
- console.error(`Error recieved from sink stream in driver created using driverFromAsync(${ functionName }): ${ err }`);
4921
+ console.error(`Error received from sink stream in driver created using driverFromAsync(${functionName}):`, err);
4718
4922
  },
4719
4923
  complete: () => {
4720
- console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${ functionName })`);
4924
+ console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${functionName})`);
4721
4925
  }
4722
4926
  });
4723
4927
 
@@ -4733,6 +4937,9 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
4733
4937
  }
4734
4938
 
4735
4939
  function processForm(form, options={}) {
4940
+ if (!form || typeof form.events !== 'function') {
4941
+ throw new Error('processForm: first argument must have an .events() method (e.g. DOM.select(...))')
4942
+ }
4736
4943
  let { events = ['input', 'submit'], preventDefault = true } = options;
4737
4944
  if (typeof events === 'string') events = [events];
4738
4945
 
@@ -4760,6 +4967,12 @@ function processForm(form, options={}) {
4760
4967
  }
4761
4968
 
4762
4969
  function processDrag({ draggable, dropZone } = {}, options = {}) {
4970
+ if (draggable && typeof draggable.events !== 'function') {
4971
+ throw new Error('processDrag: draggable must have an .events() method (e.g. DOM.select(...))')
4972
+ }
4973
+ if (dropZone && typeof dropZone.events !== 'function') {
4974
+ throw new Error('processDrag: dropZone must have an .events() method (e.g. DOM.select(...))')
4975
+ }
4763
4976
  const { effectAllowed = 'move' } = options;
4764
4977
 
4765
4978
  const dragStart$ = draggable
@@ -4988,7 +5201,8 @@ function eventBusDriver(out$) {
4988
5201
  const events = new EventTarget();
4989
5202
 
4990
5203
  out$.subscribe({
4991
- next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
5204
+ next: event => events.dispatchEvent(new CustomEvent('data', { detail: event })),
5205
+ error: err => console.error('[EVENTS driver] Error in sink stream:', err)
4992
5206
  });
4993
5207
 
4994
5208
  return {
@@ -5016,11 +5230,266 @@ function logDriver(out$) {
5016
5230
  out$.addListener({
5017
5231
  next: (val) => {
5018
5232
  console.log(val);
5233
+ },
5234
+ error: (err) => {
5235
+ console.error('[LOG driver] Error in sink stream:', err);
5019
5236
  }
5020
5237
  });
5021
5238
  }
5022
5239
 
5240
+ const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__';
5241
+ const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__';
5242
+ const DEFAULT_MAX_HISTORY = 200;
5243
+
5244
+ class SygnalDevTools {
5245
+ constructor() {
5246
+ this._connected = false;
5247
+ this._components = new Map();
5248
+ this._stateHistory = [];
5249
+ this._maxHistory = DEFAULT_MAX_HISTORY;
5250
+ }
5251
+
5252
+ get connected() {
5253
+ return this._connected && typeof window !== 'undefined'
5254
+ }
5255
+
5256
+ // ─── Initialization ─────────────────────────────────────────────────────────
5257
+
5258
+ init() {
5259
+ if (typeof window === 'undefined') return
5260
+
5261
+ window.__SYGNAL_DEVTOOLS__ = this;
5262
+
5263
+ window.addEventListener('message', (event) => {
5264
+ if (event.source !== window) return
5265
+ if (event.data?.source === EXTENSION_SOURCE) {
5266
+ this._handleExtensionMessage(event.data);
5267
+ }
5268
+ });
5269
+ }
5270
+
5271
+ _handleExtensionMessage(msg) {
5272
+ switch (msg.type) {
5273
+ case 'CONNECT':
5274
+ this._connected = true;
5275
+ if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory;
5276
+ this._sendFullTree();
5277
+ break
5278
+ case 'DISCONNECT':
5279
+ this._connected = false;
5280
+ break
5281
+ case 'SET_DEBUG':
5282
+ this._setDebug(msg.payload);
5283
+ break
5284
+ case 'TIME_TRAVEL':
5285
+ this._timeTravel(msg.payload);
5286
+ break
5287
+ case 'GET_STATE':
5288
+ this._sendComponentState(msg.payload.componentId);
5289
+ break
5290
+ }
5291
+ }
5292
+
5293
+ // ─── Hooks (called from component.js) ────────────────────────────────────────
5294
+
5295
+ onComponentCreated(componentNumber, name, instance) {
5296
+ const meta = {
5297
+ id: componentNumber,
5298
+ name: name,
5299
+ isSubComponent: instance.isSubComponent,
5300
+ hasModel: !!instance.model,
5301
+ hasIntent: !!instance.intent,
5302
+ hasContext: !!instance.context,
5303
+ hasCalculated: !!instance.calculated,
5304
+ components: Object.keys(instance.components || {}),
5305
+ parentId: null,
5306
+ children: [],
5307
+ debug: instance._debug,
5308
+ createdAt: Date.now(),
5309
+ _instanceRef: new WeakRef(instance),
5310
+ };
5311
+ this._components.set(componentNumber, meta);
5312
+
5313
+ if (!this.connected) return
5314
+ this._post('COMPONENT_CREATED', this._serializeMeta(meta));
5315
+ }
5316
+
5317
+ onStateChanged(componentNumber, name, state) {
5318
+ if (!this.connected) return
5319
+
5320
+ const entry = {
5321
+ componentId: componentNumber,
5322
+ componentName: name,
5323
+ timestamp: Date.now(),
5324
+ state: this._safeClone(state),
5325
+ };
5326
+
5327
+ this._stateHistory.push(entry);
5328
+ if (this._stateHistory.length > this._maxHistory) {
5329
+ this._stateHistory.shift();
5330
+ }
5331
+
5332
+ this._post('STATE_CHANGED', {
5333
+ componentId: componentNumber,
5334
+ componentName: name,
5335
+ state: entry.state,
5336
+ historyIndex: this._stateHistory.length - 1,
5337
+ });
5338
+ }
5339
+
5340
+ onActionDispatched(componentNumber, name, actionType, data) {
5341
+ if (!this.connected) return
5342
+ this._post('ACTION_DISPATCHED', {
5343
+ componentId: componentNumber,
5344
+ componentName: name,
5345
+ actionType: actionType,
5346
+ data: this._safeClone(data),
5347
+ timestamp: Date.now(),
5348
+ });
5349
+ }
5350
+
5351
+ onSubComponentRegistered(parentNumber, childNumber) {
5352
+ const parent = this._components.get(parentNumber);
5353
+ const child = this._components.get(childNumber);
5354
+ if (parent && child) {
5355
+ child.parentId = parentNumber;
5356
+ if (!parent.children.includes(childNumber)) {
5357
+ parent.children.push(childNumber);
5358
+ }
5359
+ }
5360
+
5361
+ if (!this.connected) return
5362
+ this._post('TREE_UPDATED', {
5363
+ parentId: parentNumber,
5364
+ childId: childNumber,
5365
+ });
5366
+ }
5367
+
5368
+ onContextChanged(componentNumber, name, context) {
5369
+ if (!this.connected) return
5370
+ this._post('CONTEXT_CHANGED', {
5371
+ componentId: componentNumber,
5372
+ componentName: name,
5373
+ context: this._safeClone(context),
5374
+ });
5375
+ }
5376
+
5377
+ onDebugLog(componentNumber, message) {
5378
+ if (!this.connected) return
5379
+ this._post('DEBUG_LOG', {
5380
+ componentId: componentNumber,
5381
+ message: message,
5382
+ timestamp: Date.now(),
5383
+ });
5384
+ }
5385
+
5386
+ // ─── Commands (from extension to page) ───────────────────────────────────────
5387
+
5388
+ _setDebug({ componentId, enabled }) {
5389
+ if (typeof componentId === 'undefined' || componentId === null) {
5390
+ if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false;
5391
+ this._post('DEBUG_TOGGLED', { global: true, enabled });
5392
+ return
5393
+ }
5394
+
5395
+ const meta = this._components.get(componentId);
5396
+ if (meta && meta._instanceRef) {
5397
+ const instance = meta._instanceRef.deref();
5398
+ if (instance) {
5399
+ instance._debug = enabled;
5400
+ meta.debug = enabled;
5401
+ this._post('DEBUG_TOGGLED', { componentId, enabled });
5402
+ }
5403
+ }
5404
+ }
5405
+
5406
+ _timeTravel({ historyIndex }) {
5407
+ const entry = this._stateHistory[historyIndex];
5408
+ if (!entry) return
5409
+
5410
+ const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__;
5411
+ if (app?.sinks?.STATE?.shamefullySendNext) {
5412
+ app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }));
5413
+ this._post('TIME_TRAVEL_APPLIED', {
5414
+ historyIndex,
5415
+ state: entry.state,
5416
+ });
5417
+ }
5418
+ }
5419
+
5420
+ _sendComponentState(componentId) {
5421
+ const meta = this._components.get(componentId);
5422
+ if (meta && meta._instanceRef) {
5423
+ const instance = meta._instanceRef.deref();
5424
+ if (instance) {
5425
+ this._post('COMPONENT_STATE', {
5426
+ componentId,
5427
+ state: this._safeClone(instance.currentState),
5428
+ context: this._safeClone(instance.currentContext),
5429
+ props: this._safeClone(instance.currentProps),
5430
+ });
5431
+ }
5432
+ }
5433
+ }
5434
+
5435
+ _sendFullTree() {
5436
+ const tree = [];
5437
+ for (const [id, meta] of this._components) {
5438
+ const instance = meta._instanceRef?.deref();
5439
+ tree.push({
5440
+ ...this._serializeMeta(meta),
5441
+ state: instance ? this._safeClone(instance.currentState) : null,
5442
+ context: instance ? this._safeClone(instance.currentContext) : null,
5443
+ });
5444
+ }
5445
+ this._post('FULL_TREE', {
5446
+ components: tree,
5447
+ history: this._stateHistory,
5448
+ });
5449
+ }
5450
+
5451
+ // ─── Transport ───────────────────────────────────────────────────────────────
5452
+
5453
+ _post(type, payload) {
5454
+ if (typeof window === 'undefined') return
5455
+ window.postMessage({
5456
+ source: DEVTOOLS_SOURCE,
5457
+ type,
5458
+ payload,
5459
+ }, '*');
5460
+ }
5461
+
5462
+ _safeClone(obj) {
5463
+ if (obj === undefined || obj === null) return obj
5464
+ try {
5465
+ return JSON.parse(JSON.stringify(obj))
5466
+ } catch (e) {
5467
+ return '[unserializable]'
5468
+ }
5469
+ }
5470
+
5471
+ _serializeMeta(meta) {
5472
+ const { _instanceRef, ...rest } = meta;
5473
+ return rest
5474
+ }
5475
+ }
5476
+
5477
+ // ─── Singleton ────────────────────────────────────────────────────────────────
5478
+
5479
+ let instance = null;
5480
+
5481
+ function getDevTools() {
5482
+ if (!instance) instance = new SygnalDevTools();
5483
+ return instance
5484
+ }
5485
+
5023
5486
  function run(app, drivers={}, options={}) {
5487
+ // Initialize DevTools instrumentation bridge early (before component creation)
5488
+ if (typeof window !== 'undefined') {
5489
+ const dt = getDevTools();
5490
+ dt.init();
5491
+ }
5492
+
5024
5493
  const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options;
5025
5494
  if (!app.isSygnalComponent) {
5026
5495
  const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
@@ -5070,6 +5539,11 @@ function run(app, drivers={}, options={}) {
5070
5539
 
5071
5540
  const exposed = { sources, sinks, dispose };
5072
5541
 
5542
+ // Store app reference for time-travel
5543
+ if (typeof window !== 'undefined') {
5544
+ window.__SYGNAL_DEVTOOLS_APP__ = exposed;
5545
+ }
5546
+
5073
5547
  const swapToComponent = (newComponent, state) => {
5074
5548
  const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined;
5075
5549
  const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState;
@@ -5572,4 +6046,4 @@ sampleCombine = function sampleCombine() {
5572
6046
  };
5573
6047
  var _default = sampleCombine$1.default = sampleCombine;
5574
6048
 
5575
- export { ABORT, Collection, Switchable, classes, collection, component, _default$2 as debounce, _default$4 as delay, driverFromAsync, _default$5 as dropRepeats, enableHMR, exactState, makeDragDriver, processDrag, processForm, run, _default as sampleCombine, switchable, _default$1 as throttle, xs$1 as xs };
6049
+ export { ABORT, Collection, Switchable, classes, collection, component, _default$2 as debounce, _default$4 as delay, driverFromAsync, _default$5 as dropRepeats, enableHMR, exactState, getDevTools, makeDragDriver, processDrag, processForm, run, _default as sampleCombine, switchable, _default$1 as throttle, xs$1 as xs };