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