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.
@@ -0,0 +1,181 @@
1
+ import xs, {Stream} from 'xstream';
2
+ import {adapt} from '../cycle/run/adapt';
3
+
4
+ // ── Types ────────────────────────────────────────────────────
5
+
6
+ export interface ServiceWorkerSource {
7
+ select(type?: string): any;
8
+ }
9
+
10
+ export interface ServiceWorkerCommand {
11
+ action: 'skipWaiting' | 'postMessage' | 'unregister';
12
+ data?: any;
13
+ }
14
+
15
+ export interface ServiceWorkerOptions {
16
+ scope?: string;
17
+ }
18
+
19
+ export interface InstallPrompt {
20
+ select(type: 'beforeinstallprompt' | 'appinstalled'): any;
21
+ prompt(): Promise<any> | undefined;
22
+ }
23
+
24
+ // ── makeServiceWorkerDriver ──────────────────────────────────
25
+
26
+ function trackWorker(worker: ServiceWorker, events: EventTarget) {
27
+ const emit = (type: string, data: any) =>
28
+ events.dispatchEvent(new CustomEvent('data', {detail: {type, data}}));
29
+
30
+ worker.addEventListener('statechange', () => {
31
+ if (worker.state === 'installed') emit('installed', true);
32
+ if (worker.state === 'activated') emit('activated', true);
33
+ });
34
+
35
+ if (worker.state === 'installed') emit('waiting', worker);
36
+ if (worker.state === 'activated') emit('activated', true);
37
+ }
38
+
39
+ export function makeServiceWorkerDriver(
40
+ scriptUrl: string,
41
+ options: ServiceWorkerOptions = {},
42
+ ): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource {
43
+ return function serviceWorkerDriver(sink$: Stream<ServiceWorkerCommand>): ServiceWorkerSource {
44
+ const events = new EventTarget();
45
+
46
+ if (typeof navigator !== 'undefined' && 'serviceWorker' in navigator) {
47
+ navigator.serviceWorker
48
+ .register(scriptUrl, {scope: options.scope})
49
+ .then((reg) => {
50
+ const emit = (type: string, data: any) =>
51
+ events.dispatchEvent(new CustomEvent('data', {detail: {type, data}}));
52
+
53
+ if (reg.installing) trackWorker(reg.installing, events);
54
+ if (reg.waiting) emit('waiting', reg.waiting);
55
+ if (reg.active) emit('activated', true);
56
+
57
+ reg.addEventListener('updatefound', () => {
58
+ if (reg.installing) trackWorker(reg.installing, events);
59
+ });
60
+
61
+ navigator.serviceWorker.addEventListener('controllerchange', () => {
62
+ emit('controlling', true);
63
+ });
64
+
65
+ navigator.serviceWorker.addEventListener('message', (e) => {
66
+ emit('message', (e as MessageEvent).data);
67
+ });
68
+ })
69
+ .catch((err) => {
70
+ events.dispatchEvent(new CustomEvent('data', {detail: {type: 'error', data: err}}));
71
+ });
72
+
73
+ sink$.addListener({
74
+ next: (cmd: ServiceWorkerCommand) => {
75
+ if (cmd.action === 'skipWaiting') {
76
+ navigator.serviceWorker.ready.then((r) => {
77
+ if (r.waiting) r.waiting.postMessage({type: 'SKIP_WAITING'});
78
+ });
79
+ } else if (cmd.action === 'postMessage') {
80
+ navigator.serviceWorker.ready.then((r) => {
81
+ if (r.active) r.active.postMessage(cmd.data);
82
+ });
83
+ } else if (cmd.action === 'unregister') {
84
+ navigator.serviceWorker.ready.then((r) => r.unregister());
85
+ }
86
+ },
87
+ error: (err: any) => console.error('[SW driver] Error in sink stream:', err),
88
+ });
89
+ }
90
+
91
+ return {
92
+ select(type?: string) {
93
+ let cb: ((e: Event) => void) | undefined;
94
+ const in$ = xs.create<any>({
95
+ start: (listener) => {
96
+ cb = ({detail}: any) => {
97
+ if (!type || detail.type === type) listener.next(detail.data);
98
+ };
99
+ events.addEventListener('data', cb);
100
+ },
101
+ stop: () => {
102
+ if (cb) events.removeEventListener('data', cb);
103
+ },
104
+ });
105
+ return adapt(in$);
106
+ },
107
+ };
108
+ };
109
+ }
110
+
111
+ // ── onlineStatus$ ────────────────────────────────────────────
112
+
113
+ function _createOnlineStatus(): Stream<boolean> {
114
+ if (typeof window === 'undefined') {
115
+ return xs.of(true);
116
+ }
117
+
118
+ let cleanup: (() => void) | undefined;
119
+
120
+ return xs.create<boolean>({
121
+ start(listener) {
122
+ listener.next(navigator.onLine);
123
+ const on = () => listener.next(true);
124
+ const off = () => listener.next(false);
125
+ window.addEventListener('online', on);
126
+ window.addEventListener('offline', off);
127
+ cleanup = () => {
128
+ window.removeEventListener('online', on);
129
+ window.removeEventListener('offline', off);
130
+ };
131
+ },
132
+ stop() {
133
+ cleanup?.();
134
+ cleanup = undefined;
135
+ },
136
+ });
137
+ }
138
+
139
+ export const onlineStatus$: Stream<boolean> = _createOnlineStatus();
140
+
141
+ // ── createInstallPrompt ──────────────────────────────────────
142
+
143
+ export function createInstallPrompt(): InstallPrompt {
144
+ let deferredPrompt: any = null;
145
+ const events = new EventTarget();
146
+
147
+ if (typeof window !== 'undefined') {
148
+ window.addEventListener('beforeinstallprompt', (e) => {
149
+ e.preventDefault();
150
+ deferredPrompt = e;
151
+ events.dispatchEvent(new CustomEvent('data', {detail: {type: 'beforeinstallprompt', data: true}}));
152
+ });
153
+
154
+ window.addEventListener('appinstalled', () => {
155
+ deferredPrompt = null;
156
+ events.dispatchEvent(new CustomEvent('data', {detail: {type: 'appinstalled', data: true}}));
157
+ });
158
+ }
159
+
160
+ return {
161
+ select(type: 'beforeinstallprompt' | 'appinstalled') {
162
+ let cb: ((e: Event) => void) | undefined;
163
+ const in$ = xs.create<any>({
164
+ start: (listener) => {
165
+ cb = ({detail}: any) => {
166
+ if (detail.type === type) listener.next(detail.data);
167
+ };
168
+ events.addEventListener('data', cb);
169
+ },
170
+ stop: () => {
171
+ if (cb) events.removeEventListener('data', cb);
172
+ },
173
+ });
174
+ return adapt(in$);
175
+ },
176
+
177
+ prompt() {
178
+ return deferredPrompt?.prompt();
179
+ },
180
+ };
181
+ }
package/src/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;
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export { createCommand } from './extra/command'
22
22
  export { createRef, createRef$ } from './extra/ref'
23
23
  export { renderComponent } from './extra/testing'
24
24
  export { set, toggle, emit } from './extra/reducers'
25
+ export { makeServiceWorkerDriver, onlineStatus$, createInstallPrompt } from './extra/pwa'
25
26
  export { renderToString } from './extra/ssr'
26
27
  export { default as xs } from './extra/xstreamCompat'
27
28
  export { getDevTools } from './extra/devtools'