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.
@@ -0,0 +1,249 @@
1
+ 'use strict'
2
+
3
+ const DEVTOOLS_SOURCE = '__SYGNAL_DEVTOOLS_PAGE__'
4
+ const EXTENSION_SOURCE = '__SYGNAL_DEVTOOLS_EXTENSION__'
5
+ const DEFAULT_MAX_HISTORY = 200
6
+
7
+ class SygnalDevTools {
8
+ constructor() {
9
+ this._connected = false
10
+ this._components = new Map()
11
+ this._stateHistory = []
12
+ this._maxHistory = DEFAULT_MAX_HISTORY
13
+ }
14
+
15
+ get connected() {
16
+ return this._connected && typeof window !== 'undefined'
17
+ }
18
+
19
+ // ─── Initialization ─────────────────────────────────────────────────────────
20
+
21
+ init() {
22
+ if (typeof window === 'undefined') return
23
+
24
+ window.__SYGNAL_DEVTOOLS__ = this
25
+
26
+ window.addEventListener('message', (event) => {
27
+ if (event.source !== window) return
28
+ if (event.data?.source === EXTENSION_SOURCE) {
29
+ this._handleExtensionMessage(event.data)
30
+ }
31
+ })
32
+ }
33
+
34
+ _handleExtensionMessage(msg) {
35
+ switch (msg.type) {
36
+ case 'CONNECT':
37
+ this._connected = true
38
+ if (msg.payload?.maxHistory) this._maxHistory = msg.payload.maxHistory
39
+ this._sendFullTree()
40
+ break
41
+ case 'DISCONNECT':
42
+ this._connected = false
43
+ break
44
+ case 'SET_DEBUG':
45
+ this._setDebug(msg.payload)
46
+ break
47
+ case 'TIME_TRAVEL':
48
+ this._timeTravel(msg.payload)
49
+ break
50
+ case 'GET_STATE':
51
+ this._sendComponentState(msg.payload.componentId)
52
+ break
53
+ }
54
+ }
55
+
56
+ // ─── Hooks (called from component.js) ────────────────────────────────────────
57
+
58
+ onComponentCreated(componentNumber, name, instance) {
59
+ const meta = {
60
+ id: componentNumber,
61
+ name: name,
62
+ isSubComponent: instance.isSubComponent,
63
+ hasModel: !!instance.model,
64
+ hasIntent: !!instance.intent,
65
+ hasContext: !!instance.context,
66
+ hasCalculated: !!instance.calculated,
67
+ components: Object.keys(instance.components || {}),
68
+ parentId: null,
69
+ children: [],
70
+ debug: instance._debug,
71
+ createdAt: Date.now(),
72
+ _instanceRef: new WeakRef(instance),
73
+ }
74
+ this._components.set(componentNumber, meta)
75
+
76
+ if (!this.connected) return
77
+ this._post('COMPONENT_CREATED', this._serializeMeta(meta))
78
+ }
79
+
80
+ onStateChanged(componentNumber, name, state) {
81
+ if (!this.connected) return
82
+
83
+ const entry = {
84
+ componentId: componentNumber,
85
+ componentName: name,
86
+ timestamp: Date.now(),
87
+ state: this._safeClone(state),
88
+ }
89
+
90
+ this._stateHistory.push(entry)
91
+ if (this._stateHistory.length > this._maxHistory) {
92
+ this._stateHistory.shift()
93
+ }
94
+
95
+ this._post('STATE_CHANGED', {
96
+ componentId: componentNumber,
97
+ componentName: name,
98
+ state: entry.state,
99
+ historyIndex: this._stateHistory.length - 1,
100
+ })
101
+ }
102
+
103
+ onActionDispatched(componentNumber, name, actionType, data) {
104
+ if (!this.connected) return
105
+ this._post('ACTION_DISPATCHED', {
106
+ componentId: componentNumber,
107
+ componentName: name,
108
+ actionType: actionType,
109
+ data: this._safeClone(data),
110
+ timestamp: Date.now(),
111
+ })
112
+ }
113
+
114
+ onSubComponentRegistered(parentNumber, childNumber) {
115
+ const parent = this._components.get(parentNumber)
116
+ const child = this._components.get(childNumber)
117
+ if (parent && child) {
118
+ child.parentId = parentNumber
119
+ if (!parent.children.includes(childNumber)) {
120
+ parent.children.push(childNumber)
121
+ }
122
+ }
123
+
124
+ if (!this.connected) return
125
+ this._post('TREE_UPDATED', {
126
+ parentId: parentNumber,
127
+ childId: childNumber,
128
+ })
129
+ }
130
+
131
+ onContextChanged(componentNumber, name, context) {
132
+ if (!this.connected) return
133
+ this._post('CONTEXT_CHANGED', {
134
+ componentId: componentNumber,
135
+ componentName: name,
136
+ context: this._safeClone(context),
137
+ })
138
+ }
139
+
140
+ onDebugLog(componentNumber, message) {
141
+ if (!this.connected) return
142
+ this._post('DEBUG_LOG', {
143
+ componentId: componentNumber,
144
+ message: message,
145
+ timestamp: Date.now(),
146
+ })
147
+ }
148
+
149
+ // ─── Commands (from extension to page) ───────────────────────────────────────
150
+
151
+ _setDebug({ componentId, enabled }) {
152
+ if (typeof componentId === 'undefined' || componentId === null) {
153
+ if (typeof window !== 'undefined') window.SYGNAL_DEBUG = enabled ? 'true' : false
154
+ this._post('DEBUG_TOGGLED', { global: true, enabled })
155
+ return
156
+ }
157
+
158
+ const meta = this._components.get(componentId)
159
+ if (meta && meta._instanceRef) {
160
+ const instance = meta._instanceRef.deref()
161
+ if (instance) {
162
+ instance._debug = enabled
163
+ meta.debug = enabled
164
+ this._post('DEBUG_TOGGLED', { componentId, enabled })
165
+ }
166
+ }
167
+ }
168
+
169
+ _timeTravel({ historyIndex }) {
170
+ const entry = this._stateHistory[historyIndex]
171
+ if (!entry) return
172
+
173
+ const app = typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS_APP__
174
+ if (app?.sinks?.STATE?.shamefullySendNext) {
175
+ app.sinks.STATE.shamefullySendNext(() => ({ ...entry.state }))
176
+ this._post('TIME_TRAVEL_APPLIED', {
177
+ historyIndex,
178
+ state: entry.state,
179
+ })
180
+ }
181
+ }
182
+
183
+ _sendComponentState(componentId) {
184
+ const meta = this._components.get(componentId)
185
+ if (meta && meta._instanceRef) {
186
+ const instance = meta._instanceRef.deref()
187
+ if (instance) {
188
+ this._post('COMPONENT_STATE', {
189
+ componentId,
190
+ state: this._safeClone(instance.currentState),
191
+ context: this._safeClone(instance.currentContext),
192
+ props: this._safeClone(instance.currentProps),
193
+ })
194
+ }
195
+ }
196
+ }
197
+
198
+ _sendFullTree() {
199
+ const tree = []
200
+ for (const [id, meta] of this._components) {
201
+ const instance = meta._instanceRef?.deref()
202
+ tree.push({
203
+ ...this._serializeMeta(meta),
204
+ state: instance ? this._safeClone(instance.currentState) : null,
205
+ context: instance ? this._safeClone(instance.currentContext) : null,
206
+ })
207
+ }
208
+ this._post('FULL_TREE', {
209
+ components: tree,
210
+ history: this._stateHistory,
211
+ })
212
+ }
213
+
214
+ // ─── Transport ───────────────────────────────────────────────────────────────
215
+
216
+ _post(type, payload) {
217
+ if (typeof window === 'undefined') return
218
+ window.postMessage({
219
+ source: DEVTOOLS_SOURCE,
220
+ type,
221
+ payload,
222
+ }, '*')
223
+ }
224
+
225
+ _safeClone(obj) {
226
+ if (obj === undefined || obj === null) return obj
227
+ try {
228
+ return JSON.parse(JSON.stringify(obj))
229
+ } catch (e) {
230
+ return '[unserializable]'
231
+ }
232
+ }
233
+
234
+ _serializeMeta(meta) {
235
+ const { _instanceRef, ...rest } = meta
236
+ return rest
237
+ }
238
+ }
239
+
240
+ // ─── Singleton ────────────────────────────────────────────────────────────────
241
+
242
+ let instance = null
243
+
244
+ export function getDevTools() {
245
+ if (!instance) instance = new SygnalDevTools()
246
+ return instance
247
+ }
248
+
249
+ export default getDevTools
@@ -14,11 +14,11 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
14
14
  const functionName = promiseReturningFunction.name || '[anonymous function]'
15
15
  const functionArgsType = typeof functionArgs
16
16
  if (functionArgsType !== 'string' && functionArgsType !== 'function' && !(Array.isArray(functionArgs) && functionArgs.every((arg) => typeof arg === 'string'))) {
17
- throw new Error(`The 'args' option for driverFromAsync(${ functionName }) must be a string, array of strings, or a function. Received ${functionArgsType}`)
17
+ throw new Error(`The 'args' option for driverFromAsync(${functionName}) must be a string, array of strings, or a function. Received ${functionArgsType}`)
18
18
  }
19
19
 
20
20
  if (typeof selectorProperty !== 'string') {
21
- throw new Error(`The 'selector' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof selectorProperty}`)
21
+ throw new Error(`The 'selector' option for driverFromAsync(${functionName}) must be a string. Received ${typeof selectorProperty}`)
22
22
  }
23
23
 
24
24
  return (fromApp$) => {
@@ -47,7 +47,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
47
47
  argArr = functionArgs.map((arg) => preProcessed[arg])
48
48
  }
49
49
  }
50
- const errMsg = `Error in driver created using driverFromAsync(${ functionName })`
50
+ const errMsg = `Error in driver created using driverFromAsync(${functionName})`
51
51
  promiseReturningFunction(...argArr)
52
52
  .then((innerVal) => {
53
53
  const constructReply = (rawVal) => {
@@ -57,7 +57,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
57
57
  if (typeof outgoing === 'object' && outgoing !== null) {
58
58
  outgoing[selectorProperty] = incoming[selectorProperty]
59
59
  } else {
60
- console.warn(`The 'return' option for driverFromAsync(${ functionName }) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`)
60
+ console.warn(`The 'return' option for driverFromAsync(${functionName}) was not set, but the promise returned an non-object. The result will be returned as-is, but the '${selectorProperty}' property will not be set, so will not be filtered by the 'select' method of the driver.`)
61
61
  }
62
62
  } else if (typeof returnProperty === 'string') {
63
63
  outgoing = {
@@ -65,7 +65,7 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
65
65
  [selectorProperty]: incoming[selectorProperty]
66
66
  }
67
67
  } else {
68
- throw new Error(`The 'return' option for driverFromAsync(${ functionName }) must be a string. Received ${typeof returnProperty}`)
68
+ throw new Error(`The 'return' option for driverFromAsync(${functionName}) must be a string. Received ${typeof returnProperty}`)
69
69
  }
70
70
  return outgoing
71
71
  }
@@ -81,12 +81,12 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
81
81
  .then((innerProcessedOutgoing) => {
82
82
  sendFn(constructReply(innerProcessedOutgoing))
83
83
  })
84
- .catch((err) => console.error(`${ errMsg }: ${ err }`))
84
+ .catch((err) => console.error(`${errMsg}: ${err}`))
85
85
  } else {
86
- sendFn(constructReply(rocessedOutgoing))
86
+ sendFn(constructReply(processedOutgoing))
87
87
  }
88
88
  })
89
- .catch((err) => console.error(`${ errMsg }: ${ err }`))
89
+ .catch((err) => console.error(`${errMsg}: ${err}`))
90
90
  } else {
91
91
  const processedOutgoing = postFunction(innerVal, incoming)
92
92
  if (typeof processedOutgoing.then === 'function') {
@@ -94,19 +94,19 @@ function driverFromAsync(promiseReturningFunction, opts = {}) {
94
94
  .then((innerProcessedOutgoing) => {
95
95
  sendFn(constructReply(innerProcessedOutgoing))
96
96
  })
97
- .catch((err) => console.error(`${ errMsg }: ${ err }`))
97
+ .catch((err) => console.error(`${errMsg}: ${err}`))
98
98
  } else {
99
99
  sendFn(constructReply(processedOutgoing))
100
100
  }
101
101
  }
102
102
  })
103
- .catch((err) => console.error(`${ errMsg }: ${ err }`))
103
+ .catch((err) => console.error(`${errMsg}: ${err}`))
104
104
  },
105
105
  error: (err) => {
106
- console.error(`Error recieved from sink stream in driver created using driverFromAsync(${ functionName }): ${ err }`)
106
+ console.error(`Error received from sink stream in driver created using driverFromAsync(${functionName}):`, err)
107
107
  },
108
108
  complete: () => {
109
- console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${ functionName })`)
109
+ console.warn(`Unexpected completion of sink stream to driver created using driverFromAsync(${functionName})`)
110
110
  }
111
111
  })
112
112
 
@@ -9,7 +9,8 @@ export default function eventBusDriver(out$) {
9
9
  const events = new EventTarget()
10
10
 
11
11
  out$.subscribe({
12
- next: event => events.dispatchEvent(new CustomEvent('data', { detail: event }))
12
+ next: event => events.dispatchEvent(new CustomEvent('data', { detail: event })),
13
+ error: err => console.error('[EVENTS driver] Error in sink stream:', err)
13
14
  })
14
15
 
15
16
  return {
@@ -4,6 +4,9 @@ export default function logDriver(out$) {
4
4
  out$.addListener({
5
5
  next: (val) => {
6
6
  console.log(val)
7
+ },
8
+ error: (err) => {
9
+ console.error('[LOG driver] Error in sink stream:', err)
7
10
  }
8
11
  })
9
12
  }
@@ -3,6 +3,12 @@
3
3
  import xs from './xstreamCompat.js'
4
4
 
5
5
  export default function processDrag({ draggable, dropZone } = {}, options = {}) {
6
+ if (draggable && typeof draggable.events !== 'function') {
7
+ throw new Error('processDrag: draggable must have an .events() method (e.g. DOM.select(...))')
8
+ }
9
+ if (dropZone && typeof dropZone.events !== 'function') {
10
+ throw new Error('processDrag: dropZone must have an .events() method (e.g. DOM.select(...))')
11
+ }
6
12
  const { effectAllowed = 'move' } = options
7
13
 
8
14
  const dragStart$ = draggable
@@ -3,6 +3,9 @@
3
3
  import xs from './xstreamCompat.js'
4
4
 
5
5
  export default function processForm(form, options={}) {
6
+ if (!form || typeof form.events !== 'function') {
7
+ throw new Error('processForm: first argument must have an .events() method (e.g. DOM.select(...))')
8
+ }
6
9
  let { events = ['input', 'submit'], preventDefault = true } = options
7
10
  if (typeof events === 'string') events = [events]
8
11
 
package/src/extra/run.js CHANGED
@@ -4,8 +4,15 @@ import { makeDOMDriver } from "@cycle/dom"
4
4
  import eventBusDriver from "./eventDriver"
5
5
  import logDriver from "./logDriver"
6
6
  import component, { ABORT } from "../component"
7
+ import { getDevTools } from "./devtools"
7
8
 
8
9
  export default function run(app, drivers={}, options={}) {
10
+ // Initialize DevTools instrumentation bridge early (before component creation)
11
+ if (typeof window !== 'undefined') {
12
+ const dt = getDevTools()
13
+ dt.init()
14
+ }
15
+
9
16
  const { mountPoint='#root', fragments=true, useDefaultDrivers=true } = options
10
17
  if (!app.isSygnalComponent) {
11
18
  const name = app.name || app.componentName || app.label || "FUNCTIONAL_COMPONENT"
@@ -55,6 +62,11 @@ export default function run(app, drivers={}, options={}) {
55
62
 
56
63
  const exposed = { sources, sinks, dispose }
57
64
 
65
+ // Store app reference for time-travel
66
+ if (typeof window !== 'undefined') {
67
+ window.__SYGNAL_DEVTOOLS_APP__ = exposed
68
+ }
69
+
58
70
  const swapToComponent = (newComponent, state) => {
59
71
  const persistedState = (typeof window !== 'undefined') ? window.__SYGNAL_HMR_PERSISTED_STATE : undefined
60
72
  const fallbackState = typeof persistedState !== 'undefined' ? persistedState : app.initialState
package/src/index.d.ts CHANGED
@@ -3,7 +3,8 @@ import type { StateSource } from '@cycle/state'
3
3
  import xsDefault from 'xstream'
4
4
  import type { MemoryStream, Stream } from 'xstream'
5
5
 
6
- export type ABORT = '~#~#~ABORT~#~#~'
6
+ export declare const ABORT: unique symbol
7
+ export type ABORT = typeof ABORT
7
8
 
8
9
  export type DriverSpec<SOURCE = any, SINK = any> = {
9
10
  source: SOURCE;
@@ -188,9 +189,13 @@ interface ComponentIntent<STATE, DRIVERS, ACTIONS> {
188
189
  (args: CombinedSources<STATE, DRIVERS>): Partial<Actions<ACTIONS>>
189
190
  }
190
191
 
192
+ type CalculatedFieldValue<FULL_STATE, RETURN> =
193
+ | StateOnlyReducer<FULL_STATE, RETURN>
194
+ | [ReadonlyArray<string & keyof FULL_STATE>, StateOnlyReducer<FULL_STATE, RETURN>]
195
+
191
196
  type Calculated<STATE, CALCULATED> = keyof CALCULATED extends never
192
- ? { [field: string]: boolean | StateOnlyReducer<STATE, any> }
193
- : { [CALCULATED_KEY in keyof CALCULATED]: boolean | StateOnlyReducer<STATE, CALCULATED[CALCULATED_KEY]> }
197
+ ? { [field: string]: boolean | CalculatedFieldValue<STATE, any> }
198
+ : { [CALCULATED_KEY in keyof CALCULATED]: boolean | CalculatedFieldValue<STATE & CALCULATED, CALCULATED[CALCULATED_KEY]> }
194
199
 
195
200
  type Context<STATE, CONTEXT> = keyof CONTEXT extends never
196
201
  ? { [field: string]: boolean | StateOnlyReducer<STATE, any> }
@@ -444,7 +449,6 @@ export function driverFromAsync<INCOMING = any, RETURN = any, OUTGOING = any>(
444
449
  options?: DriverFromAsyncOptions<INCOMING, OUTGOING, RETURN>
445
450
  ): (fromApp$: Stream<INCOMING>) => AsyncDriverFromFunction<INCOMING, OUTGOING>
446
451
 
447
- export const ABORT: ABORT
448
452
  export const xs: typeof xsDefault
449
453
 
450
454
  export { default as debounce } from 'xstream/extra/debounce'
package/src/index.js CHANGED
@@ -13,6 +13,7 @@ export { default as run } from './extra/run'
13
13
  export { default as enableHMR } from './extra/hmr'
14
14
  export { default as classes } from './extra/classes'
15
15
  export { default as xs } from './extra/xstreamCompat.js'
16
+ export { getDevTools } from './extra/devtools'
16
17
 
17
18
  // export dom helper functions (h, div, ...)
18
19
  export * from '@cycle/dom'
@@ -13,17 +13,29 @@ const createTextElement = (text) => !is.text(text) ? undefined : {
13
13
  key: undefined
14
14
  }
15
15
 
16
- const considerSvg = (vnode) => !is.svg(vnode) ? vnode :
17
- fn.assign(vnode,
18
- { data: fn.omit('props', fn.extend(vnode.data,
19
- { ns: 'http://www.w3.org/2000/svg', attrs: fn.omit('className', fn.extend(vnode.data.props,
20
- { class: vnode.data.props ? vnode.data.props.className : undefined }
21
- )) }
16
+ const applySvg = (vnode) => {
17
+ // Skip text vnodes (sel is undefined) and nullish values
18
+ if (!vnode || is.undefinedv(vnode.sel)) return vnode
19
+
20
+ const data = vnode.data || {}
21
+ const props = data.props || {}
22
+ const propsWithoutClassName = fn.omit('className', props)
23
+ const classAttr = props.className !== undefined ? { class: props.className } : {}
24
+ const mergedAttrs = fn.assign({}, propsWithoutClassName, classAttr, data.attrs || {})
25
+
26
+ return fn.assign(vnode,
27
+ { data: fn.omit('props', fn.assign({}, data,
28
+ { ns: 'http://www.w3.org/2000/svg', attrs: mergedAttrs }
22
29
  )) },
23
- { children: is.undefinedv(vnode.children) ? undefined :
24
- vnode.children.map((child) => considerSvg(child))
30
+ // foreignObject contains HTML, not SVG — do not recurse into its children
31
+ { children: (!Array.isArray(vnode.children) || vnode.sel === 'foreignObject')
32
+ ? vnode.children
33
+ : vnode.children.map((child) => applySvg(child))
25
34
  }
26
35
  )
36
+ }
37
+
38
+ const considerSvg = (vnode) => !is.svg(vnode) ? vnode : applySvg(vnode)
27
39
 
28
40
  const rewrites = {
29
41
  for: 'attrs',
@@ -79,7 +91,7 @@ const applyFocusProps = (data) => {
79
91
  return data
80
92
  }
81
93
 
82
- const sanitizeData = (data, modules) => applyFocusProps(considerSvg(rewriteModules(fn.deepifyKeys(data, modules), modules)))
94
+ const sanitizeData = (data, modules) => applyFocusProps(rewriteModules(fn.deepifyKeys(data, modules), modules))
83
95
 
84
96
  const sanitizeText = (children) => children.length > 1 || !is.text(children[0]) ? undefined : children[0].toString()
85
97
 
package/src/pragma/is.js CHANGED
@@ -17,7 +17,32 @@ export const fun = (v) => typeof v === 'function'
17
17
 
18
18
  export const vnode = (v) => object(v) && 'sel' in v && 'data' in v && 'children' in v && 'text' in v
19
19
 
20
- const svgPropsMap = { svg: 1, circle: 1, ellipse: 1, line: 1, polygon: 1,
21
- polyline: 1, rect: 1, g: 1, path: 1, text: 1 }
20
+ const svgPropsMap = {
21
+ // Container / structural
22
+ svg: 1, g: 1, defs: 1, symbol: 1, use: 1,
23
+ // Shape
24
+ circle: 1, ellipse: 1, line: 1, path: 1, polygon: 1, polyline: 1, rect: 1,
25
+ // Text (no HTML collision: HTML has no <text>, <tspan>, or <textPath>)
26
+ text: 1, tspan: 1, textPath: 1,
27
+ // Gradient / paint
28
+ linearGradient: 1, radialGradient: 1, stop: 1, pattern: 1,
29
+ // Clipping / masking
30
+ clipPath: 1, mask: 1,
31
+ // Marker
32
+ marker: 1,
33
+ // Filter primitives
34
+ filter: 1, feBlend: 1, feColorMatrix: 1, feComponentTransfer: 1,
35
+ feComposite: 1, feConvolveMatrix: 1, feDiffuseLighting: 1,
36
+ feDisplacementMap: 1, feDropShadow: 1, feFlood: 1, feGaussianBlur: 1,
37
+ feImage: 1, feMerge: 1, feMergeNode: 1, feMorphology: 1, feOffset: 1,
38
+ fePointLight: 1, feSpecularLighting: 1, feSpotLight: 1, feTile: 1,
39
+ feTurbulence: 1, feFuncR: 1, feFuncG: 1, feFuncB: 1, feFuncA: 1,
40
+ // Descriptive (excluding 'title' — collides with HTML <title>)
41
+ desc: 1, metadata: 1,
42
+ // Other (excluding 'a', 'image', 'style', 'script' — collide with HTML)
43
+ foreignObject: 1, switch: 1,
44
+ // Animation
45
+ animate: 1, animateMotion: 1, animateTransform: 1, set: 1, mpath: 1,
46
+ }
22
47
 
23
48
  export const svg = (v) => v.sel in svgPropsMap
package/src/switchable.js CHANGED
@@ -30,7 +30,7 @@ export default function switchable(factories, name$, initial, opts={}) {
30
30
  const mapFunction = (nameType === 'function' && name$) || (state => state[name$])
31
31
  return sources => {
32
32
  const state$ = sources && ((typeof stateSourceName === 'string' && sources[stateSourceName]) || sources.STATE || sources.state).stream
33
- if (!state$ instanceof Stream) throw new Error(`Could not find the state source: ${ stateSourceName }`)
33
+ if (!(state$ instanceof Stream)) throw new Error(`Could not find the state source: ${stateSourceName}`)
34
34
  const _name$ = state$
35
35
  .map(mapFunction)
36
36
  .filter(name => typeof name === 'string')