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/src/extra/pwa.ts
ADDED
|
@@ -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'
|