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 // --- SW globals ---
125
126 export function SkipWaiting() {
127 self.skipWaiting();
128 }
129
130 export function ClaimClients(done) {
131 self.clients.claim().then(() => { if (done) done(); });
132 }
133
134 export function MatchClients(fn) {
135 self.clients.matchAll({ type: 'window' }).then((all) => {
136 for (const c of all) {
137 fn(_storeClient(c));
138 }
139 });
140 }
141
142 export function PostMessage(clientId, msg) {
143 const c = _clients.get(clientId);
144 if (c) c.postMessage(msg);
145 }
146
147 export function PostMessageJSON(clientId, json) {
148 const c = _clients.get(clientId);
149 if (c) c.postMessage(json);
150 }
151
152 export function Navigate(clientId, url) {
153 const c = _clients.get(clientId);
154 if (c) c.navigate(url || c.url);
155 }
156
157 // --- Cache ---
158
159 export function CacheOpen(name, fn) {
160 caches.open(name).then((cache) => {
161 const id = _nextCacheId++;
162 _caches.set(id, cache);
163 fn(id);
164 });
165 }
166
167 export function CacheAddAll(cacheId, urls, done) {
168 const cache = _caches.get(cacheId);
169 if (!cache) { if (done) done(); return; }
170 cache.addAll(urls).then(() => { if (done) done(); });
171 }
172
173 export function CachePut(cacheId, url, respId, done) {
174 const cache = _caches.get(cacheId);
175 const resp = _responses.get(respId);
176 if (!cache || !resp) { if (done) done(); return; }
177 cache.put(new Request(url), resp).then(() => { if (done) done(); });
178 }
179
180 export function CacheMatch(url, fn) {
181 caches.match(new Request(url)).then((resp) => {
182 fn(_storeResponse(resp));
183 });
184 }
185
186 export function CacheDelete(name, done) {
187 caches.delete(name).then(() => { if (done) done(); });
188 }
189
190 // --- Fetch ---
191
192 export function Fetch(url, fn) {
193 fetch(url).then(
194 (resp) => fn(_storeResponse(resp), true),
195 () => fn(0, false)
196 );
197 }
198
199 export function ResponseOK(respId) {
200 const resp = _responses.get(respId);
201 return resp ? resp.ok : false;
202 }
203
204 // --- SSE ---
205
206 export function SSEConnect(url, onMessage) {
207 const id = _nextSseId++;
208 const es = new EventSource(url);
209 _sseConns.set(id, es);
210 es.onmessage = (event) => {
211 if (onMessage) onMessage(event.data);
212 };
213 return id;
214 }
215
216 export function SSEClose(sseId) {
217 const es = _sseConns.get(sseId);
218 if (es) {
219 es.close();
220 _sseConns.delete(sseId);
221 }
222 }
223
224 // --- Logging ---
225
226 export function Log(msg) {
227 console.log('sw:', msg);
228 }
229