sygnal 5.2.1 → 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 +20 -1
- package/dist/astro/client.cjs.js +242 -12
- package/dist/astro/client.mjs +242 -12
- package/dist/index.cjs.js +436 -15
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +434 -16
- package/dist/sygnal.min.js +1 -1
- package/package.json +1 -1
- package/src/component.ts +34 -4
- 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/index.d.ts +29 -0
- package/src/index.ts +1 -0
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ cd my-app
|
|
|
26
26
|
npm run dev
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
Choose from Vite (SPA), Vike (SSR), or Astro templates in JavaScript or TypeScript.
|
|
29
|
+
Choose from Vite (SPA), Vite + PWA, Vike (SSR), or Astro templates in JavaScript or TypeScript.
|
|
30
30
|
|
|
31
31
|
**Or add to an existing project:**
|
|
32
32
|
|
|
@@ -325,6 +325,25 @@ MyComponent.model = {
|
|
|
325
325
|
|
|
326
326
|
For advanced cases needing stream composition, the `dispose$` source is also available in intent.
|
|
327
327
|
|
|
328
|
+
### PWA Helpers
|
|
329
|
+
|
|
330
|
+
Built-in service worker driver, online/offline detection, and install prompt handling:
|
|
331
|
+
|
|
332
|
+
```jsx
|
|
333
|
+
import { run, makeServiceWorkerDriver, onlineStatus$, createInstallPrompt } from 'sygnal'
|
|
334
|
+
|
|
335
|
+
const installPrompt = createInstallPrompt()
|
|
336
|
+
|
|
337
|
+
run(App, { SW: makeServiceWorkerDriver('/sw.js') })
|
|
338
|
+
|
|
339
|
+
App.intent = ({ DOM, SW }) => ({
|
|
340
|
+
ONLINE_CHANGED: onlineStatus$(),
|
|
341
|
+
UPDATE_READY: SW.select('waiting'),
|
|
342
|
+
APPLY_UPDATE: DOM.click('.update-btn'),
|
|
343
|
+
INSTALL: DOM.click('.install-btn'),
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
328
347
|
### Testing
|
|
329
348
|
|
|
330
349
|
Test components in isolation with `renderComponent`:
|
package/dist/astro/client.cjs.js
CHANGED
|
@@ -5029,7 +5029,12 @@ function makeDOMDriver(container, options = {}) {
|
|
|
5029
5029
|
function eventBusDriver(out$) {
|
|
5030
5030
|
const events = new EventTarget();
|
|
5031
5031
|
out$.subscribe({
|
|
5032
|
-
next: (event) =>
|
|
5032
|
+
next: (event) => {
|
|
5033
|
+
events.dispatchEvent(new CustomEvent('data', { detail: event }));
|
|
5034
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5035
|
+
window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
5036
|
+
}
|
|
5037
|
+
},
|
|
5033
5038
|
error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
|
|
5034
5039
|
});
|
|
5035
5040
|
return {
|
|
@@ -5683,6 +5688,9 @@ class Component {
|
|
|
5683
5688
|
this.sources.props$ = props$.map((val) => {
|
|
5684
5689
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
|
|
5685
5690
|
this.currentProps = sanitizedProps;
|
|
5691
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5692
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
|
|
5693
|
+
}
|
|
5686
5694
|
return val;
|
|
5687
5695
|
});
|
|
5688
5696
|
}
|
|
@@ -5754,6 +5762,9 @@ class Component {
|
|
|
5754
5762
|
}
|
|
5755
5763
|
}
|
|
5756
5764
|
dispose() {
|
|
5765
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
5766
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
|
|
5767
|
+
}
|
|
5757
5768
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
5758
5769
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
|
|
5759
5770
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -6151,6 +6162,12 @@ class Component {
|
|
|
6151
6162
|
else {
|
|
6152
6163
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
|
|
6153
6164
|
}
|
|
6165
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
6166
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
6167
|
+
const _componentNumber = this._componentNumber;
|
|
6168
|
+
const _name = this.name;
|
|
6169
|
+
acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
|
|
6170
|
+
}
|
|
6154
6171
|
return acc;
|
|
6155
6172
|
}, {});
|
|
6156
6173
|
this.sinks[this.DOMSourceName] = this.vdom$;
|
|
@@ -6673,6 +6690,13 @@ class Component {
|
|
|
6673
6690
|
if (!isObj(sink$)) {
|
|
6674
6691
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
|
|
6675
6692
|
}
|
|
6693
|
+
// Notify devtools of collection mount
|
|
6694
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
6695
|
+
const itemName = typeof collectionOf === 'function'
|
|
6696
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
6697
|
+
: String(collectionOf);
|
|
6698
|
+
window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
|
|
6699
|
+
}
|
|
6676
6700
|
return sink$;
|
|
6677
6701
|
}
|
|
6678
6702
|
instantiateSwitchable(el, props$, children$) {
|
|
@@ -6811,6 +6835,7 @@ class Component {
|
|
|
6811
6835
|
for (const key of Object.keys(props)) {
|
|
6812
6836
|
const val = props[key];
|
|
6813
6837
|
if (val && val.__sygnalCommand) {
|
|
6838
|
+
val._targetComponentName = componentName;
|
|
6814
6839
|
sources.commands$ = makeCommandSource(val);
|
|
6815
6840
|
break;
|
|
6816
6841
|
}
|
|
@@ -6853,10 +6878,15 @@ class Component {
|
|
|
6853
6878
|
const wasReady = this._childReadyState[id];
|
|
6854
6879
|
this._childReadyState[id] = !!ready;
|
|
6855
6880
|
// When READY state changes, trigger a re-render
|
|
6856
|
-
if (wasReady !== !!ready
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6881
|
+
if (wasReady !== !!ready) {
|
|
6882
|
+
if (this._readyChangedListener) {
|
|
6883
|
+
setTimeout(() => {
|
|
6884
|
+
this._readyChangedListener?.next(null);
|
|
6885
|
+
}, 0);
|
|
6886
|
+
}
|
|
6887
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
6888
|
+
window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
|
|
6889
|
+
}
|
|
6860
6890
|
}
|
|
6861
6891
|
},
|
|
6862
6892
|
error: () => { },
|
|
@@ -7492,6 +7522,12 @@ class SygnalDevTools {
|
|
|
7492
7522
|
case 'TIME_TRAVEL':
|
|
7493
7523
|
this._timeTravel(msg.payload);
|
|
7494
7524
|
break;
|
|
7525
|
+
case 'SNAPSHOT':
|
|
7526
|
+
this._takeSnapshot();
|
|
7527
|
+
break;
|
|
7528
|
+
case 'RESTORE_SNAPSHOT':
|
|
7529
|
+
this._restoreSnapshot(msg.payload);
|
|
7530
|
+
break;
|
|
7495
7531
|
case 'GET_STATE':
|
|
7496
7532
|
this._sendComponentState(msg.payload.componentId);
|
|
7497
7533
|
break;
|
|
@@ -7511,6 +7547,7 @@ class SygnalDevTools {
|
|
|
7511
7547
|
children: [],
|
|
7512
7548
|
debug: instance._debug,
|
|
7513
7549
|
createdAt: Date.now(),
|
|
7550
|
+
mviGraph: this._extractMviGraph(instance),
|
|
7514
7551
|
_instanceRef: new WeakRef(instance),
|
|
7515
7552
|
};
|
|
7516
7553
|
this._components.set(componentNumber, meta);
|
|
@@ -7535,7 +7572,6 @@ class SygnalDevTools {
|
|
|
7535
7572
|
componentId: componentNumber,
|
|
7536
7573
|
componentName: name,
|
|
7537
7574
|
state: entry.state,
|
|
7538
|
-
historyIndex: this._stateHistory.length - 1,
|
|
7539
7575
|
});
|
|
7540
7576
|
}
|
|
7541
7577
|
onActionDispatched(componentNumber, name, actionType, data) {
|
|
@@ -7572,6 +7608,76 @@ class SygnalDevTools {
|
|
|
7572
7608
|
componentId: componentNumber,
|
|
7573
7609
|
componentName: name,
|
|
7574
7610
|
context: this._safeClone(context),
|
|
7611
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
7612
|
+
});
|
|
7613
|
+
}
|
|
7614
|
+
onPropsChanged(componentNumber, name, props) {
|
|
7615
|
+
if (!this.connected)
|
|
7616
|
+
return;
|
|
7617
|
+
this._post('PROPS_CHANGED', {
|
|
7618
|
+
componentId: componentNumber,
|
|
7619
|
+
componentName: name,
|
|
7620
|
+
props: this._safeClone(props),
|
|
7621
|
+
});
|
|
7622
|
+
}
|
|
7623
|
+
onBusEvent(event) {
|
|
7624
|
+
if (!this.connected)
|
|
7625
|
+
return;
|
|
7626
|
+
this._post('BUS_EVENT', {
|
|
7627
|
+
type: event.type,
|
|
7628
|
+
data: this._safeClone(event.data),
|
|
7629
|
+
componentId: event.__emitterId ?? null,
|
|
7630
|
+
componentName: event.__emitterName ?? null,
|
|
7631
|
+
timestamp: Date.now(),
|
|
7632
|
+
});
|
|
7633
|
+
}
|
|
7634
|
+
onCommandSent(type, data, targetComponentId, targetComponentName) {
|
|
7635
|
+
if (!this.connected)
|
|
7636
|
+
return;
|
|
7637
|
+
this._post('COMMAND_SENT', {
|
|
7638
|
+
type,
|
|
7639
|
+
data: this._safeClone(data),
|
|
7640
|
+
targetComponentName: targetComponentName ?? null,
|
|
7641
|
+
timestamp: Date.now(),
|
|
7642
|
+
});
|
|
7643
|
+
}
|
|
7644
|
+
onReadyChanged(parentId, parentName, childKey, ready) {
|
|
7645
|
+
if (!this.connected)
|
|
7646
|
+
return;
|
|
7647
|
+
this._post('READY_CHANGED', {
|
|
7648
|
+
parentId,
|
|
7649
|
+
parentName,
|
|
7650
|
+
childKey,
|
|
7651
|
+
ready,
|
|
7652
|
+
timestamp: Date.now(),
|
|
7653
|
+
});
|
|
7654
|
+
}
|
|
7655
|
+
onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
|
|
7656
|
+
const meta = this._components.get(parentId);
|
|
7657
|
+
if (meta) {
|
|
7658
|
+
meta.collection = { itemComponent: itemComponentName, stateField };
|
|
7659
|
+
}
|
|
7660
|
+
if (!this.connected)
|
|
7661
|
+
return;
|
|
7662
|
+
this._post('COLLECTION_MOUNTED', {
|
|
7663
|
+
parentId,
|
|
7664
|
+
parentName,
|
|
7665
|
+
itemComponent: itemComponentName,
|
|
7666
|
+
stateField,
|
|
7667
|
+
});
|
|
7668
|
+
}
|
|
7669
|
+
onComponentDisposed(componentNumber, name) {
|
|
7670
|
+
const meta = this._components.get(componentNumber);
|
|
7671
|
+
if (meta) {
|
|
7672
|
+
meta.disposed = true;
|
|
7673
|
+
meta.disposedAt = Date.now();
|
|
7674
|
+
}
|
|
7675
|
+
if (!this.connected)
|
|
7676
|
+
return;
|
|
7677
|
+
this._post('COMPONENT_DISPOSED', {
|
|
7678
|
+
componentId: componentNumber,
|
|
7679
|
+
componentName: name,
|
|
7680
|
+
timestamp: Date.now(),
|
|
7575
7681
|
});
|
|
7576
7682
|
}
|
|
7577
7683
|
onDebugLog(componentNumber, message) {
|
|
@@ -7600,20 +7706,79 @@ class SygnalDevTools {
|
|
|
7600
7706
|
}
|
|
7601
7707
|
}
|
|
7602
7708
|
}
|
|
7603
|
-
_timeTravel({
|
|
7604
|
-
|
|
7605
|
-
|
|
7709
|
+
_timeTravel({ componentId, componentName, state }) {
|
|
7710
|
+
if (componentId == null || !state) {
|
|
7711
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
|
|
7606
7712
|
return;
|
|
7713
|
+
}
|
|
7607
7714
|
if (typeof window === 'undefined')
|
|
7608
7715
|
return;
|
|
7716
|
+
const newState = this._safeClone(state);
|
|
7717
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
7718
|
+
const meta = this._components.get(componentId);
|
|
7719
|
+
if (meta) {
|
|
7720
|
+
const instance = meta._instanceRef?.deref();
|
|
7721
|
+
if (!instance) {
|
|
7722
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
7723
|
+
}
|
|
7724
|
+
else {
|
|
7725
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
7726
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
7727
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
7728
|
+
if (stateSink?.shamefullySendNext) {
|
|
7729
|
+
stateSink.shamefullySendNext(() => ({ ...newState }));
|
|
7730
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
7731
|
+
componentId,
|
|
7732
|
+
componentName,
|
|
7733
|
+
state: newState,
|
|
7734
|
+
});
|
|
7735
|
+
return;
|
|
7736
|
+
}
|
|
7737
|
+
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'}`);
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
else {
|
|
7741
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
7742
|
+
}
|
|
7743
|
+
// Fall back to root STATE sink for root-level components
|
|
7609
7744
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
7610
7745
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
7611
|
-
app.sinks.STATE.shamefullySendNext(() => ({ ...
|
|
7746
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
|
|
7612
7747
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
7613
|
-
|
|
7614
|
-
|
|
7748
|
+
componentId,
|
|
7749
|
+
componentName,
|
|
7750
|
+
state: newState,
|
|
7615
7751
|
});
|
|
7616
7752
|
}
|
|
7753
|
+
else {
|
|
7754
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
7755
|
+
}
|
|
7756
|
+
}
|
|
7757
|
+
_takeSnapshot() {
|
|
7758
|
+
const entries = [];
|
|
7759
|
+
for (const [id, meta] of this._components) {
|
|
7760
|
+
if (meta.disposed)
|
|
7761
|
+
continue;
|
|
7762
|
+
const instance = meta._instanceRef?.deref();
|
|
7763
|
+
if (instance?.currentState != null) {
|
|
7764
|
+
entries.push({
|
|
7765
|
+
componentId: id,
|
|
7766
|
+
componentName: meta.name,
|
|
7767
|
+
state: this._safeClone(instance.currentState),
|
|
7768
|
+
});
|
|
7769
|
+
}
|
|
7770
|
+
}
|
|
7771
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
7772
|
+
entries,
|
|
7773
|
+
timestamp: Date.now(),
|
|
7774
|
+
});
|
|
7775
|
+
}
|
|
7776
|
+
_restoreSnapshot(snapshot) {
|
|
7777
|
+
if (!snapshot?.entries)
|
|
7778
|
+
return;
|
|
7779
|
+
for (const entry of snapshot.entries) {
|
|
7780
|
+
this._timeTravel(entry);
|
|
7781
|
+
}
|
|
7617
7782
|
}
|
|
7618
7783
|
_sendComponentState(componentId) {
|
|
7619
7784
|
const meta = this._components.get(componentId);
|
|
@@ -7624,11 +7789,38 @@ class SygnalDevTools {
|
|
|
7624
7789
|
componentId,
|
|
7625
7790
|
state: this._safeClone(instance.currentState),
|
|
7626
7791
|
context: this._safeClone(instance.currentContext),
|
|
7792
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
7627
7793
|
props: this._safeClone(instance.currentProps),
|
|
7628
7794
|
});
|
|
7629
7795
|
}
|
|
7630
7796
|
}
|
|
7631
7797
|
}
|
|
7798
|
+
_buildContextTrace(componentId, context) {
|
|
7799
|
+
if (!context || typeof context !== 'object')
|
|
7800
|
+
return [];
|
|
7801
|
+
const trace = [];
|
|
7802
|
+
const fields = Object.keys(context);
|
|
7803
|
+
for (const field of fields) {
|
|
7804
|
+
// Walk up parent chain to find which component provides this field
|
|
7805
|
+
let currentId = componentId;
|
|
7806
|
+
let found = false;
|
|
7807
|
+
while (currentId != null) {
|
|
7808
|
+
const meta = this._components.get(currentId);
|
|
7809
|
+
if (!meta)
|
|
7810
|
+
break;
|
|
7811
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
7812
|
+
trace.push({ field, providerId: meta.id, providerName: meta.name });
|
|
7813
|
+
found = true;
|
|
7814
|
+
break;
|
|
7815
|
+
}
|
|
7816
|
+
currentId = meta.parentId;
|
|
7817
|
+
}
|
|
7818
|
+
if (!found) {
|
|
7819
|
+
trace.push({ field, providerId: -1, providerName: 'unknown' });
|
|
7820
|
+
}
|
|
7821
|
+
}
|
|
7822
|
+
return trace;
|
|
7823
|
+
}
|
|
7632
7824
|
_sendFullTree() {
|
|
7633
7825
|
const tree = [];
|
|
7634
7826
|
for (const [, meta] of this._components) {
|
|
@@ -7663,6 +7855,44 @@ class SygnalDevTools {
|
|
|
7663
7855
|
return '[unserializable]';
|
|
7664
7856
|
}
|
|
7665
7857
|
}
|
|
7858
|
+
_extractMviGraph(instance) {
|
|
7859
|
+
if (!instance.model)
|
|
7860
|
+
return null;
|
|
7861
|
+
try {
|
|
7862
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
7863
|
+
const actions = [];
|
|
7864
|
+
const model = instance.model;
|
|
7865
|
+
for (const key of Object.keys(model)) {
|
|
7866
|
+
let actionName = key;
|
|
7867
|
+
let entry = model[key];
|
|
7868
|
+
// Handle shorthand 'ACTION | SINK'
|
|
7869
|
+
if (key.includes('|')) {
|
|
7870
|
+
const parts = key.split('|').map((s) => s.trim());
|
|
7871
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
7872
|
+
actionName = parts[0];
|
|
7873
|
+
actions.push({ name: actionName, sinks: [parts[1]] });
|
|
7874
|
+
continue;
|
|
7875
|
+
}
|
|
7876
|
+
}
|
|
7877
|
+
// Function → targets STATE
|
|
7878
|
+
if (typeof entry === 'function') {
|
|
7879
|
+
actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
|
|
7880
|
+
continue;
|
|
7881
|
+
}
|
|
7882
|
+
// Object → keys are sink names
|
|
7883
|
+
if (entry && typeof entry === 'object') {
|
|
7884
|
+
actions.push({ name: actionName, sinks: Object.keys(entry) });
|
|
7885
|
+
continue;
|
|
7886
|
+
}
|
|
7887
|
+
}
|
|
7888
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
7889
|
+
? Object.keys(instance.context) : [];
|
|
7890
|
+
return { sources, actions, contextProvides };
|
|
7891
|
+
}
|
|
7892
|
+
catch (e) {
|
|
7893
|
+
return null;
|
|
7894
|
+
}
|
|
7895
|
+
}
|
|
7666
7896
|
_serializeMeta(meta) {
|
|
7667
7897
|
const { _instanceRef, ...rest } = meta;
|
|
7668
7898
|
return rest;
|