@naanlang/naan 1.0.1 → 1.0.4

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.
Files changed (37) hide show
  1. package/LICENSE.md +1 -1
  2. package/README.md +5 -5
  3. package/bin/index.js +18 -15
  4. package/dist/env_web.js +146 -16
  5. package/dist/naan.min.js +5 -5
  6. package/frameworks/browser/sworker.js +299 -91
  7. package/frameworks/browser/terminals.nlg +23 -10
  8. package/frameworks/browser/workers.nlg +26 -12
  9. package/frameworks/client/apiclient.nlg +17 -4
  10. package/frameworks/client/psm_client.nlg +178 -81
  11. package/frameworks/common/common.nlg +14 -4
  12. package/frameworks/node/apiserver.nlg +31 -15
  13. package/frameworks/node/filesystem.nlg +230 -46
  14. package/frameworks/node/gitter.nlg +27 -6
  15. package/frameworks/node/psm_server.nlg +113 -37
  16. package/frameworks/project/build.nlg +40 -23
  17. package/frameworks/project/projects.nlg +50 -27
  18. package/frameworks/running/debugnub.nlg +2 -5
  19. package/frameworks/storage/csv.nlg +120 -0
  20. package/frameworks/storage/dbt_pouch.nlg +41 -25
  21. package/frameworks/storage/psm.nlg +108 -28
  22. package/frameworks/storage/psm_dbtables.nlg +7 -3
  23. package/frameworks/storage/resources.nlg +30 -2
  24. package/lib/browser/env_web.js +148 -18
  25. package/lib/browser/env_webworker.js +1 -1
  26. package/lib/browser/require.js +1 -1
  27. package/lib/core/naanlib.js +5 -5
  28. package/lib/env_node.js +9 -1
  29. package/package.json +2 -1
  30. package/plugins/serviceAws/aws_cloudwatchlogs.nlg +1 -1
  31. package/plugins/serviceAws/aws_dynamo.nlg +625 -141
  32. package/plugins/serviceAws/aws_dynextra.nlg +294 -0
  33. package/plugins/serviceAws/dbt_aws.nlg +31 -11
  34. package/plugins/serviceAws/psm_aws.nlg +42 -26
  35. package/plugins/serviceAws/serviceAws.nlg +1 -0
  36. package/plugins/serviceGitHub/psm_github.nlg +41 -25
  37. package/plugins/serviceGitLab/psm_gitlab.nlg +41 -25
@@ -4,66 +4,309 @@
4
4
  *
5
5
  * Nide service worker script.
6
6
  *
7
+ * The goal of this service worker is to 1) cache fetch requests for static content, and 2) relay
8
+ * fetch requests in the /run/ URL path namespace to instances of the IDE that can provide it. The
9
+ * identifier for the various browser windows is by "clientId", which is a GUID reported by the
10
+ * service worker and message channel APIs. Here we use the term clientId to refer to windows that
11
+ * consume data in the /run/ path, i.e. the IDE targets, and the term sourceId to refer to windows
12
+ * that provide data, i.e. the IDE instances.
13
+ * When the service worker is first executed it sends messages to all known windows requesting
14
+ * that they establish a message channel and forward the service worker's port to it. This secondary
15
+ * channel is used to communicate fetch requests from the service worker to the source IDE and then
16
+ * get the response. If an IDE is executed subsequently then it registers itself by sending the port
17
+ * unilatterally, instead of waiting to be requested.
18
+ * The service worker gets a fetch request when a non-cached resource is requested by a client
19
+ * window. If the URL path is not within /run/ then this executes a normal network request. IF the
20
+ * URL path is within /run/ then this asks the IDE source(s) to fetch it. Initially every IDE source
21
+ * is sent the request, but when one provides a 200 response then this stores that source port under
22
+ * the clientId in fetchPorts so that subsequent requests can go only to the correct IDE. This works
23
+ * great until an IDE goes away or becomes unresponsive.
24
+ * Unfortunately there is no event that tells us when a window is closed or goes away. The first
25
+ * indication of trouble is when an IDE fails to respond within a reasonable amount of time, which
26
+ * could be anything. We can enumerates the valid clients, which gives us a chance to validate our
27
+ * knowledge of which ones are responding. If a request takes longer than a second to respond then
28
+ * this executes a validate operation, which eliminates any clients that have gone away and rejects
29
+ * their pending operations.
30
+ * One potential problem is stale caches. When an older version is discovered then we notify the
31
+ * client and invalidate the old cache.
32
+ *
7
33
  * column positioning: // // !
8
34
  *
9
- * Copyright (c) 2020 by Richard C. Zulch
35
+ * Copyright (c) 2020-2022 by Richard C. Zulch
10
36
  *
11
37
  */
12
38
 
13
- var CurrentCacheName = "Naan IDE 1.0.1-2"
39
+ var CurrentCacheName = "Naanlang-1.0.4-2";
14
40
 
15
41
 
16
42
  //
17
43
  // locals
18
44
  //
19
45
 
20
- var fetchQueue = [];
21
- var fetchNextSeq = 1;
22
- var msgport;
46
+ var fetchQueue = []; // queue for outstanding requests to sources
47
+ var fetchNextSeq = 1; // sequence number for source requests
48
+ var msgPorts = {}; // message ports keyed by sourceId
49
+ var fetchPorts = {}; // message ports keyed by clientId
50
+ var waitingForInit = []; // initialization functions, or false after done
23
51
 
24
52
 
25
53
  /*
26
- * message event
54
+ * initialize
27
55
  *
28
- * Received when someone sends us a.
56
+ * Initiate listening for incoming messages from our clients. This waits for any client responses
57
+ * and then quits. Any clients that want to participate after that must unilaterally send a port for
58
+ * communication.
29
59
  *
30
60
  */
31
61
 
32
- var promiseport = new Promise(function (resolve, reject) {
62
+ (function initialize() {
63
+ if (!waitingForInit)
64
+ return; // already initialized
65
+ var pending = 0;
66
+
67
+ // doneInit
68
+ //
69
+ // Call this to resolve any waiting promises and release them for attempting to fetch from the
70
+ // IDEs (sources.)
71
+ //
72
+ function doneInit() {
73
+ if (!waitingForInit)
74
+ return; // already done
75
+ var waiters = waitingForInit;
76
+ waitingForInit = false; // no more waiters allowed
77
+ for (var waiter in waiters)
78
+ waiter(); // call each waiter
79
+ }
80
+
81
+ // self.onmessage events
82
+ //
83
+ // This receives channel port messages from the IDEs as they either 1) respond to a request to
84
+ // register, or 2) spontaneously register themselves as they notice that we're running.
85
+ //
33
86
  self.addEventListener('message', function(event) {
34
- console.log("[1.0.1-2] port received");
35
- msgport = event.data.hereIsYourPort;
87
+ var msgport = event.data.hereIsYourPort; // message port of a client
88
+ var pubID = event.data.hereIsMyID;
89
+ var pubVersion = event.data.hereIsMyVersion;
90
+ var sourceId = event.source.id; // client id of sender of message
91
+ msgPorts[sourceId] = msgport;
92
+ console.log("[1.0.4-2] received new msgport for", sourceId, pubID, "-", pubVersion);
93
+ if (pubVersion != "1.0.4-2")
94
+ msgport.postMessage({ // notify new version available
95
+ id: "upgrade",
96
+ version: "1.0.4-2"
97
+ });
98
+ Reaper(); // clean up obsolete info
99
+
100
+ // msgport.onmessage events
101
+ //
102
+ // This receives channel port messages that are responding to our fetch requests. The IDE
103
+ // can also send a text message for logging, but that is used only for debugging.
104
+ //
36
105
  msgport.onmessage = function(msg) {
37
106
  msg = msg.data;
38
107
  if (msg.id == "response")
39
- postResponse(msg);
108
+ processResponse(msg);
40
109
  else if (msg.id == "text")
41
- console.log("[1.0.1-2] msg received:", msg.text);
110
+ console.log("[1.0.4-2] msg received:", msg.text);
42
111
  };
43
- /*
44
- // don't clutter the log
45
- msgport.postMessage({
46
- id: "text",
47
- text: "port received by 1.0.1-2",
48
- });
49
- */
50
- resolve(msgport);
51
-
112
+
113
+ // send text to IDE log
52
114
  //
53
- // postResponse
115
+ // This is only for debugging.
54
116
  //
55
-
56
- function postResponse(msg) {
57
- var fqdex, response, item;
58
- for (fqdex = 0; fqdex < fetchQueue.length; ++fqdex)
59
- if (fetchQueue[fqdex].seq === msg.seq) {
60
- item = fetchQueue.splice(fqdex, 1);
61
- item[0].resolve(new Response(msg.body, msg.init));
62
- break;
63
- }
117
+ /*
118
+ // don't clutter the log
119
+ msgport.postMessage({
120
+ id: "text",
121
+ text: "port received by 1.0.4-2",
122
+ });
123
+ */
124
+ if (--pending === 0)
125
+ doneInit(); // we finished all known clients
126
+ });
127
+
128
+ // initialize known IDE instances.
129
+ //
130
+ // This sends a port request to each known IDE instance. Fetches that occur prior to completion
131
+ // of initialization will wait for these instances to respond. A timeout prevents one errant IDE
132
+ // from hanging all requests, by releasing the fetches even before all source IDEs respond.
133
+ //
134
+ self.clients.matchAll({
135
+ includeUncontrolled: true
136
+ }).then(function(clientList) {
137
+ clientList.every(function(client) {
138
+ console.log("[1.0.4-2] requesting new msgport for", client.id);
139
+ ++pending;
140
+ client.postMessage({ // tell client(s) we need this fetch source
141
+ msg: "Naan_need_fetch_port",
142
+ });
143
+ });
144
+ setTimeout(doneInit, 1000); // release fetches
145
+ });
146
+
147
+ // processResponse
148
+ //
149
+ // An IDE source has responded to our fetch request. If this was successful then we record the
150
+ // IDE (sourceId) so that subsequent requests from that target (clientId) will go to the same
151
+ // source.
152
+ //
153
+ function processResponse(msg) {
154
+ var fqdex, response, item;
155
+ for (fqdex = 0; fqdex < fetchQueue.length; ++fqdex)
156
+ if (fetchQueue[fqdex].seq === msg.seq) {
157
+ item = fetchQueue.splice(fqdex, 1)[0];
158
+ if (item.clientId && msg.init.status == 200)
159
+ fetchPorts[item.clientId] = item.msgport; // successful lookup
160
+ item.resolve(new Response(msg.body, msg.init));
161
+ break;
162
+ }
163
+ }
164
+ })();
165
+
166
+
167
+ /*
168
+ * Reaper
169
+ *
170
+ * Periodic cleanup returning a promise. This removes anything that is obsolete and resolve all
171
+ * associated transactions.
172
+ *
173
+ */
174
+
175
+ function Reaper() {
176
+ var promise = self.clients.matchAll({
177
+ includeUncontrolled: true
178
+ }).then(function(clientList) { // both sources and clients
179
+ var clients = {};
180
+ for (var clidex in clientList)
181
+ clients[clientList[clidex].id] = clientList[clidex];
182
+ for (var sourceId in msgPorts)
183
+ if (!clients[sourceId]) {
184
+ console.log("[1.0.4-2] source gone:", sourceId);
185
+ delete msgPorts[sourceId]; // no longer a source
186
+ }
187
+ for (var clientId in fetchPorts)
188
+ if (!clients[clientId]) {
189
+ console.log("[1.0.4-2] client gone:", clientId);
190
+ delete fetchPorts[clientId]; // no longer a client
191
+ }
192
+ for (var fqdex = 0; fqdex < fetchQueue.length; ++fqdex) {
193
+ var item = fetchQueue[fqdex];
194
+ while (item.clientId && !clients[item.clientId]
195
+ || !Object.values(msgPorts).includes(item.msgport)) {
196
+ fetchQueue.splice(fqdex, 1); // either client or source missing
197
+ item.resolve(new Response(undefined, {
198
+ status: 404,
199
+ statusText: "Source Disappeared" // if client is gone this disappears too
200
+ }));
201
+ if (fqdex >= fetchQueue.length)
202
+ break; // we've run out of transactions
203
+ item = fetchQueue[fqdex];
204
+ }
64
205
  }
65
206
  });
66
- });
207
+ return (promise);
208
+ }
209
+
210
+
211
+ /*
212
+ * ClearCaches
213
+ *
214
+ * Clear obsolete caches, returning a promise.
215
+ *
216
+ */
217
+
218
+ function ClearCaches() {
219
+ var promise = caches.keys().then(function(cacheNames) { // delete old cache entries
220
+ return (Promise.all(
221
+ cacheNames.map(function(cacheName) {
222
+ if (cacheName != CurrentCacheName) {
223
+ console.log('[1.0.4-2] deleting old cache:', cacheName);
224
+ return (caches.delete(cacheName));
225
+ }
226
+ })
227
+ ));
228
+ }).then(function() { // claim all clients
229
+ console.log('[1.0.4-2] claiming clients');
230
+ return (self.clients.claim());
231
+ });
232
+ return (promise);
233
+ }
234
+
235
+
236
+ /*
237
+ * GetClientResponse
238
+ *
239
+ * Get message ports from the current clients if they can supply contents for the specified URL.
240
+ * This returns a promise that is resolved after the msgPorts object is updated. Note that the
241
+ * clientId argument is the requester, but the client that actually has the available data will
242
+ * often be a different window.
243
+ * On the first request from a given client, every source is requested to respond to the URL
244
+ * pathname, and the first one that does is designated the official source for that client. While
245
+ * asking all the sources, this checks every second to see if any of them have gone away so that it
246
+ * does not hang waiting. A source can be unresponsive and delay the fetch forever, e.g. in the
247
+ * debugger, but if the window has closed then it will get reaped and the fetch will complete.
248
+ * On subsequent requests, to a known source, this does not have a timeout. If the source goes
249
+ * away then the fetch will not complete, but the client is screwed anyway in that case. Eventually
250
+ * the reaper will be called for other reasons and it will clean things up then.
251
+ *
252
+ */
253
+
254
+ function GetClientResponse(event, urlpath) {
255
+
256
+ // delegate
257
+ //
258
+ // Delegate to a source window and return a promise
259
+ //
260
+ function delegate(msgport, clientId) {
261
+ return (new Promise(function(resolve, reject) {
262
+ var seqno = fetchNextSeq++;
263
+ fetchQueue.push({
264
+ seq: seqno,
265
+ msgport: msgport,
266
+ clientId: clientId,
267
+ resolve: resolve
268
+ });
269
+ msgport.postMessage({
270
+ id: "fetch",
271
+ seq: seqno,
272
+ version: "1.0.4-2",
273
+ request: {
274
+ method: event.request.method,
275
+ url: event.request.url
276
+ },
277
+ });
278
+ }));
279
+ }
280
+
281
+ var clientId;
282
+ if (event.clientId !== "") {
283
+ clientId = event.clientId;
284
+ if (fetchPorts[clientId])
285
+ return (delegate(fetchPorts[clientId], false));
286
+ }
287
+ else
288
+ clientId = event.resultingClientId; // new window
289
+ var pending = [];
290
+ var sourceIds = Object.getOwnPropertyNames(msgPorts);
291
+ for (var mdex = 0; mdex < sourceIds.length; ++mdex)
292
+ {
293
+ var sourceId = sourceIds[mdex];
294
+ pending.push(delegate(msgPorts[sourceId], clientId));
295
+ }
296
+ if (pending.length === 0)
297
+ return (Promise.resolve(new Response(undefined, {
298
+ status: 404,
299
+ statusText: "No Sources"
300
+ })));
301
+ var toid = setInterval(Reaper, 1000); // reap every second while pending
302
+ return (Promise.all(pending).then(function(responses) {
303
+ clearInterval(toid); // all completed
304
+ for (var idex in responses)
305
+ if (responses[idex].status == 200)
306
+ return (responses[idex]); // this is the one we want
307
+ return (responses[0]); // fall back to first
308
+ }));
309
+ }
67
310
 
68
311
 
69
312
  /*
@@ -74,7 +317,7 @@ var promiseport = new Promise(function (resolve, reject) {
74
317
  */
75
318
 
76
319
  self.addEventListener('install', function(event) {
77
- console.log("[1.0.1-2] install");
320
+ console.log("[1.0.4-2] install");
78
321
  self.skipWaiting();
79
322
  });
80
323
 
@@ -95,53 +338,30 @@ self.addEventListener('fetch', function(event) {
95
338
  var tid;
96
339
  var promise;
97
340
  var url = new URL(event.request.url);
98
- var nocache = event.method != "GET" || url.search.length > 0;
99
- if (nocache && url.pathname.startsWith("/run/")) {
100
- if (msgport)
101
- promise = Promise.resolve(msgport);
102
- else {
103
- promise = self.clients.matchAll({
104
- includeUncontrolled: true
105
- }).then(function(clientList) {
106
- console.log("[1.0.1-2] requesting new msgport");
107
- var urls = clientList.map(function(client) {
108
- client.postMessage({ // tell client(s) we need a fetch source
109
- msg: "Naan_need_fetch_port"
110
- });
111
- return (client.url);
341
+ var nocache = event.request.method != "GET"
342
+ || url.search.length !== 0
343
+ || event.request.headers.get('range');
344
+ if (url.pathname.startsWith("/run/")) {
345
+ nocache = true;
346
+ if (waitingForInit) {
347
+ promise = new Promise(function(resolve, reject) {
348
+ waitingForInit.push(function(){
349
+ resolve(); // init was complete
112
350
  });
113
- console.log('[1.0.1-2] matching clients:', urls.join(', '));
114
- return (promiseport);
351
+ }).then(function(){
352
+ return (GetClientResponse(event, url.pathname)); // then get the response
115
353
  });
116
- }
117
- promise = promise.then(function(mp) { return new Promise(function(resolve, reject) {
118
- /*
119
- tid = setTimeout( function() {
120
- reject("timeout!");
121
- }, 250); */
122
- var seqno = fetchNextSeq++;
123
- if (msgport) {
124
- fetchQueue.push({
125
- seq: seqno,
126
- resolve: resolve
127
- });
128
- msgport.postMessage({
129
- id: "fetch",
130
- seq: seqno,
131
- version: "1.0.1-2",
132
- request: {
133
- method: event.request.method,
134
- url: event.request.url
135
- },
136
- });
137
- } else {
138
- console.log("[1.0.1-2] fetch has no msgport");
139
- return (reject("fatal delegation error"));
140
- }
141
- }) });
354
+ } else
355
+ promise = GetClientResponse(event, url.pathname);
142
356
  }
143
357
  else
144
- promise = fetch(event.request);
358
+ promise = fetch(event.request).catch(function (e) {
359
+ console.log("[1.0.4-2] fetch failed", e);
360
+ return (new Response(undefined, {
361
+ status: 404,
362
+ statusText: "Fetch Failed"
363
+ }));
364
+ });
145
365
  return (promise.then(function(response) {
146
366
  // delete tid timer ###
147
367
  if (!nocache) {
@@ -165,28 +385,16 @@ self.addEventListener('fetch', function(event) {
165
385
  */
166
386
 
167
387
  self.addEventListener('activate', function(event) {
168
- console.log("[1.0.1-2] activate");
388
+ console.log("[1.0.4-2] activate");
169
389
  self.clients.matchAll({ // for debugging, list controlled clients
170
390
  includeUncontrolled: true
171
391
  }).then(function(clientList) {
172
392
  var urls = clientList.map(function(client) {
173
393
  return (client.url);
174
394
  });
175
- console.log('[1.0.1-2] matching clients:', urls.join(', '));
395
+ console.log('[1.0.4-2] matching clients:', urls.join(', '));
176
396
  });
177
- var promise = caches.keys().then(function(cacheNames) { // delete old cache entries
178
- return (Promise.all(
179
- cacheNames.map(function(cacheName) {
180
- if (cacheName !== CurrentCacheName) {
181
- console.log('[1.0.1-2] deleting old cache:', cacheName);
182
- return (caches.delete(cacheName));
183
- }
184
- })
185
- ));
186
- }).then(function() { // claim all clients
187
- console.log('[1.0.1-2] claiming clients for version', CurrentCacheName);
188
- return (self.clients.claim());
189
- });
397
+ var promise = ClearCaches();
190
398
  if (event.waitUntil)
191
399
  event.waitUntil(promise);
192
400
  });
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * column positioning: // // !
8
8
  *
9
- * Copyright (c) 2019-2021 by Richard C. Zulch
9
+ * Copyright (c) 2019-2022 by Richard C. Zulch
10
10
  *
11
11
  */
12
12
 
@@ -77,7 +77,7 @@ closure TermHost(track, api, name, workerID, options,
77
77
  if !target.written
78
78
  target.term.WriteLn("(reconnected)")
79
79
  }, 1000).run()
80
- else
80
+ else if message.respOp
81
81
  debuglog("unknown host response message:", message.respOp)
82
82
  }
83
83
 
@@ -170,6 +170,7 @@ closure TermHost(track, api, name, workerID, options,
170
170
 
171
171
  target.sendControl = function sendControl(workerOp, payload) {
172
172
  target.ws.send(js.w.JSON.stringify({
173
+ guid: api.guid
173
174
  clientID: clientID
174
175
  serverID: serverID
175
176
  workerID: target.workerID
@@ -355,7 +356,7 @@ closure TermHost(track, api, name, workerID, options,
355
356
  */
356
357
 
357
358
  closure TermLocal(track, name, workerID, options,
358
- local target, nlregex)
359
+ local target, nlregex, listener)
359
360
  {
360
361
  if workerID == "NaanIDE" {
361
362
  if !js.t
@@ -363,8 +364,6 @@ closure TermLocal(track, name, workerID, options,
363
364
  termInstallVirtualWatcher(track, js.t)
364
365
  return (termIDE(track, name, workerID, js.t)) // connect to IDE execution instance itself
365
366
  }
366
- if not js.w.Worker
367
- return (list(Error("Local environment does not support workers")))
368
367
  if dictionary(options)
369
368
  options = new(options)
370
369
  else
@@ -372,13 +371,27 @@ closure TermLocal(track, name, workerID, options,
372
371
  target = runningExecutors.ExecutorBase(track, "Local", name)
373
372
  nlregex = RegExp("\\n", "g")
374
373
  target.workerID = workerID
375
- target.worker = xnew(js.w.Worker, "env_webworker.js")
376
374
  target.msgQueue = []
375
+ if workerID == "NaanIDE-Debug" {
376
+ target.worker = js.w.open(js.w.location.href, "_blank")
377
+ target.worker.addEventListener("beforeunload", function(e) { // our target is closing
378
+ destroy()
379
+ })
380
+ js.w.addEventListener("beforeunload", function(e) { // we are closing
381
+ // ### tell target we are closing (if needed)
382
+ })
383
+ listener = js.w
384
+ } else {
385
+ if not js.w.Worker
386
+ return (list(Error("Local environment does not support workers")))
387
+ target.worker = xnew(js.w.Worker, "env_webworker.js")
388
+ listener = target.worker
389
+ }
377
390
 
378
391
  // onmessage
379
392
  // A message was received from the worker.
380
393
 
381
- target.worker.onmessage = function(e, msg) {
394
+ listener.addEventListener("message", function(e, msg) {
382
395
  msg = new(e.data)
383
396
  if msg.id != "targetout" && msg.id != "loaded" {
384
397
  if msg.id == "status"
@@ -388,14 +401,14 @@ closure TermLocal(track, name, workerID, options,
388
401
  target.msgQueue.push(msg)
389
402
  return } }
390
403
  target.doMessage(msg)
391
- }
404
+ })
392
405
 
393
406
  // onerror
394
407
  // The worker had an error.
395
408
 
396
- target.worker.onerror = function(e) {
409
+ target.worker.addEventListener("error", function(e) {
397
410
  target.execClosed(Error("instance error", e.message))
398
- }
411
+ })
399
412
 
400
413
  //
401
414
  // oninterrupt
@@ -18,7 +18,7 @@
18
18
  *
19
19
  */
20
20
 
21
- closure ServiceWorker(local serv, callback) {
21
+ closure ServiceWorker(pubID, pubVersion, local serv, callback) {
22
22
  serv = new(object, this)
23
23
 
24
24
  //
@@ -32,7 +32,11 @@ closure ServiceWorker(local serv, callback) {
32
32
  mchan = xnew(js.w.MessageChannel)
33
33
  serv.msgport = mchan.port2
34
34
  serv.msgport.onmessage = workerMsg.proc
35
- serv.sw.postMessage({ hereIsYourPort: mchan.port1 }, [mchan.port1])
35
+ serv.sw.postMessage({
36
+ hereIsYourPort: mchan.port1
37
+ hereIsMyID: pubID
38
+ hereIsMyVersion: pubVersion
39
+ }, [mchan.port1])
36
40
  }
37
41
 
38
42
  //
@@ -50,7 +54,16 @@ closure ServiceWorker(local serv, callback) {
50
54
  request: msg.request
51
55
  version: msg.version
52
56
  })
53
- } else if msg.id == "text"
57
+ } else if msg.id == "upgrade" {
58
+ if serv.upgrade != msg.version {
59
+ serv.upgrade = msg.version // only report once
60
+ callback(false, {
61
+ message: "upgrade"
62
+ version: msg.version
63
+ })
64
+ }
65
+ }
66
+ else if msg.id == "text"
54
67
  debuglog("sw[".concat(serv.scope, "]: "), msg.text) // handy remote debug capability
55
68
  }
56
69
 
@@ -174,10 +187,10 @@ closure ServiceWorker(local serv, callback) {
174
187
  *
175
188
  */
176
189
 
177
- closure ScopedServiceWorker(local scoper, result) {
190
+ closure ScopedServiceWorker(pubID, pubVersion, callback, local scoper, result) {
178
191
  scoper = new(object, ScopedServiceWorker)
179
192
  scoper.scopes = {}
180
- scoper.serv = ServiceWorker()
193
+ scoper.serv = ServiceWorker(pubID, pubVersion)
181
194
  result = scoper.serv.register("/", closure(error, data, local response, filepath, scope) {
182
195
  if data.message == "fetch" {
183
196
  response = {
@@ -201,7 +214,7 @@ closure ScopedServiceWorker(local scoper, result) {
201
214
  filepath = xnew(js.w.URL, data.request.url).pathname
202
215
  filepath = DecodeURIComponent(filepath) // "/run/<scope>/path..."
203
216
  filepath = filepath.slice(4) // "/<scope>/path..."
204
- error = true
217
+ error = true // stays true iff not in scope
205
218
  for scope in scoper.scopes {
206
219
  if filepath.startsWith(scope) {
207
220
  filepath = filepath.slice(scope.length+1) // remove scope including "/"
@@ -209,8 +222,6 @@ closure ScopedServiceWorker(local scoper, result) {
209
222
  break
210
223
  }
211
224
  }
212
- if error === true
213
- debuglog("ScopedServiceWorker: couldn't find scope", scope, "in", totuple(scoper.scopes))
214
225
 
215
226
  // read resource failed
216
227
  if error {
@@ -226,7 +237,8 @@ closure ScopedServiceWorker(local scoper, result) {
226
237
  response.body = data
227
238
  scoper.serv.msgport.postMessage(response)
228
239
  return
229
- }
240
+ } else if data.message == "upgrade"
241
+ callback(data.version)
230
242
  })
231
243
  if result.0 {
232
244
  ErrorDebuglog("ScopedServiceWorker failed", result)
@@ -353,12 +365,14 @@ closure TrackedScope(track, scope, fs, rootpath, local trope, result) {
353
365
  /*
354
366
  * TrackServiceWorkers
355
367
  *
356
- * Configure service workers to be tracked in the execution tracker.
368
+ * Configure service workers to be tracked in the execution tracker. The pubID identifies us as
369
+ * the publisher for this service worker, and the version allows it to clear its cache when versions
370
+ * change.
357
371
  *
358
372
  */
359
373
 
360
- closure TrackServiceWorkers(track, local error) {
361
- `(error, serviceWorker) = ScopedServiceWorker()
374
+ closure TrackServiceWorkers(track, pubID, pubVersion, callback, local error) {
375
+ `(error, serviceWorker) = ScopedServiceWorker(pubID, pubVersion, callback)
362
376
  if error
363
377
  ErrorDebugLog("TrackServiceWorkers: cannot load service worker", error)
364
378
  track.register(TrackedScope, "V-Site")