@banou/ripple 0.0.31 → 0.0.33

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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banou/ripple",
3
- "version": "0.0.31",
3
+ "version": "0.0.33",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
package/sw.js CHANGED
@@ -8,10 +8,12 @@ const live = new Map()
8
8
  /* Stamped at build time. These are the dev defaults and must never reach production: a worker whose
9
9
  BUILD never changes is never reinstalled, so the update this feature announces would never fire. */
10
10
  const MODE = "cache" // @stamp:mode
11
- const BUILD = "e9395b9ff96b0935" // @stamp:build
12
- const MANIFEST = [["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-DleyqKUt.js","27516279f85831f1"],["/assets/worker-ue47p8CF.js","4a1f9c2389c91279"],["/build-B7te3F-u.js","47b056c564f078c5"],["/remote-DvdN47iT.js","b0649250a9e3a079"],["/volume-normalizer-worklet.js","4021752281e110ba"]] // @stamp:manifest
11
+ const BUILD = "fc846249cd2cf506" // @stamp:build
12
+ const MANIFEST = [["/assets/empty-BvRk9kiK.js","e3b0c44298fc1c14"],["/assets/libtorrent-DleyqKUt.js","27516279f85831f1"],["/assets/worker-kaROsro7.js","3f29b36a9bea36cc"],["/build-B7te3F-u.js","47b056c564f078c5"],["/remote-DvdN47iT.js","b0649250a9e3a079"]] // @stamp:manifest
13
13
 
14
14
  const CACHE = 'ripple-assets-' + BUILD
15
+ /** Holds the name of the generation that activated last, so the next activation can keep it. */
16
+ const ACTIVE = 'ripple-active'
15
17
 
16
18
  /**
17
19
  * Which requests this worker may answer from its cache, and the whole of it.
@@ -30,25 +32,49 @@ const CACHE = 'ripple-assets-' + BUILD
30
32
  * SPA fallback: 200, text/html, which the browser then runs as a module worker. Measured against
31
33
  * production, an earlier build's chunk is byte-identical to one that never existed.
32
34
  */
33
- const HASHED = /^\/(?:assets\/)?[A-Za-z0-9_.$-]+-[A-Za-z0-9_-]{8,}\.js$/
35
+ const HASHED = /^\/(?:assets\/)?[A-Za-z0-9_.$-]+-[A-Za-z0-9_-]{8}\.js$/
36
+
37
+ // An HTML answer for a chunk path is Cloudflare's SPA fallback for a chunk that is gone, not the chunk.
38
+ const isScript = (response) => response.ok && (response.headers.get('content-type') || '').includes('javascript')
39
+
40
+ const PRECACHE_ATTEMPTS = 3
41
+
42
+ /** Resolves once every manifest chunk is stored, asking again only for the ones that failed. */
43
+ const precache = async () => {
44
+ const cache = await caches.open(CACHE)
45
+ let missing = MANIFEST.map(([path]) => path)
46
+ for (let attempt = 0; attempt < PRECACHE_ATTEMPTS && missing.length > 0; attempt++) {
47
+ const failed = await Promise.all(missing.map(async (path) => {
48
+ try {
49
+ const response = await fetch(path)
50
+ if (!isScript(response)) return path
51
+ await cache.put(path, response)
52
+ return null
53
+ } catch {
54
+ return path
55
+ }
56
+ }))
57
+ missing = failed.filter((path) => path !== null)
58
+ }
59
+ if (missing.length > 0) throw new Error('precache incomplete: ' + missing.join(', '))
60
+ }
34
61
 
35
62
  self.addEventListener('install', (event) => {
36
63
  // PURGE is the remote kill switch: ship src/sw.mode as `purge` and the next deploy's worker takes
37
64
  // over at once, deletes every cache, and answers nothing. It needs no cooperation from the page.
38
65
  if (MODE === 'purge') { self.skipWaiting(); return }
39
66
  /*
40
- * No skipWaiting. A new worker installs and WAITS, so a page keeps the build it loaded with until
41
- * somebody presses Update. That is the opposite of what this worker used to do, and it is the
42
- * point: taking over immediately is what would swap the chunk set under a running engine.
67
+ * No skipWaiting: a new worker installs and WAITS, and the page decides when it takes over.
68
+ *
69
+ * An installed worker is what the page reads as "the update is downloaded", so with a worker
70
+ * already active an incomplete precache fails the install: the browser drops this worker and
71
+ * installs it again at its next update check, while the active one keeps serving. The first
72
+ * install never fails on it, because an origin with no worker at all is worse than one whose
73
+ * cache fills on demand.
43
74
  */
44
- event.waitUntil(
45
- caches.open(CACHE)
46
- .then((cache) => cache.addAll(MANIFEST.map(([path]) => path)))
47
- // A failed precache must not fail the install. The worker still routes and still fills the
48
- // cache on demand; a rejected install would leave the origin with no worker at all, which is
49
- // strictly worse than one with a cold cache.
50
- .catch(() => undefined),
51
- )
75
+ event.waitUntil(precache().catch((error) => {
76
+ if (self.registration.active) throw error
77
+ }))
52
78
  })
53
79
 
54
80
  self.addEventListener('activate', (event) => {
@@ -58,11 +84,21 @@ self.addEventListener('activate', (event) => {
58
84
  await self.clients.claim()
59
85
  return
60
86
  }
61
- // every generation but this one: a hashed chunk is immutable, so old generations are only ever
62
- // wasted room, never wrong answers
63
- for (const key of await caches.keys()) {
64
- if (key.startsWith('ripple-assets-') && key !== CACHE) await caches.delete(key)
87
+ /*
88
+ * Every generation but this one and the one active before it. A page the update gate leaves on
89
+ * the previous build (held by a save or a video, or framed by another page) still loads its lazy
90
+ * chunks, the torrent engine's included, and this worker controls it from the claim below. Older
91
+ * generations are only wasted room: a hashed chunk is immutable, so none of them is ever wrong.
92
+ * With no record yet (the first activation that keeps one) nothing is deleted.
93
+ */
94
+ const record = await caches.open(ACTIVE)
95
+ const previous = await (await record.match('/'))?.text()
96
+ if (previous !== undefined) {
97
+ for (const key of await caches.keys()) {
98
+ if (key.startsWith('ripple-assets-') && key !== CACHE && key !== previous) await caches.delete(key)
99
+ }
65
100
  }
101
+ await record.put('/', new Response(CACHE))
66
102
  await self.clients.claim()
67
103
  })())
68
104
  })
@@ -82,8 +118,8 @@ self.addEventListener('message', (event) => {
82
118
  event.source.postMessage({ type: 'pong', id: data.id })
83
119
  return
84
120
  }
85
- // Pressed Update. Only a WAITING worker ever receives this, and taking over fires controllerchange
86
- // in every open page, which is what reloads them all together.
121
+ // From a page's update gate. Only a WAITING worker ever receives this, and taking over fires
122
+ // controllerchange in every open page, each of which then reloads at its own safe moment.
87
123
  if (data.type === 'take-over') self.skipWaiting()
88
124
  if (data.type === 'which-build' && event.source) {
89
125
  event.source.postMessage({ type: 'build', build: BUILD, mode: MODE })
@@ -110,15 +146,14 @@ self.addEventListener('fetch', (event) => {
110
146
  const path = new URL(url).pathname
111
147
  if (new URL(url).origin === self.location.origin && HASHED.test(path)) {
112
148
  event.respondWith((async () => {
113
- const cache = await caches.open(CACHE)
114
- const hit = await cache.match(path)
149
+ // any generation: a page still on the previous build asks for that build's chunks
150
+ const hit = await caches.match(path)
115
151
  if (hit) return hit
116
152
  const fresh = await fetch(event.request)
117
153
  // An HTML answer here IS the bug this worker exists for: the chunk is gone and Cloudflare
118
154
  // served the SPA fallback with a 200. Never store that, or the cache would preserve the
119
155
  // failure across the very reload meant to cure it.
120
- const type = fresh.headers.get('content-type') || ''
121
- if (fresh.ok && type.includes('javascript')) await cache.put(path, fresh.clone())
156
+ if (isScript(fresh)) await (await caches.open(CACHE)).put(path, fresh.clone())
122
157
  return fresh
123
158
  })())
124
159
  return