sw.mjs raw
1 // TinyJS Runtime — Service Worker Bridge
2 // Provides Go-callable Service Worker, Cache, Fetch, and SSE operations.
3
4 // --- Internal state ---
5
6 const _events = new Map();
7 let _nextEventId = 1;
8 const _caches = new Map();
9 let _nextCacheId = 1;
10 const _responses = new Map();
11 let _nextRespId = 1;
12 const _clients = new Map();
13 let _nextClientId = 1;
14 const _sseConns = new Map();
15 let _nextSseId = 1;
16
17 function _storeEvent(ev) {
18 const id = _nextEventId++;
19 _events.set(id, ev);
20 return id;
21 }
22
23 function _storeResponse(resp) {
24 if (!resp) return 0;
25 const id = _nextRespId++;
26 _responses.set(id, resp);
27 return id;
28 }
29
30 function _storeClient(client) {
31 const id = _nextClientId++;
32 _clients.set(id, client);
33 return id;
34 }
35
36 // --- Lifecycle ---
37
38 export function OnInstall(fn) {
39 self.addEventListener('install', (event) => {
40 fn(_storeEvent(event));
41 });
42 }
43
44 export function OnActivate(fn) {
45 self.addEventListener('activate', (event) => {
46 fn(_storeEvent(event));
47 });
48 }
49
50 // Fetch events use a deferred promise pattern:
51 // Go calls RespondWithCache or RespondWithNetwork synchronously to pick strategy.
52 // The runtime creates the promise and calls event.respondWith() synchronously.
53 const _fetchResolvers = new Map();
54
55 export function OnFetch(fn) {
56 self.addEventListener('fetch', (event) => {
57 const id = _storeEvent(event);
58 fn(id);
59 // Check if Go code set up a response strategy.
60 const resolver = _fetchResolvers.get(id);
61 if (resolver) {
62 event.respondWith(resolver);
63 _fetchResolvers.delete(id);
64 }
65 // If no resolver was set, browser handles the fetch normally.
66 });
67 }
68
69 export function OnMessage(fn) {
70 self.addEventListener('message', (event) => {
71 fn(_storeEvent(event));
72 });
73 }
74
75 // --- Event methods ---
76
77 export function WaitUntil(eventId, fn) {
78 const ev = _events.get(eventId);
79 if (!ev) return;
80 ev.waitUntil(new Promise((resolve) => {
81 fn(resolve);
82 }));
83 }
84
85 export function RespondWith(eventId, respId) {
86 const resp = _responses.get(respId);
87 if (resp) {
88 _fetchResolvers.set(eventId, Promise.resolve(resp));
89 }
90 }
91
92 export function RespondWithNetwork(eventId) {
93 // Don't set a resolver — browser handles the fetch.
94 }
95
96 // RespondWithCacheFirst tries cache, falls back to network.
97 // This is the common pattern and must be called synchronously from onFetch.
98 export function RespondWithCacheFirst(eventId) {
99 const ev = _events.get(eventId);
100 if (!ev) return;
101 _fetchResolvers.set(eventId,
102 caches.match(ev.request).then(cached => cached || fetch(ev.request))
103 );
104 }
105
106 export function GetRequestURL(eventId) {
107 const ev = _events.get(eventId);
108 return ev ? ev.request.url : '';
109 }
110
111 export function GetRequestPath(eventId) {
112 const ev = _events.get(eventId);
113 if (!ev) return '';
114 return new URL(ev.request.url).pathname;
115 }
116
117 export function GetMessageData(eventId) {
118 const ev = _events.get(eventId);
119 if (!ev) return '';
120 const d = ev.data;
121 return typeof d === 'string' ? d : JSON.stringify(d);
122 }
123
124 export function GetMessageClientID(eventId) {
125 const ev = _events.get(eventId);
126 if (!ev) return '';
127 return ev.source ? ev.source.id || '' : '';
128 }
129
130 // --- SW globals ---
131
132 export function Origin() {
133 return self.location.origin;
134 }
135
136 export function SkipWaiting() {
137 self.skipWaiting();
138 }
139
140 export function ClaimClients(done) {
141 self.clients.claim().then(() => { if (done) done(); }).catch(() => { if (done) done(); });
142 }
143
144 export function MatchClients(fn) {
145 self.clients.matchAll({ type: 'window' }).then((all) => {
146 for (const c of all) {
147 fn(_storeClient(c));
148 }
149 });
150 }
151
152 export function PostMessage(clientId, msg) {
153 const c = _clients.get(clientId);
154 if (c) c.postMessage(msg);
155 }
156
157 export function PostMessageJSON(clientId, json) {
158 const c = _clients.get(clientId);
159 if (c) {
160 try { c.postMessage(JSON.parse(json)); }
161 catch { c.postMessage(json); }
162 }
163 }
164
165 export function GetClientByID(id, fn) {
166 self.clients.get(id).then((client) => {
167 if (client) {
168 fn(_storeClient(client), true);
169 } else {
170 fn(0, false);
171 }
172 }).catch(() => fn(0, false));
173 }
174
175 export function Navigate(clientId, url) {
176 const c = _clients.get(clientId);
177 if (c) c.navigate(url || c.url);
178 }
179
180 // --- Cache ---
181
182 export function CacheOpen(name, fn) {
183 caches.open(name).then((cache) => {
184 const id = _nextCacheId++;
185 _caches.set(id, cache);
186 fn(id);
187 }).catch(() => fn(0));
188 }
189
190 export function CacheAddAll(cacheId, urls, done) {
191 const cache = _caches.get(cacheId);
192 if (!cache) { if (done) done(); return; }
193 cache.addAll(urls).then(() => { if (done) done(); }).catch(() => { if (done) done(); });
194 }
195
196 export function CachePut(cacheId, url, respId, done) {
197 const cache = _caches.get(cacheId);
198 const resp = _responses.get(respId);
199 if (!cache || !resp) { if (done) done(); return; }
200 cache.put(new Request(url), resp).then(() => { if (done) done(); }).catch(() => { if (done) done(); });
201 }
202
203 export function CacheMatch(url, fn) {
204 caches.match(new Request(url)).then((resp) => {
205 fn(_storeResponse(resp));
206 }).catch(() => fn(0));
207 }
208
209 export function CacheDelete(name, done) {
210 caches.delete(name).then(() => { if (done) done(); }).catch(() => { if (done) done(); });
211 }
212
213 // --- Fetch ---
214
215 export function Fetch(url, fn) {
216 fetch(url).then(
217 (resp) => fn(_storeResponse(resp), true),
218 () => fn(0, false)
219 );
220 }
221
222 export function FetchAll(urls, onEach, onDone) {
223 const arr = urls.$array
224 ? urls.$array.slice(urls.$offset, urls.$offset + urls.$length)
225 : urls;
226 if (arr.length === 0) { if (onDone) onDone(); return; }
227 let remaining = arr.length;
228 for (let i = 0; i < arr.length; i++) {
229 ((idx) => {
230 fetch(arr[idx]).then(
231 (resp) => { onEach(idx, _storeResponse(resp), true); if (--remaining === 0 && onDone) onDone(); },
232 () => { onEach(idx, 0, false); if (--remaining === 0 && onDone) onDone(); }
233 );
234 })(i);
235 }
236 }
237
238 export function ResponseOK(respId) {
239 const resp = _responses.get(respId);
240 return resp ? resp.ok : false;
241 }
242
243 // --- SSE ---
244
245 export function SSEConnect(url, onMessage) {
246 const id = _nextSseId++;
247 if (typeof EventSource !== 'undefined') {
248 const es = new EventSource(url);
249 _sseConns.set(id, es);
250 es.onmessage = (event) => {
251 if (onMessage) onMessage(event.data);
252 };
253 } else {
254 // SW scope: EventSource not available. Poll with fetch.
255 let active = true;
256 _sseConns.set(id, { close() { active = false; } });
257 (async function poll() {
258 let backoff = 3000;
259 while (active) {
260 try {
261 const resp = await fetch(url, { headers: { 'Accept': 'text/event-stream' } });
262 backoff = 3000;
263 const reader = resp.body.getReader();
264 const decoder = new TextDecoder();
265 let buf = '';
266 while (active) {
267 const { value, done } = await reader.read();
268 if (done) break;
269 buf += decoder.decode(value, { stream: true });
270 let idx;
271 while ((idx = buf.indexOf('\n\n')) !== -1) {
272 const msg = buf.substring(0, idx);
273 buf = buf.substring(idx + 2);
274 for (const line of msg.split('\n')) {
275 if (line.startsWith('data: ') && onMessage) {
276 onMessage(line.substring(6));
277 }
278 }
279 }
280 }
281 } catch (e) {
282 if (active) {
283 await new Promise(r => setTimeout(r, backoff));
284 backoff = Math.min(backoff * 2, 60000);
285 }
286 }
287 }
288 })();
289 }
290 return id;
291 }
292
293 export function SSEClose(sseId) {
294 const es = _sseConns.get(sseId);
295 if (es) {
296 es.close();
297 _sseConns.delete(sseId);
298 }
299 }
300
301 // --- Timers ---
302
303 export function SetTimeout(ms, fn) {
304 return setTimeout(fn, ms);
305 }
306
307 export function ClearTimeout(t) {
308 clearTimeout(t);
309 }
310
311 export function NowSeconds() {
312 return Math.floor(Date.now() / 1000);
313 }
314
315 export function NowMillis() {
316 return Date.now();
317 }
318
319 // --- Logging ---
320
321 export function Log(msg) {
322 console.log('sw:', msg);
323 }
324