sygnal 5.2.0 → 5.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/README.md +21 -2
- package/dist/astro/client.cjs.js +271 -14
- package/dist/astro/client.mjs +271 -14
- package/dist/astro/server.cjs.js +18 -0
- package/dist/astro/server.mjs +18 -0
- package/dist/index.cjs.js +543 -17
- package/dist/index.d.ts +63 -0
- package/dist/index.esm.js +538 -18
- package/dist/sygnal.min.js +1 -1
- package/dist/vike/+config.cjs.js +5 -1
- package/dist/vike/+config.js +5 -1
- package/dist/vike/ClientOnly.cjs.js +34 -0
- package/dist/vike/ClientOnly.mjs +32 -0
- package/dist/vike/onRenderClient.cjs.js +292 -35
- package/dist/vike/onRenderClient.mjs +292 -35
- package/dist/vike/onRenderHtml.cjs.js +71 -34
- package/dist/vike/onRenderHtml.mjs +71 -34
- package/dist/vite/plugin.cjs.js +6 -4
- package/dist/vite/plugin.mjs +6 -4
- package/package.json +5 -1
- package/src/component.ts +63 -6
- package/src/extra/command.ts +13 -2
- package/src/extra/devtools.ts +221 -7
- package/src/extra/dragDriver.ts +59 -5
- package/src/extra/eventDriver.ts +6 -2
- package/src/extra/pwa.ts +179 -0
- package/src/extra/reducers.ts +64 -0
- package/src/extra/ssr.ts +19 -0
- package/src/index.d.ts +63 -0
- package/src/index.ts +2 -0
- package/src/vike/+config.ts +5 -1
- package/src/vike/ClientOnly.ts +10 -0
- package/src/vike/onRenderClient.ts +319 -36
- package/src/vike/onRenderHtml.ts +77 -33
- package/src/vike/types.ts +2 -0
- package/src/vite/plugin.ts +6 -4
package/dist/vite/plugin.cjs.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* export default defineConfig({ plugins: [sygnal()] })
|
|
11
11
|
*
|
|
12
12
|
* What it does:
|
|
13
|
-
* 1. Configures
|
|
13
|
+
* 1. Configures OXC for automatic JSX transform with sygnal as the import source
|
|
14
14
|
* 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
|
|
15
15
|
*
|
|
16
16
|
* The HMR transform finds the pattern:
|
|
@@ -34,9 +34,11 @@ function sygnal(options = {}) {
|
|
|
34
34
|
if (disableJsx)
|
|
35
35
|
return;
|
|
36
36
|
return {
|
|
37
|
-
|
|
38
|
-
jsx:
|
|
39
|
-
|
|
37
|
+
oxc: {
|
|
38
|
+
jsx: {
|
|
39
|
+
runtime: 'automatic',
|
|
40
|
+
importSource: 'sygnal',
|
|
41
|
+
},
|
|
40
42
|
},
|
|
41
43
|
};
|
|
42
44
|
},
|
package/dist/vite/plugin.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* export default defineConfig({ plugins: [sygnal()] })
|
|
9
9
|
*
|
|
10
10
|
* What it does:
|
|
11
|
-
* 1. Configures
|
|
11
|
+
* 1. Configures OXC for automatic JSX transform with sygnal as the import source
|
|
12
12
|
* 2. Detects files that call `run()` from sygnal and auto-injects HMR wiring
|
|
13
13
|
*
|
|
14
14
|
* The HMR transform finds the pattern:
|
|
@@ -32,9 +32,11 @@ function sygnal(options = {}) {
|
|
|
32
32
|
if (disableJsx)
|
|
33
33
|
return;
|
|
34
34
|
return {
|
|
35
|
-
|
|
36
|
-
jsx:
|
|
37
|
-
|
|
35
|
+
oxc: {
|
|
36
|
+
jsx: {
|
|
37
|
+
runtime: 'automatic',
|
|
38
|
+
importSource: 'sygnal',
|
|
39
|
+
},
|
|
38
40
|
},
|
|
39
41
|
};
|
|
40
42
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sygnal",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "An intuitive framework for building fast and small components or applications based on Cycle.js",
|
|
5
5
|
"main": "./dist/index.cjs.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
"import": "./dist/vike/onRenderClient.mjs",
|
|
70
70
|
"require": "./dist/vike/onRenderClient.cjs.js"
|
|
71
71
|
},
|
|
72
|
+
"./vike/ClientOnly": {
|
|
73
|
+
"import": "./dist/vike/ClientOnly.mjs",
|
|
74
|
+
"require": "./dist/vike/ClientOnly.cjs.js"
|
|
75
|
+
},
|
|
72
76
|
"./types": {
|
|
73
77
|
"import": "./dist/index.d.ts",
|
|
74
78
|
"require": "./dist/index.d.ts"
|
package/src/component.ts
CHANGED
|
@@ -91,6 +91,7 @@ export interface ComponentOptions {
|
|
|
91
91
|
stateSourceName?: string;
|
|
92
92
|
requestSourceName?: string;
|
|
93
93
|
isolateOpts?: string | boolean | Record<string, any>;
|
|
94
|
+
isolatedState?: boolean;
|
|
94
95
|
onError?: (error: Error, info: { componentName: string }) => any;
|
|
95
96
|
debug?: boolean;
|
|
96
97
|
}
|
|
@@ -169,6 +170,7 @@ class Component {
|
|
|
169
170
|
sourceNames: string[];
|
|
170
171
|
_debug: boolean;
|
|
171
172
|
onError: ((error: Error, info: { componentName: string }) => any) | undefined;
|
|
173
|
+
isolatedState: boolean;
|
|
172
174
|
isSubComponent: boolean;
|
|
173
175
|
currentState: any;
|
|
174
176
|
currentProps: any;
|
|
@@ -202,7 +204,7 @@ class Component {
|
|
|
202
204
|
_readyChanged$: any;
|
|
203
205
|
_readyChangedListener: any;
|
|
204
206
|
|
|
205
|
-
constructor({name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', onError, debug = false}: ComponentOptions) {
|
|
207
|
+
constructor({name = 'NO NAME', sources, intent, model, hmrActions, context, response, view, peers = {}, components = {}, initialState, calculated, storeCalculatedInState = true, DOMSourceName = 'DOM', stateSourceName = 'STATE', requestSourceName = 'HTTP', isolatedState = false, onError, debug = false}: ComponentOptions) {
|
|
206
208
|
if (!sources || !isObj(sources)) throw new Error(`[${name}] Missing or invalid sources`)
|
|
207
209
|
|
|
208
210
|
this._componentNumber = COMPONENT_COUNT++
|
|
@@ -225,6 +227,7 @@ class Component {
|
|
|
225
227
|
this.requestSourceName = requestSourceName
|
|
226
228
|
this.sourceNames = Object.keys(sources)
|
|
227
229
|
this.onError = onError
|
|
230
|
+
this.isolatedState = isolatedState
|
|
228
231
|
this._debug = debug
|
|
229
232
|
|
|
230
233
|
// Warn if calculated fields shadow base state keys
|
|
@@ -366,6 +369,9 @@ class Component {
|
|
|
366
369
|
this.sources.props$ = props$.map((val: any) => {
|
|
367
370
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps }: any = val
|
|
368
371
|
this.currentProps = sanitizedProps
|
|
372
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
373
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps)
|
|
374
|
+
}
|
|
369
375
|
return val
|
|
370
376
|
})
|
|
371
377
|
}
|
|
@@ -447,6 +453,9 @@ class Component {
|
|
|
447
453
|
}
|
|
448
454
|
|
|
449
455
|
dispose(): void {
|
|
456
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
457
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name)
|
|
458
|
+
}
|
|
450
459
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
451
460
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION))
|
|
452
461
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -649,7 +658,7 @@ class Component {
|
|
|
649
658
|
const hmrState = ENVIRONMENT?.__SYGNAL_HMR_STATE
|
|
650
659
|
const effectiveInitialState = (typeof hmrState !== 'undefined') ? hmrState : this.initialState
|
|
651
660
|
const initial = { type: INITIALIZE_ACTION, data: effectiveInitialState }
|
|
652
|
-
if (this.isSubComponent && this.initialState) {
|
|
661
|
+
if (this.isSubComponent && this.initialState && !this.isolatedState) {
|
|
653
662
|
console.warn(`[${this.name}] Initial state provided to sub-component. This will overwrite any state provided by the parent component.`)
|
|
654
663
|
}
|
|
655
664
|
const hasInitialState = (typeof effectiveInitialState !== 'undefined')
|
|
@@ -851,6 +860,7 @@ class Component {
|
|
|
851
860
|
.map((vdom: any) => processLazy(vdom, this))
|
|
852
861
|
.map(processPortals)
|
|
853
862
|
.map(processTransitions)
|
|
863
|
+
.map(processClientOnly)
|
|
854
864
|
.compose(this.instantiateSubComponents.bind(this))
|
|
855
865
|
.filter((val: any) => val !== undefined)
|
|
856
866
|
.compose(this.renderVdom.bind(this))
|
|
@@ -866,6 +876,12 @@ class Component {
|
|
|
866
876
|
} else {
|
|
867
877
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []))
|
|
868
878
|
}
|
|
879
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
880
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
881
|
+
const _componentNumber = this._componentNumber
|
|
882
|
+
const _name = this.name
|
|
883
|
+
acc[name] = acc[name].map((ev: any) => ({...ev, __emitterId: _componentNumber, __emitterName: _name}))
|
|
884
|
+
}
|
|
869
885
|
return acc
|
|
870
886
|
}, {} as Record<string, any>)
|
|
871
887
|
|
|
@@ -1416,6 +1432,18 @@ class Component {
|
|
|
1416
1432
|
if (!isObj(sink$)) {
|
|
1417
1433
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`)
|
|
1418
1434
|
}
|
|
1435
|
+
|
|
1436
|
+
// Notify devtools of collection mount
|
|
1437
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1438
|
+
const itemName = typeof collectionOf === 'function'
|
|
1439
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
1440
|
+
: String(collectionOf)
|
|
1441
|
+
;(window as any).__SYGNAL_DEVTOOLS__.onCollectionMounted(
|
|
1442
|
+
this._componentNumber, this.name, itemName,
|
|
1443
|
+
typeof stateField === 'string' ? stateField : null
|
|
1444
|
+
)
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1419
1447
|
return sink$
|
|
1420
1448
|
}
|
|
1421
1449
|
|
|
@@ -1567,6 +1595,7 @@ class Component {
|
|
|
1567
1595
|
for (const key of Object.keys(props)) {
|
|
1568
1596
|
const val = props[key]
|
|
1569
1597
|
if (val && val.__sygnalCommand) {
|
|
1598
|
+
val._targetComponentName = componentName
|
|
1570
1599
|
sources.commands$ = makeCommandSource(val as Command)
|
|
1571
1600
|
break
|
|
1572
1601
|
}
|
|
@@ -1616,10 +1645,15 @@ class Component {
|
|
|
1616
1645
|
const wasReady = this._childReadyState[id]
|
|
1617
1646
|
this._childReadyState[id] = !!ready
|
|
1618
1647
|
// When READY state changes, trigger a re-render
|
|
1619
|
-
if (wasReady !== !!ready
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1648
|
+
if (wasReady !== !!ready) {
|
|
1649
|
+
if (this._readyChangedListener) {
|
|
1650
|
+
setTimeout(() => {
|
|
1651
|
+
this._readyChangedListener?.next(null)
|
|
1652
|
+
}, 0)
|
|
1653
|
+
}
|
|
1654
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
1655
|
+
(window as any).__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready)
|
|
1656
|
+
}
|
|
1623
1657
|
}
|
|
1624
1658
|
},
|
|
1625
1659
|
error: () => {},
|
|
@@ -1876,6 +1910,29 @@ function processTransitions(vnode: any): any {
|
|
|
1876
1910
|
return vnode
|
|
1877
1911
|
}
|
|
1878
1912
|
|
|
1913
|
+
function processClientOnly(vnode: any): any {
|
|
1914
|
+
if (!vnode || !vnode.sel) return vnode
|
|
1915
|
+
if (vnode.sel === 'clientonly') {
|
|
1916
|
+
// On the client, unwrap to children (render them normally)
|
|
1917
|
+
const children = vnode.children || []
|
|
1918
|
+
if (children.length === 0) return { sel: 'div', data: {}, children: [] }
|
|
1919
|
+
if (children.length === 1) return processClientOnly(children[0])
|
|
1920
|
+
// Multiple children: wrap in a div
|
|
1921
|
+
return {
|
|
1922
|
+
sel: 'div',
|
|
1923
|
+
data: {},
|
|
1924
|
+
children: children.map(processClientOnly),
|
|
1925
|
+
text: undefined,
|
|
1926
|
+
elm: undefined,
|
|
1927
|
+
key: undefined,
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
if (vnode.children && vnode.children.length > 0) {
|
|
1931
|
+
vnode.children = vnode.children.map(processClientOnly)
|
|
1932
|
+
}
|
|
1933
|
+
return vnode
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1879
1936
|
function applyTransitionHooks(vnode: any, name: string, duration?: number): any {
|
|
1880
1937
|
const existingInsert = vnode.data?.hook?.insert
|
|
1881
1938
|
const existingRemove = vnode.data?.hook?.remove
|
package/src/extra/command.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface Command {
|
|
|
16
16
|
_stream: Stream<CommandMessage>;
|
|
17
17
|
/** @internal — marker for component.ts detection */
|
|
18
18
|
__sygnalCommand: true;
|
|
19
|
+
/** @internal — stamped by component.ts when wired to a child */
|
|
20
|
+
_targetComponentId?: number;
|
|
21
|
+
/** @internal — stamped by component.ts when wired to a child */
|
|
22
|
+
_targetComponentName?: string;
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
export function createCommand(): Command {
|
|
@@ -26,11 +30,18 @@ export function createCommand(): Command {
|
|
|
26
30
|
stop() { listener.next = () => {}; },
|
|
27
31
|
});
|
|
28
32
|
|
|
29
|
-
|
|
30
|
-
send: (type: string, data?: any) =>
|
|
33
|
+
const cmd: Command = {
|
|
34
|
+
send: (type: string, data?: any) => {
|
|
35
|
+
listener.next({ type, data });
|
|
36
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
37
|
+
(window as any).__SYGNAL_DEVTOOLS__.onCommandSent(type, data, cmd._targetComponentId, cmd._targetComponentName);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
31
40
|
_stream,
|
|
32
41
|
__sygnalCommand: true as const,
|
|
33
42
|
};
|
|
43
|
+
|
|
44
|
+
return cmd;
|
|
34
45
|
}
|
|
35
46
|
|
|
36
47
|
export function makeCommandSource(cmd: Command): CommandSource {
|
package/src/extra/devtools.ts
CHANGED
|
@@ -26,6 +26,12 @@ interface StateHistoryEntry {
|
|
|
26
26
|
state: any;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
interface MviGraphData {
|
|
30
|
+
sources: string[];
|
|
31
|
+
actions: {name: string; sinks: string[]}[];
|
|
32
|
+
contextProvides: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
interface ComponentMeta {
|
|
30
36
|
id: number;
|
|
31
37
|
name: string;
|
|
@@ -39,6 +45,7 @@ interface ComponentMeta {
|
|
|
39
45
|
children: number[];
|
|
40
46
|
debug: boolean;
|
|
41
47
|
createdAt: number;
|
|
48
|
+
mviGraph: MviGraphData | null;
|
|
42
49
|
_instanceRef: WeakRef<any>;
|
|
43
50
|
}
|
|
44
51
|
|
|
@@ -94,6 +101,12 @@ class SygnalDevTools {
|
|
|
94
101
|
case 'TIME_TRAVEL':
|
|
95
102
|
this._timeTravel(msg.payload);
|
|
96
103
|
break;
|
|
104
|
+
case 'SNAPSHOT':
|
|
105
|
+
this._takeSnapshot();
|
|
106
|
+
break;
|
|
107
|
+
case 'RESTORE_SNAPSHOT':
|
|
108
|
+
this._restoreSnapshot(msg.payload);
|
|
109
|
+
break;
|
|
97
110
|
case 'GET_STATE':
|
|
98
111
|
this._sendComponentState(msg.payload.componentId);
|
|
99
112
|
break;
|
|
@@ -114,6 +127,7 @@ class SygnalDevTools {
|
|
|
114
127
|
children: [],
|
|
115
128
|
debug: instance._debug,
|
|
116
129
|
createdAt: Date.now(),
|
|
130
|
+
mviGraph: this._extractMviGraph(instance),
|
|
117
131
|
_instanceRef: new WeakRef(instance),
|
|
118
132
|
};
|
|
119
133
|
this._components.set(componentNumber, meta);
|
|
@@ -141,7 +155,6 @@ class SygnalDevTools {
|
|
|
141
155
|
componentId: componentNumber,
|
|
142
156
|
componentName: name,
|
|
143
157
|
state: entry.state,
|
|
144
|
-
historyIndex: this._stateHistory.length - 1,
|
|
145
158
|
});
|
|
146
159
|
}
|
|
147
160
|
|
|
@@ -179,6 +192,77 @@ class SygnalDevTools {
|
|
|
179
192
|
componentId: componentNumber,
|
|
180
193
|
componentName: name,
|
|
181
194
|
context: this._safeClone(context),
|
|
195
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
onPropsChanged(componentNumber: number, name: string, props: any): void {
|
|
200
|
+
if (!this.connected) return;
|
|
201
|
+
this._post('PROPS_CHANGED', {
|
|
202
|
+
componentId: componentNumber,
|
|
203
|
+
componentName: name,
|
|
204
|
+
props: this._safeClone(props),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
onBusEvent(event: {type: string; data?: any; __emitterId?: number; __emitterName?: string}): void {
|
|
209
|
+
if (!this.connected) return;
|
|
210
|
+
this._post('BUS_EVENT', {
|
|
211
|
+
type: event.type,
|
|
212
|
+
data: this._safeClone(event.data),
|
|
213
|
+
componentId: event.__emitterId ?? null,
|
|
214
|
+
componentName: event.__emitterName ?? null,
|
|
215
|
+
timestamp: Date.now(),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
onCommandSent(type: string, data: any, targetComponentId?: number, targetComponentName?: string): void {
|
|
220
|
+
if (!this.connected) return;
|
|
221
|
+
this._post('COMMAND_SENT', {
|
|
222
|
+
type,
|
|
223
|
+
data: this._safeClone(data),
|
|
224
|
+
targetComponentName: targetComponentName ?? null,
|
|
225
|
+
timestamp: Date.now(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
onReadyChanged(parentId: number, parentName: string, childKey: string, ready: boolean): void {
|
|
230
|
+
if (!this.connected) return;
|
|
231
|
+
this._post('READY_CHANGED', {
|
|
232
|
+
parentId,
|
|
233
|
+
parentName,
|
|
234
|
+
childKey,
|
|
235
|
+
ready,
|
|
236
|
+
timestamp: Date.now(),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
onCollectionMounted(parentId: number, parentName: string, itemComponentName: string, stateField: string | null): void {
|
|
241
|
+
const meta = this._components.get(parentId);
|
|
242
|
+
if (meta) {
|
|
243
|
+
(meta as any).collection = {itemComponent: itemComponentName, stateField};
|
|
244
|
+
}
|
|
245
|
+
if (!this.connected) return;
|
|
246
|
+
this._post('COLLECTION_MOUNTED', {
|
|
247
|
+
parentId,
|
|
248
|
+
parentName,
|
|
249
|
+
itemComponent: itemComponentName,
|
|
250
|
+
stateField,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
onComponentDisposed(componentNumber: number, name: string): void {
|
|
255
|
+
const meta = this._components.get(componentNumber);
|
|
256
|
+
if (meta) {
|
|
257
|
+
(meta as any).disposed = true;
|
|
258
|
+
(meta as any).disposedAt = Date.now();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.connected) return;
|
|
262
|
+
this._post('COMPONENT_DISPOSED', {
|
|
263
|
+
componentId: componentNumber,
|
|
264
|
+
componentName: name,
|
|
265
|
+
timestamp: Date.now(),
|
|
182
266
|
});
|
|
183
267
|
}
|
|
184
268
|
|
|
@@ -209,18 +293,78 @@ class SygnalDevTools {
|
|
|
209
293
|
}
|
|
210
294
|
}
|
|
211
295
|
|
|
212
|
-
_timeTravel({
|
|
213
|
-
|
|
214
|
-
|
|
296
|
+
_timeTravel({componentId, componentName, state}: {componentId: number; componentName: string; state: any}): void {
|
|
297
|
+
if (componentId == null || !state) {
|
|
298
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', {componentId, hasState: !!state});
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
215
301
|
|
|
216
302
|
if (typeof window === 'undefined') return;
|
|
303
|
+
|
|
304
|
+
const newState = this._safeClone(state);
|
|
305
|
+
|
|
306
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
307
|
+
const meta = this._components.get(componentId);
|
|
308
|
+
if (meta) {
|
|
309
|
+
const instance = meta._instanceRef?.deref();
|
|
310
|
+
if (!instance) {
|
|
311
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
312
|
+
} else {
|
|
313
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
314
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
315
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
316
|
+
if (stateSink?.shamefullySendNext) {
|
|
317
|
+
stateSink.shamefullySendNext(() => ({...newState}));
|
|
318
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
319
|
+
componentId,
|
|
320
|
+
componentName,
|
|
321
|
+
state: newState,
|
|
322
|
+
});
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
console.warn(`[Sygnal DevTools] _timeTravel: component #${componentId} (${componentName}) has no STATE sink with shamefullySendNext. sinkName=${stateSinkName}, hasSinks=${!!instance.sinks}, sinkKeys=${instance.sinks ? Object.keys(instance.sinks).join(',') : 'none'}`);
|
|
326
|
+
}
|
|
327
|
+
} else {
|
|
328
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Fall back to root STATE sink for root-level components
|
|
217
332
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
218
333
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
219
|
-
app.sinks.STATE.shamefullySendNext(() => ({...
|
|
334
|
+
app.sinks.STATE.shamefullySendNext(() => ({...newState}));
|
|
220
335
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
221
|
-
|
|
222
|
-
|
|
336
|
+
componentId,
|
|
337
|
+
componentName,
|
|
338
|
+
state: newState,
|
|
223
339
|
});
|
|
340
|
+
} else {
|
|
341
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
_takeSnapshot(): void {
|
|
346
|
+
const entries: {componentId: number; componentName: string; state: any}[] = [];
|
|
347
|
+
for (const [id, meta] of this._components) {
|
|
348
|
+
if ((meta as any).disposed) continue;
|
|
349
|
+
const instance = meta._instanceRef?.deref();
|
|
350
|
+
if (instance?.currentState != null) {
|
|
351
|
+
entries.push({
|
|
352
|
+
componentId: id,
|
|
353
|
+
componentName: meta.name,
|
|
354
|
+
state: this._safeClone(instance.currentState),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
359
|
+
entries,
|
|
360
|
+
timestamp: Date.now(),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_restoreSnapshot(snapshot: {entries: {componentId: number; componentName: string; state: any}[]}): void {
|
|
365
|
+
if (!snapshot?.entries) return;
|
|
366
|
+
for (const entry of snapshot.entries) {
|
|
367
|
+
this._timeTravel(entry);
|
|
224
368
|
}
|
|
225
369
|
}
|
|
226
370
|
|
|
@@ -233,12 +377,39 @@ class SygnalDevTools {
|
|
|
233
377
|
componentId,
|
|
234
378
|
state: this._safeClone(instance.currentState),
|
|
235
379
|
context: this._safeClone(instance.currentContext),
|
|
380
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
236
381
|
props: this._safeClone(instance.currentProps),
|
|
237
382
|
});
|
|
238
383
|
}
|
|
239
384
|
}
|
|
240
385
|
}
|
|
241
386
|
|
|
387
|
+
_buildContextTrace(componentId: number, context: any): {field: string; providerId: number; providerName: string}[] {
|
|
388
|
+
if (!context || typeof context !== 'object') return [];
|
|
389
|
+
const trace: {field: string; providerId: number; providerName: string}[] = [];
|
|
390
|
+
const fields = Object.keys(context);
|
|
391
|
+
|
|
392
|
+
for (const field of fields) {
|
|
393
|
+
// Walk up parent chain to find which component provides this field
|
|
394
|
+
let currentId: number | null = componentId;
|
|
395
|
+
let found = false;
|
|
396
|
+
while (currentId != null) {
|
|
397
|
+
const meta = this._components.get(currentId);
|
|
398
|
+
if (!meta) break;
|
|
399
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
400
|
+
trace.push({field, providerId: meta.id, providerName: meta.name});
|
|
401
|
+
found = true;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
currentId = meta.parentId;
|
|
405
|
+
}
|
|
406
|
+
if (!found) {
|
|
407
|
+
trace.push({field, providerId: -1, providerName: 'unknown'});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return trace;
|
|
411
|
+
}
|
|
412
|
+
|
|
242
413
|
_sendFullTree(): void {
|
|
243
414
|
const tree: any[] = [];
|
|
244
415
|
for (const [, meta] of this._components) {
|
|
@@ -276,6 +447,49 @@ class SygnalDevTools {
|
|
|
276
447
|
}
|
|
277
448
|
}
|
|
278
449
|
|
|
450
|
+
_extractMviGraph(instance: any): MviGraphData | null {
|
|
451
|
+
if (!instance.model) return null;
|
|
452
|
+
try {
|
|
453
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
454
|
+
const actions: {name: string; sinks: string[]}[] = [];
|
|
455
|
+
const model = instance.model;
|
|
456
|
+
|
|
457
|
+
for (const key of Object.keys(model)) {
|
|
458
|
+
let actionName = key;
|
|
459
|
+
let entry = model[key];
|
|
460
|
+
|
|
461
|
+
// Handle shorthand 'ACTION | SINK'
|
|
462
|
+
if (key.includes('|')) {
|
|
463
|
+
const parts = key.split('|').map((s: string) => s.trim());
|
|
464
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
465
|
+
actionName = parts[0];
|
|
466
|
+
actions.push({name: actionName, sinks: [parts[1]]});
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Function → targets STATE
|
|
472
|
+
if (typeof entry === 'function') {
|
|
473
|
+
actions.push({name: actionName, sinks: [instance.stateSourceName || 'STATE']});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Object → keys are sink names
|
|
478
|
+
if (entry && typeof entry === 'object') {
|
|
479
|
+
actions.push({name: actionName, sinks: Object.keys(entry)});
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
485
|
+
? Object.keys(instance.context) : [];
|
|
486
|
+
|
|
487
|
+
return {sources, actions, contextProvides};
|
|
488
|
+
} catch (e) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
279
493
|
_serializeMeta(meta: ComponentMeta): Omit<ComponentMeta, '_instanceRef'> {
|
|
280
494
|
const {_instanceRef, ...rest} = meta;
|
|
281
495
|
return rest;
|
package/src/extra/dragDriver.ts
CHANGED
|
@@ -8,19 +8,72 @@ export interface DragConfig {
|
|
|
8
8
|
dragImage?: string;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// ─── Enriched DND Event Streams ──────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
export interface EnrichedDragStream<T = any> extends Stream<T> {
|
|
14
|
+
/** Extract a `data-*` attribute value from the payload's element/dataset.
|
|
15
|
+
* Optional transform fn: `.data('id', Number)` */
|
|
16
|
+
data(name: string): EnrichedDragStream<string | undefined>;
|
|
17
|
+
data<R>(name: string, fn: (val: string | undefined) => R): EnrichedDragStream<R>;
|
|
18
|
+
|
|
19
|
+
/** Extract the dragged element (dragstart) or dropZone element (drop) */
|
|
20
|
+
element(): EnrichedDragStream<Element | null>;
|
|
21
|
+
element<R>(fn: (el: Element | null) => R): EnrichedDragStream<R>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Adds chainable convenience methods to a DND event stream,
|
|
26
|
+
* mirroring the DOM driver's `enrichEventStream` pattern.
|
|
27
|
+
*
|
|
28
|
+
* DND.dragstart('task').data('taskId')
|
|
29
|
+
* DND.dragstart('task').data('taskId', Number)
|
|
30
|
+
* DND.drop('lane').data('laneId')
|
|
31
|
+
* DND.dragstart('task').element()
|
|
32
|
+
*/
|
|
33
|
+
function enrichDragStream(stream$: any): any {
|
|
34
|
+
// .data(name, fn?) — extract dataset[name] from dragstart payload,
|
|
35
|
+
// or dropZone.dataset[name] from drop payload
|
|
36
|
+
stream$.data = function data(name: string, fn?: (val: any) => any): any {
|
|
37
|
+
const mapped = stream$.map((e: any) => {
|
|
38
|
+
// dragstart payload: { element, dataset }
|
|
39
|
+
// drop payload: { dropZone, insertBefore }
|
|
40
|
+
const val = e?.dataset?.[name]
|
|
41
|
+
?? (e?.dropZone as HTMLElement)?.dataset?.[name]
|
|
42
|
+
?? (e?.element as HTMLElement)?.dataset?.[name]
|
|
43
|
+
return fn ? fn(val) : val
|
|
44
|
+
});
|
|
45
|
+
return enrichDragStream(mapped);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// .element(fn?) — extract the primary element from the payload
|
|
49
|
+
stream$.element = function element(fn?: (el: any) => any): any {
|
|
50
|
+
const mapped = stream$.map((e: any) => {
|
|
51
|
+
const el = e?.element ?? e?.dropZone ?? null
|
|
52
|
+
return fn ? fn(el) : el
|
|
53
|
+
});
|
|
54
|
+
return enrichDragStream(mapped);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return stream$
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── DND Source Interfaces ───────────────────────────────────────────────────
|
|
61
|
+
|
|
11
62
|
export interface DragSourceEvents {
|
|
12
|
-
events(eventType: string):
|
|
63
|
+
events(eventType: string): EnrichedDragStream<any>;
|
|
13
64
|
}
|
|
14
65
|
|
|
15
66
|
export interface DragSource {
|
|
16
67
|
select(category: string): DragSourceEvents;
|
|
17
|
-
dragstart(category: string):
|
|
68
|
+
dragstart(category: string): EnrichedDragStream<any>;
|
|
18
69
|
dragend(category: string): Stream<any>;
|
|
19
|
-
drop(category: string):
|
|
70
|
+
drop(category: string): EnrichedDragStream<any>;
|
|
20
71
|
dragover(category: string): Stream<any>;
|
|
21
72
|
dispose(): void;
|
|
22
73
|
}
|
|
23
74
|
|
|
75
|
+
// ─── Driver Factory ──────────────────────────────────────────────────────────
|
|
76
|
+
|
|
24
77
|
export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
25
78
|
return function dragDriver(sink$: Stream<any>): DragSource {
|
|
26
79
|
const categories = new Map<string, DragConfig>();
|
|
@@ -114,10 +167,10 @@ export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
|
114
167
|
const source: DragSource = {
|
|
115
168
|
select(category: string): DragSourceEvents {
|
|
116
169
|
return {
|
|
117
|
-
events(eventType: string):
|
|
170
|
+
events(eventType: string): EnrichedDragStream<any> {
|
|
118
171
|
const busEventName = `${category}:${eventType}`;
|
|
119
172
|
let handler: ((e: Event) => void) | undefined;
|
|
120
|
-
|
|
173
|
+
const stream$ = xs.create({
|
|
121
174
|
start(listener) {
|
|
122
175
|
handler = ({detail}: any) => listener.next(detail);
|
|
123
176
|
bus.addEventListener(busEventName, handler);
|
|
@@ -126,6 +179,7 @@ export function makeDragDriver(): (sink$: Stream<any>) => DragSource {
|
|
|
126
179
|
if (handler) bus.removeEventListener(busEventName, handler);
|
|
127
180
|
},
|
|
128
181
|
});
|
|
182
|
+
return enrichDragStream(stream$);
|
|
129
183
|
},
|
|
130
184
|
};
|
|
131
185
|
},
|
package/src/extra/eventDriver.ts
CHANGED
|
@@ -14,8 +14,12 @@ export default function eventBusDriver(out$: Stream<BusEvent>): EventBusSource {
|
|
|
14
14
|
const events = new EventTarget();
|
|
15
15
|
|
|
16
16
|
out$.subscribe({
|
|
17
|
-
next: (event: BusEvent) =>
|
|
18
|
-
events.dispatchEvent(new CustomEvent('data', {detail: event}))
|
|
17
|
+
next: (event: BusEvent) => {
|
|
18
|
+
events.dispatchEvent(new CustomEvent('data', {detail: event}));
|
|
19
|
+
if (typeof window !== 'undefined' && (window as any).__SYGNAL_DEVTOOLS__?.connected) {
|
|
20
|
+
(window as any).__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
19
23
|
error: (err: any) =>
|
|
20
24
|
console.error('[EVENTS driver] Error in sink stream:', err),
|
|
21
25
|
});
|