sygnal 5.2.1 → 5.3.1
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 +437 -15
- package/dist/index.d.ts +29 -0
- package/dist/index.esm.js +435 -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 +181 -0
- package/src/index.d.ts +29 -0
- package/src/index.ts +1 -0
package/dist/index.cjs.js
CHANGED
|
@@ -2163,11 +2163,17 @@ function createCommand() {
|
|
|
2163
2163
|
start(l) { listener.next = (val) => l.next(val); },
|
|
2164
2164
|
stop() { listener.next = () => { }; },
|
|
2165
2165
|
});
|
|
2166
|
-
|
|
2167
|
-
send: (type, data) =>
|
|
2166
|
+
const cmd = {
|
|
2167
|
+
send: (type, data) => {
|
|
2168
|
+
listener.next({ type, data });
|
|
2169
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2170
|
+
window.__SYGNAL_DEVTOOLS__.onCommandSent(type, data, cmd._targetComponentId, cmd._targetComponentName);
|
|
2171
|
+
}
|
|
2172
|
+
},
|
|
2168
2173
|
_stream,
|
|
2169
2174
|
__sygnalCommand: true,
|
|
2170
2175
|
};
|
|
2176
|
+
return cmd;
|
|
2171
2177
|
}
|
|
2172
2178
|
function makeCommandSource(cmd) {
|
|
2173
2179
|
return {
|
|
@@ -2421,6 +2427,9 @@ class Component {
|
|
|
2421
2427
|
this.sources.props$ = props$.map((val) => {
|
|
2422
2428
|
const { sygnalFactory, sygnalOptions, ...sanitizedProps } = val;
|
|
2423
2429
|
this.currentProps = sanitizedProps;
|
|
2430
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2431
|
+
window.__SYGNAL_DEVTOOLS__.onPropsChanged(this._componentNumber, this.name, sanitizedProps);
|
|
2432
|
+
}
|
|
2424
2433
|
return val;
|
|
2425
2434
|
});
|
|
2426
2435
|
}
|
|
@@ -2492,6 +2501,9 @@ class Component {
|
|
|
2492
2501
|
}
|
|
2493
2502
|
}
|
|
2494
2503
|
dispose() {
|
|
2504
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
2505
|
+
window.__SYGNAL_DEVTOOLS__.onComponentDisposed(this._componentNumber, this.name);
|
|
2506
|
+
}
|
|
2495
2507
|
// Fire the DISPOSE built-in action so model handlers can run cleanup logic
|
|
2496
2508
|
const hasDispose = this.model && (this.model[DISPOSE_ACTION] || Object.keys(this.model).some(k => k.includes('|') && k.split('|')[0].trim() === DISPOSE_ACTION));
|
|
2497
2509
|
if (hasDispose && this.action$ && typeof this.action$.shamefullySendNext === 'function') {
|
|
@@ -2889,6 +2901,12 @@ class Component {
|
|
|
2889
2901
|
else {
|
|
2890
2902
|
acc[name] = xs.merge((this.model$[name] || xs.never()), subComponentSink$, ...(this.peers$[name] || []));
|
|
2891
2903
|
}
|
|
2904
|
+
// Stamp EVENTS sink emissions with emitter component info for devtools
|
|
2905
|
+
if (name === 'EVENTS' && acc[name]) {
|
|
2906
|
+
const _componentNumber = this._componentNumber;
|
|
2907
|
+
const _name = this.name;
|
|
2908
|
+
acc[name] = acc[name].map((ev) => ({ ...ev, __emitterId: _componentNumber, __emitterName: _name }));
|
|
2909
|
+
}
|
|
2892
2910
|
return acc;
|
|
2893
2911
|
}, {});
|
|
2894
2912
|
this.sinks[this.DOMSourceName] = this.vdom$;
|
|
@@ -3411,6 +3429,13 @@ class Component {
|
|
|
3411
3429
|
if (!isObj(sink$)) {
|
|
3412
3430
|
throw new Error(`[${this.name}] Invalid sinks returned from component factory of collection element`);
|
|
3413
3431
|
}
|
|
3432
|
+
// Notify devtools of collection mount
|
|
3433
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3434
|
+
const itemName = typeof collectionOf === 'function'
|
|
3435
|
+
? (collectionOf.componentName || collectionOf.label || collectionOf.name || 'anonymous')
|
|
3436
|
+
: String(collectionOf);
|
|
3437
|
+
window.__SYGNAL_DEVTOOLS__.onCollectionMounted(this._componentNumber, this.name, itemName, typeof stateField === 'string' ? stateField : null);
|
|
3438
|
+
}
|
|
3414
3439
|
return sink$;
|
|
3415
3440
|
}
|
|
3416
3441
|
instantiateSwitchable(el, props$, children$) {
|
|
@@ -3549,6 +3574,7 @@ class Component {
|
|
|
3549
3574
|
for (const key of Object.keys(props)) {
|
|
3550
3575
|
const val = props[key];
|
|
3551
3576
|
if (val && val.__sygnalCommand) {
|
|
3577
|
+
val._targetComponentName = componentName;
|
|
3552
3578
|
sources.commands$ = makeCommandSource(val);
|
|
3553
3579
|
break;
|
|
3554
3580
|
}
|
|
@@ -3591,10 +3617,15 @@ class Component {
|
|
|
3591
3617
|
const wasReady = this._childReadyState[id];
|
|
3592
3618
|
this._childReadyState[id] = !!ready;
|
|
3593
3619
|
// When READY state changes, trigger a re-render
|
|
3594
|
-
if (wasReady !== !!ready
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3620
|
+
if (wasReady !== !!ready) {
|
|
3621
|
+
if (this._readyChangedListener) {
|
|
3622
|
+
setTimeout(() => {
|
|
3623
|
+
this._readyChangedListener?.next(null);
|
|
3624
|
+
}, 0);
|
|
3625
|
+
}
|
|
3626
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
3627
|
+
window.__SYGNAL_DEVTOOLS__.onReadyChanged(this._componentNumber, this.name, id, !!ready);
|
|
3628
|
+
}
|
|
3598
3629
|
}
|
|
3599
3630
|
},
|
|
3600
3631
|
error: () => { },
|
|
@@ -4435,6 +4466,40 @@ function processDrag({ draggable, dropZone } = {}, options = {}) {
|
|
|
4435
4466
|
return { dragStart$, dragEnd$, dragOver$, drop$ };
|
|
4436
4467
|
}
|
|
4437
4468
|
|
|
4469
|
+
/**
|
|
4470
|
+
* Adds chainable convenience methods to a DND event stream,
|
|
4471
|
+
* mirroring the DOM driver's `enrichEventStream` pattern.
|
|
4472
|
+
*
|
|
4473
|
+
* DND.dragstart('task').data('taskId')
|
|
4474
|
+
* DND.dragstart('task').data('taskId', Number)
|
|
4475
|
+
* DND.drop('lane').data('laneId')
|
|
4476
|
+
* DND.dragstart('task').element()
|
|
4477
|
+
*/
|
|
4478
|
+
function enrichDragStream(stream$) {
|
|
4479
|
+
// .data(name, fn?) — extract dataset[name] from dragstart payload,
|
|
4480
|
+
// or dropZone.dataset[name] from drop payload
|
|
4481
|
+
stream$.data = function data(name, fn) {
|
|
4482
|
+
const mapped = stream$.map((e) => {
|
|
4483
|
+
// dragstart payload: { element, dataset }
|
|
4484
|
+
// drop payload: { dropZone, insertBefore }
|
|
4485
|
+
const val = e?.dataset?.[name]
|
|
4486
|
+
?? e?.dropZone?.dataset?.[name]
|
|
4487
|
+
?? e?.element?.dataset?.[name];
|
|
4488
|
+
return fn ? fn(val) : val;
|
|
4489
|
+
});
|
|
4490
|
+
return enrichDragStream(mapped);
|
|
4491
|
+
};
|
|
4492
|
+
// .element(fn?) — extract the primary element from the payload
|
|
4493
|
+
stream$.element = function element(fn) {
|
|
4494
|
+
const mapped = stream$.map((e) => {
|
|
4495
|
+
const el = e?.element ?? e?.dropZone ?? null;
|
|
4496
|
+
return fn ? fn(el) : el;
|
|
4497
|
+
});
|
|
4498
|
+
return enrichDragStream(mapped);
|
|
4499
|
+
};
|
|
4500
|
+
return stream$;
|
|
4501
|
+
}
|
|
4502
|
+
// ─── Driver Factory ──────────────────────────────────────────────────────────
|
|
4438
4503
|
function makeDragDriver() {
|
|
4439
4504
|
return function dragDriver(sink$) {
|
|
4440
4505
|
const categories = new Map();
|
|
@@ -4526,7 +4591,7 @@ function makeDragDriver() {
|
|
|
4526
4591
|
events(eventType) {
|
|
4527
4592
|
const busEventName = `${category}:${eventType}`;
|
|
4528
4593
|
let handler;
|
|
4529
|
-
|
|
4594
|
+
const stream$ = xs$1.create({
|
|
4530
4595
|
start(listener) {
|
|
4531
4596
|
handler = ({ detail }) => listener.next(detail);
|
|
4532
4597
|
bus.addEventListener(busEventName, handler);
|
|
@@ -4536,6 +4601,7 @@ function makeDragDriver() {
|
|
|
4536
4601
|
bus.removeEventListener(busEventName, handler);
|
|
4537
4602
|
},
|
|
4538
4603
|
});
|
|
4604
|
+
return enrichDragStream(stream$);
|
|
4539
4605
|
},
|
|
4540
4606
|
};
|
|
4541
4607
|
},
|
|
@@ -4715,7 +4781,12 @@ function setupReusable(drivers) {
|
|
|
4715
4781
|
function eventBusDriver(out$) {
|
|
4716
4782
|
const events = new EventTarget();
|
|
4717
4783
|
out$.subscribe({
|
|
4718
|
-
next: (event) =>
|
|
4784
|
+
next: (event) => {
|
|
4785
|
+
events.dispatchEvent(new CustomEvent('data', { detail: event }));
|
|
4786
|
+
if (typeof window !== 'undefined' && window.__SYGNAL_DEVTOOLS__?.connected) {
|
|
4787
|
+
window.__SYGNAL_DEVTOOLS__.onBusEvent(event);
|
|
4788
|
+
}
|
|
4789
|
+
},
|
|
4719
4790
|
error: (err) => console.error('[EVENTS driver] Error in sink stream:', err),
|
|
4720
4791
|
});
|
|
4721
4792
|
return {
|
|
@@ -4795,6 +4866,12 @@ class SygnalDevTools {
|
|
|
4795
4866
|
case 'TIME_TRAVEL':
|
|
4796
4867
|
this._timeTravel(msg.payload);
|
|
4797
4868
|
break;
|
|
4869
|
+
case 'SNAPSHOT':
|
|
4870
|
+
this._takeSnapshot();
|
|
4871
|
+
break;
|
|
4872
|
+
case 'RESTORE_SNAPSHOT':
|
|
4873
|
+
this._restoreSnapshot(msg.payload);
|
|
4874
|
+
break;
|
|
4798
4875
|
case 'GET_STATE':
|
|
4799
4876
|
this._sendComponentState(msg.payload.componentId);
|
|
4800
4877
|
break;
|
|
@@ -4814,6 +4891,7 @@ class SygnalDevTools {
|
|
|
4814
4891
|
children: [],
|
|
4815
4892
|
debug: instance._debug,
|
|
4816
4893
|
createdAt: Date.now(),
|
|
4894
|
+
mviGraph: this._extractMviGraph(instance),
|
|
4817
4895
|
_instanceRef: new WeakRef(instance),
|
|
4818
4896
|
};
|
|
4819
4897
|
this._components.set(componentNumber, meta);
|
|
@@ -4838,7 +4916,6 @@ class SygnalDevTools {
|
|
|
4838
4916
|
componentId: componentNumber,
|
|
4839
4917
|
componentName: name,
|
|
4840
4918
|
state: entry.state,
|
|
4841
|
-
historyIndex: this._stateHistory.length - 1,
|
|
4842
4919
|
});
|
|
4843
4920
|
}
|
|
4844
4921
|
onActionDispatched(componentNumber, name, actionType, data) {
|
|
@@ -4875,6 +4952,76 @@ class SygnalDevTools {
|
|
|
4875
4952
|
componentId: componentNumber,
|
|
4876
4953
|
componentName: name,
|
|
4877
4954
|
context: this._safeClone(context),
|
|
4955
|
+
contextTrace: this._buildContextTrace(componentNumber, context),
|
|
4956
|
+
});
|
|
4957
|
+
}
|
|
4958
|
+
onPropsChanged(componentNumber, name, props) {
|
|
4959
|
+
if (!this.connected)
|
|
4960
|
+
return;
|
|
4961
|
+
this._post('PROPS_CHANGED', {
|
|
4962
|
+
componentId: componentNumber,
|
|
4963
|
+
componentName: name,
|
|
4964
|
+
props: this._safeClone(props),
|
|
4965
|
+
});
|
|
4966
|
+
}
|
|
4967
|
+
onBusEvent(event) {
|
|
4968
|
+
if (!this.connected)
|
|
4969
|
+
return;
|
|
4970
|
+
this._post('BUS_EVENT', {
|
|
4971
|
+
type: event.type,
|
|
4972
|
+
data: this._safeClone(event.data),
|
|
4973
|
+
componentId: event.__emitterId ?? null,
|
|
4974
|
+
componentName: event.__emitterName ?? null,
|
|
4975
|
+
timestamp: Date.now(),
|
|
4976
|
+
});
|
|
4977
|
+
}
|
|
4978
|
+
onCommandSent(type, data, targetComponentId, targetComponentName) {
|
|
4979
|
+
if (!this.connected)
|
|
4980
|
+
return;
|
|
4981
|
+
this._post('COMMAND_SENT', {
|
|
4982
|
+
type,
|
|
4983
|
+
data: this._safeClone(data),
|
|
4984
|
+
targetComponentName: targetComponentName ?? null,
|
|
4985
|
+
timestamp: Date.now(),
|
|
4986
|
+
});
|
|
4987
|
+
}
|
|
4988
|
+
onReadyChanged(parentId, parentName, childKey, ready) {
|
|
4989
|
+
if (!this.connected)
|
|
4990
|
+
return;
|
|
4991
|
+
this._post('READY_CHANGED', {
|
|
4992
|
+
parentId,
|
|
4993
|
+
parentName,
|
|
4994
|
+
childKey,
|
|
4995
|
+
ready,
|
|
4996
|
+
timestamp: Date.now(),
|
|
4997
|
+
});
|
|
4998
|
+
}
|
|
4999
|
+
onCollectionMounted(parentId, parentName, itemComponentName, stateField) {
|
|
5000
|
+
const meta = this._components.get(parentId);
|
|
5001
|
+
if (meta) {
|
|
5002
|
+
meta.collection = { itemComponent: itemComponentName, stateField };
|
|
5003
|
+
}
|
|
5004
|
+
if (!this.connected)
|
|
5005
|
+
return;
|
|
5006
|
+
this._post('COLLECTION_MOUNTED', {
|
|
5007
|
+
parentId,
|
|
5008
|
+
parentName,
|
|
5009
|
+
itemComponent: itemComponentName,
|
|
5010
|
+
stateField,
|
|
5011
|
+
});
|
|
5012
|
+
}
|
|
5013
|
+
onComponentDisposed(componentNumber, name) {
|
|
5014
|
+
const meta = this._components.get(componentNumber);
|
|
5015
|
+
if (meta) {
|
|
5016
|
+
meta.disposed = true;
|
|
5017
|
+
meta.disposedAt = Date.now();
|
|
5018
|
+
}
|
|
5019
|
+
if (!this.connected)
|
|
5020
|
+
return;
|
|
5021
|
+
this._post('COMPONENT_DISPOSED', {
|
|
5022
|
+
componentId: componentNumber,
|
|
5023
|
+
componentName: name,
|
|
5024
|
+
timestamp: Date.now(),
|
|
4878
5025
|
});
|
|
4879
5026
|
}
|
|
4880
5027
|
onDebugLog(componentNumber, message) {
|
|
@@ -4903,20 +5050,79 @@ class SygnalDevTools {
|
|
|
4903
5050
|
}
|
|
4904
5051
|
}
|
|
4905
5052
|
}
|
|
4906
|
-
_timeTravel({
|
|
4907
|
-
|
|
4908
|
-
|
|
5053
|
+
_timeTravel({ componentId, componentName, state }) {
|
|
5054
|
+
if (componentId == null || !state) {
|
|
5055
|
+
console.warn('[Sygnal DevTools] _timeTravel: missing componentId or state', { componentId, hasState: !!state });
|
|
4909
5056
|
return;
|
|
5057
|
+
}
|
|
4910
5058
|
if (typeof window === 'undefined')
|
|
4911
5059
|
return;
|
|
5060
|
+
const newState = this._safeClone(state);
|
|
5061
|
+
// Try per-component time-travel via the component's STATE sink (reducer stream)
|
|
5062
|
+
const meta = this._components.get(componentId);
|
|
5063
|
+
if (meta) {
|
|
5064
|
+
const instance = meta._instanceRef?.deref();
|
|
5065
|
+
if (!instance) {
|
|
5066
|
+
console.warn(`[Sygnal DevTools] _timeTravel: WeakRef for component #${componentId} (${componentName}) has been GC'd`);
|
|
5067
|
+
}
|
|
5068
|
+
else {
|
|
5069
|
+
// sinks[stateSourceName] is the reducer stream — push a reducer that replaces state
|
|
5070
|
+
const stateSinkName = instance.stateSourceName || 'STATE';
|
|
5071
|
+
const stateSink = instance.sinks?.[stateSinkName];
|
|
5072
|
+
if (stateSink?.shamefullySendNext) {
|
|
5073
|
+
stateSink.shamefullySendNext(() => ({ ...newState }));
|
|
5074
|
+
this._post('TIME_TRAVEL_APPLIED', {
|
|
5075
|
+
componentId,
|
|
5076
|
+
componentName,
|
|
5077
|
+
state: newState,
|
|
5078
|
+
});
|
|
5079
|
+
return;
|
|
5080
|
+
}
|
|
5081
|
+
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'}`);
|
|
5082
|
+
}
|
|
5083
|
+
}
|
|
5084
|
+
else {
|
|
5085
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no meta for componentId ${componentId}`);
|
|
5086
|
+
}
|
|
5087
|
+
// Fall back to root STATE sink for root-level components
|
|
4912
5088
|
const app = window.__SYGNAL_DEVTOOLS_APP__;
|
|
4913
5089
|
if (app?.sinks?.STATE?.shamefullySendNext) {
|
|
4914
|
-
app.sinks.STATE.shamefullySendNext(() => ({ ...
|
|
5090
|
+
app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
|
|
4915
5091
|
this._post('TIME_TRAVEL_APPLIED', {
|
|
4916
|
-
|
|
4917
|
-
|
|
5092
|
+
componentId,
|
|
5093
|
+
componentName,
|
|
5094
|
+
state: newState,
|
|
4918
5095
|
});
|
|
4919
5096
|
}
|
|
5097
|
+
else {
|
|
5098
|
+
console.warn(`[Sygnal DevTools] _timeTravel: no fallback root STATE sink available`);
|
|
5099
|
+
}
|
|
5100
|
+
}
|
|
5101
|
+
_takeSnapshot() {
|
|
5102
|
+
const entries = [];
|
|
5103
|
+
for (const [id, meta] of this._components) {
|
|
5104
|
+
if (meta.disposed)
|
|
5105
|
+
continue;
|
|
5106
|
+
const instance = meta._instanceRef?.deref();
|
|
5107
|
+
if (instance?.currentState != null) {
|
|
5108
|
+
entries.push({
|
|
5109
|
+
componentId: id,
|
|
5110
|
+
componentName: meta.name,
|
|
5111
|
+
state: this._safeClone(instance.currentState),
|
|
5112
|
+
});
|
|
5113
|
+
}
|
|
5114
|
+
}
|
|
5115
|
+
this._post('SNAPSHOT_TAKEN', {
|
|
5116
|
+
entries,
|
|
5117
|
+
timestamp: Date.now(),
|
|
5118
|
+
});
|
|
5119
|
+
}
|
|
5120
|
+
_restoreSnapshot(snapshot) {
|
|
5121
|
+
if (!snapshot?.entries)
|
|
5122
|
+
return;
|
|
5123
|
+
for (const entry of snapshot.entries) {
|
|
5124
|
+
this._timeTravel(entry);
|
|
5125
|
+
}
|
|
4920
5126
|
}
|
|
4921
5127
|
_sendComponentState(componentId) {
|
|
4922
5128
|
const meta = this._components.get(componentId);
|
|
@@ -4927,11 +5133,38 @@ class SygnalDevTools {
|
|
|
4927
5133
|
componentId,
|
|
4928
5134
|
state: this._safeClone(instance.currentState),
|
|
4929
5135
|
context: this._safeClone(instance.currentContext),
|
|
5136
|
+
contextTrace: this._buildContextTrace(componentId, instance.currentContext),
|
|
4930
5137
|
props: this._safeClone(instance.currentProps),
|
|
4931
5138
|
});
|
|
4932
5139
|
}
|
|
4933
5140
|
}
|
|
4934
5141
|
}
|
|
5142
|
+
_buildContextTrace(componentId, context) {
|
|
5143
|
+
if (!context || typeof context !== 'object')
|
|
5144
|
+
return [];
|
|
5145
|
+
const trace = [];
|
|
5146
|
+
const fields = Object.keys(context);
|
|
5147
|
+
for (const field of fields) {
|
|
5148
|
+
// Walk up parent chain to find which component provides this field
|
|
5149
|
+
let currentId = componentId;
|
|
5150
|
+
let found = false;
|
|
5151
|
+
while (currentId != null) {
|
|
5152
|
+
const meta = this._components.get(currentId);
|
|
5153
|
+
if (!meta)
|
|
5154
|
+
break;
|
|
5155
|
+
if (meta.mviGraph?.contextProvides?.includes(field)) {
|
|
5156
|
+
trace.push({ field, providerId: meta.id, providerName: meta.name });
|
|
5157
|
+
found = true;
|
|
5158
|
+
break;
|
|
5159
|
+
}
|
|
5160
|
+
currentId = meta.parentId;
|
|
5161
|
+
}
|
|
5162
|
+
if (!found) {
|
|
5163
|
+
trace.push({ field, providerId: -1, providerName: 'unknown' });
|
|
5164
|
+
}
|
|
5165
|
+
}
|
|
5166
|
+
return trace;
|
|
5167
|
+
}
|
|
4935
5168
|
_sendFullTree() {
|
|
4936
5169
|
const tree = [];
|
|
4937
5170
|
for (const [, meta] of this._components) {
|
|
@@ -4966,6 +5199,44 @@ class SygnalDevTools {
|
|
|
4966
5199
|
return '[unserializable]';
|
|
4967
5200
|
}
|
|
4968
5201
|
}
|
|
5202
|
+
_extractMviGraph(instance) {
|
|
5203
|
+
if (!instance.model)
|
|
5204
|
+
return null;
|
|
5205
|
+
try {
|
|
5206
|
+
const sources = instance.sourceNames ? [...instance.sourceNames] : [];
|
|
5207
|
+
const actions = [];
|
|
5208
|
+
const model = instance.model;
|
|
5209
|
+
for (const key of Object.keys(model)) {
|
|
5210
|
+
let actionName = key;
|
|
5211
|
+
let entry = model[key];
|
|
5212
|
+
// Handle shorthand 'ACTION | SINK'
|
|
5213
|
+
if (key.includes('|')) {
|
|
5214
|
+
const parts = key.split('|').map((s) => s.trim());
|
|
5215
|
+
if (parts.length === 2 && parts[0] && parts[1]) {
|
|
5216
|
+
actionName = parts[0];
|
|
5217
|
+
actions.push({ name: actionName, sinks: [parts[1]] });
|
|
5218
|
+
continue;
|
|
5219
|
+
}
|
|
5220
|
+
}
|
|
5221
|
+
// Function → targets STATE
|
|
5222
|
+
if (typeof entry === 'function') {
|
|
5223
|
+
actions.push({ name: actionName, sinks: [instance.stateSourceName || 'STATE'] });
|
|
5224
|
+
continue;
|
|
5225
|
+
}
|
|
5226
|
+
// Object → keys are sink names
|
|
5227
|
+
if (entry && typeof entry === 'object') {
|
|
5228
|
+
actions.push({ name: actionName, sinks: Object.keys(entry) });
|
|
5229
|
+
continue;
|
|
5230
|
+
}
|
|
5231
|
+
}
|
|
5232
|
+
const contextProvides = instance.context && typeof instance.context === 'object'
|
|
5233
|
+
? Object.keys(instance.context) : [];
|
|
5234
|
+
return { sources, actions, contextProvides };
|
|
5235
|
+
}
|
|
5236
|
+
catch (e) {
|
|
5237
|
+
return null;
|
|
5238
|
+
}
|
|
5239
|
+
}
|
|
4969
5240
|
_serializeMeta(meta) {
|
|
4970
5241
|
const { _instanceRef, ...rest } = meta;
|
|
4971
5242
|
return rest;
|
|
@@ -5926,6 +6197,154 @@ function emit(type, data) {
|
|
|
5926
6197
|
};
|
|
5927
6198
|
}
|
|
5928
6199
|
|
|
6200
|
+
// ── makeServiceWorkerDriver ──────────────────────────────────
|
|
6201
|
+
function trackWorker(worker, events) {
|
|
6202
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6203
|
+
worker.addEventListener('statechange', () => {
|
|
6204
|
+
if (worker.state === 'installed')
|
|
6205
|
+
emit('installed', true);
|
|
6206
|
+
if (worker.state === 'activated')
|
|
6207
|
+
emit('activated', true);
|
|
6208
|
+
});
|
|
6209
|
+
if (worker.state === 'installed')
|
|
6210
|
+
emit('waiting', worker);
|
|
6211
|
+
if (worker.state === 'activated')
|
|
6212
|
+
emit('activated', true);
|
|
6213
|
+
}
|
|
6214
|
+
function makeServiceWorkerDriver(scriptUrl, options = {}) {
|
|
6215
|
+
return function serviceWorkerDriver(sink$) {
|
|
6216
|
+
const events = new EventTarget();
|
|
6217
|
+
if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
|
|
6218
|
+
navigator.serviceWorker
|
|
6219
|
+
.register(scriptUrl, { scope: options.scope })
|
|
6220
|
+
.then((reg) => {
|
|
6221
|
+
const emit = (type, data) => events.dispatchEvent(new CustomEvent('data', { detail: { type, data } }));
|
|
6222
|
+
if (reg.installing)
|
|
6223
|
+
trackWorker(reg.installing, events);
|
|
6224
|
+
if (reg.waiting)
|
|
6225
|
+
emit('waiting', reg.waiting);
|
|
6226
|
+
if (reg.active)
|
|
6227
|
+
emit('activated', true);
|
|
6228
|
+
reg.addEventListener('updatefound', () => {
|
|
6229
|
+
if (reg.installing)
|
|
6230
|
+
trackWorker(reg.installing, events);
|
|
6231
|
+
});
|
|
6232
|
+
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
|
6233
|
+
emit('controlling', true);
|
|
6234
|
+
});
|
|
6235
|
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
6236
|
+
emit('message', e.data);
|
|
6237
|
+
});
|
|
6238
|
+
})
|
|
6239
|
+
.catch((err) => {
|
|
6240
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'error', data: err } }));
|
|
6241
|
+
});
|
|
6242
|
+
sink$.addListener({
|
|
6243
|
+
next: (cmd) => {
|
|
6244
|
+
if (cmd.action === 'skipWaiting') {
|
|
6245
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6246
|
+
if (r.waiting)
|
|
6247
|
+
r.waiting.postMessage({ type: 'SKIP_WAITING' });
|
|
6248
|
+
});
|
|
6249
|
+
}
|
|
6250
|
+
else if (cmd.action === 'postMessage') {
|
|
6251
|
+
navigator.serviceWorker.ready.then((r) => {
|
|
6252
|
+
if (r.active)
|
|
6253
|
+
r.active.postMessage(cmd.data);
|
|
6254
|
+
});
|
|
6255
|
+
}
|
|
6256
|
+
else if (cmd.action === 'unregister') {
|
|
6257
|
+
navigator.serviceWorker.ready.then((r) => r.unregister());
|
|
6258
|
+
}
|
|
6259
|
+
},
|
|
6260
|
+
error: (err) => console.error('[SW driver] Error in sink stream:', err),
|
|
6261
|
+
});
|
|
6262
|
+
}
|
|
6263
|
+
return {
|
|
6264
|
+
select(type) {
|
|
6265
|
+
let cb;
|
|
6266
|
+
const in$ = xs$1.create({
|
|
6267
|
+
start: (listener) => {
|
|
6268
|
+
cb = ({ detail }) => {
|
|
6269
|
+
if (!type || detail.type === type)
|
|
6270
|
+
listener.next(detail.data);
|
|
6271
|
+
};
|
|
6272
|
+
events.addEventListener('data', cb);
|
|
6273
|
+
},
|
|
6274
|
+
stop: () => {
|
|
6275
|
+
if (cb)
|
|
6276
|
+
events.removeEventListener('data', cb);
|
|
6277
|
+
},
|
|
6278
|
+
});
|
|
6279
|
+
return adapt(in$);
|
|
6280
|
+
},
|
|
6281
|
+
};
|
|
6282
|
+
};
|
|
6283
|
+
}
|
|
6284
|
+
// ── onlineStatus$ ────────────────────────────────────────────
|
|
6285
|
+
function _createOnlineStatus() {
|
|
6286
|
+
if (typeof window === 'undefined') {
|
|
6287
|
+
return xs$1.of(true);
|
|
6288
|
+
}
|
|
6289
|
+
let cleanup;
|
|
6290
|
+
return xs$1.create({
|
|
6291
|
+
start(listener) {
|
|
6292
|
+
listener.next(navigator.onLine);
|
|
6293
|
+
const on = () => listener.next(true);
|
|
6294
|
+
const off = () => listener.next(false);
|
|
6295
|
+
window.addEventListener('online', on);
|
|
6296
|
+
window.addEventListener('offline', off);
|
|
6297
|
+
cleanup = () => {
|
|
6298
|
+
window.removeEventListener('online', on);
|
|
6299
|
+
window.removeEventListener('offline', off);
|
|
6300
|
+
};
|
|
6301
|
+
},
|
|
6302
|
+
stop() {
|
|
6303
|
+
cleanup?.();
|
|
6304
|
+
cleanup = undefined;
|
|
6305
|
+
},
|
|
6306
|
+
});
|
|
6307
|
+
}
|
|
6308
|
+
const onlineStatus$ = _createOnlineStatus();
|
|
6309
|
+
// ── createInstallPrompt ──────────────────────────────────────
|
|
6310
|
+
function createInstallPrompt() {
|
|
6311
|
+
let deferredPrompt = null;
|
|
6312
|
+
const events = new EventTarget();
|
|
6313
|
+
if (typeof window !== 'undefined') {
|
|
6314
|
+
window.addEventListener('beforeinstallprompt', (e) => {
|
|
6315
|
+
e.preventDefault();
|
|
6316
|
+
deferredPrompt = e;
|
|
6317
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'beforeinstallprompt', data: true } }));
|
|
6318
|
+
});
|
|
6319
|
+
window.addEventListener('appinstalled', () => {
|
|
6320
|
+
deferredPrompt = null;
|
|
6321
|
+
events.dispatchEvent(new CustomEvent('data', { detail: { type: 'appinstalled', data: true } }));
|
|
6322
|
+
});
|
|
6323
|
+
}
|
|
6324
|
+
return {
|
|
6325
|
+
select(type) {
|
|
6326
|
+
let cb;
|
|
6327
|
+
const in$ = xs$1.create({
|
|
6328
|
+
start: (listener) => {
|
|
6329
|
+
cb = ({ detail }) => {
|
|
6330
|
+
if (detail.type === type)
|
|
6331
|
+
listener.next(detail.data);
|
|
6332
|
+
};
|
|
6333
|
+
events.addEventListener('data', cb);
|
|
6334
|
+
},
|
|
6335
|
+
stop: () => {
|
|
6336
|
+
if (cb)
|
|
6337
|
+
events.removeEventListener('data', cb);
|
|
6338
|
+
},
|
|
6339
|
+
});
|
|
6340
|
+
return adapt(in$);
|
|
6341
|
+
},
|
|
6342
|
+
prompt() {
|
|
6343
|
+
return deferredPrompt?.prompt();
|
|
6344
|
+
},
|
|
6345
|
+
};
|
|
6346
|
+
}
|
|
6347
|
+
|
|
5929
6348
|
/**
|
|
5930
6349
|
* Server-Side Rendering utilities for Sygnal components.
|
|
5931
6350
|
*
|
|
@@ -6616,6 +7035,7 @@ exports.collection = collection;
|
|
|
6616
7035
|
exports.component = component;
|
|
6617
7036
|
exports.createCommand = createCommand;
|
|
6618
7037
|
exports.createElement = createElement;
|
|
7038
|
+
exports.createInstallPrompt = createInstallPrompt;
|
|
6619
7039
|
exports.createRef = createRef;
|
|
6620
7040
|
exports.createRef$ = createRef$;
|
|
6621
7041
|
exports.driverFromAsync = driverFromAsync;
|
|
@@ -6626,7 +7046,9 @@ exports.getDevTools = getDevTools;
|
|
|
6626
7046
|
exports.lazy = lazy;
|
|
6627
7047
|
exports.makeDOMDriver = makeDOMDriver;
|
|
6628
7048
|
exports.makeDragDriver = makeDragDriver;
|
|
7049
|
+
exports.makeServiceWorkerDriver = makeServiceWorkerDriver;
|
|
6629
7050
|
exports.mockDOMSource = mockDOMSource;
|
|
7051
|
+
exports.onlineStatus$ = onlineStatus$;
|
|
6630
7052
|
exports.portal = Portal;
|
|
6631
7053
|
exports.processDrag = processDrag;
|
|
6632
7054
|
exports.processForm = processForm;
|
package/dist/index.d.ts
CHANGED
|
@@ -595,6 +595,35 @@ export interface CommandSource {
|
|
|
595
595
|
|
|
596
596
|
export function createCommand(): Command
|
|
597
597
|
|
|
598
|
+
// ── PWA Helpers ──────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
export interface ServiceWorkerSource {
|
|
601
|
+
select(type?: 'installed' | 'activated' | 'waiting' | 'controlling' | 'error' | 'message' | string): Stream<any>;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
export interface ServiceWorkerCommand {
|
|
605
|
+
action: 'skipWaiting' | 'postMessage' | 'unregister';
|
|
606
|
+
data?: any;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
export interface ServiceWorkerOptions {
|
|
610
|
+
scope?: string;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
export function makeServiceWorkerDriver(
|
|
614
|
+
scriptUrl: string,
|
|
615
|
+
options?: ServiceWorkerOptions
|
|
616
|
+
): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource
|
|
617
|
+
|
|
618
|
+
export const onlineStatus$: Stream<boolean>
|
|
619
|
+
|
|
620
|
+
export interface InstallPrompt {
|
|
621
|
+
select(type: 'beforeinstallprompt' | 'appinstalled'): Stream<any>;
|
|
622
|
+
prompt(): Promise<any> | undefined;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function createInstallPrompt(): InstallPrompt
|
|
626
|
+
|
|
598
627
|
export interface RenderOptions {
|
|
599
628
|
/** Override initial state (defaults to component's .initialState) */
|
|
600
629
|
initialState?: any;
|