sygnal 2.9.4 → 3.0.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
@@ -3144,10 +3144,11 @@ var _default$2 = debounce$1.default = debounce;
3144
3144
  const ENVIRONMENT = ((typeof window != 'undefined' && window) || (process && process.env)) || {};
3145
3145
 
3146
3146
 
3147
- const REQUEST_SELECTOR_METHOD = 'request';
3148
- const BOOTSTRAP_ACTION = 'BOOTSTRAP';
3149
- const INITIALIZE_ACTION = 'INITIALIZE';
3150
- const HYDRATE_ACTION = 'HYDRATE';
3147
+ const BOOTSTRAP_ACTION = 'BOOTSTRAP';
3148
+ const INITIALIZE_ACTION = 'INITIALIZE';
3149
+ const HYDRATE_ACTION = 'HYDRATE';
3150
+ const PARENT_SINK_NAME = 'PARENT';
3151
+ const CHILD_SOURCE_NAME = 'CHILD';
3151
3152
 
3152
3153
 
3153
3154
  let COMPONENT_COUNT = 0;
@@ -3201,12 +3202,10 @@ class Component {
3201
3202
  // name
3202
3203
  // sources
3203
3204
  // intent
3204
- // request
3205
3205
  // model
3206
3206
  // context
3207
- // response
3208
3207
  // view
3209
- // children
3208
+ // peers
3210
3209
  // components
3211
3210
  // initialState
3212
3211
  // calculated
@@ -3222,15 +3221,16 @@ class Component {
3222
3221
  // action$
3223
3222
  // model$
3224
3223
  // context$
3225
- // response$
3226
- // sendResponse$
3227
- // children$
3224
+ // peers$
3225
+ // childSources
3228
3226
  // vdom$
3229
3227
  // currentState
3228
+ // currentProps
3229
+ // currentChildren
3230
3230
  // currentContext
3231
3231
  // subComponentSink$
3232
- // unmountRequest$
3233
- // unmount()
3232
+ // unmountRequest$ <- TODO
3233
+ // unmount() <- TODO
3234
3234
  // _debug
3235
3235
 
3236
3236
  // [ INSTANTIATED STREAM OPERATOR ]
@@ -3239,18 +3239,17 @@ class Component {
3239
3239
  // [ OUTPUT ]
3240
3240
  // sinks
3241
3241
 
3242
- constructor({ name='NO NAME', sources, intent, request, model, context, response, view, children={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
3242
+ constructor({ name='NO NAME', sources, intent, model, context, response, view, peers={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
3243
3243
  if (!sources || !isObj(sources)) throw new Error('Missing or invalid sources')
3244
3244
 
3245
3245
  this.name = name;
3246
3246
  this.sources = sources;
3247
3247
  this.intent = intent;
3248
- this.request = request;
3249
3248
  this.model = model;
3250
3249
  this.context = context;
3251
3250
  this.response = response;
3252
3251
  this.view = view;
3253
- this.children = children;
3252
+ this.peers = peers;
3254
3253
  this.components = components;
3255
3254
  this.initialState = initialState;
3256
3255
  this.calculated = calculated;
@@ -3273,6 +3272,23 @@ class Component {
3273
3272
  }));
3274
3273
  }
3275
3274
 
3275
+ const props$ = sources.props$;
3276
+ if (props$) {
3277
+ this.sources.props$ = props$.map(val => {
3278
+ this.currentProps = val;
3279
+ return val
3280
+ });
3281
+ }
3282
+
3283
+ const children$ = sources.children$;
3284
+ if (children$) {
3285
+ this.sources.children$ = children$.map(val => {
3286
+ this.currentChildren = val;
3287
+ return val
3288
+ });
3289
+ }
3290
+
3291
+
3276
3292
  // Ensure that the root component has an intent and model
3277
3293
  // This is necessary to ensure that the component tree's state sink is subscribed to
3278
3294
  if (!this.isSubComponent && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
@@ -3288,14 +3304,13 @@ class Component {
3288
3304
  this.addCalculated = this.createMemoizedAddCalculated();
3289
3305
  this.log = makeLog(`${componentNumber} | ${name}`);
3290
3306
 
3307
+ this.initChildSources$();
3291
3308
  this.initIntent$();
3292
3309
  this.initAction$();
3293
- this.initResponse$();
3294
3310
  this.initState();
3295
3311
  this.initContext();
3296
3312
  this.initModel$();
3297
- this.initSendResponse$();
3298
- this.initChildren$();
3313
+ this.initPeers$();
3299
3314
  this.initSubComponentSink$();
3300
3315
  this.initSubComponentsRendered$();
3301
3316
  this.initVdom$();
@@ -3307,7 +3322,7 @@ class Component {
3307
3322
  }
3308
3323
 
3309
3324
  get debug() {
3310
- return this._debug || (ENVIRONMENT.DEBUG === 'true' || ENVIRONMENT.DEBUG === true)
3325
+ return this._debug || (ENVIRONMENT.SYGNAL_DEBUG === 'true' || ENVIRONMENT.SYGNAL_DEBUG === true)
3311
3326
  }
3312
3327
 
3313
3328
  initIntent$() {
@@ -3344,7 +3359,7 @@ class Component {
3344
3359
 
3345
3360
  const action$ = ((runner instanceof xs$1.Stream) ? runner : (runner.apply && runner(this.sources) || xs$1.never()));
3346
3361
  const bootstrap$ = xs$1.of({ type: BOOTSTRAP_ACTION }).compose(_default$4(10));
3347
- const wrapped$ = _default$3(bootstrap$, action$);
3362
+ const wrapped$ = this.model[BOOTSTRAP_ACTION] ? _default$3(bootstrap$, action$) : action$;
3348
3363
 
3349
3364
 
3350
3365
  let initialApiData;
@@ -3361,92 +3376,6 @@ class Component {
3361
3376
  .compose(this.log(({ type }) => `<${ type }> Action triggered`));
3362
3377
  }
3363
3378
 
3364
- initResponse$() {
3365
- if (typeof this.request == 'undefined') {
3366
- return
3367
- } else if (!isObj(this.request)) {
3368
- throw new Error('The request parameter must be an object')
3369
- }
3370
-
3371
- const router$ = this.sources[this.requestSourceName];
3372
- const methods = Object.entries(this.request);
3373
-
3374
- const wrapped = methods.reduce((acc, [method, routes]) => {
3375
- const _method = method.toLowerCase();
3376
- if (typeof router$[_method] != 'function') {
3377
- throw new Error('Invalid method in request object:', method)
3378
- }
3379
- const entries = Object.entries(routes);
3380
- const mapped = entries.reduce((acc, [route, action]) => {
3381
- const routeString = `[${_method.toUpperCase()}]:${route || 'none'}`;
3382
- const actionType = typeof action;
3383
- if (actionType === 'undefined') {
3384
- throw new Error(`Action for '${ route }' route in request object not specified`)
3385
- } else if (actionType !== 'string' && actionType !== 'function') {
3386
- throw new Error(`Invalid action for '${ route }' route: expecting string or function`)
3387
- }
3388
- const actionString = (actionType === 'function') ? '[ FUNCTION ]' : `< ${ action } >`;
3389
- console.log(`[${ this.name }] Adding ${ this.requestSourceName } route:`, _method.toUpperCase(), `'${ route }' <${ actionString }>`);
3390
- const route$ = router$[_method](route)
3391
- .compose(_default$5((a, b) => a.id == b.id))
3392
- .map(req => {
3393
- if (!req || !req.id) {
3394
- throw new Error(`No id found in request: ${ routeString }`)
3395
- }
3396
- try {
3397
- const _reqId = req.id;
3398
- const params = req.params;
3399
- const body = req.body;
3400
- const cookies = req.cookies;
3401
- const type = (actionType === 'function') ? 'FUNCTION' : action;
3402
- const data = { params, body, cookies, req };
3403
- const obj = { type, data: body, req, _reqId, _action: type };
3404
-
3405
- const timestamp = (new Date()).toISOString();
3406
- const ip = req.get ? req.get('host') : '0.0.0.0';
3407
-
3408
- console.log(`${ timestamp } ${ ip } ${ req.method } ${ req.url }`);
3409
-
3410
- if (this.debug) {
3411
- this.action$.setDebugListener({next: ({ type }) => this.log(`[${ this.name }] Action from ${ this.requestSourceName } request: <${ type }>`, true)});
3412
- }
3413
-
3414
- if (actionType === 'function') {
3415
- const enhancedState = this.addCalculated(this.currentState);
3416
- const result = action(enhancedState, req);
3417
- return xs$1.of({ ...obj, data: result })
3418
- } else {
3419
- this.action$.shamefullySendNext(obj);
3420
-
3421
- const sourceEntries = Object.entries(this.sources);
3422
- const responses = sourceEntries.reduce((acc, [name, source]) => {
3423
- if (!source || typeof source[REQUEST_SELECTOR_METHOD] != 'function') return acc
3424
- const selected$ = source[REQUEST_SELECTOR_METHOD](_reqId);
3425
- return [ ...acc, selected$ ]
3426
- }, []);
3427
- return xs$1.merge(...responses)
3428
- }
3429
- } catch(err) {
3430
- console.error(err);
3431
- }
3432
- }).flatten();
3433
- return [ ...acc, route$ ]
3434
- }, []);
3435
- const mapped$ = xs$1.merge(...mapped);
3436
- return [ ...acc, mapped$ ]
3437
- }, []);
3438
-
3439
- this.response$ = xs$1.merge(...wrapped)
3440
- .compose(this.log(res => {
3441
- if (res._action) return `[${ this.requestSourceName }] response data received for Action: <${ res._action }>`
3442
- return `[${ this.requestSourceName }] response data received from FUNCTION`
3443
- }));
3444
-
3445
- if (typeof this.response != 'undefined' && typeof this.response$ == 'undefined') {
3446
- throw new Error('Cannot have a response parameter without a request parameter')
3447
- }
3448
- }
3449
-
3450
3379
  initState() {
3451
3380
  if (this.model != undefined) {
3452
3381
  if (this.model[INITIALIZE_ACTION] === undefined) {
@@ -3544,15 +3473,18 @@ class Component {
3544
3473
  sinkEntries.forEach((entry) => {
3545
3474
  const [sink, reducer] = entry;
3546
3475
 
3547
- const isStateSink = (sink == this.stateSourceName);
3476
+ const isStateSink = (sink === this.stateSourceName);
3477
+ const isParentSink = (sink === PARENT_SINK_NAME);
3548
3478
 
3549
3479
  const on = isStateSink ? onState() : onNormal();
3550
- const on$ = on(action, reducer);
3480
+ const on$ = isParentSink ? on(action, reducer).map(value => ({ name: this.name, value })) : on(action, reducer);
3551
3481
 
3552
3482
  const wrapped$ = on$
3553
3483
  .compose(this.log(data => {
3554
3484
  if (isStateSink) {
3555
3485
  return `<${ action }> State reducer added`
3486
+ } else if (isParentSink) {
3487
+ return `<${ action }> Data sent to parent component: ${ JSON.stringify(data.value).replaceAll('"', '') }`
3556
3488
  } else {
3557
3489
  const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
3558
3490
  return `<${ action }> Data sent to [${ sink }]: ${ JSON.stringify(extra).replaceAll('"', '') }`
@@ -3576,51 +3508,7 @@ class Component {
3576
3508
  this.model$ = model$;
3577
3509
  }
3578
3510
 
3579
- initSendResponse$() {
3580
- const responseType = typeof this.response;
3581
- if (responseType != 'function' && responseType != 'undefined') {
3582
- throw new Error('The response parameter must be a function')
3583
- }
3584
-
3585
- if (responseType == 'undefined') {
3586
- if (this.response$) {
3587
- this.response$.subscribe({
3588
- next: this.log(({ _reqId, _action }) => `Unhandled response for request: ${ _action } ${ _reqId }`)
3589
- });
3590
- }
3591
- this.sendResponse$ = xs$1.never();
3592
- return
3593
- }
3594
-
3595
- const selectable = {
3596
- select: (actions) => {
3597
- if (typeof actions == 'undefined') return this.response$
3598
- if (!Array.isArray(actions)) actions = [actions];
3599
- return this.response$.filter(({_action}) => (actions.length > 0) ? (_action === 'FUNCTION' || actions.includes(_action)) : true)
3600
- }
3601
- };
3602
-
3603
- const out = this.response(selectable);
3604
- if (!isObj(out)) {
3605
- throw new Error('The response function must return an object')
3606
- }
3607
-
3608
- const entries = Object.entries(out);
3609
- const out$ = entries.reduce((acc, [command, response$]) => {
3610
- const mapped$ = response$.map(({ _reqId, _action, data }) => {
3611
- if (!_reqId) {
3612
- throw new Error(`No request id found for response for: ${ command }`)
3613
- }
3614
- return { _reqId, _action, command, data }
3615
- });
3616
- return [ ...acc, mapped$ ]
3617
- }, []);
3618
-
3619
- this.sendResponse$ = xs$1.merge(...out$)
3620
- .compose(this.log(({ _reqId, _action }) => `[${ this.requestSourceName }] response sent for: <${ _action }>`));
3621
- }
3622
-
3623
- initChildren$() {
3511
+ initPeers$() {
3624
3512
  const initial = this.sourceNames.reduce((acc, name) => {
3625
3513
  if (name == this.DOMSourceName) {
3626
3514
  acc[name] = {};
@@ -3630,19 +3518,46 @@ class Component {
3630
3518
  return acc
3631
3519
  }, {});
3632
3520
 
3633
- this.children$ = Object.entries(this.children).reduce((acc, [childName, childFactory]) => {
3634
- const child$ = childFactory(this.sources);
3521
+ this.peers$ = Object.entries(this.peers).reduce((acc, [peerName, peerFactory]) => {
3522
+ const peer$ = peerFactory(this.sources);
3635
3523
  this.sourceNames.forEach(source => {
3636
3524
  if (source == this.DOMSourceName) {
3637
- acc[source][childName] = child$[source];
3525
+ acc[source][peerName] = peer$[source];
3638
3526
  } else {
3639
- acc[source].push(child$[source]);
3527
+ acc[source].push(peer$[source]);
3640
3528
  }
3641
3529
  });
3642
3530
  return acc
3643
3531
  }, initial);
3644
3532
  }
3645
3533
 
3534
+ initChildSources$() {
3535
+ let newSourcesNext;
3536
+ const childSources$ = xs$1.create({
3537
+ start: listener => {
3538
+ newSourcesNext = listener.next.bind(listener);
3539
+ },
3540
+ stop: _ => {
3541
+
3542
+ }
3543
+ }).map(sources => xs$1.merge(...sources)).flatten();
3544
+
3545
+ // childSources$.subscribe({ next: _ => _})
3546
+
3547
+ this.sources[CHILD_SOURCE_NAME] = {
3548
+ select: (name) => {
3549
+ const all$ = childSources$;
3550
+ const filtered$ = name ? all$.filter(entry => entry.name === name) : all$;
3551
+ const unwrapped$ = filtered$.map(entry => entry.value);
3552
+ return unwrapped$
3553
+ }
3554
+ };
3555
+
3556
+ this.newChildSources = (sources) => {
3557
+ if (typeof newSourcesNext === 'function') newSourcesNext(sources);
3558
+ };
3559
+ }
3560
+
3646
3561
  initSubComponentSink$() {
3647
3562
  const subComponentSink$ = xs$1.create({
3648
3563
  start: listener => {
@@ -3689,17 +3604,17 @@ class Component {
3689
3604
  initSinks() {
3690
3605
  this.sinks = this.sourceNames.reduce((acc, name) => {
3691
3606
  if (name == this.DOMSourceName) return acc
3692
- const subComponentSink$ = this.subComponentSink$ ? this.subComponentSink$.map(sinks => sinks[name]).filter(sink => !!sink).flatten() : xs$1.never();
3607
+ const subComponentSink$ = (this.subComponentSink$ && name !== PARENT_SINK_NAME) ? this.subComponentSink$.map(sinks => sinks[name]).filter(sink => !!sink).flatten() : xs$1.never();
3693
3608
  if (name === this.stateSourceName) {
3694
- acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter(_ => false), ...this.children$[name]);
3609
+ acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter(_ => false), ...(this.peers$[name] || []));
3695
3610
  } else {
3696
- acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, ...this.children$[name]);
3611
+ acc[name] = xs$1.merge((this.model$[name] || xs$1.never()), subComponentSink$, ...(this.peers$[name] || []));
3697
3612
  }
3698
3613
  return acc
3699
3614
  }, {});
3700
3615
 
3701
- this.sinks[this.DOMSourceName] = this.vdom$;
3702
- this.sinks[this.requestSourceName] = xs$1.merge(this.sendResponse$ ,this.sinks[this.requestSourceName]);
3616
+ this.sinks[this.DOMSourceName] = this.vdom$;
3617
+ this.sinks[PARENT_SINK_NAME] = this.model$[PARENT_SINK_NAME] || xs$1.never();
3703
3618
  }
3704
3619
 
3705
3620
  makeOnAction(action$, isStateSink=true, rootAction$) {
@@ -3712,33 +3627,30 @@ class Component {
3712
3627
  returnStream$ = filtered$.map(action => {
3713
3628
  const next = (type, data, delay=10) => {
3714
3629
  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.`)
3715
- const _reqId = action._reqId || (action.req && action.req.id);
3716
- const _data = _reqId ? (isObj(data) ? { ...data, _reqId, _action: name } : { data, _reqId, _action: name }) : data;
3717
3630
  // put the "next" action request at the end of the event loop so the "current" action completes first
3718
3631
  setTimeout(() => {
3719
3632
  // push the "next" action request into the action$ stream
3720
- rootAction$.shamefullySendNext({ type, data: _data });
3633
+ rootAction$.shamefullySendNext({ type, data });
3721
3634
  }, delay);
3722
3635
  this.log(`<${ name }> Triggered a next() action: <${ type }> ${ delay }ms delay`, true);
3723
3636
  };
3724
3637
 
3638
+ const extra = { props: this.currentProps, children: this.currentChildren, context: this.currentContext };
3639
+
3725
3640
  let data = action.data;
3726
- if (data && data.data && data._reqId) data = data.data;
3727
3641
  if (isStateSink) {
3728
3642
  return (state) => {
3729
3643
  const _state = this.isSubComponent ? this.currentState : state;
3730
3644
  const enhancedState = this.addCalculated(_state);
3731
- const newState = reducer(enhancedState, data, next, action.req);
3645
+ const newState = reducer(enhancedState, data, next, extra);
3732
3646
  if (newState == ABORT) return _state
3733
3647
  return this.cleanupCalculated(newState)
3734
3648
  }
3735
3649
  } else {
3736
3650
  const enhancedState = this.addCalculated(this.currentState);
3737
- const reduced = reducer(enhancedState, data, next, action.req);
3651
+ const reduced = reducer(enhancedState, data, next, extra);
3738
3652
  const type = typeof reduced;
3739
- const _reqId = action._reqId || (action.req && action.req.id);
3740
- if (['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3741
- if (isObj(reduced)) return { ...reduced, _reqId, _action: name }
3653
+ if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
3742
3654
  if (type == 'undefined') {
3743
3655
  console.warn(`'undefined' value sent to ${ name }`);
3744
3656
  return reduced
@@ -3767,23 +3679,15 @@ class Component {
3767
3679
  return lastResult
3768
3680
  }
3769
3681
  if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3770
- const entries = Object.entries(this.calculated);
3771
- if (entries.length === 0) {
3682
+
3683
+ const calculated = this.getCalculatedValues(state);
3684
+ if (!calculated) {
3772
3685
  lastState = state;
3773
3686
  lastResult = state;
3774
3687
  return state
3775
3688
  }
3776
- const calculated = entries.reduce((acc, [field, fn]) => {
3777
- if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3778
- try {
3779
- acc[field] = fn(state);
3780
- } catch(e) {
3781
- console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3782
- }
3783
- return acc
3784
- }, {});
3785
3689
 
3786
- const newState = { ...state, ...calculated, __context: this.currentContext };
3690
+ const newState = { ...state, ...calculated };
3787
3691
 
3788
3692
  lastState = state;
3789
3693
  lastResult = newState;
@@ -3792,12 +3696,27 @@ class Component {
3792
3696
  }
3793
3697
  }
3794
3698
 
3699
+ getCalculatedValues(state) {
3700
+ const entries = Object.entries(this.calculated || {});
3701
+ if (entries.length === 0) {
3702
+ return
3703
+ }
3704
+ return entries.reduce((acc, [field, fn]) => {
3705
+ if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3706
+ try {
3707
+ acc[field] = fn(state);
3708
+ } catch(e) {
3709
+ console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3710
+ }
3711
+ return acc
3712
+ }, {})
3713
+ }
3714
+
3795
3715
  cleanupCalculated(incomingState) {
3796
3716
  if (!incomingState || !isObj(incomingState) || Array.isArray(incomingState)) return incomingState
3797
3717
  const state = this.storeCalculatedInState ? this.addCalculated(incomingState) : incomingState;
3798
- const { __props, __children, __context, ...sanitized } = state;
3799
- const copy = { ...sanitized };
3800
- if (!this.calculated) return copy
3718
+ const copy = { ...state };
3719
+ if (!this.calculated || this.storeCalculatedInState) return copy
3801
3720
  const keys = Object.keys(this.calculated);
3802
3721
  keys.forEach(key => {
3803
3722
  if (this.initialState && typeof this.initialState[key] !== 'undefined') {
@@ -3811,7 +3730,7 @@ class Component {
3811
3730
 
3812
3731
  collectRenderParameters() {
3813
3732
  const state = this.sources[this.stateSourceName];
3814
- const renderParams = { ...this.children$[this.DOMSourceName] };
3733
+ const renderParams = { ...this.peers$[this.DOMSourceName] };
3815
3734
 
3816
3735
  const enhancedState = state && state.isolateSource(state, { get: state => this.addCalculated(state) });
3817
3736
  const stateStream = (enhancedState && enhancedState.stream) || xs$1.never();
@@ -3820,15 +3739,15 @@ class Component {
3820
3739
  renderParams.state = stateStream.compose(_default$5(objIsEqual));
3821
3740
 
3822
3741
  if (this.sources.props$) {
3823
- renderParams.__props = this.sources.props$.compose(_default$5(objIsEqual));
3742
+ renderParams.props = this.sources.props$.compose(_default$5(propsIsEqual));
3824
3743
  }
3825
3744
 
3826
3745
  if (this.sources.children$) {
3827
- renderParams.__children = this.sources.children$.compose(_default$5(objIsEqual));
3746
+ renderParams.children = this.sources.children$.compose(_default$5(objIsEqual));
3828
3747
  }
3829
3748
 
3830
3749
  if (this.context$) {
3831
- renderParams.__context = this.context$.compose(_default$5(objIsEqual));
3750
+ renderParams.context = this.context$.compose(_default$5(objIsEqual));
3832
3751
  }
3833
3752
 
3834
3753
  const names = [];
@@ -3845,10 +3764,10 @@ class Component {
3845
3764
  .map(arr => {
3846
3765
  const params = names.reduce((acc, name, index) => {
3847
3766
  acc[name] = arr[index];
3848
- if (name === 'state') acc[this.stateSourceName] = arr[index];
3849
- if (name === '__props') acc.props = arr[index];
3850
- if (name === '__children') acc.children = arr[index];
3851
- if (name === '__context') acc.context = arr[index];
3767
+ if (name === 'state') {
3768
+ acc[this.stateSourceName] = arr[index];
3769
+ acc.calculated = (arr[index] && this.getCalculatedValues(arr[index])) || {};
3770
+ }
3852
3771
  return acc
3853
3772
  }, {});
3854
3773
  return params
@@ -3870,6 +3789,7 @@ class Component {
3870
3789
  }
3871
3790
 
3872
3791
  const sinkArrsByType = {};
3792
+ const childSources = [];
3873
3793
  let newInstanceCount = 0;
3874
3794
 
3875
3795
  const newComponents = entries.reduce((acc, [id, el]) => {
@@ -3881,9 +3801,13 @@ class Component {
3881
3801
  const isSwitchable = data.isSwitchable || false;
3882
3802
 
3883
3803
  const addSinks = (sinks) => {
3884
- Object.entries(sinks).map(([name, stream]) => {
3804
+ Object.entries(sinks).forEach(([name, stream]) => {
3885
3805
  sinkArrsByType[name] ||= [];
3886
- if (name !== this.DOMSourceName) sinkArrsByType[name].push(stream);
3806
+ if (name === PARENT_SINK_NAME) {
3807
+ childSources.push(stream);
3808
+ } else if (name !== this.DOMSourceName) {
3809
+ sinkArrsByType[name].push(stream);
3810
+ }
3887
3811
  });
3888
3812
  };
3889
3813
 
@@ -3930,6 +3854,8 @@ class Component {
3930
3854
  }, {});
3931
3855
 
3932
3856
  this.newSubComponentSinks(mergedSinksByType);
3857
+ this.newChildSources(childSources);
3858
+
3933
3859
 
3934
3860
  if (newInstanceCount > 0) this.log(`New sub components instantiated: ${ newInstanceCount }`, true);
3935
3861
 
@@ -3953,18 +3879,30 @@ class Component {
3953
3879
  instantiateCollection(el, props$, children$) {
3954
3880
  const data = el.data;
3955
3881
  const props = data.props || {};
3956
- el.children || [];
3957
- let filter = typeof props.filter === 'function' ? props.filter : _ => true;
3882
+ let filter = typeof props.filter === 'function' ? props.filter : undefined;
3883
+ let sort = sortFunctionFromProp(props.sort);
3958
3884
 
3959
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
3960
- .map(([state, __props, __children]) => {
3961
- if (__props.filter && typeof __props.filter === 'function') {
3962
- filter = __props.filter;
3885
+ const arrayOperators = {
3886
+ filter,
3887
+ sort
3888
+ };
3889
+
3890
+ const state$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$.startWith(props))
3891
+ // this debounce is important. it forces state and prop updates to happen at the same time
3892
+ // without this, changes to sort or filter won't happen properly
3893
+ .compose(_default$2(1))
3894
+ .map(([state, props]) => {
3895
+ if (props.filter !== arrayOperators.filter) {
3896
+ arrayOperators.filter = typeof props.filter === 'function' ? props.filter : undefined;
3897
+ }
3898
+ if (props.sort !== arrayOperators.sort) {
3899
+ arrayOperators.sort = sortFunctionFromProp(props.sort);
3963
3900
  }
3964
- return isObj(state) ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
3901
+
3902
+ return isObj(state) ? this.addCalculated(state) : state
3965
3903
  });
3966
3904
 
3967
- const stateSource = new state.StateSource(combined$);
3905
+ const stateSource = new state.StateSource(state$);
3968
3906
  const stateField = props.from;
3969
3907
  const collectionOf = props.of;
3970
3908
  const idField = props.idfield || 'id';
@@ -3976,10 +3914,10 @@ class Component {
3976
3914
  if (collectionOf.isSygnalComponent) {
3977
3915
  factory = collectionOf;
3978
3916
  } else {
3979
- const name = (typeof collectionOf.name === 'string') ? collectionOf.name : 'FUNCTION_COMPONENT';
3917
+ const name = collectionOf.componentName || collectionOf.label || collectionOf.name || 'FUNCTION_COMPONENT';
3980
3918
  const view = collectionOf;
3981
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = collectionOf;
3982
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
3919
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = collectionOf;
3920
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
3983
3921
  factory = component(options);
3984
3922
  }
3985
3923
  } else if (this.components[collectionOf]) {
@@ -3988,22 +3926,17 @@ class Component {
3988
3926
  throw new Error(`[${this.name}] Invalid 'of' propery in collection: ${ collectionOf }`)
3989
3927
  }
3990
3928
 
3991
- const sanitizeItems = item => {
3992
- if (isObj(item)) {
3993
- const { __props, __children, __context, ...sanitized } = item;
3994
- return sanitized
3995
- } else {
3996
- return item
3997
- }
3998
- };
3999
-
4000
3929
  const fieldLense = {
4001
3930
  get: state => {
4002
- const { __props, __children } = state;
4003
3931
  if (!Array.isArray(state[stateField])) return []
4004
- return state[stateField].filter(filter).map((item, index) => {
4005
- return (isObj(item)) ? { ...item, [idField]: item[idField] || index, __props, __children, __context: this.currentContext } : { value: item, [idField]: index, __props, __children, __context: this.currentContext }
4006
- })
3932
+ const items = state[stateField];
3933
+ const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items;
3934
+ const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered;
3935
+ const mapped = sorted.map((item, index) => {
3936
+ return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
3937
+ });
3938
+
3939
+ return mapped
4007
3940
  },
4008
3941
  set: (oldState, newState) => {
4009
3942
  if (this.calculated && stateField in this.calculated) {
@@ -4011,15 +3944,15 @@ class Component {
4011
3944
  return oldState
4012
3945
  }
4013
3946
  const updated = [];
4014
- for (const oldItem of oldState[stateField].map((item, index) => ({ __primitive: true, value: item, [idField]: index }))) {
4015
- if (!filter(oldItem)) {
3947
+ for (const oldItem of oldState[stateField].map((item, index) => (isObj(item) ? { ...item, [idField]: item[idField] || index } : { __primitive: true, value: item, [idField]: index }))) {
3948
+ if (typeof arrayOperators.filter === 'function' && !arrayOperators.filter(oldItem)) {
4016
3949
  updated.push(oldItem.__primitive ? oldItem.value : oldItem);
4017
3950
  } else {
4018
- const newItem = newState.find(newItem => newItem.id === oldItem.id);
3951
+ const newItem = newState.find(item => item[idField] === oldItem[idField]);
4019
3952
  if (typeof newItem !== 'undefined') updated.push(oldItem.__primitive ? newItem.value : newItem);
4020
3953
  }
4021
3954
  }
4022
- return { ...oldState, [stateField]: newState.map(sanitizeItems) }
3955
+ return { ...oldState, [stateField]: updated }
4023
3956
  }
4024
3957
  };
4025
3958
 
@@ -4074,7 +4007,7 @@ class Component {
4074
4007
  lense = undefined;
4075
4008
  }
4076
4009
 
4077
- const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$ };
4010
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null };
4078
4011
  const sink$ = collection(factory, lense, { container: null })(sources);
4079
4012
  if (!isObj(sink$)) {
4080
4013
  throw new Error('Invalid sinks returned from component factory of collection element')
@@ -4085,40 +4018,31 @@ class Component {
4085
4018
  instantiateSwitchable(el, props$, children$) {
4086
4019
  const data = el.data;
4087
4020
  const props = data.props || {};
4088
- el.children || [];
4089
4021
 
4090
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
4091
- .map(([state, __props, __children]) => {
4092
- return isObj(state) ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
4022
+ const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
4023
+ .map((state) => {
4024
+ return isObj(state) ? this.addCalculated(state) : state
4093
4025
  });
4094
4026
 
4095
- const stateSource = new state.StateSource(combined$);
4096
- const stateField = props.state;
4027
+ const stateSource = new state.StateSource(state$);
4028
+ const stateField = props.state;
4097
4029
  let lense;
4098
4030
 
4099
4031
  const fieldLense = {
4100
- get: state => {
4101
- const { __props, __children } = state;
4102
- return (isObj(state[stateField])) ? { ...state[stateField], __props, __children, __context: this.currentContext } : { value: state[stateField], __props, __children, __context: this.currentContext }
4103
- },
4032
+ get: state => state[stateField],
4104
4033
  set: (oldState, newState) => {
4105
4034
  if (this.calculated && stateField in this.calculated) {
4106
4035
  console.warn(`Switchable sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
4107
4036
  return oldState
4108
4037
  }
4109
4038
  if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
4110
- const { __props, __children, __context, ...sanitized } = newState;
4111
- return { ...oldState, [stateField]: sanitized }
4039
+ return { ...oldState, [stateField]: newState }
4112
4040
  }
4113
4041
  };
4114
4042
 
4115
4043
  const baseLense = {
4116
4044
  get: state => state,
4117
- set: (oldState, newState) => {
4118
- if (!isObj(newState) || Array.isArray(newState)) return newState
4119
- const { __props, __children, __context, ...sanitized } = newState;
4120
- return sanitized
4121
- }
4045
+ set: (oldState, newState) => newState
4122
4046
  };
4123
4047
 
4124
4048
  if (typeof stateField === 'undefined') {
@@ -4142,10 +4066,10 @@ class Component {
4142
4066
  keys.forEach(key => {
4143
4067
  const current = switchableComponents[key];
4144
4068
  if (!current.isSygnalComponent) {
4145
- const name = (typeof current.name === 'string') ? current.name : 'FUNCTION_COMPONENT';
4069
+ const name = current.componentName || current.label || current.name || 'FUNCTION_COMPONENT';
4146
4070
  const view = current;
4147
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = current;
4148
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4071
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = current;
4072
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4149
4073
  switchableComponents[key] = component(options);
4150
4074
  }
4151
4075
  });
@@ -4164,14 +4088,13 @@ class Component {
4164
4088
  const componentName = el.sel;
4165
4089
  const data = el.data;
4166
4090
  const props = data.props || {};
4167
- el.children || [];
4168
4091
 
4169
- const combined$ = xs$1.combine(this.sources[this.stateSourceName].stream.startWith(this.currentState), props$, children$)
4170
- .map(([state, __props, __children]) => {
4171
- return isObj(state) ? { ...this.addCalculated(state), __props, __children, __context: this.currentContext } : { value: state, __props, __children, __context: this.currentContext }
4092
+ const state$ = this.sources[this.stateSourceName].stream.startWith(this.currentState)
4093
+ .map((state) => {
4094
+ return isObj(state) ? this.addCalculated(state) : state
4172
4095
  });
4173
4096
 
4174
- const stateSource = new state.StateSource(combined$);
4097
+ const stateSource = new state.StateSource(state$);
4175
4098
  const stateField = props.state;
4176
4099
 
4177
4100
  if (typeof props.sygnalFactory !== 'function' && isObj(props.sygnalOptions)) {
@@ -4187,10 +4110,7 @@ class Component {
4187
4110
  let lense;
4188
4111
 
4189
4112
  const fieldLense = {
4190
- get: state => {
4191
- const { __props, __children } = state;
4192
- return isObj(state[stateField]) ? { ...state[stateField], __props, __children, __context: this.currentContext } : { value: state[stateField], __props, __children, __context: this.currentContext }
4193
- },
4113
+ get: state => state[stateField],
4194
4114
  set: (oldState, newState) => {
4195
4115
  if (this.calculated && stateField in this.calculated) {
4196
4116
  console.warn(`Sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
@@ -4202,11 +4122,7 @@ class Component {
4202
4122
 
4203
4123
  const baseLense = {
4204
4124
  get: state => state,
4205
- set: (oldState, newState) => {
4206
- if (!isObj(newState) || Array.isArray(newState)) return newState
4207
- const { __props, __children, __context, ...sanitized } = newState;
4208
- return sanitized
4209
- }
4125
+ set: (oldState, newState) => newState
4210
4126
  };
4211
4127
 
4212
4128
  if (typeof stateField === 'undefined') {
@@ -4418,9 +4334,11 @@ function deepCopyVdom(obj) {
4418
4334
  return { ...obj, children: Array.isArray(obj.children) ? obj.children.map(deepCopyVdom) : undefined, data: obj.data && { ...obj.data, componentsInjected: false } }
4419
4335
  }
4420
4336
 
4421
- function objIsEqual(objA, objB, maxDepth = 5, depth = 0) {
4422
- const obj1 = sanitizeObject(objA);
4423
- const obj2 = sanitizeObject(objB);
4337
+ function propsIsEqual(obj1, obj2) {
4338
+ return objIsEqual(sanitizeObject(obj1))
4339
+ }
4340
+
4341
+ function objIsEqual(obj1, obj2, maxDepth = 5, depth = 0) {
4424
4342
  // Base case: if the current depth exceeds maxDepth, return true
4425
4343
  if (depth > maxDepth) {
4426
4344
  return false;
@@ -4473,7 +4391,7 @@ function objIsEqual(objA, objB, maxDepth = 5, depth = 0) {
4473
4391
 
4474
4392
  function sanitizeObject(obj) {
4475
4393
  if (!isObj(obj)) return obj
4476
- const {state, of, from, _reqId, _action, __props, __children, __context, ...sanitized} = obj;
4394
+ const {state, of, from, filter, ...sanitized} = obj;
4477
4395
  return sanitized
4478
4396
  }
4479
4397
 
@@ -4481,6 +4399,80 @@ function isObj(obj) {
4481
4399
  return typeof obj === 'object' && obj !== null && !Array.isArray(obj)
4482
4400
  }
4483
4401
 
4402
+ function __baseSort(a, b, ascending=true) {
4403
+ const direction = ascending ? 1 : -1;
4404
+ switch(true) {
4405
+ case a > b: return 1 * direction
4406
+ case a < b: return -1 * direction
4407
+ default: return 0
4408
+ }
4409
+ }
4410
+
4411
+ function __sortFunctionFromObj(item) {
4412
+ const entries = Object.entries(item);
4413
+ if (entries.length > 1) {
4414
+ console.error('Sort objects can only have one key:', item);
4415
+ return undefined
4416
+ }
4417
+ const entry = entries[0];
4418
+ const [field, directionRaw] = entry;
4419
+ if (!['string', 'number'].includes(typeof directionRaw)) {
4420
+ console.error('Sort object properties must be a string or number:', item);
4421
+ return undefined
4422
+ }
4423
+ let ascending = true;
4424
+ if (typeof directionRaw === 'string') {
4425
+ if (!['asc', 'dec'].includes(directionRaw.toLowerCase())) {
4426
+ console.error('Sort object string values must be asc or dec:', item);
4427
+ return undefined
4428
+ }
4429
+ ascending = directionRaw.toLowerCase() === 'asc';
4430
+ }
4431
+ if (typeof directionRaw === 'number') {
4432
+ if (directionRaw !== 1 && directionRaw !== -1) {
4433
+ console.error('Sort object number values must be 1 or -1:', item);
4434
+ return undefined
4435
+ }
4436
+ ascending = directionRaw === 1;
4437
+ }
4438
+ return (a, b) => __baseSort(a[field], b[field], ascending)
4439
+ }
4440
+
4441
+ function sortFunctionFromProp(sortProp) {
4442
+ if (!sortProp) return undefined
4443
+ const propType = typeof sortProp;
4444
+ // if function do nothing
4445
+ if (propType === 'function') return sortProp
4446
+ if (propType === 'string') {
4447
+ // if passed either 'asc' or 'dec' sort on the entire item
4448
+ if (sortProp.toLowerCase() === 'asc' || sortProp.toLowerCase() === 'dec') {
4449
+ const ascending = sortProp.toLowerCase() === 'asc';
4450
+ return (a, b) => __baseSort(a, b, ascending)
4451
+ }
4452
+ // assume it's a field/property name, and sort it ascending
4453
+ const field = sortProp;
4454
+ return (a, b) => __baseSort(a[field], b[field], true)
4455
+ } else if (Array.isArray(sortProp)) {
4456
+ const sorters = sortProp.map(item => {
4457
+ if (typeof item === 'function') return item
4458
+ if (typeof item === 'string' && !['asc', 'dec'].includes(item.toLowerCase())) return (a, b) => __baseSort(a[item], b[item], true)
4459
+ if (isObj(item)) {
4460
+ return __sortFunctionFromObj(item)
4461
+ }
4462
+ });
4463
+
4464
+ return (a, b) => sorters.filter(sorter => typeof sorter === 'function').reduce((comparisonSoFar, currentSorter) => {
4465
+ if (comparisonSoFar !== 0) return comparisonSoFar
4466
+ return currentSorter(a, b)
4467
+ }, 0)
4468
+ } else if (isObj(sortProp)) {
4469
+ return __sortFunctionFromObj(sortProp)
4470
+ } else {
4471
+ console.error('Invalid sort option (ignoring):', item);
4472
+ return undefined
4473
+ }
4474
+ }
4475
+
4484
4476
  function driverFromAsync(promiseReturningFunction, opts = {}) {
4485
4477
  const {
4486
4478
  selector: selectorProperty = 'category',
@@ -4694,10 +4686,10 @@ function logDriver(out$) {
4694
4686
  function run(app, drivers={}, options={}) {
4695
4687
  const { mountPoint='#root', fragments=true } = options;
4696
4688
  if (!app.isSygnalComponent) {
4697
- const name = app.name || "FUNCTIONAL_COMPONENT";
4689
+ const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT";
4698
4690
  const view = app;
4699
- const { model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = app;
4700
- const options = { name, view, model, intent, context, children, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4691
+ const { model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug } = app;
4692
+ const options = { name, view, model, intent, context, peers, components, initialState, calculated, storeCalculatedInState, DOMSourceName, stateSourceName, debug };
4701
4693
 
4702
4694
  app = component(options);
4703
4695
  }