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/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
- return {
2167
- send: (type, data) => listener.next({ 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 && this._readyChangedListener) {
3595
- setTimeout(() => {
3596
- this._readyChangedListener?.next(null);
3597
- }, 0);
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
- return xs$1.create({
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) => events.dispatchEvent(new CustomEvent('data', { detail: 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({ historyIndex }) {
4907
- const entry = this._stateHistory[historyIndex];
4908
- if (!entry)
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(() => ({ ...entry.state }));
5090
+ app.sinks.STATE.shamefullySendNext(() => ({ ...newState }));
4915
5091
  this._post('TIME_TRAVEL_APPLIED', {
4916
- historyIndex,
4917
- state: entry.state,
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;