sygnal 2.4.0 → 2.5.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.
@@ -0,0 +1,1300 @@
1
+ import isolate from '@cycle/isolate';
2
+ import { makeCollection, StateSource, withState } from '@cycle/state';
3
+ import xs, { Stream } from 'xstream';
4
+ export { default as xs } from 'xstream';
5
+ import dropRepeats from 'xstream/extra/dropRepeats';
6
+ export { default as dropRepeats } from 'xstream/extra/dropRepeats';
7
+ import delay from 'xstream/extra/delay.js';
8
+ import concat from 'xstream/extra/concat.js';
9
+ import debounce from 'xstream/extra/debounce.js';
10
+ import dropRepeats$1 from 'xstream/extra/dropRepeats.js';
11
+ import { run as run$1 } from '@cycle/run';
12
+ import { makeDOMDriver } from '@cycle/dom';
13
+ export * from '@cycle/dom';
14
+ import { adapt } from '@cycle/run/lib/adapt';
15
+ export { default as debounce } from 'xstream/extra/debounce';
16
+ export { default as throttle } from 'xstream/extra/throttle';
17
+ export { default as delay } from 'xstream/extra/delay';
18
+ export { default as sampleCombine } from 'xstream/extra/sampleCombine';
19
+
20
+ function collection(component, stateLense, opts={}) {
21
+ const {
22
+ combineList = ['DOM'],
23
+ globalList = ['EVENTS'],
24
+ stateSourceName = 'STATE',
25
+ domSourceName = 'DOM',
26
+ container = 'div',
27
+ containerClass
28
+ } = opts;
29
+
30
+ return (sources) => {
31
+ const key = Date.now();
32
+ const collectionOpts = {
33
+ item: component,
34
+ itemKey: (state, ind) => typeof state.id !== 'undefined' ? state.id : ind,
35
+ itemScope: key => key,
36
+ channel: stateSourceName,
37
+ collectSinks: instances => {
38
+ return Object.entries(sources).reduce((acc, [name, stream]) => {
39
+ if (combineList.includes(name)) {
40
+ const combined = instances.pickCombine(name);
41
+ if (name === domSourceName && container) {
42
+ acc.DOM = combined.map(children => {
43
+ const data = (containerClass) ? { props: { className: containerClass } } : {};
44
+ return { sel: container, data, children, key, text: undefined, elm: undefined}
45
+ });
46
+ } else {
47
+ console.warn('Collections without wrapping containers will fail in unpredictable ways when used inside JSX fragments');
48
+ acc[name] = combined;
49
+ }
50
+ } else {
51
+ acc[name] = instances.pickMerge(name);
52
+ }
53
+ return acc
54
+ }, {})
55
+ }
56
+ };
57
+
58
+ const isolateOpts = {[stateSourceName]: stateLense};
59
+
60
+ globalList.forEach(global => isolateOpts[global] = null);
61
+ combineList.forEach(combine => isolateOpts[combine] = null);
62
+
63
+ return makeIsolatedCollection(collectionOpts, isolateOpts, sources)
64
+ }
65
+ }
66
+
67
+ /**
68
+ * instantiate a cycle collection and isolate
69
+ * (makes the code for doing isolated collections more readable)
70
+ *
71
+ * @param {Object} collectionOpts options for the makeCollection function (see cycle/state documentation)
72
+ * @param {String|Object} isolateOpts options for the isolate function (see cycle/isolate documentation)
73
+ * @param {Object} sources object of cycle style sources to use for the created collection
74
+ * @return {Object} collection of component sinks
75
+ */
76
+ function makeIsolatedCollection (collectionOpts, isolateOpts, sources) {
77
+ return isolate(makeCollection(collectionOpts), isolateOpts)(sources)
78
+ }
79
+
80
+ function switchable(factories, name$, initial, opts={}) {
81
+ const {
82
+ switched=['DOM'],
83
+ stateSourceName='STATE'
84
+ } = opts;
85
+ const nameType = typeof name$;
86
+
87
+ if (!name$) throw new Error(`Missing 'name$' parameter for switchable()`)
88
+ if (!(nameType === 'string' || nameType === 'function' || name$ instanceof Stream)) {
89
+ throw new Error(`Invalid 'name$' parameter for switchable(): expects Stream, String, or Function`)
90
+ }
91
+
92
+ if (name$ instanceof Stream) {
93
+ const withInitial$ = name$
94
+ .compose(dropRepeats())
95
+ .startWith(initial)
96
+ .remember();
97
+ return sources => _switchable(factories, sources, withInitial$, switched)
98
+ } else {
99
+ const mapFunction = (nameType === 'function' && name$) || (state => state[name$]);
100
+ return sources => {
101
+ const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream;
102
+ if (!state$ instanceof Stream) throw new Error(`Could not find the state source: ${ stateSourceName }`)
103
+ const _name$ = state$
104
+ .map(mapFunction)
105
+ .filter(name => typeof name === 'string')
106
+ .compose(dropRepeats())
107
+ .startWith(initial)
108
+ .remember();
109
+ return _switchable(factories, sources, _name$, switched, stateSourceName)
110
+ }
111
+ }
112
+ }
113
+
114
+
115
+
116
+ /**
117
+ * create a group of components which can be switched between based on a stream of component names
118
+ *
119
+ * @param {Object} factories maps names to component creation functions
120
+ * @param {Object} sources standard cycle sources object provided to each component
121
+ * @param {Observable} name$ stream of names corresponding to the component names
122
+ * @param {Array} switched which cycle sinks from the components should be `switched` when a new `name$` is emitted
123
+ * @return {Object} cycle sinks object where the selected sinks are switched to the last component name emitted to `name$`
124
+ *
125
+ * any component sinks not dsignated in `switched` will be merged across all components
126
+ */
127
+ function _switchable (factories, sources, name$, switched=['DOM'], stateSourceName='STATE') {
128
+ if (typeof switched === 'string') switched = [switched];
129
+
130
+ const sinks = Object.entries(factories)
131
+ .map(([name, factory]) => {
132
+ if (sources[stateSourceName]) {
133
+ const state$ = sources[stateSourceName].stream;
134
+ const switched = xs.combine(name$, state$)
135
+ .filter(([newComponentName, _]) => newComponentName == name)
136
+ .map(([_, state]) => state)
137
+ .remember();
138
+
139
+ const state = new sources[stateSourceName].constructor(switched, sources[stateSourceName]._name);
140
+ return [name, factory({ ...sources, state })]
141
+ }
142
+ return [name, factory(sources)]
143
+ });
144
+
145
+ const switchedSinks = Object.keys(sources)
146
+ .reduce((obj, sinkName) => {
147
+ if (switched.includes(sinkName)) {
148
+ obj[sinkName] = name$
149
+ .map( newComponentName => {
150
+ const sink = sinks.find(([componentName, _]) => componentName === newComponentName);
151
+ return (sink && sink[1][sinkName]) || xs.never()
152
+ })
153
+ .flatten()
154
+ .remember()
155
+ .startWith(undefined);
156
+ } else {
157
+ const definedSinks = sinks.filter(([_,sink]) => sink[sinkName] !== undefined)
158
+ .map(([_,sink]) => sink[sinkName]);
159
+ obj[sinkName] = xs.merge(...definedSinks);
160
+ }
161
+ return obj
162
+ }, {});
163
+
164
+ return switchedSinks
165
+ }
166
+
167
+ // import syntax has bugs for xstream in Node context
168
+ // this attempts to normalize to work in both Node and browser
169
+ // if (!xs.never && xs.default && xs.default.never) {
170
+ // xs.never = xs.default.never
171
+ // xs.merge = xs.default.merge
172
+ // xs.of = xs.default.of
173
+ // }
174
+ // const concat = (Concat && Concat.default) ? Concat.default : Concat
175
+ // const delay = (Delay && Delay.default) ? Delay.default : Delay
176
+ // const dropRepeats = (DropRepeats && DropRepeats.default) ? DropRepeats.default : DropRepeats
177
+
178
+ const ENVIRONMENT = ((typeof window != 'undefined' && window) || (process && process.env)) || {};
179
+
180
+
181
+ const REQUEST_SELECTOR_METHOD = 'request';
182
+ const BOOTSTRAP_ACTION = 'BOOTSTRAP';
183
+ const INITIALIZE_ACTION = 'INITIALIZE';
184
+ const HYDRATE_ACTION = 'HYDRATE';
185
+
186
+
187
+ let IS_ROOT_COMPONENT = true;
188
+
189
+
190
+ const ABORT = '~#~#~ABORT~#~#~';
191
+
192
+ function component (opts) {
193
+ const { name, sources, isolateOpts, stateSourceName='STATE' } = opts;
194
+
195
+ if (sources && typeof sources !== 'object') {
196
+ throw new Error('Sources must be a Cycle.js sources object:', name)
197
+ }
198
+
199
+ let fixedIsolateOpts;
200
+ if (typeof isolateOpts == 'string') {
201
+ fixedIsolateOpts = { [stateSourceName]: isolateOpts };
202
+ } else {
203
+ if (isolateOpts === true) {
204
+ fixedIsolateOpts = {};
205
+ } else {
206
+ fixedIsolateOpts = isolateOpts;
207
+ }
208
+ }
209
+
210
+ const currySources = typeof sources === 'undefined';
211
+
212
+ if (typeof fixedIsolateOpts == 'object') {
213
+ const wrapped = (sources) => {
214
+ const fixedOpts = { ...opts, sources };
215
+ return (new Component(fixedOpts)).sinks
216
+ };
217
+ return currySources ? isolate(wrapped, fixedIsolateOpts) : isolate(wrapped, fixedIsolateOpts)(sources)
218
+ } else {
219
+ return currySources ? (sources) => (new Component({ ...opts, sources })).sinks : (new Component(opts)).sinks
220
+ }
221
+ }
222
+
223
+
224
+
225
+
226
+
227
+ class Component {
228
+ // [ PASSED PARAMETERS ]
229
+ // name
230
+ // sources
231
+ // intent
232
+ // request
233
+ // model
234
+ // response
235
+ // view
236
+ // children
237
+ // initialState
238
+ // calculated
239
+ // storeCalculatedInState
240
+ // DOMSourceName
241
+ // stateSourceName
242
+ // requestSourceName
243
+
244
+ // [ PRIVATE / CALCULATED VALUES ]
245
+ // sourceNames
246
+ // intent$
247
+ // action$
248
+ // model$
249
+ // response$
250
+ // sendResponse$
251
+ // children$
252
+ // vdom$
253
+ // subComponentSink$
254
+
255
+ // [ INSTANTIATED STREAM OPERATOR ]
256
+ // log
257
+
258
+ // [ OUTPUT ]
259
+ // sinks
260
+
261
+ constructor({ name='NO NAME', sources, intent, request, model, response, view, children={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP' }) {
262
+ if (!sources || typeof sources != 'object') throw new Error('Missing or invalid sources')
263
+
264
+ this.name = name;
265
+ this.sources = sources;
266
+ this.intent = intent;
267
+ this.request = request;
268
+ this.model = model;
269
+ this.response = response;
270
+ this.view = view;
271
+ this.children = children;
272
+ this.components = components;
273
+ this.initialState = initialState;
274
+ this.calculated = calculated;
275
+ this.storeCalculatedInState = storeCalculatedInState;
276
+ this.DOMSourceName = DOMSourceName;
277
+ this.stateSourceName = stateSourceName;
278
+ this.requestSourceName = requestSourceName;
279
+ this.sourceNames = Object.keys(sources);
280
+
281
+ this.isSubComponent = this.sourceNames.includes('props$');
282
+
283
+ const state$ = sources[stateSourceName] && sources[stateSourceName].stream;
284
+
285
+ if (state$) {
286
+ this.currentState = initialState || {};
287
+ this.sources[this.stateSourceName] = new StateSource(state$.map(val => {
288
+ this.currentState = val;
289
+ return val
290
+ }));
291
+ }
292
+
293
+ if (IS_ROOT_COMPONENT && typeof this.intent === 'undefined' && typeof this.model === 'undefined') {
294
+ this.initialState = initialState || true;
295
+ this.intent = _ => ({__NOOP_ACTION__:xs.never()});
296
+ this.model = {
297
+ __NOOP_ACTION__: state => state
298
+ };
299
+ }
300
+ IS_ROOT_COMPONENT = false;
301
+
302
+ this.log = makeLog(name);
303
+
304
+ this.initIntent$();
305
+ this.initAction$();
306
+ this.initResponse$();
307
+ this.initState();
308
+ this.initModel$();
309
+ this.initSendResponse$();
310
+ this.initChildren$();
311
+ this.initSubComponentSink$();
312
+ this.initVdom$();
313
+ this.initSinks();
314
+ }
315
+
316
+ initIntent$() {
317
+ if (!this.intent) {
318
+ return
319
+ }
320
+ if (typeof this.intent != 'function') {
321
+ throw new Error('Intent must be a function')
322
+ }
323
+
324
+ this.intent$ = this.intent(this.sources);
325
+
326
+ if (!(this.intent$ instanceof Stream) && (typeof this.intent$ != 'object')) {
327
+ throw new Error('Intent must return either an action$ stream or map of event streams')
328
+ }
329
+ }
330
+
331
+ initAction$() {
332
+ const requestSource = (this.sources && this.sources[this.requestSourceName]) || null;
333
+
334
+ if (!this.intent$) {
335
+ this.action$ = xs.never();
336
+ return
337
+ }
338
+
339
+ let runner;
340
+ if (this.intent$ instanceof Stream) {
341
+ runner = this.intent$;
342
+ } else {
343
+ const mapped = Object.entries(this.intent$)
344
+ .map(([type, data$]) => data$.map(data => ({type, data})));
345
+ runner = xs.merge(xs.never(), ...mapped);
346
+ }
347
+
348
+ const action$ = ((runner instanceof Stream) ? runner : (runner.apply && runner(this.sources) || xs.never()));
349
+ const wrapped$ = concat(xs.of({ type: BOOTSTRAP_ACTION }), action$)
350
+ .compose(delay(10));
351
+
352
+ let initialApiData;
353
+ if (requestSource && typeof requestSource.select == 'function') {
354
+ initialApiData = requestSource.select('initial')
355
+ .flatten();
356
+ } else {
357
+ initialApiData = xs.never();
358
+ }
359
+
360
+ const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }));
361
+
362
+ this.action$ = xs.merge(wrapped$, hydrate$)
363
+ .compose(this.log(({ type }) => `Action triggered: <${ type }>`));
364
+ }
365
+
366
+ initResponse$() {
367
+ if (typeof this.request == 'undefined') {
368
+ return
369
+ } else if (typeof this.request != 'object') {
370
+ throw new Error('The request parameter must be an object')
371
+ }
372
+
373
+ const router$ = this.sources[this.requestSourceName];
374
+ const methods = Object.entries(this.request);
375
+
376
+ const wrapped = methods.reduce((acc, [method, routes]) => {
377
+ const _method = method.toLowerCase();
378
+ if (typeof router$[_method] != 'function') {
379
+ throw new Error('Invalid method in request object:', method)
380
+ }
381
+ const entries = Object.entries(routes);
382
+ const mapped = entries.reduce((acc, [route, action]) => {
383
+ const routeString = `[${_method.toUpperCase()}]:${route || 'none'}`;
384
+ const actionType = typeof action;
385
+ if (actionType === 'undefined') {
386
+ throw new Error(`Action for '${ route }' route in request object not specified`)
387
+ } else if (actionType !== 'string' && actionType !== 'function') {
388
+ throw new Error(`Invalid action for '${ route }' route: expecting string or function`)
389
+ }
390
+ const actionString = (actionType === 'function') ? '[ FUNCTION ]' : `< ${ action } >`;
391
+ console.log(`[${ this.name }] Adding ${ this.requestSourceName } route:`, _method.toUpperCase(), `'${ route }' <${ actionString }>`);
392
+ const route$ = router$[_method](route)
393
+ .compose(dropRepeats$1((a, b) => a.id == b.id))
394
+ .map(req => {
395
+ if (!req || !req.id) {
396
+ throw new Error(`No id found in request: ${ routeString }`)
397
+ }
398
+ try {
399
+ const _reqId = req.id;
400
+ const params = req.params;
401
+ const body = req.body;
402
+ const cookies = req.cookies;
403
+ const type = (actionType === 'function') ? 'FUNCTION' : action;
404
+ const data = { params, body, cookies, req };
405
+ const obj = { type, data: body, req, _reqId, _action: type };
406
+
407
+ const timestamp = (new Date()).toISOString();
408
+ const ip = req.get ? req.get('host') : '0.0.0.0';
409
+
410
+ console.log(`${ timestamp } ${ ip } ${ req.method } ${ req.url }`);
411
+
412
+ if (ENVIRONMENT.DEBUG) {
413
+ this.action$.setDebugListener({next: ({ type }) => console.log(`[${ this.name }] Action from ${ this.requestSourceName } request: <${ type }>`)});
414
+ }
415
+
416
+ if (actionType === 'function') {
417
+ const enhancedState = this.addCalculated(this.currentState);
418
+ const result = action(enhancedState, req);
419
+ return xs.of({ ...obj, data: result })
420
+ } else {
421
+ this.action$.shamefullySendNext(obj);
422
+
423
+ const sourceEntries = Object.entries(this.sources);
424
+ const responses = sourceEntries.reduce((acc, [name, source]) => {
425
+ if (!source || typeof source[REQUEST_SELECTOR_METHOD] != 'function') return acc
426
+ const selected$ = source[REQUEST_SELECTOR_METHOD](_reqId);
427
+ return [ ...acc, selected$ ]
428
+ }, []);
429
+ return xs.merge(...responses)
430
+ }
431
+ } catch(err) {
432
+ console.error(err);
433
+ }
434
+ }).flatten();
435
+ return [ ...acc, route$ ]
436
+ }, []);
437
+ const mapped$ = xs.merge(...mapped);
438
+ return [ ...acc, mapped$ ]
439
+ }, []);
440
+
441
+ this.response$ = xs.merge(...wrapped)
442
+ .compose(this.log(res => {
443
+ if (res._action) return `[${ this.requestSourceName }] response data received for Action: <${ res._action }>`
444
+ return `[${ this.requestSourceName }] response data received from FUNCTION`
445
+ }));
446
+
447
+ if (typeof this.response != 'undefined' && typeof this.response$ == 'undefined') {
448
+ throw new Error('Cannot have a response parameter without a request parameter')
449
+ }
450
+ }
451
+
452
+ initState() {
453
+ if (this.model != undefined) {
454
+ if (this.model[INITIALIZE_ACTION] === undefined) {
455
+ this.model[INITIALIZE_ACTION] = {
456
+ [this.stateSourceName]: (_, data) => ({ ...this.addCalculated(data) })
457
+ };
458
+ } else {
459
+ Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
460
+ if (name !== this.stateSourceName) {
461
+ console.warn(`${ INITIALIZE_ACTION } can only be used with the ${ this.stateSourceName } source... disregarding ${ name }`);
462
+ delete this.model[INITIALIZE_ACTION][name];
463
+ }
464
+ });
465
+ }
466
+ }
467
+ }
468
+
469
+ initModel$() {
470
+ if (typeof this.model == 'undefined') {
471
+ this.model$ = this.sourceNames.reduce((a,s) => {
472
+ a[s] = xs.never();
473
+ return a
474
+ }, {});
475
+ return
476
+ }
477
+
478
+ const initial = { type: INITIALIZE_ACTION, data: this.initialState };
479
+ const shimmed$ = this.initialState ? concat(xs.of(initial), this.action$).compose(delay(0)) : this.action$;
480
+ const onState = this.makeOnAction(shimmed$, true, this.action$);
481
+ const onNormal = this.makeOnAction(this.action$, false, this.action$);
482
+
483
+
484
+ const modelEntries = Object.entries(this.model);
485
+
486
+ const reducers = {};
487
+
488
+ modelEntries.forEach((entry) => {
489
+ let [action, sinks] = entry;
490
+
491
+ if (typeof sinks === 'function') {
492
+ sinks = { [this.stateSourceName]: sinks };
493
+ }
494
+
495
+ if (typeof sinks !== 'object') {
496
+ throw new Error(`Entry for each action must be an object: ${ this.name } ${ action }`)
497
+ }
498
+
499
+ const sinkEntries = Object.entries(sinks);
500
+
501
+ sinkEntries.forEach((entry) => {
502
+ const [sink, reducer] = entry;
503
+
504
+ const isStateSink = (sink == this.stateSourceName);
505
+
506
+ const on = isStateSink ? onState : onNormal;
507
+ const onned = on(action, reducer);
508
+
509
+ const wrapped = onned.compose(this.log(data => {
510
+ if (isStateSink) {
511
+ return `State reducer added: <${ action }>`
512
+ } else {
513
+ const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data);
514
+ return `Data sent to [${ sink }]: <${ action }> ${ extra }`
515
+ }
516
+ }));
517
+
518
+ if (Array.isArray(reducers[sink])) {
519
+ reducers[sink].push(wrapped);
520
+ } else {
521
+ reducers[sink] = [wrapped];
522
+ }
523
+ });
524
+ });
525
+
526
+ const model$ = Object.entries(reducers).reduce((acc, entry) => {
527
+ const [sink, streams] = entry;
528
+ acc[sink] = xs.merge(xs.never(), ...streams);
529
+ return acc
530
+ }, {});
531
+
532
+ this.model$ = model$;
533
+ }
534
+
535
+ initSendResponse$() {
536
+ const responseType = typeof this.response;
537
+ if (responseType != 'function' && responseType != 'undefined') {
538
+ throw new Error('The response parameter must be a function')
539
+ }
540
+
541
+ if (responseType == 'undefined') {
542
+ if (this.response$) {
543
+ this.response$.subscribe({
544
+ next: this.log(({ _reqId, _action }) => `Unhandled response for request: ${ _action } ${ _reqId }`)
545
+ });
546
+ }
547
+ this.sendResponse$ = xs.never();
548
+ return
549
+ }
550
+
551
+ const selectable = {
552
+ select: (actions) => {
553
+ if (typeof actions == 'undefined') return this.response$
554
+ if (!Array.isArray(actions)) actions = [actions];
555
+ return this.response$.filter(({_action}) => (actions.length > 0) ? (_action === 'FUNCTION' || actions.includes(_action)) : true)
556
+ }
557
+ };
558
+
559
+ const out = this.response(selectable);
560
+ if (typeof out != 'object') {
561
+ throw new Error('The response function must return an object')
562
+ }
563
+
564
+ const entries = Object.entries(out);
565
+ const out$ = entries.reduce((acc, [command, response$]) => {
566
+ const mapped$ = response$.map(({ _reqId, _action, data }) => {
567
+ if (!_reqId) {
568
+ throw new Error(`No request id found for response for: ${ command }`)
569
+ }
570
+ return { _reqId, _action, command, data }
571
+ });
572
+ return [ ...acc, mapped$ ]
573
+ }, []);
574
+
575
+ this.sendResponse$ = xs.merge(...out$)
576
+ .compose(this.log(({ _reqId, _action }) => `[${ this.requestSourceName }] response sent for: <${ _action }>`));
577
+ }
578
+
579
+ initChildren$() {
580
+ const initial = this.sourceNames.reduce((acc, name) => {
581
+ if (name == this.DOMSourceName) {
582
+ acc[name] = {};
583
+ } else {
584
+ acc[name] = [];
585
+ }
586
+ return acc
587
+ }, {});
588
+
589
+ this.children$ = Object.entries(this.children).reduce((acc, [childName, childFactory]) => {
590
+ const child$ = childFactory(this.sources);
591
+ this.sourceNames.forEach(source => {
592
+ if (source == this.DOMSourceName) {
593
+ acc[source][childName] = child$[source];
594
+ } else {
595
+ acc[source].push(child$[source]);
596
+ }
597
+ });
598
+ return acc
599
+ }, initial);
600
+ }
601
+
602
+ initSubComponentSink$() {
603
+ const subComponentSink$ = xs.create({
604
+ start: listener => {
605
+ this.newSubComponentSinks = listener.next.bind(listener);
606
+ },
607
+ stop: _ => {
608
+
609
+ }
610
+ });
611
+ subComponentSink$.subscribe({ next: _ => _ });
612
+ this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0);
613
+ }
614
+
615
+ initVdom$() {
616
+ if (typeof this.view != 'function') {
617
+ this.vdom$ = xs.of(null);
618
+ return
619
+ }
620
+
621
+ const state = this.sources[this.stateSourceName];
622
+ const renderParams = { ...this.children$[this.DOMSourceName] };
623
+
624
+ const enhancedState = state && state.isolateSource(state, { get: state => this.addCalculated(state) });
625
+ const stateStream = (enhancedState && enhancedState.stream) || xs.never();
626
+
627
+ renderParams.state = stateStream;
628
+ renderParams[this.stateSourceName] = stateStream;
629
+
630
+ if (this.sources.props$) {
631
+ renderParams.props = this.sources.props$;
632
+ }
633
+
634
+ if (this.sources.children$) {
635
+ renderParams.children = this.sources.children$;
636
+ }
637
+
638
+ const pulled = Object.entries(renderParams).reduce((acc, [name, stream]) => {
639
+ acc.names.push(name);
640
+ acc.streams.push(stream);
641
+ return acc
642
+ }, {names: [], streams: []});
643
+
644
+ const merged = xs.combine(...pulled.streams);
645
+
646
+ const throttled = merged
647
+ .compose(debounce(5))
648
+ .map(arr => {
649
+ return pulled.names.reduce((acc, name, index) => {
650
+ acc[name] = arr[index];
651
+ return acc
652
+ }, {})
653
+ });
654
+
655
+ const componentNames = Object.keys(this.components);
656
+
657
+ const subComponentRenderedProxy$ = xs.create();
658
+ const vDom$ = throttled.map((params) => params).map(this.view).map(vDom => vDom || { sel: 'div', data: {}, children: [] });
659
+
660
+
661
+ const componentInstances$ = vDom$
662
+ .fold((previousComponents, vDom) => {
663
+ const foundComponents = getComponents(vDom, componentNames);
664
+ const entries = Object.entries(foundComponents);
665
+
666
+ const rootEntry = { '::ROOT::': vDom };
667
+
668
+ if (entries.length === 0) {
669
+ return rootEntry
670
+ }
671
+
672
+ const sinkArrsByType = {};
673
+
674
+ const newComponents = entries.reduce((acc, [id, el]) => {
675
+ const componentName = el.sel;
676
+ const data = el.data;
677
+ const props = data.props || {};
678
+ const children = el.children || [];
679
+ const isCollection = data.isCollection || false;
680
+ const isSwitchable = data.isSwitchable || false;
681
+
682
+ if (previousComponents[id]) {
683
+ const entry = previousComponents[id];
684
+ acc[id] = entry;
685
+ entry.props$.shamefullySendNext(props);
686
+ entry.children$.shamefullySendNext(children);
687
+ return acc
688
+ }
689
+
690
+ const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory);
691
+ if (!factory && !isCollection && !isSwitchable) {
692
+ 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.`)
693
+ throw new Error(`Component not found: ${ componentName }`)
694
+ }
695
+
696
+ const props$ = xs.create().startWith(props);
697
+ const children$ = xs.create().startWith(children);
698
+ let stateSource = new StateSource(this.sources[this.stateSourceName].stream.startWith(this.currentState));
699
+ let sink$;
700
+ let preventStateUpdates = true;
701
+
702
+ if (isCollection) {
703
+ let field, lense;
704
+
705
+ const stateGetter = state => {
706
+ const arr = state[field];
707
+ if (typeof arr === 'undefined') return
708
+ if (!Array.isArray(arr)) {
709
+ const label = typeof data.props.of === 'string' ? data.props.of : 'components';
710
+ console.warn(`Collection of ${ label } does not have a valid array in the 'for' property: expects either an array or a string of the name of an array property on the state`);
711
+ return []
712
+ }
713
+ return arr
714
+ };
715
+
716
+ if (typeof props.for === 'undefined') {
717
+ lense = {
718
+ get: state => {
719
+ if (!Array.isArray(state)) {
720
+ console.warn(`Collection sub-component of ${ this.name } has no 'for' attribute and the parent state is not an array: Provide a 'for' attribute with either an array or the name of a state property containing an array`);
721
+ return []
722
+ }
723
+ return state
724
+ },
725
+ set: (oldState, newState) => newState
726
+ };
727
+ preventStateUpdates = false;
728
+ } else if (typeof props.for === 'string') {
729
+ field = props.for;
730
+ lense = {
731
+ get: stateGetter,
732
+ set: (state, arr) => {
733
+ if (this.calculated && field in this.calculated) {
734
+ console.warn(`Collection sub-component of ${ this.name } attempted to update state on a calculated field '${ field }': Update ignored`);
735
+ return state
736
+ }
737
+ return { ...state, [field]: arr }
738
+ }
739
+ };
740
+ preventStateUpdates = false;
741
+ } else {
742
+ field = 'for';
743
+ stateSource = new StateSource(props$.remember());
744
+ lense = {
745
+ get: stateGetter,
746
+ set: (state, arr) => state
747
+ };
748
+ }
749
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$ };
750
+ const factory = typeof data.props.of === 'function' ? data.props.of : this.components[data.props.of];
751
+ sink$ = collection(factory, lense, { container: null })(sources);
752
+ if (typeof sink$ !== 'object') {
753
+ throw new Error('Invalid sinks returned from component factory of collection element')
754
+ }
755
+ } else if (isSwitchable) {
756
+ const stateField = data.props.state;
757
+ let isolateSwitchable = false;
758
+ let lense;
759
+ if (typeof stateField === 'string') {
760
+ isolateSwitchable = true;
761
+ lense = {
762
+ get: state => {
763
+ return state[stateField]
764
+ },
765
+ set: (oldState, newState) => {
766
+ if (this.calculated && stateField in this.calculated) {
767
+ console.warn(`Switchable sub-component of ${ this.name } attempted to update state on a calculated field '${ stateField }': Update ignored`);
768
+ return oldState
769
+ }
770
+ return { ...oldState, [stateField]: newState }
771
+ }
772
+ };
773
+ preventStateUpdates = false;
774
+ } else if (typeof stateField === 'undefined') {
775
+ isolateSwitchable = true;
776
+ lense = {
777
+ get: state => state,
778
+ set: (oldState, newState) => newState
779
+ };
780
+ preventStateUpdates = false;
781
+ } else if (typeof stateField === 'object') {
782
+ stateSource = new StateSource(props$.map(props => props.state));
783
+ } else {
784
+ throw new Error(`Invalid state provided to collection sub-component of ${ this.name }: Expecting string, object, or none, but found ${ typeof stateField }`)
785
+ }
786
+ const switchableComponents = data.props.of;
787
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$ };
788
+ if (isolateSwitchable) {
789
+ sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources);
790
+ } else {
791
+ sink$ = switchable(switchableComponents, props$.map(props => props.current))(sources);
792
+ }
793
+ if (typeof sink$ !== 'object') {
794
+ throw new Error('Invalid sinks returned from component factory of switchable element')
795
+ }
796
+ } else {
797
+ const { state: stateProp, sygnalFactory, id, ...sanitizedProps } = props;
798
+ if (typeof stateProp === 'undefined' && (typeof sanitizedProps !== 'object' || Object.keys(sanitizedProps).length === 0)) {
799
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$: xs.never().startWith(null), children$ };
800
+ sink$ = factory(sources);
801
+ preventStateUpdates = false;
802
+ } else {
803
+ const lense = (props) => {
804
+ const state = props.state;
805
+ if (typeof state === 'undefined') return props
806
+ if (typeof state !== 'object') return state
807
+
808
+ const copy = { ...props };
809
+ delete copy.state;
810
+ return { ...copy, ...state }
811
+ };
812
+ stateSource = new StateSource(props$.map(lense));
813
+ const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$ };
814
+ sink$ = factory(sources);
815
+ }
816
+ if (typeof sink$ !== 'object') {
817
+ const name = componentName === 'sygnal-factory' ? 'custom element' : componentName;
818
+ throw new Error('Invalid sinks returned from component factory:', name)
819
+ }
820
+ }
821
+
822
+ if (preventStateUpdates) {
823
+ const originalStateSink = sink$[this.stateSourceName];
824
+ sink$[this.stateSourceName] = originalStateSink.filter(state => {
825
+ console.warn('State update attempt from component with inderect link to state: Components with state set through HTML properties/attributes cannot update application state directly');
826
+ return false
827
+ });
828
+ }
829
+
830
+ const originalDOMSink = sink$[this.DOMSourceName].remember();
831
+ const repeatChecker = (a, b) => {
832
+ const aa = JSON.stringify(a);
833
+ const bb = JSON.stringify(b);
834
+ return aa === bb
835
+ };
836
+ sink$[this.DOMSourceName] = stateSource.stream.compose(dropRepeats$1(repeatChecker)).map(state => {
837
+ subComponentRenderedProxy$.shamefullySendNext(null);
838
+ return originalDOMSink
839
+ }).compose(debounce(10)).flatten().remember();
840
+ acc[id] = { sink$, props$, children$ };
841
+
842
+ Object.entries(sink$).map(([name, stream]) => {
843
+ sinkArrsByType[name] ||= [];
844
+ if (name !== this.DOMSourceName) sinkArrsByType[name].push(stream);
845
+ });
846
+
847
+ return acc
848
+ }, rootEntry);
849
+
850
+ const mergedSinksByType = Object.entries(sinkArrsByType).reduce((acc, [name, streamArr]) => {
851
+ if (streamArr.length === 0) return acc
852
+ acc[name] = streamArr.length === 1 ? streamArr[0] : xs.merge(...streamArr);
853
+ return acc
854
+ }, {});
855
+
856
+ this.newSubComponentSinks(mergedSinksByType);
857
+
858
+ // subComponentRenderedProxy$.shamefullySendNext(null)
859
+ return newComponents
860
+ }, {});
861
+
862
+
863
+ this.vdom$ = xs.combine(subComponentRenderedProxy$.startWith(null), componentInstances$).map(([_, components]) => {
864
+
865
+ const root = components['::ROOT::'];
866
+ let ids = [];
867
+ const entries = Object.entries(components).filter(([id]) => id !== '::ROOT::');
868
+
869
+ if (entries.length === 0) {
870
+ return xs.of(root)
871
+ }
872
+
873
+ const vdom$ = entries
874
+ .map(([id, val]) => {
875
+ ids.push(id);
876
+ return val.sink$[this.DOMSourceName].startWith(undefined)
877
+ });
878
+
879
+ if (vdom$.length === 0) return xs.of(root)
880
+
881
+ return xs.combine(...vdom$).compose(debounce(5)).map(vdoms => {
882
+ const withIds = vdoms.reduce((acc, vdom, index) => {
883
+ acc[ids[index]] = vdom;
884
+ return acc
885
+ }, {});
886
+ const rootCopy = deepCopyVdom(root);
887
+ const injected = injectComponents(rootCopy, withIds, componentNames);
888
+ return injected
889
+ })
890
+ })
891
+ .flatten()
892
+ .filter(val => !!val)
893
+ .compose(debounce(5))
894
+ .remember()
895
+ .compose(this.log('View Rendered'));
896
+ }
897
+
898
+ initSinks() {
899
+ this.sinks = this.sourceNames.reduce((acc, name) => {
900
+ if (name == this.DOMSourceName) return acc
901
+ const subComponentSink$ = this.subComponentSink$ ? this.subComponentSink$.map(sinks => sinks[name]).filter(sink => !!sink).flatten() : xs.never();
902
+ if (name === this.stateSourceName) {
903
+ acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, this.sources[this.stateSourceName].stream.filter(_ => false), ...this.children$[name]);
904
+ } else {
905
+ acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...this.children$[name]);
906
+ }
907
+ return acc
908
+ }, {});
909
+
910
+ this.sinks[this.DOMSourceName] = this.vdom$;
911
+ this.sinks[this.requestSourceName] = xs.merge(this.sendResponse$ ,this.sinks[this.requestSourceName]);
912
+ }
913
+
914
+ makeOnAction(action$, isStateSink=true, rootAction$) {
915
+ rootAction$ = rootAction$ || action$;
916
+ return (name, reducer) => {
917
+ const filtered$ = action$.filter(({type}) => type == name);
918
+
919
+ let returnStream$;
920
+ if (typeof reducer === 'function') {
921
+ returnStream$ = filtered$.map(action => {
922
+ const next = (type, data) => {
923
+ const _reqId = action._reqId || (action.req && action.req.id);
924
+ const _data = _reqId ? (typeof data == 'object' ? { ...data, _reqId, _action: name } : { data, _reqId, _action: name }) : data;
925
+ // put the "next" action request at the end of the event loop so the "current" action completes first
926
+ setTimeout(() => {
927
+ // push the "next" action request into the action$ stream
928
+ rootAction$.shamefullySendNext({ type, data: _data });
929
+ }, 10);
930
+ };
931
+
932
+ let data = action.data;
933
+ if (data && data.data && data._reqId) data = data.data;
934
+ if (isStateSink) {
935
+ return (state) => {
936
+ const _state = this.isSubComponent ? this.currentState : state;
937
+ const enhancedState = this.addCalculated(_state);
938
+ const newState = reducer(enhancedState, data, next, action.req);
939
+ if (newState == ABORT) return _state
940
+ return this.cleanupCalculated(newState)
941
+ }
942
+ } else {
943
+ const enhancedState = this.addCalculated(this.currentState);
944
+ const reduced = reducer(enhancedState, data, next, action.req);
945
+ const type = typeof reduced;
946
+ const _reqId = action._reqId || (action.req && action.req.id);
947
+ if (['string', 'number', 'boolean', 'function'].includes(type)) return reduced
948
+ if (type == 'object') return { ...reduced, _reqId, _action: name }
949
+ if (type == 'undefined') {
950
+ console.warn(`'undefined' value sent to ${ name }`);
951
+ return reduced
952
+ }
953
+ throw new Error(`Invalid reducer type for ${ name } ${ type }`)
954
+ }
955
+ }).filter(result => result != ABORT);
956
+ } else if (reducer === undefined || reducer === true) {
957
+ returnStream$ = filtered$.map(({data}) => data);
958
+ } else {
959
+ const value = reducer;
960
+ returnStream$ = filtered$.mapTo(value);
961
+ }
962
+
963
+ return returnStream$
964
+ }
965
+ }
966
+
967
+ addCalculated(state) {
968
+ if (!this.calculated || typeof state !== 'object') return state
969
+ if (typeof this.calculated !== 'object') throw new Error(`'calculated' parameter must be an object mapping calculated state field named to functions`)
970
+ const entries = Object.entries(this.calculated);
971
+ const calculated = entries.reduce((acc, [field, fn]) => {
972
+ if (typeof fn !== 'function') throw new Error(`Missing or invalid calculator function for calculated field '${ field }`)
973
+ try {
974
+ acc[field] = fn(state);
975
+ } catch(e) {
976
+ console.warn(`Calculated field '${ field }' threw an error during calculation: ${ e.message }`);
977
+ }
978
+ return acc
979
+ }, {});
980
+ return { ...state, ...calculated }
981
+ }
982
+
983
+ cleanupCalculated(state) {
984
+ if (this.storeCalculatedInState) return this.addCalculated(state)
985
+ if (!this.calculated || !state || typeof state !== 'object') return state
986
+ const keys = Object.keys(this.calculated);
987
+ const copy = { ...state };
988
+ keys.forEach(key => {
989
+ if (this.initialState && typeof this.initialState[key] !== 'undefined') {
990
+ copy[key] = this.initialState[key];
991
+ } else {
992
+ delete copy[key];
993
+ }
994
+ });
995
+ return copy
996
+ }
997
+
998
+ }
999
+
1000
+
1001
+
1002
+
1003
+
1004
+
1005
+
1006
+ /**
1007
+ * factory to create a logging function meant to be used inside of an xstream .compose()
1008
+ *
1009
+ * @param {String} context name of the component or file to be prepended to any messages
1010
+ * @return {Function}
1011
+ *
1012
+ * returned function accepts either a `String` of `Function`
1013
+ * `String` values will be logged to `console` as is
1014
+ * `Function` values will be called with the current `stream` value and the result will be logged to `console`
1015
+ * all output will be prepended with the `context` (ex. "[CONTEXT] My output")
1016
+ * ONLY outputs if the global `DEBUG` variable is set to `true`
1017
+ */
1018
+ function makeLog (context) {
1019
+ return function (msg) {
1020
+ const fixedMsg = (typeof msg === 'function') ? msg : _ => msg;
1021
+ return stream => {
1022
+ return stream.debug(msg => {
1023
+ if (ENVIRONMENT.DEBUG == 'true' || ENVIRONMENT.DEBUG === true) {
1024
+ console.log(`[${context}] ${fixedMsg(msg)}`);
1025
+ }
1026
+ })
1027
+ }
1028
+ }
1029
+ }
1030
+
1031
+
1032
+
1033
+ function getComponents(currentElement, componentNames, depth=0, index=0) {
1034
+ if (!currentElement) return {}
1035
+
1036
+ if (currentElement.data?.componentsProcessed) return {}
1037
+ if (depth === 0) currentElement.data.componentsProcessed = true;
1038
+
1039
+ const sel = currentElement.sel;
1040
+ const isCollection = sel && sel.toLowerCase() === 'collection';
1041
+ const isSwitchable = sel && sel.toLowerCase() === 'switchable';
1042
+ const isComponent = sel && (['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(currentElement.sel)) || typeof currentElement.data?.props?.sygnalFactory === 'function';
1043
+ const props = (currentElement.data && currentElement.data.props) || {};
1044
+ const attrs = (currentElement.data && currentElement.data.attrs) || {};
1045
+ const children = currentElement.children || [];
1046
+
1047
+ let found = {};
1048
+
1049
+ if (isComponent) {
1050
+ const id = getComponentIdFromElement(currentElement, depth, index);
1051
+ if (isCollection) {
1052
+ if (!props.of) throw new Error(`Collection element missing required 'component' property`)
1053
+ 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`)
1054
+ if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${ props.of }`)
1055
+ if (typeof attrs.for !== 'undefined' && !(typeof attrs.for === 'string' || Array.isArray(attrs.for))) console.warn(`No valid array found in the 'value' property of collection ${ typeof props.of === 'string' ? props.of : 'function component' }: no collection components will be created`);
1056
+ currentElement.data.isCollection = true;
1057
+ currentElement.data.props ||= {};
1058
+ currentElement.data.props.for = attrs.for;
1059
+ currentElement.data.attrs = undefined;
1060
+ } else if (isSwitchable) {
1061
+ if (!props.of) throw new Error(`Switchable element missing required 'of' property`)
1062
+ if (typeof props.of !== 'object') throw new Error(`Invalid 'components' property of switchable element: found ${ typeof props.of } requires object mapping names to component factories`)
1063
+ const switchableComponents = Object.values(props.of);
1064
+ if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
1065
+ 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`)
1066
+ const switchableComponentNames = Object.keys(props.of);
1067
+ if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${ props.current }' not found in switchable element`)
1068
+ currentElement.data.isSwitchable = true;
1069
+ } else ;
1070
+ found[id] = currentElement;
1071
+ }
1072
+
1073
+ if (children.length > 0) {
1074
+ children.map((child, i) => getComponents(child, componentNames, depth + 1, i))
1075
+ .forEach((child) => {
1076
+ Object.entries(child).forEach(([id, el]) => found[id] = el);
1077
+ });
1078
+ }
1079
+
1080
+ return found
1081
+ }
1082
+
1083
+ function injectComponents(currentElement, components, componentNames, depth=0, index) {
1084
+ if (!currentElement) return
1085
+ if (currentElement.data?.componentsInjected) return currentElement
1086
+ if (depth === 0 && currentElement.data) currentElement.data.componentsInjected = true;
1087
+
1088
+
1089
+ const sel = currentElement.sel || 'NO SELECTOR';
1090
+ const isComponent = ['collection', 'switchable', 'sygnal-factory', ...componentNames].includes(sel) || typeof currentElement.data?.props?.sygnalFactory === 'function';
1091
+ const isCollection = currentElement?.data?.isCollection;
1092
+ const isSwitchable = currentElement?.data?.isSwitchable;
1093
+ (currentElement.data && currentElement.data.props) || {};
1094
+ const children = currentElement.children || [];
1095
+
1096
+ if (isComponent) {
1097
+ const id = getComponentIdFromElement(currentElement, depth, index);
1098
+ const component = components[id];
1099
+ if (isCollection) {
1100
+ currentElement.sel = 'div';
1101
+ currentElement.children = Array.isArray(component) ? component : [component];
1102
+ return currentElement
1103
+ } else if (isSwitchable) {
1104
+ return component
1105
+ } else {
1106
+ return component
1107
+ }
1108
+ } else if (children.length > 0) {
1109
+ currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, depth + 1, i)).flat();
1110
+ return currentElement
1111
+ } else {
1112
+ return currentElement
1113
+ }
1114
+ }
1115
+
1116
+ const selMap = new Map();
1117
+ function getComponentIdFromElement(el, depth, index) {
1118
+ const sel = el.sel;
1119
+ const name = typeof sel === 'string' ? sel : 'functionComponent';
1120
+ let base = selMap.get(sel);
1121
+ if (!base) {
1122
+ const date = Date.now();
1123
+ const rand = Math.floor(Math.random() * 10000);
1124
+ base = `${date}-${rand}`;
1125
+ selMap.set(sel, base);
1126
+ }
1127
+ const uid = `${base}-${depth}-${index}`;
1128
+ const props = (el.data && el.data.props) || {};
1129
+ const id = (props.id && JSON.stringify(props.id)) || uid;
1130
+ const fullId = `${ name }::${ id }`;
1131
+ return fullId
1132
+ }
1133
+
1134
+
1135
+ function deepCopyVdom(obj) {
1136
+ if (typeof obj === 'undefined') return obj
1137
+ return { ...obj, children: Array.isArray(obj.children) ? obj.children.map(deepCopyVdom) : undefined, data: obj.data && { ...obj.data, componentsInjected: false } }
1138
+ }
1139
+
1140
+ function processForm(form, options={}) {
1141
+ let { events = ['input', 'submit'], preventDefault = true } = options;
1142
+ if (typeof events === 'string') events = [events];
1143
+
1144
+ const eventStream$ = events.map(event => form.events(event));
1145
+
1146
+ const merged$ = xs.merge(...eventStream$);
1147
+
1148
+ return merged$.map((e) => {
1149
+ if (preventDefault) e.preventDefault();
1150
+ const form = (e.type === 'submit') ? e.srcElement : e.currentTarget;
1151
+ const formData = new FormData(form);
1152
+ let entries = {};
1153
+ entries.event = e;
1154
+ entries.eventType = e.type;
1155
+ const submitBtn = form.querySelector('input[type=submit]:focus');
1156
+ if (submitBtn) {
1157
+ const { name, value } = submitBtn;
1158
+ entries[name || 'submit'] = value;
1159
+ }
1160
+ for (let [name, value] of formData.entries()) {
1161
+ entries[name] = value;
1162
+ }
1163
+ return entries
1164
+ })
1165
+ }
1166
+
1167
+ function eventBusDriver(out$) {
1168
+ const events = new EventTarget();
1169
+
1170
+ out$.subscribe({
1171
+ next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
1172
+ });
1173
+
1174
+ return {
1175
+ select: (type) => {
1176
+ const all = !type;
1177
+ const _type = (Array.isArray(type)) ? type : [type];
1178
+ let cb;
1179
+ const in$ = xs.create({
1180
+ start: (listener) => {
1181
+ cb = ({detail: event}) => {
1182
+ const data = (event && event.data) || null;
1183
+ if (all || _type.includes(event.type)) listener.next(data);
1184
+ };
1185
+ events.addEventListener('data', cb);
1186
+ },
1187
+ stop: _ => events.removeEventListener('data', cb)
1188
+ });
1189
+
1190
+ return adapt(in$)
1191
+ }
1192
+ }
1193
+ }
1194
+
1195
+ function logDriver(out$) {
1196
+ out$.addListener({
1197
+ next: (val) => {
1198
+ console.log(val);
1199
+ }
1200
+ });
1201
+ }
1202
+
1203
+ function run(app, drivers={}, options={}) {
1204
+ const { mountPoint='#root', fragments=true } = options;
1205
+
1206
+ const wrapped = withState(app, 'STATE');
1207
+
1208
+ const baseDrivers = {
1209
+ EVENTS: eventBusDriver,
1210
+ DOM: makeDOMDriver(mountPoint, { snabbdomOptions: { experimental: { fragments } } }),
1211
+ LOG: logDriver
1212
+ };
1213
+
1214
+ const combinedDrivers = { ...baseDrivers, ...drivers };
1215
+
1216
+ return run$1(wrapped, combinedDrivers)
1217
+ }
1218
+
1219
+ /**
1220
+ * return a validated and properly separated string of CSS class names from any number of strings, arrays, and objects
1221
+ *
1222
+ * @param {...String|Array|Object} args any number of strings or arrays with valid CSS class names, or objects where the keys are valid class names and the values evaluate to true or false
1223
+ * @return {String} list of `active` classes separated by spaces
1224
+ *
1225
+ * any `string` or `array` arguments are simply validated and appended to the result
1226
+ * `objects` will evaluate the values (which can be booleans or functions), and the keys with `thruthy` values will be validated and appended to the result
1227
+ * this function makes it easier to set dynamic classes on HTML elements
1228
+ */
1229
+ function classes(...args) {
1230
+ return args.reduce((acc, arg) => {
1231
+ if (typeof arg === 'string' && !acc.includes(arg)) {
1232
+ acc.push(...classes_processString(arg));
1233
+ } else if (Array.isArray(arg)) {
1234
+ acc.push(...classes_processArray(arg));
1235
+ } else if (typeof arg === 'object') {
1236
+ acc.push(...classes_processObject(arg));
1237
+ }
1238
+ return acc
1239
+ }, []).join(' ')
1240
+ }
1241
+
1242
+
1243
+
1244
+ /**
1245
+ * validate a string as a CSS class name
1246
+ *
1247
+ * @param {String} className CSS class name to validate
1248
+ * @return {Boolean} true if the name is a valid CSS class, false otherwise
1249
+ */
1250
+ function isValidClassName (className) {
1251
+ return /^[a-zA-Z0-9-_]+$/.test(className)
1252
+ }
1253
+
1254
+ /**
1255
+ * find and validate CSS class names in a string
1256
+ *
1257
+ * @param {String} str string containing one or more CSS class names
1258
+ * @return {Array} valid CSS classnames from the provided string
1259
+ */
1260
+ function classes_processString(str) {
1261
+ if (typeof str !== 'string') throw new Error('Class name must be a string')
1262
+ return str.trim().split(' ').reduce((acc, item) => {
1263
+ if (item.trim().length === 0) return acc
1264
+ if (!isValidClassName(item)) throw new Error(`${item} is not a valid CSS class name`)
1265
+ acc.push(item);
1266
+ return acc
1267
+ }, [])
1268
+ }
1269
+
1270
+ /**
1271
+ * find and validate CSS class names in an array of strings
1272
+ *
1273
+ * @param {Array} arr array containing one or more strings with valid CSS class names
1274
+ * @return {Array} valid CSS class names from the provided array
1275
+ */
1276
+ function classes_processArray(arr) {
1277
+ return arr.map(classes_processString).flat()
1278
+ }
1279
+
1280
+ /**
1281
+ * find and validate CSS class names in an object, and exclude keys whose value evaluates to `false`
1282
+ *
1283
+ * @param {Object} obj object with keys as CSS class names and values which if `truthy` cause the associated key to be returned
1284
+ * @return {Array} valid CSS class names from the keys of the provided object where the associated value evaluated to `true`
1285
+ *
1286
+ * the value for each key can be either a value that evaluates to a boolean or a function that returns a boolean
1287
+ * if the value is a function, it will be run and the returned value will be used
1288
+ */
1289
+ function classes_processObject(obj) {
1290
+ const ret = Object.entries(obj)
1291
+ .filter(([key, predicate]) => (typeof predicate === 'function') ? predicate() : !!predicate)
1292
+ .map(([key, _]) => {
1293
+ const trimmed = key.trim();
1294
+ if (!isValidClassName(trimmed)) throw new Error (`${trimmed} is not a valid CSS class name`)
1295
+ return trimmed
1296
+ });
1297
+ return ret
1298
+ }
1299
+
1300
+ export { ABORT, classes, collection, component, processForm, run, switchable };