sygnal 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/astro/client.cjs.js +547 -82
- package/dist/astro/client.mjs +547 -82
- package/dist/index.cjs.js +569 -94
- package/dist/index.esm.js +569 -95
- package/dist/jsx-dev-runtime.cjs.js +49 -12
- package/dist/jsx-dev-runtime.esm.js +49 -12
- package/dist/jsx-runtime.cjs.js +49 -12
- package/dist/jsx-runtime.esm.js +49 -12
- package/dist/jsx.cjs.js +49 -12
- package/dist/jsx.esm.js +49 -12
- package/dist/sygnal.min.js +1 -1
- package/package.json +6 -3
- package/src/collection.js +5 -2
- package/src/component.js +278 -79
- package/src/extra/devtools.js +249 -0
- package/src/extra/driverFactories.js +12 -12
- package/src/extra/eventDriver.js +2 -1
- package/src/extra/logDriver.js +3 -0
- package/src/extra/processDrag.js +6 -0
- package/src/extra/processForm.js +3 -0
- package/src/extra/run.js +12 -0
- package/src/index.d.ts +8 -4
- package/src/index.js +1 -0
- package/src/pragma/index.js +21 -9
- package/src/pragma/is.js +27 -2
- package/src/switchable.js +1 -1
package/src/component.js
CHANGED
|
@@ -42,13 +42,27 @@ function wrapDOMSource(domSource) {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
export const ABORT = '
|
|
45
|
+
export const ABORT = Symbol('ABORT')
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
function normalizeCalculatedEntry(field, entry) {
|
|
49
|
+
if (typeof entry === 'function') {
|
|
50
|
+
return { fn: entry, deps: null }
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(entry) && entry.length === 2
|
|
53
|
+
&& Array.isArray(entry[0]) && typeof entry[1] === 'function') {
|
|
54
|
+
return { fn: entry[1], deps: entry[0] }
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Invalid calculated field '${field}': expected a function or [depsArray, function]`
|
|
58
|
+
)
|
|
59
|
+
}
|
|
46
60
|
|
|
47
61
|
export default function component (opts) {
|
|
48
62
|
const { name, sources, isolateOpts, stateSourceName='STATE' } = opts
|
|
49
63
|
|
|
50
64
|
if (sources && !isObj(sources)) {
|
|
51
|
-
throw new Error(
|
|
65
|
+
throw new Error(`[${name}] Sources must be a Cycle.js sources object`)
|
|
52
66
|
}
|
|
53
67
|
|
|
54
68
|
let fixedIsolateOpts
|
|
@@ -128,7 +142,9 @@ class Component {
|
|
|
128
142
|
// sinks
|
|
129
143
|
|
|
130
144
|
constructor({ name='NO NAME', sources, intent, model, hmrActions, context, response, view, peers={}, components={}, initialState, calculated, storeCalculatedInState=true, DOMSourceName='DOM', stateSourceName='STATE', requestSourceName='HTTP', debug=false }) {
|
|
131
|
-
if (!sources || !isObj(sources)) throw new Error(
|
|
145
|
+
if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
|
|
146
|
+
|
|
147
|
+
this._componentNumber = COMPONENT_COUNT++
|
|
132
148
|
|
|
133
149
|
this.name = name
|
|
134
150
|
this.sources = sources
|
|
@@ -149,6 +165,123 @@ class Component {
|
|
|
149
165
|
this.sourceNames = Object.keys(sources)
|
|
150
166
|
this._debug = debug
|
|
151
167
|
|
|
168
|
+
// Warn if calculated fields shadow base state keys
|
|
169
|
+
if (this.calculated && this.initialState
|
|
170
|
+
&& isObj(this.calculated) && isObj(this.initialState)) {
|
|
171
|
+
for (const key of Object.keys(this.calculated)) {
|
|
172
|
+
if (key in this.initialState) {
|
|
173
|
+
console.warn(
|
|
174
|
+
`[${name}] Calculated field '${key}' shadows a key in initialState. ` +
|
|
175
|
+
`The initialState value will be overwritten on every state update.`
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Normalize calculated entries, build dependency graph, topological sort
|
|
182
|
+
if (this.calculated && isObj(this.calculated)) {
|
|
183
|
+
const calcEntries = Object.entries(this.calculated)
|
|
184
|
+
|
|
185
|
+
// Normalize all entries to { fn, deps } shape
|
|
186
|
+
this._calculatedNormalized = {}
|
|
187
|
+
for (const [field, entry] of calcEntries) {
|
|
188
|
+
this._calculatedNormalized[field] = normalizeCalculatedEntry(field, entry)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
this._calculatedFieldNames = new Set(Object.keys(this._calculatedNormalized))
|
|
192
|
+
|
|
193
|
+
// Warn on deps referencing nonexistent keys
|
|
194
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
195
|
+
if (deps !== null) {
|
|
196
|
+
for (const dep of deps) {
|
|
197
|
+
if (!this._calculatedFieldNames.has(dep)
|
|
198
|
+
&& this.initialState && !(dep in this.initialState)) {
|
|
199
|
+
console.warn(
|
|
200
|
+
`[${name}] Calculated field '${field}' declares dependency '${dep}' ` +
|
|
201
|
+
`which is not in initialState or calculated fields`
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Build adjacency: for each field, which other calculated fields must run first?
|
|
209
|
+
const calcDeps = {}
|
|
210
|
+
for (const [field, { deps }] of Object.entries(this._calculatedNormalized)) {
|
|
211
|
+
if (deps === null) {
|
|
212
|
+
calcDeps[field] = []
|
|
213
|
+
} else {
|
|
214
|
+
calcDeps[field] = deps.filter(d => this._calculatedFieldNames.has(d))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Kahn's algorithm for topological sort
|
|
219
|
+
const inDegree = {}
|
|
220
|
+
const reverseGraph = {}
|
|
221
|
+
for (const field of this._calculatedFieldNames) {
|
|
222
|
+
inDegree[field] = 0
|
|
223
|
+
reverseGraph[field] = []
|
|
224
|
+
}
|
|
225
|
+
for (const [field, depList] of Object.entries(calcDeps)) {
|
|
226
|
+
inDegree[field] = depList.length
|
|
227
|
+
for (const dep of depList) {
|
|
228
|
+
reverseGraph[dep].push(field)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const queue = []
|
|
233
|
+
for (const [field, degree] of Object.entries(inDegree)) {
|
|
234
|
+
if (degree === 0) queue.push(field)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const sorted = []
|
|
238
|
+
while (queue.length > 0) {
|
|
239
|
+
const current = queue.shift()
|
|
240
|
+
sorted.push(current)
|
|
241
|
+
for (const dependent of reverseGraph[current]) {
|
|
242
|
+
inDegree[dependent]--
|
|
243
|
+
if (inDegree[dependent] === 0) queue.push(dependent)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (sorted.length !== this._calculatedFieldNames.size) {
|
|
248
|
+
// Cycle detected — build error message with cycle path
|
|
249
|
+
const inCycle = [...this._calculatedFieldNames].filter(f => !sorted.includes(f))
|
|
250
|
+
const visited = new Set()
|
|
251
|
+
const path = []
|
|
252
|
+
const traceCycle = (node) => {
|
|
253
|
+
if (visited.has(node)) { path.push(node); return true }
|
|
254
|
+
visited.add(node)
|
|
255
|
+
path.push(node)
|
|
256
|
+
for (const dep of calcDeps[node]) {
|
|
257
|
+
if (inCycle.includes(dep) && traceCycle(dep)) return true
|
|
258
|
+
}
|
|
259
|
+
path.pop()
|
|
260
|
+
visited.delete(node)
|
|
261
|
+
return false
|
|
262
|
+
}
|
|
263
|
+
traceCycle(inCycle[0])
|
|
264
|
+
const start = path[path.length - 1]
|
|
265
|
+
const cycle = path.slice(path.indexOf(start))
|
|
266
|
+
throw new Error(`Circular calculated dependency: ${cycle.join(' \u2192 ')}`)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
this._calculatedOrder = sorted.map(f => [f, this._calculatedNormalized[f]])
|
|
270
|
+
|
|
271
|
+
// Initialize per-field memoization caches for fields with declared deps
|
|
272
|
+
this._calculatedFieldCache = {}
|
|
273
|
+
for (const [field, { deps }] of this._calculatedOrder) {
|
|
274
|
+
if (deps !== null) {
|
|
275
|
+
this._calculatedFieldCache[field] = { lastDepValues: undefined, lastResult: undefined }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
this._calculatedOrder = null
|
|
280
|
+
this._calculatedNormalized = null
|
|
281
|
+
this._calculatedFieldNames = null
|
|
282
|
+
this._calculatedFieldCache = null
|
|
283
|
+
}
|
|
284
|
+
|
|
152
285
|
this.isSubComponent = this.sourceNames.includes('props$')
|
|
153
286
|
|
|
154
287
|
const state$ = sources[stateSourceName] && sources[stateSourceName].stream
|
|
@@ -157,6 +290,9 @@ class Component {
|
|
|
157
290
|
this.currentState = initialState || {}
|
|
158
291
|
this.sources[stateSourceName] = new StateSource(state$.map(val => {
|
|
159
292
|
this.currentState = val
|
|
293
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
294
|
+
window.__SYGNAL_DEVTOOLS__.onStateChanged(this._componentNumber, this.name, val)
|
|
295
|
+
}
|
|
160
296
|
return val
|
|
161
297
|
}))
|
|
162
298
|
}
|
|
@@ -192,10 +328,8 @@ class Component {
|
|
|
192
328
|
}
|
|
193
329
|
}
|
|
194
330
|
|
|
195
|
-
const componentNumber = COMPONENT_COUNT++
|
|
196
|
-
|
|
197
331
|
this.addCalculated = this.createMemoizedAddCalculated()
|
|
198
|
-
this.log = makeLog(`${
|
|
332
|
+
this.log = makeLog(`${this._componentNumber} | ${name}`)
|
|
199
333
|
|
|
200
334
|
this.initChildSources$()
|
|
201
335
|
this.initIntent$()
|
|
@@ -210,9 +344,20 @@ class Component {
|
|
|
210
344
|
this.initVdom$()
|
|
211
345
|
this.initSinks()
|
|
212
346
|
|
|
213
|
-
this.sinks.__index =
|
|
347
|
+
this.sinks.__index = this._componentNumber
|
|
214
348
|
|
|
215
349
|
this.log(`Instantiated`, true)
|
|
350
|
+
|
|
351
|
+
// Hook 1: Register with DevTools
|
|
352
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__) {
|
|
353
|
+
window.__SYGNAL_DEVTOOLS__.onComponentCreated(this._componentNumber, name, this)
|
|
354
|
+
|
|
355
|
+
// Hook 1b: Register parent-child relationship
|
|
356
|
+
const parentNum = sources?.__parentComponentNumber
|
|
357
|
+
if (typeof parentNum === 'number') {
|
|
358
|
+
window.__SYGNAL_DEVTOOLS__.onSubComponentRegistered(parentNum, this._componentNumber)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
216
361
|
}
|
|
217
362
|
|
|
218
363
|
get debug() {
|
|
@@ -224,13 +369,13 @@ class Component {
|
|
|
224
369
|
return
|
|
225
370
|
}
|
|
226
371
|
if (typeof this.intent != 'function') {
|
|
227
|
-
throw new Error(
|
|
372
|
+
throw new Error(`[${this.name}] Intent must be a function`)
|
|
228
373
|
}
|
|
229
374
|
|
|
230
375
|
this.intent$ = this.intent(this.sources)
|
|
231
376
|
|
|
232
377
|
if (!(this.intent$ instanceof Stream) && (!isObj(this.intent$))) {
|
|
233
|
-
throw new Error(
|
|
378
|
+
throw new Error(`[${this.name}] Intent must return either an action$ stream or map of event streams`)
|
|
234
379
|
}
|
|
235
380
|
}
|
|
236
381
|
|
|
@@ -243,10 +388,10 @@ class Component {
|
|
|
243
388
|
this.hmrActions = [this.hmrActions]
|
|
244
389
|
}
|
|
245
390
|
if (!Array.isArray(this.hmrActions)) {
|
|
246
|
-
throw new Error(`[${
|
|
391
|
+
throw new Error(`[${this.name}] hmrActions must be the name of an action or an array of names of actions to run when a component is hot-reloaded`)
|
|
247
392
|
}
|
|
248
393
|
if (this.hmrActions.some(action => typeof action !== 'string')) {
|
|
249
|
-
throw new Error(`[${
|
|
394
|
+
throw new Error(`[${this.name}] hmrActions must be the name of an action or an array of names of actions to run when a component is hot-reloaded`)
|
|
250
395
|
}
|
|
251
396
|
this.hmrAction$ = xs.fromArray(this.hmrActions.map(action => ({ type: action })))
|
|
252
397
|
}
|
|
@@ -285,7 +430,15 @@ class Component {
|
|
|
285
430
|
const hydrate$ = initialApiData.map(data => ({ type: HYDRATE_ACTION, data }))
|
|
286
431
|
|
|
287
432
|
this.action$ = xs.merge(wrapped$, hydrate$)
|
|
288
|
-
.compose(this.log(({ type }) => `<${
|
|
433
|
+
.compose(this.log(({ type }) => `<${type}> Action triggered`))
|
|
434
|
+
.map(action => {
|
|
435
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
436
|
+
window.__SYGNAL_DEVTOOLS__.onActionDispatched(
|
|
437
|
+
this._componentNumber, this.name, action.type, action.data
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
return action
|
|
441
|
+
})
|
|
289
442
|
}
|
|
290
443
|
|
|
291
444
|
initState() {
|
|
@@ -297,7 +450,7 @@ class Component {
|
|
|
297
450
|
} else if (isObj(this.model[INITIALIZE_ACTION])) {
|
|
298
451
|
Object.keys(this.model[INITIALIZE_ACTION]).forEach(name => {
|
|
299
452
|
if (name !== this.stateSourceName) {
|
|
300
|
-
console.warn(`${
|
|
453
|
+
console.warn(`${INITIALIZE_ACTION} can only be used with the ${this.stateSourceName} source... disregarding ${name}`)
|
|
301
454
|
delete this.model[INITIALIZE_ACTION][name]
|
|
302
455
|
}
|
|
303
456
|
})
|
|
@@ -332,7 +485,7 @@ class Component {
|
|
|
332
485
|
} else if (valueType === 'function') {
|
|
333
486
|
_value = value(state)
|
|
334
487
|
} else {
|
|
335
|
-
console.error(`[${
|
|
488
|
+
console.error(`[${this.name}] Invalid context entry '${name}': must be the name of a state property or a function returning a value to use`)
|
|
336
489
|
return acc
|
|
337
490
|
}
|
|
338
491
|
acc[name] = _value
|
|
@@ -340,11 +493,14 @@ class Component {
|
|
|
340
493
|
}, {})
|
|
341
494
|
const newContext = { ..._parent, ...values }
|
|
342
495
|
this.currentContext = newContext
|
|
496
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
497
|
+
window.__SYGNAL_DEVTOOLS__.onContextChanged(this._componentNumber, this.name, newContext)
|
|
498
|
+
}
|
|
343
499
|
return newContext
|
|
344
500
|
})
|
|
345
501
|
.compose(dropRepeats(objIsEqual))
|
|
346
502
|
.startWith({})
|
|
347
|
-
this.context$.subscribe({ next: _ => _ })
|
|
503
|
+
this.context$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in context stream:`, err) })
|
|
348
504
|
}
|
|
349
505
|
|
|
350
506
|
initModel$() {
|
|
@@ -360,7 +516,7 @@ class Component {
|
|
|
360
516
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
|
|
361
517
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
|
|
362
518
|
if (this.isSubComponent && this.initialState) {
|
|
363
|
-
console.warn(`[${
|
|
519
|
+
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`)
|
|
364
520
|
}
|
|
365
521
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined')
|
|
366
522
|
const shouldInjectInitialState = hasInitialState && (ENVIRONMENT?.__SYGNAL_HMR_UPDATING !== true || typeof hmrState !== 'undefined')
|
|
@@ -381,7 +537,7 @@ class Component {
|
|
|
381
537
|
}
|
|
382
538
|
|
|
383
539
|
if (!isObj(sinks)) {
|
|
384
|
-
throw new Error(`Entry for each action must be an object: ${
|
|
540
|
+
throw new Error(`[${this.name}] Entry for each action must be an object: ${action}`)
|
|
385
541
|
}
|
|
386
542
|
|
|
387
543
|
const sinkEntries = Object.entries(sinks)
|
|
@@ -398,12 +554,12 @@ class Component {
|
|
|
398
554
|
const wrapped$ = on$
|
|
399
555
|
.compose(this.log(data => {
|
|
400
556
|
if (isStateSink) {
|
|
401
|
-
return `<${
|
|
557
|
+
return `<${action}> State reducer added`
|
|
402
558
|
} else if (isParentSink) {
|
|
403
|
-
return `<${
|
|
559
|
+
return `<${action}> Data sent to parent component: ${JSON.stringify(data.value).replaceAll('"', '')}`
|
|
404
560
|
} else {
|
|
405
561
|
const extra = data && (data.type || data.command || data.name || data.key || (Array.isArray(data) && 'Array') || data)
|
|
406
|
-
return `<${
|
|
562
|
+
return `<${action}> Data sent to [${sink}]: ${JSON.stringify(extra).replaceAll('"', '')}`
|
|
407
563
|
}
|
|
408
564
|
}))
|
|
409
565
|
|
|
@@ -481,7 +637,7 @@ class Component {
|
|
|
481
637
|
|
|
482
638
|
}
|
|
483
639
|
})
|
|
484
|
-
subComponentSink$.subscribe({ next: _ => _ })
|
|
640
|
+
subComponentSink$.subscribe({ next: _ => _, error: err => console.error(`[${this.name}] Error in sub-component sink stream:`, err) })
|
|
485
641
|
this.subComponentSink$ = subComponentSink$.filter(sinks => Object.keys(sinks).length > 0)
|
|
486
642
|
}
|
|
487
643
|
|
|
@@ -544,13 +700,13 @@ class Component {
|
|
|
544
700
|
if (typeof reducer === 'function') {
|
|
545
701
|
returnStream$ = filtered$.map(action => {
|
|
546
702
|
const next = (type, data, delay=10) => {
|
|
547
|
-
if (typeof delay !== 'number') throw new Error(`[${
|
|
703
|
+
if (typeof delay !== 'number') throw new Error(`[${this.name}] Invalid delay value provided to next() function in model action '${name}'. Must be a number in ms.`)
|
|
548
704
|
// put the "next" action request at the end of the event loop so the "current" action completes first
|
|
549
705
|
setTimeout(() => {
|
|
550
706
|
// push the "next" action request into the action$ stream
|
|
551
707
|
rootAction$.shamefullySendNext({ type, data })
|
|
552
708
|
}, delay)
|
|
553
|
-
this.log(`<${
|
|
709
|
+
this.log(`<${name}> Triggered a next() action: <${type}> ${delay}ms delay`, true)
|
|
554
710
|
}
|
|
555
711
|
|
|
556
712
|
const props = { ...this.currentProps, children: this.currentChildren, context: this.currentContext }
|
|
@@ -562,7 +718,7 @@ class Component {
|
|
|
562
718
|
const enhancedState = this.addCalculated(_state)
|
|
563
719
|
props.state = enhancedState
|
|
564
720
|
const newState = reducer(enhancedState, data, next, props)
|
|
565
|
-
if (newState
|
|
721
|
+
if (newState === ABORT) return _state
|
|
566
722
|
return this.cleanupCalculated(newState)
|
|
567
723
|
}
|
|
568
724
|
} else {
|
|
@@ -571,13 +727,13 @@ class Component {
|
|
|
571
727
|
const reduced = reducer(enhancedState, data, next, props)
|
|
572
728
|
const type = typeof reduced
|
|
573
729
|
if (isObj(reduced) || ['string', 'number', 'boolean', 'function'].includes(type)) return reduced
|
|
574
|
-
if (type
|
|
575
|
-
console.warn(`'undefined' value sent to ${
|
|
730
|
+
if (type === 'undefined') {
|
|
731
|
+
console.warn(`[${this.name}] 'undefined' value sent to ${name}`)
|
|
576
732
|
return reduced
|
|
577
733
|
}
|
|
578
|
-
throw new Error(`Invalid reducer type for ${
|
|
734
|
+
throw new Error(`[${this.name}] Invalid reducer type for action '${name}': ${type}`)
|
|
579
735
|
}
|
|
580
|
-
}).filter(result => result
|
|
736
|
+
}).filter(result => result !== ABORT)
|
|
581
737
|
} else if (reducer === undefined || reducer === true) {
|
|
582
738
|
returnStream$ = filtered$.map(({data}) => data)
|
|
583
739
|
} else {
|
|
@@ -598,7 +754,7 @@ class Component {
|
|
|
598
754
|
if (state === lastState) {
|
|
599
755
|
return lastResult
|
|
600
756
|
}
|
|
601
|
-
if (!isObj(this.calculated)) throw new Error(`'calculated' parameter must be an object mapping calculated state field
|
|
757
|
+
if (!isObj(this.calculated)) throw new Error(`[${this.name}] 'calculated' parameter must be an object mapping calculated state field names to functions`)
|
|
602
758
|
|
|
603
759
|
const calculated = this.getCalculatedValues(state)
|
|
604
760
|
if (!calculated) {
|
|
@@ -617,19 +773,55 @@ class Component {
|
|
|
617
773
|
}
|
|
618
774
|
|
|
619
775
|
getCalculatedValues(state) {
|
|
620
|
-
|
|
621
|
-
if (entries.length === 0) {
|
|
776
|
+
if (!this._calculatedOrder || this._calculatedOrder.length === 0) {
|
|
622
777
|
return
|
|
623
778
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
779
|
+
|
|
780
|
+
const mergedState = { ...state }
|
|
781
|
+
const computedSoFar = {}
|
|
782
|
+
|
|
783
|
+
for (const [field, { fn, deps }] of this._calculatedOrder) {
|
|
784
|
+
if (deps !== null && this._calculatedFieldCache) {
|
|
785
|
+
const cache = this._calculatedFieldCache[field]
|
|
786
|
+
const currentDepValues = deps.map(d => mergedState[d])
|
|
787
|
+
|
|
788
|
+
if (cache.lastDepValues !== undefined) {
|
|
789
|
+
let unchanged = true
|
|
790
|
+
for (let i = 0; i < currentDepValues.length; i++) {
|
|
791
|
+
if (currentDepValues[i] !== cache.lastDepValues[i]) {
|
|
792
|
+
unchanged = false
|
|
793
|
+
break
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
if (unchanged) {
|
|
797
|
+
computedSoFar[field] = cache.lastResult
|
|
798
|
+
mergedState[field] = cache.lastResult
|
|
799
|
+
continue
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
try {
|
|
804
|
+
const result = fn(mergedState)
|
|
805
|
+
cache.lastDepValues = currentDepValues
|
|
806
|
+
cache.lastResult = result
|
|
807
|
+
computedSoFar[field] = result
|
|
808
|
+
mergedState[field] = result
|
|
809
|
+
} catch (e) {
|
|
810
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`)
|
|
811
|
+
}
|
|
812
|
+
} else {
|
|
813
|
+
// No deps declared — always recompute
|
|
814
|
+
try {
|
|
815
|
+
const result = fn(mergedState)
|
|
816
|
+
computedSoFar[field] = result
|
|
817
|
+
mergedState[field] = result
|
|
818
|
+
} catch (e) {
|
|
819
|
+
console.warn(`Calculated field '${field}' threw an error during calculation: ${e.message}`)
|
|
820
|
+
}
|
|
630
821
|
}
|
|
631
|
-
|
|
632
|
-
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return computedSoFar
|
|
633
825
|
}
|
|
634
826
|
|
|
635
827
|
cleanupCalculated(incomingState) {
|
|
@@ -777,7 +969,7 @@ class Component {
|
|
|
777
969
|
this.newChildSources(childSources)
|
|
778
970
|
|
|
779
971
|
|
|
780
|
-
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${
|
|
972
|
+
if (newInstanceCount > 0) this.log(`New sub components instantiated: ${newInstanceCount}`, true)
|
|
781
973
|
|
|
782
974
|
return newComponents
|
|
783
975
|
}, {})
|
|
@@ -843,7 +1035,7 @@ class Component {
|
|
|
843
1035
|
} else if (this.components[collectionOf]) {
|
|
844
1036
|
factory = this.components[collectionOf]
|
|
845
1037
|
} else {
|
|
846
|
-
throw new Error(`[${this.name}] Invalid 'of'
|
|
1038
|
+
throw new Error(`[${this.name}] Invalid 'of' property in collection: ${collectionOf}`)
|
|
847
1039
|
}
|
|
848
1040
|
|
|
849
1041
|
const fieldLense = {
|
|
@@ -851,7 +1043,7 @@ class Component {
|
|
|
851
1043
|
if (!Array.isArray(state[stateField])) return []
|
|
852
1044
|
const items = state[stateField]
|
|
853
1045
|
const filtered = typeof arrayOperators.filter === 'function' ? items.filter(arrayOperators.filter) : items
|
|
854
|
-
const sorted = typeof arrayOperators.sort ? filtered.sort(arrayOperators.sort) : filtered
|
|
1046
|
+
const sorted = typeof arrayOperators.sort === 'function' ? filtered.sort(arrayOperators.sort) : filtered
|
|
855
1047
|
const mapped = sorted.map((item, index) => {
|
|
856
1048
|
return (isObj(item)) ? { ...item, [idField]: item[idField] || index } : { value: item, [idField]: index }
|
|
857
1049
|
})
|
|
@@ -860,7 +1052,7 @@ class Component {
|
|
|
860
1052
|
},
|
|
861
1053
|
set: (oldState, newState) => {
|
|
862
1054
|
if (this.calculated && stateField in this.calculated) {
|
|
863
|
-
console.warn(`Collection sub-component of ${
|
|
1055
|
+
console.warn(`Collection sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`)
|
|
864
1056
|
return oldState
|
|
865
1057
|
}
|
|
866
1058
|
const updated = []
|
|
@@ -889,17 +1081,17 @@ class Component {
|
|
|
889
1081
|
} else if (typeof stateField === 'string') {
|
|
890
1082
|
if (isObj(this.currentState)) {
|
|
891
1083
|
if(!(this.currentState && stateField in this.currentState) && !(this.calculated && stateField in this.calculated)) {
|
|
892
|
-
console.error(`Collection component in ${
|
|
1084
|
+
console.error(`Collection component in ${this.name} is attempting to use non-existent state property '${stateField}': To fix this error, specify a valid array property on the state. Attempting to use parent component state.`)
|
|
893
1085
|
lense = undefined
|
|
894
1086
|
} else if (!Array.isArray(this.currentState[stateField])) {
|
|
895
|
-
console.warn(`State property '${
|
|
1087
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`)
|
|
896
1088
|
lense = fieldLense
|
|
897
1089
|
} else {
|
|
898
1090
|
lense = fieldLense
|
|
899
1091
|
}
|
|
900
1092
|
} else {
|
|
901
1093
|
if (!Array.isArray(this.currentState[stateField])) {
|
|
902
|
-
console.warn(`State property '${
|
|
1094
|
+
console.warn(`[${this.name}] State property '${stateField}' in collection component is not an array: No components will be instantiated in the collection.`)
|
|
903
1095
|
lense = fieldLense
|
|
904
1096
|
} else {
|
|
905
1097
|
lense = fieldLense
|
|
@@ -907,14 +1099,14 @@ class Component {
|
|
|
907
1099
|
}
|
|
908
1100
|
} else if (isObj(stateField)) {
|
|
909
1101
|
if (typeof stateField.get !== 'function') {
|
|
910
|
-
console.error(`Collection component in ${
|
|
1102
|
+
console.error(`Collection component in ${this.name} has an invalid 'from' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting child state from the current state. Attempting to use parent component state.`)
|
|
911
1103
|
lense = undefined
|
|
912
1104
|
} else {
|
|
913
1105
|
lense = {
|
|
914
1106
|
get: (state) => {
|
|
915
1107
|
const newState = stateField.get(state)
|
|
916
1108
|
if (!Array.isArray(newState)) {
|
|
917
|
-
console.warn(`State getter function in collection component of ${
|
|
1109
|
+
console.warn(`State getter function in collection component of ${this.name} did not return an array: No components will be instantiated in the collection. Returned value:`, newState)
|
|
918
1110
|
return []
|
|
919
1111
|
}
|
|
920
1112
|
return newState
|
|
@@ -923,14 +1115,14 @@ class Component {
|
|
|
923
1115
|
}
|
|
924
1116
|
}
|
|
925
1117
|
} else {
|
|
926
|
-
console.error(`Collection component in ${
|
|
1118
|
+
console.error(`Collection component in ${this.name} has an invalid 'from' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting child state from the current state. Attempting to use parent component state.`)
|
|
927
1119
|
lense = undefined
|
|
928
1120
|
}
|
|
929
1121
|
|
|
930
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null }
|
|
1122
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, PARENT: null, __parentComponentNumber: this._componentNumber }
|
|
931
1123
|
const sink$ = collection(factory, lense, { container: null })(sources)
|
|
932
1124
|
if (!isObj(sink$)) {
|
|
933
|
-
throw new Error(
|
|
1125
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
934
1126
|
}
|
|
935
1127
|
return sink$
|
|
936
1128
|
}
|
|
@@ -952,7 +1144,7 @@ class Component {
|
|
|
952
1144
|
get: state => state[stateField],
|
|
953
1145
|
set: (oldState, newState) => {
|
|
954
1146
|
if (this.calculated && stateField in this.calculated) {
|
|
955
|
-
console.warn(`Switchable sub-component of ${
|
|
1147
|
+
console.warn(`Switchable sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`)
|
|
956
1148
|
return oldState
|
|
957
1149
|
}
|
|
958
1150
|
if (!isObj(newState) || Array.isArray(newState)) return { ...oldState, [stateField]: newState }
|
|
@@ -971,13 +1163,13 @@ class Component {
|
|
|
971
1163
|
lense = fieldLense
|
|
972
1164
|
} else if (isObj(stateField)) {
|
|
973
1165
|
if (typeof stateField.get !== 'function') {
|
|
974
|
-
console.error(`Switchable component in ${
|
|
1166
|
+
console.error(`Switchable component in ${this.name} has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`)
|
|
975
1167
|
lense = baseLense
|
|
976
1168
|
} else {
|
|
977
1169
|
lense = { get: stateField.get, set: stateField.set }
|
|
978
1170
|
}
|
|
979
1171
|
} else {
|
|
980
|
-
console.error(`Invalid state provided to switchable sub-component of ${
|
|
1172
|
+
console.error(`Invalid state provided to switchable sub-component of ${this.name}: Expecting string, object, or undefined, but found ${typeof stateField}. Attempting to use parent component state.`)
|
|
981
1173
|
lense = baseLense
|
|
982
1174
|
}
|
|
983
1175
|
|
|
@@ -993,12 +1185,12 @@ class Component {
|
|
|
993
1185
|
switchableComponents[key] = component(options)
|
|
994
1186
|
}
|
|
995
1187
|
})
|
|
996
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
1188
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
|
|
997
1189
|
|
|
998
1190
|
const sink$ = isolate(switchable(switchableComponents, props$.map(props => props.current)), { [this.stateSourceName]: lense })(sources)
|
|
999
1191
|
|
|
1000
1192
|
if (!isObj(sink$)) {
|
|
1001
|
-
throw new Error(
|
|
1193
|
+
throw new Error(`[${this.name}] Invalid sinks returned from component factory of switchable element`)
|
|
1002
1194
|
}
|
|
1003
1195
|
|
|
1004
1196
|
return sink$
|
|
@@ -1024,7 +1216,7 @@ class Component {
|
|
|
1024
1216
|
const factory = componentName === 'sygnal-factory' ? props.sygnalFactory : (this.components[componentName] || props.sygnalFactory)
|
|
1025
1217
|
if (!factory) {
|
|
1026
1218
|
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.`)
|
|
1027
|
-
throw new Error(`Component not found: ${
|
|
1219
|
+
throw new Error(`Component not found: ${componentName}`)
|
|
1028
1220
|
}
|
|
1029
1221
|
|
|
1030
1222
|
let lense
|
|
@@ -1033,7 +1225,7 @@ class Component {
|
|
|
1033
1225
|
get: state => state[stateField],
|
|
1034
1226
|
set: (oldState, newState) => {
|
|
1035
1227
|
if (this.calculated && stateField in this.calculated) {
|
|
1036
|
-
console.warn(`Sub-component of ${
|
|
1228
|
+
console.warn(`Sub-component of ${this.name} attempted to update state on a calculated field '${stateField}': Update ignored`)
|
|
1037
1229
|
return oldState
|
|
1038
1230
|
}
|
|
1039
1231
|
return { ...oldState, [stateField]: newState }
|
|
@@ -1051,17 +1243,17 @@ class Component {
|
|
|
1051
1243
|
lense = fieldLense
|
|
1052
1244
|
} else if (isObj(stateField)) {
|
|
1053
1245
|
if (typeof stateField.get !== 'function') {
|
|
1054
|
-
console.error(`Sub-component in ${
|
|
1246
|
+
console.error(`Sub-component in ${this.name} has an invalid 'state' field: Expecting 'undefined', a string indicating an array property in the state, or an object with 'get' and 'set' functions for retrieving and setting sub-component state from the current state. Attempting to use parent component state.`)
|
|
1055
1247
|
lense = baseLense
|
|
1056
1248
|
} else {
|
|
1057
1249
|
lense = { get: stateField.get, set: stateField.set }
|
|
1058
1250
|
}
|
|
1059
1251
|
} else {
|
|
1060
|
-
console.error(`Invalid state provided to sub-component of ${
|
|
1252
|
+
console.error(`Invalid state provided to sub-component of ${this.name}: Expecting string, object, or undefined, but found ${typeof stateField}. Attempting to use parent component state.`)
|
|
1061
1253
|
lense = baseLense
|
|
1062
1254
|
}
|
|
1063
1255
|
|
|
1064
|
-
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context
|
|
1256
|
+
const sources = { ...this.sources, [this.stateSourceName]: stateSource, props$, children$, __parentContext$: this.context$, __parentComponentNumber: this._componentNumber }
|
|
1065
1257
|
const sink$ = isolate(factory, { [this.stateSourceName]: lense })(sources)
|
|
1066
1258
|
|
|
1067
1259
|
if (!isObj(sink$)) {
|
|
@@ -1136,14 +1328,22 @@ class Component {
|
|
|
1136
1328
|
const fixedMsg = (typeof msg === 'function') ? msg : _ => msg
|
|
1137
1329
|
if (immediate) {
|
|
1138
1330
|
if (this.debug) {
|
|
1139
|
-
|
|
1331
|
+
const text = `[${context}] ${fixedMsg(msg)}`
|
|
1332
|
+
console.log(text)
|
|
1333
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
1334
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
|
|
1335
|
+
}
|
|
1140
1336
|
}
|
|
1141
1337
|
return
|
|
1142
1338
|
} else {
|
|
1143
1339
|
return stream => {
|
|
1144
1340
|
return stream.debug(msg => {
|
|
1145
1341
|
if (this.debug) {
|
|
1146
|
-
|
|
1342
|
+
const text = `[${context}] ${fixedMsg(msg)}`
|
|
1343
|
+
console.log(text)
|
|
1344
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
1345
|
+
window.__SYGNAL_DEVTOOLS__.onDebugLog(this._componentNumber, text)
|
|
1346
|
+
}
|
|
1147
1347
|
}
|
|
1148
1348
|
})
|
|
1149
1349
|
}
|
|
@@ -1153,11 +1353,11 @@ class Component {
|
|
|
1153
1353
|
|
|
1154
1354
|
|
|
1155
1355
|
|
|
1156
|
-
function getComponents(currentElement, componentNames,
|
|
1356
|
+
function getComponents(currentElement, componentNames, path='r', parentId) {
|
|
1157
1357
|
if (!currentElement) return {}
|
|
1158
1358
|
|
|
1159
1359
|
if (currentElement.data?.componentsProcessed) return {}
|
|
1160
|
-
if (
|
|
1360
|
+
if (path === 'r') currentElement.data.componentsProcessed = true
|
|
1161
1361
|
|
|
1162
1362
|
const sel = currentElement.sel
|
|
1163
1363
|
const isCollection = sel && sel.toLowerCase() === 'collection'
|
|
@@ -1171,11 +1371,11 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
1171
1371
|
|
|
1172
1372
|
let id = parentId
|
|
1173
1373
|
if (isComponent) {
|
|
1174
|
-
id = getComponentIdFromElement(currentElement,
|
|
1374
|
+
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1175
1375
|
if (isCollection) {
|
|
1176
1376
|
if (!props.of) throw new Error(`Collection element missing required 'component' property`)
|
|
1177
1377
|
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`)
|
|
1178
|
-
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${
|
|
1378
|
+
if (typeof props.of !== 'function' && !componentNames.includes(props.of)) throw new Error(`Specified component for collection not found: ${props.of}`)
|
|
1179
1379
|
if (typeof props.from !== 'undefined' && !(typeof props.from === 'string' || Array.isArray(props.from) || typeof props.from.get === 'function')) console.warn(`No valid array found for collection ${ typeof props.of === 'string' ? props.of : 'function component' }: no collection components will be created`, props.from)
|
|
1180
1380
|
currentElement.data.isCollection = true
|
|
1181
1381
|
currentElement.data.props ||= {}
|
|
@@ -1186,7 +1386,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
1186
1386
|
if (!switchableComponents.every(comp => typeof comp === 'function')) throw new Error(`One or more components provided to switchable element is not a valid component factory`)
|
|
1187
1387
|
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`)
|
|
1188
1388
|
const switchableComponentNames = Object.keys(props.of)
|
|
1189
|
-
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${
|
|
1389
|
+
if (!switchableComponentNames.includes(props.current)) throw new Error(`Component '${props.current}' not found in switchable element`)
|
|
1190
1390
|
currentElement.data.isSwitchable = true
|
|
1191
1391
|
} else {
|
|
1192
1392
|
|
|
@@ -1196,7 +1396,7 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
1196
1396
|
}
|
|
1197
1397
|
|
|
1198
1398
|
if (children.length > 0) {
|
|
1199
|
-
children.map((child, i) => getComponents(child, componentNames,
|
|
1399
|
+
children.map((child, i) => getComponents(child, componentNames, `${path}.${i}`, id))
|
|
1200
1400
|
.forEach((child) => {
|
|
1201
1401
|
Object.entries(child).forEach(([id, el]) => found[id] = el)
|
|
1202
1402
|
})
|
|
@@ -1205,10 +1405,10 @@ function getComponents(currentElement, componentNames, depth=0, index=0, parentI
|
|
|
1205
1405
|
return found
|
|
1206
1406
|
}
|
|
1207
1407
|
|
|
1208
|
-
function injectComponents(currentElement, components, componentNames,
|
|
1408
|
+
function injectComponents(currentElement, components, componentNames, path='r', parentId) {
|
|
1209
1409
|
if (!currentElement) return
|
|
1210
1410
|
if (currentElement.data?.componentsInjected) return currentElement
|
|
1211
|
-
if (
|
|
1411
|
+
if (path === 'r' && currentElement.data) currentElement.data.componentsInjected = true
|
|
1212
1412
|
|
|
1213
1413
|
|
|
1214
1414
|
const sel = currentElement.sel || 'NO SELECTOR'
|
|
@@ -1220,7 +1420,7 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
1220
1420
|
|
|
1221
1421
|
let id = parentId
|
|
1222
1422
|
if (isComponent) {
|
|
1223
|
-
id = getComponentIdFromElement(currentElement,
|
|
1423
|
+
id = getComponentIdFromElement(currentElement, path, parentId)
|
|
1224
1424
|
const component = components[id]
|
|
1225
1425
|
if (isCollection) {
|
|
1226
1426
|
currentElement.sel = 'div'
|
|
@@ -1232,21 +1432,20 @@ function injectComponents(currentElement, components, componentNames, depth=0, i
|
|
|
1232
1432
|
return component
|
|
1233
1433
|
}
|
|
1234
1434
|
} else if (children.length > 0) {
|
|
1235
|
-
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames,
|
|
1435
|
+
currentElement.children = children.map((child, i) => injectComponents(child, components, componentNames, `${path}.${i}`, id)).flat()
|
|
1236
1436
|
return currentElement
|
|
1237
1437
|
} else {
|
|
1238
1438
|
return currentElement
|
|
1239
1439
|
}
|
|
1240
1440
|
}
|
|
1241
1441
|
|
|
1242
|
-
function getComponentIdFromElement(el,
|
|
1442
|
+
function getComponentIdFromElement(el, path, parentId) {
|
|
1243
1443
|
const sel = el.sel
|
|
1244
1444
|
const name = typeof sel === 'string' ? sel : 'functionComponent'
|
|
1245
|
-
const uid = `${depth}:${index}`
|
|
1246
1445
|
const props = el.data?.props || {}
|
|
1247
|
-
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) ||
|
|
1248
|
-
const parentString = parentId ? `${
|
|
1249
|
-
const fullId = `${
|
|
1446
|
+
const id = (props.id && JSON.stringify(props.id).replaceAll('"', '')) || path
|
|
1447
|
+
const parentString = parentId ? `${parentId}|` : ''
|
|
1448
|
+
const fullId = `${parentString}${name}::${id}`
|
|
1250
1449
|
return fullId
|
|
1251
1450
|
}
|
|
1252
1451
|
|
|
@@ -1390,7 +1589,7 @@ function sortFunctionFromProp(sortProp) {
|
|
|
1390
1589
|
} else if (isObj(sortProp)) {
|
|
1391
1590
|
return __sortFunctionFromObj(sortProp)
|
|
1392
1591
|
} else {
|
|
1393
|
-
console.error('Invalid sort option (ignoring):',
|
|
1592
|
+
console.error('Invalid sort option (ignoring):', sortProp)
|
|
1394
1593
|
return undefined
|
|
1395
1594
|
}
|
|
1396
1595
|
}
|