sygnal 2.6.8 → 2.7.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
@@ -3150,7 +3150,6 @@ const INITIALIZE_ACTION = 'INITIALIZE';
3150
3150
  const HYDRATE_ACTION = 'HYDRATE';
3151
3151
 
3152
3152
 
3153
- let IS_ROOT_COMPONENT = true;
3154
3153
  let COMPONENT_COUNT = 0;
3155
3154
 
3156
3155
 
@@ -3211,6 +3210,7 @@ class Component {
3211
3210
  // DOMSourceName
3212
3211
  // stateSourceName
3213
3212
  // requestSourceName
3213
+ // debug
3214
3214
 
3215
3215
  // [ PRIVATE / CALCULATED VALUES ]
3216
3216
  // sourceNames
@@ -3224,6 +3224,7 @@ class Component {
3224
3224
  // subComponentSink$
3225
3225
  // unmountRequest$
3226
3226
  // unmount()
3227
+ // stateCache
3227
3228
 
3228
3229
  // [ INSTANTIATED STREAM OPERATOR ]
3229
3230
  // log
@@ -3231,7 +3232,7 @@ class Component {
3231
3232
  // [ OUTPUT ]
3232
3233
  // sinks
3233
3234
 
3234
- constructor({ name='NO NAME', sources, intent, request, model, response, view, children={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP' }) {
3235
+ constructor({ name='NO NAME', sources, intent, request, model, response, view, children={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
3235
3236
  if (!sources || typeof sources != 'object') throw new Error('Missing or invalid sources')
3236
3237
 
3237
3238
  this.name = name;
@@ -3250,6 +3251,7 @@ class Component {
3250
3251
  this.stateSourceName = stateSourceName;
3251
3252
  this.requestSourceName = requestSourceName;
3252
3253
  this.sourceNames = Object.keys(sources);
3254
+ this._debug = debug;
3253
3255
 
3254
3256
  this.isSubComponent = this.sourceNames.includes('props$');
3255
3257
 
@@ -3263,17 +3265,17 @@ class Component {
3263
3265
  }));
3264
3266
  }
3265
3267
 
3266
- // TODO: this is a hack to allow the root component to be created without an intent or model
3267
- // refactor to avoid using a global variable
3268
- if (IS_ROOT_COMPONENT && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
3268
+ // Ensure that the root component has an intent and model
3269
+ // This is necessary to ensure that the component tree's state sink is subscribed to
3270
+ if (!this.isSubComponent && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
3269
3271
  this.initialState = initialState || true;
3270
3272
  this.intent = _ => ({__NOOP_ACTION__:xs$1.never()});
3271
3273
  this.model = {
3272
3274
  __NOOP_ACTION__: state => state
3273
3275
  };
3274
3276
  }
3275
- IS_ROOT_COMPONENT = false;
3276
3277
 
3278
+ this.addCalculated = this.createMemoizedAddCalculated();
3277
3279
  this.log = makeLog(name);
3278
3280
 
3279
3281
  this.initIntent$();
@@ -3290,11 +3292,15 @@ class Component {
3290
3292
 
3291
3293
  this.sinks.__index = COMPONENT_COUNT++;
3292
3294
 
3293
- if (ENVIRONMENT.DEBUG === true) {
3295
+ if (debug) {
3294
3296
  console.log(`[${ this.name }] Instantiated (#${ this.sinks.__index })`);
3295
3297
  }
3296
3298
  }
3297
3299
 
3300
+ get debug() {
3301
+ return this._debug || (ENVIRONMENT.DEBUG === 'true' || ENVIRONMENT.DEBUG === true)
3302
+ }
3303
+
3298
3304
  initIntent$() {
3299
3305
  if (!this.intent) {
3300
3306
  return
@@ -3392,7 +3398,7 @@ class Component {
3392
3398
 
3393
3399
  console.log(`${ timestamp } ${ ip } ${ req.method } ${ req.url }`);
3394
3400
 
3395
- if (ENVIRONMENT.DEBUG) {
3401
+ if (this.debug) {
3396
3402
  this.action$.setDebugListener({next: ({ type }) => console.log(`[${ this.name }] Action from ${ this.requestSourceName } request: <${ type }>`)});
3397
3403
  }
3398
3404
 
@@ -3698,20 +3704,39 @@ class Component {
3698
3704
  }
3699
3705
  }
3700
3706
 
3701
- addCalculated(state) {
3702
- if (!this.calculated || typeof state !== 'object' || state instanceof Array) return state
3703
- if (typeof this.calculated !== 'object') throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3704
- const entries = Object.entries(this.calculated);
3705
- const calculated = entries.reduce((acc, [field, fn]) => {
3706
- if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3707
- try {
3708
- acc[field] = fn(state);
3709
- } catch(e) {
3710
- console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3707
+ createMemoizedAddCalculated() {
3708
+ let lastState;
3709
+ let lastResult;
3710
+
3711
+ return function(state) {
3712
+ if (!this.calculated || typeof state !== 'object' || state instanceof Array) return state
3713
+ if (state === lastState) {
3714
+ return lastResult
3711
3715
  }
3712
- return acc
3713
- }, {});
3714
- return { ...state, ...calculated }
3716
+ if (typeof this.calculated !== 'object') throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
3717
+ const entries = Object.entries(this.calculated);
3718
+ if (entries.length === 0) {
3719
+ lastState = state;
3720
+ lastResult = state;
3721
+ return state
3722
+ }
3723
+ const calculated = entries.reduce((acc, [field, fn]) => {
3724
+ if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
3725
+ try {
3726
+ acc[field] = fn(state);
3727
+ } catch(e) {
3728
+ console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
3729
+ }
3730
+ return acc
3731
+ }, {});
3732
+
3733
+ const newState = { ...state, ...calculated };
3734
+
3735
+ lastState = state;
3736
+ lastResult = newState;
3737
+
3738
+ return newState
3739
+ }
3715
3740
  }
3716
3741
 
3717
3742
  cleanupCalculated(incomingState) {
@@ -3738,15 +3763,33 @@ class Component {
3738
3763
  const enhancedState = state && state.isolateSource(state, { get: state => this.addCalculated(state) });
3739
3764
  const stateStream = (enhancedState && enhancedState.stream) || xs$1.never();
3740
3765
 
3741
- renderParams.state = stateStream;
3742
- renderParams[this.stateSourceName] = stateStream;
3766
+ const objRepeatChecker = (a, b) => {
3767
+ const { state, sygnalFactory, __props, __children, ...sanitized } = a;
3768
+ const keys = Object.keys(sanitized);
3769
+ if (keys.length === 0) {
3770
+ const { state, sygnalFactory, __props, __children, ...sanitizedB } = b;
3771
+ return Object.keys(sanitizedB).length === 0
3772
+ }
3773
+ return keys.every(key => a[key] === b[key])
3774
+ };
3775
+
3776
+ const arrRepeatChecker = (a, b) => {
3777
+ if (a === b) return true
3778
+ if (a.length !== b.length) return false
3779
+ for (let i=0; i < a.length; i++) {
3780
+ if (a[i] !== b[i]) return false
3781
+ }
3782
+ return true
3783
+ };
3784
+
3785
+ renderParams.state = stateStream.compose(_default$5(objRepeatChecker));
3743
3786
 
3744
3787
  if (this.sources.props$) {
3745
- renderParams.__props = this.sources.props$;
3788
+ renderParams.__props = this.sources.props$.compose(_default$5(objRepeatChecker));
3746
3789
  }
3747
3790
 
3748
3791
  if (this.sources.children$) {
3749
- renderParams.__children = this.sources.children$;
3792
+ renderParams.__children = this.sources.children$.compose(_default$5(arrRepeatChecker));
3750
3793
  }
3751
3794
 
3752
3795
  const names = [];
@@ -3762,6 +3805,7 @@ class Component {
3762
3805
  .map(arr => {
3763
3806
  return names.reduce((acc, name, index) => {
3764
3807
  acc[name] = arr[index];
3808
+ if (name === 'state') acc[this.stateSourceName] = arr[index];
3765
3809
  return acc
3766
3810
  }, {})
3767
3811
  });
@@ -3770,6 +3814,7 @@ class Component {
3770
3814
  }
3771
3815
 
3772
3816
  instantiateSubComponents(vDom$) {
3817
+ let count = 0;
3773
3818
  return vDom$.fold((previousComponents, vDom) => {
3774
3819
  const componentNames = Object.keys(this.components);
3775
3820
  const foundComponents = getComponents(vDom, componentNames);
@@ -3800,6 +3845,7 @@ class Component {
3800
3845
 
3801
3846
 
3802
3847
  if (previousComponents[id]) {
3848
+ if (this.debug) console.log(this.name, 'sameful', count++);
3803
3849
  const entry = previousComponents[id];
3804
3850
  acc[id] = entry;
3805
3851
  entry.props$.shamefullySendNext(props);
@@ -3808,6 +3854,8 @@ class Component {
3808
3854
  return acc
3809
3855
  }
3810
3856
 
3857
+ if (this.debug) console.log(this.name, 'non-shameful', count++);
3858
+
3811
3859
  const props$ = xs$1.create().startWith(props);
3812
3860
  const children$ = xs$1.create().startWith(children);
3813
3861
 
@@ -3871,6 +3919,7 @@ class Component {
3871
3919
 
3872
3920
  const stateSource = new state.StateSource(combined$);
3873
3921
  const stateField = props.from;
3922
+ props.filter;
3874
3923
  let lense;
3875
3924
 
3876
3925
  const factory = typeof props.of === 'function' ? props.of : this.components[props.of];
@@ -4164,7 +4213,7 @@ class Component {
4164
4213
  const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
4165
4214
  return stream => {
4166
4215
  return stream.debug(msg => {
4167
- if (ENVIRONMENT.DEBUG == 'true' || ENVIRONMENT.DEBUG === true) {
4216
+ if (this.debug) {
4168
4217
  console.log(`[${context}] ${fixedMsg(msg)}`);
4169
4218
  }
4170
4219
  })
@@ -4280,6 +4329,125 @@ function deepCopyVdom(obj) {
4280
4329
  return { ...obj, children: Array.isArray(obj.children) ? obj.children.map(deepCopyVdom) : undefined, data: obj.data && { ...obj.data, componentsInjected: false } }
4281
4330
  }
4282
4331
 
4332
+ function driverFromAsync(promiseReturningFunction, opts = {}) {
4333
+ const {
4334
+ selector: selectorProperty = 'category',
4335
+ args: functionArgs = 'value',
4336
+ return: returnProperty = 'value',
4337
+ pre: preFunction = (val) => val,
4338
+ post: postFunction = (val) => val
4339
+ } = opts;
4340
+
4341
+ const functionName = promiseReturningFunction.name || '[anonymous function]';
4342
+ const functionArgsType = typeof functionArgs;
4343
+ if (functionArgsType !== 'string' && functionArgsType !== 'function' && !(Array.isArray(functionArgs) && functionArgs.every((arg) => typeof arg === 'string'))) {
4344
+ throw new Error(`The 'args' option for driverFromAsync(${ functionName }) must be a string, array of strings, or a function. Received ${functionArgsType}`)
4345
+ }
4346
+
4347
+ if (typeof selectorProperty !== 'string') {
4348
+ throw new Error(`The 'selector' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof selectorProperty}`)
4349
+ }
4350
+
4351
+ return (fromApp$) => {
4352
+ let sendFn = null;
4353
+
4354
+ const toApp$ = xs$1.create({
4355
+ start: (listener) => {
4356
+ sendFn = listener.next.bind(listener);
4357
+ },
4358
+ stop: () => {}
4359
+ });
4360
+
4361
+ fromApp$.addListener({
4362
+ next: (incoming) => {
4363
+ const preProcessed = preFunction(incoming);
4364
+ let argArr = [];
4365
+ if (typeof preProcessed === 'object' && preProcessed !== null) {
4366
+ if (typeof functionArgs === 'function') {
4367
+ const extractedArgs = functionArgs(preProcessed);
4368
+ argArr = Array.isArray(extractedArgs) ? extractedArgs : [extractedArgs];
4369
+ }
4370
+ if (typeof functionArgs === 'string') {
4371
+ argArr = [preProcessed[functionArgs]];
4372
+ }
4373
+ if (Array.isArray(functionArgs)) {
4374
+ argArr = functionArgs.map((arg) => preProcessed[arg]);
4375
+ }
4376
+ }
4377
+ const errMsg = `Error in driver created using driverFromAsync(${ functionName })`;
4378
+ promiseReturningFunction(...argArr)
4379
+ .then((innerVal) => {
4380
+ const constructReply = (rawVal) => {
4381
+ let outgoing;
4382
+ if (returnProperty === undefined) {
4383
+ outgoing = rawVal;
4384
+ if (typeof outgoing === 'object' && outgoing !== null) {
4385
+ outgoing[selectorProperty] = incoming[selectorProperty];
4386
+ } else {
4387
+ 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.`);
4388
+ }
4389
+ } else if (typeof returnProperty === 'string') {
4390
+ outgoing = {
4391
+ [returnProperty]: rawVal,
4392
+ [selectorProperty]: incoming[selectorProperty]
4393
+ };
4394
+ } else {
4395
+ throw new Error(`The 'return' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof returnProperty}`)
4396
+ }
4397
+ return outgoing
4398
+ };
4399
+
4400
+
4401
+ // handle nested promises and promises returned by postFunction
4402
+ if (typeof innerVal.then === 'function') {
4403
+ innerVal
4404
+ .then((innerOutgoing) => {
4405
+ const processedOutgoing = postFunction(innerOutgoing, incoming);
4406
+ if (typeof processedOutgoing.then === 'function') {
4407
+ processedOutgoing
4408
+ .then((innerProcessedOutgoing) => {
4409
+ sendFn(constructReply(innerProcessedOutgoing));
4410
+ })
4411
+ .catch((err) => console.error(`${ errMsg }: ${ err }`));
4412
+ } else {
4413
+ sendFn(constructReply(rocessedOutgoing));
4414
+ }
4415
+ })
4416
+ .catch((err) => console.error(`${ errMsg }: ${ err }`));
4417
+ } else {
4418
+ const processedOutgoing = postFunction(innerVal, incoming);
4419
+ if (typeof processedOutgoing.then === 'function') {
4420
+ processedOutgoing
4421
+ .then((innerProcessedOutgoing) => {
4422
+ sendFn(constructReply(innerProcessedOutgoing));
4423
+ })
4424
+ .catch((err) => console.error(`${ errMsg }: ${ err }`));
4425
+ } else {
4426
+ sendFn(constructReply(processedOutgoing));
4427
+ }
4428
+ }
4429
+ })
4430
+ .catch((err) => console.error(`${ errMsg }: ${ err }`));
4431
+ },
4432
+ error: (err) => {
4433
+ console.error(`Error recieved from sink stream in driver created using driverFromAsync(${ functionName }): ${ err }`);
4434
+ },
4435
+ complete: () => {
4436
+ console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${ functionName })`);
4437
+ }
4438
+ });
4439
+
4440
+ return {
4441
+ select: (selector) => {
4442
+ if (selector === undefined) return toApp$
4443
+ if (typeof selector === 'function') return toApp$.filter(selector)
4444
+ return toApp$.filter((val) => val?.[selectorProperty] === selector)
4445
+ }
4446
+ }
4447
+
4448
+ }
4449
+ }
4450
+
4283
4451
  function processForm(form, options={}) {
4284
4452
  let { events = ['input', 'submit'], preventDefault = true } = options;
4285
4453
  if (typeof events === 'string') events = [events];
@@ -4759,6 +4927,7 @@ exports.collection = collection;
4759
4927
  exports.component = component;
4760
4928
  exports.debounce = _default$2;
4761
4929
  exports.delay = _default$4;
4930
+ exports.driverFromAsync = driverFromAsync;
4762
4931
  exports.dropRepeats = _default$5;
4763
4932
  exports.processForm = processForm;
4764
4933
  exports.run = run;