arcane-os 0.11.3 → 0.13.0
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/CHANGELOG.md +32 -0
- package/NOTICE +1 -0
- package/README.md +27 -13
- package/browser-runtime/pwa-install.mjs +242 -0
- package/browser-runtime/pwa.mjs +131 -0
- package/docs/architecture.md +28 -12
- package/docs/reference/cli.md +42 -16
- package/docs/reference/inventory/package-api.json +44 -2
- package/docs/reference/inventory/runtime-components.json +29 -1
- package/docs/reference/pwa.md +252 -24
- package/docs/reference/runtime-components.md +64 -0
- package/docs/reference/sdk-api.md +158 -28
- package/package.json +3 -2
- package/runtime/arcane/components/pwa-install.html +216 -0
- package/src/app-descriptor.mjs +43 -1
- package/src/cli/main.mjs +29 -13
- package/src/dev-server.mjs +387 -136
- package/src/pwa-worker.mjs +361 -137
- package/src/pwa.mjs +11 -2
- package/src/targets/index.mjs +5 -1
- package/src/toolchain.mjs +62 -12
package/src/pwa-worker.mjs
CHANGED
|
@@ -1,40 +1,60 @@
|
|
|
1
|
-
export function createPwaWorkerScript(manifest) {
|
|
2
|
-
return `(${installPwaWorker.toString()})(${JSON.stringify(manifest, null, 4)});\n`;
|
|
1
|
+
export function createPwaWorkerScript(manifest, clientUrl = 'arcane/sdk/pwa.mjs') {
|
|
2
|
+
return `(${installPwaWorker.toString()})(${JSON.stringify(manifest, null, 4)}, ${JSON.stringify(clientUrl)});\n`;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
-
function installPwaWorker(manifest) {
|
|
5
|
+
function installPwaWorker(manifest, clientUrl) {
|
|
6
6
|
const scope = self.registration.scope;
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
);
|
|
7
|
+
const scopeOrigin = new URL(scope).origin;
|
|
8
|
+
const cachePrefix = `arcane-pwa|${JSON.stringify([manifest.appId, scope])}|`;
|
|
9
|
+
const cacheName = `${cachePrefix}resources`;
|
|
10
|
+
const manifestUrl = cacheUrl('arcane-offline.json');
|
|
11
|
+
const protocolUrls = new Set([cacheUrl('arcane-pwa.mjs'), cacheUrl(clientUrl)]);
|
|
12
|
+
const installationAssets = [...new Set(manifest.assets.map(cacheUrl))];
|
|
13
|
+
const resourceJobs = new Map();
|
|
14
|
+
const pendingChecks = new Set();
|
|
15
|
+
const refreshJobs = new Map();
|
|
16
|
+
const ownedUrls = new Set();
|
|
17
|
+
const navigationAliases = new Map();
|
|
18
|
+
let currentManifest = manifest;
|
|
19
|
+
let refreshTask = null;
|
|
20
|
+
let previousCaches = null;
|
|
21
|
+
let manifestRestored = false;
|
|
22
|
+
let lastChecked = null;
|
|
18
23
|
|
|
19
24
|
function cacheUrl(value) {
|
|
20
25
|
const url = new URL(value, scope);
|
|
21
|
-
//
|
|
26
|
+
// Fragments identify document positions; query variants remain distinct resources.
|
|
22
27
|
url.hash = '';
|
|
23
28
|
return url.href;
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
cacheUrl(alias),
|
|
36
|
-
new URL(asset, scope).href
|
|
31
|
+
function manifestResources(value) {
|
|
32
|
+
if (!Array.isArray(value.assets) || !value.navigationAliases || typeof value.navigationAliases !== 'object') {
|
|
33
|
+
throw new TypeError('The PWA offline inventory requires assets and navigationAliases.');
|
|
34
|
+
}
|
|
35
|
+
const assets = value.assets.map(cacheUrl);
|
|
36
|
+
const aliases = Object.entries(value.navigationAliases).map(
|
|
37
|
+
function navigationAlias([alias, asset]) {
|
|
38
|
+
return [cacheUrl(alias), new URL(asset, scope).href];
|
|
39
|
+
}
|
|
37
40
|
);
|
|
41
|
+
return {value, assets, aliases};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function useManifest({value, assets, aliases}) {
|
|
45
|
+
currentManifest = value;
|
|
46
|
+
ownedUrls.clear();
|
|
47
|
+
for (const asset of assets) {
|
|
48
|
+
ownedUrls.add(asset);
|
|
49
|
+
}
|
|
50
|
+
navigationAliases.clear();
|
|
51
|
+
for (const [alias, asset] of aliases) {
|
|
52
|
+
navigationAliases.set(alias, asset);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readManifest(response) {
|
|
57
|
+
return manifestResources(await response.json());
|
|
38
58
|
}
|
|
39
59
|
|
|
40
60
|
function errorDetails(error) {
|
|
@@ -55,9 +75,7 @@ function installPwaWorker(manifest) {
|
|
|
55
75
|
}
|
|
56
76
|
|
|
57
77
|
async function reportFailure(error) {
|
|
58
|
-
const clients = await self.clients.matchAll(
|
|
59
|
-
{type: 'window', includeUncontrolled: true}
|
|
60
|
-
);
|
|
78
|
+
const clients = await self.clients.matchAll({type: 'window', includeUncontrolled: true});
|
|
61
79
|
const message = {type: 'arcane.pwa.error', error: errorDetails(error)};
|
|
62
80
|
for (const client of clients) {
|
|
63
81
|
if (client.url.startsWith(scope)) {
|
|
@@ -66,126 +84,332 @@ function installPwaWorker(manifest) {
|
|
|
66
84
|
}
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
async function
|
|
87
|
+
async function reportFailureAndReject(error) {
|
|
88
|
+
try {
|
|
89
|
+
await reportFailure(error);
|
|
90
|
+
} catch (reportError) {
|
|
91
|
+
console.error('PWA failure could not be sent to application clients.', reportError, error);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resourceError(url, cause) {
|
|
97
|
+
const error = new Error(`PWA resource could not be cached: ${url}`, {cause});
|
|
98
|
+
error.url = url;
|
|
99
|
+
return error;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function findCachedResource(url) {
|
|
70
103
|
const cache = await caches.open(cacheName);
|
|
71
|
-
const
|
|
72
|
-
|
|
104
|
+
const response = await cache.match(url);
|
|
105
|
+
if (response) {
|
|
106
|
+
return {cache, response, current: true};
|
|
107
|
+
}
|
|
108
|
+
// Carry existing app resources forward without deleting older generations.
|
|
109
|
+
previousCaches ??= caches.keys().then(
|
|
110
|
+
function openPriorCaches(names) {
|
|
111
|
+
return Promise.all(
|
|
112
|
+
names.reverse().filter(
|
|
113
|
+
function priorAppCache(name) {
|
|
114
|
+
return name.startsWith(cachePrefix) && name !== cacheName;
|
|
115
|
+
}
|
|
116
|
+
).map(
|
|
117
|
+
function openPriorCache(name) {
|
|
118
|
+
return caches.open(name);
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
for (const previous of await previousCaches) {
|
|
125
|
+
const retained = await previous.match(url);
|
|
126
|
+
if (retained) {
|
|
127
|
+
return {cache, response: retained, current: false};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return {cache, response: null, current: true};
|
|
131
|
+
}
|
|
73
132
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
{cache: manifest.mode === 'development' ? 'no-cache' : 'reload'}
|
|
82
|
-
)
|
|
83
|
-
);
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
throw new Error(`PWA resource returned HTTP ${response.status} ${response.statusText}: ${url}`);
|
|
86
|
-
}
|
|
87
|
-
await cache.put(url, response);
|
|
88
|
-
} catch (cause) {
|
|
89
|
-
const error = new Error(
|
|
90
|
-
`PWA resource could not be cached: ${url}`,
|
|
91
|
-
{cause}
|
|
92
|
-
);
|
|
93
|
-
error.url = url;
|
|
94
|
-
failures.push(error);
|
|
133
|
+
async function restoreManifest() {
|
|
134
|
+
const cached = await findCachedResource(manifestUrl);
|
|
135
|
+
if (cached.response) {
|
|
136
|
+
const inventory = await readManifest(cached.response);
|
|
137
|
+
if (['appVersion', 'sdkVersion', 'mode', 'revision'].every(
|
|
138
|
+
function sameWorkerDeclaration(field) {
|
|
139
|
+
return inventory.value[field] === manifest[field];
|
|
95
140
|
}
|
|
141
|
+
)) {
|
|
142
|
+
useManifest(inventory);
|
|
96
143
|
}
|
|
97
144
|
}
|
|
145
|
+
}
|
|
98
146
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
useManifest(manifestResources(manifest));
|
|
148
|
+
const restored = restoreManifest().catch(
|
|
149
|
+
async function reportManifestRestoreFailure(error) {
|
|
150
|
+
try {
|
|
151
|
+
await reportFailure(error);
|
|
152
|
+
} catch (reportError) {
|
|
153
|
+
console.error('PWA manifest restore failure could not be reported.', reportError, error);
|
|
154
|
+
}
|
|
104
155
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
156
|
+
).finally(
|
|
157
|
+
function manifestRestoreComplete() {
|
|
158
|
+
manifestRestored = true;
|
|
108
159
|
}
|
|
109
|
-
|
|
160
|
+
);
|
|
110
161
|
|
|
111
|
-
async function
|
|
162
|
+
async function fetchResource(request, url, validate) {
|
|
163
|
+
const cached = await findCachedResource(url);
|
|
164
|
+
// Earlier clients cannot initiate this protocol; upgrade only its owners on migration.
|
|
165
|
+
const protocolMigration = cached.response && !cached.current && protocolUrls.has(url);
|
|
166
|
+
if (cached.response && !validate && !protocolMigration) {
|
|
167
|
+
return {
|
|
168
|
+
response: cached.response,
|
|
169
|
+
saved: cached.current ? Promise.resolve() : cached.cache.put(url, cached.response.clone()),
|
|
170
|
+
checked: false,
|
|
171
|
+
error: null
|
|
172
|
+
};
|
|
173
|
+
}
|
|
112
174
|
try {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
175
|
+
const headers = new Headers(request.headers);
|
|
176
|
+
const modified = protocolMigration ? null : cached.response?.headers.get('last-modified');
|
|
177
|
+
headers.delete('if-none-match');
|
|
178
|
+
if (modified) {
|
|
179
|
+
headers.set('if-modified-since', modified);
|
|
180
|
+
} else {
|
|
181
|
+
headers.delete('if-modified-since');
|
|
182
|
+
}
|
|
183
|
+
// This conditional request owns validation; CacheStorage owns the durable body.
|
|
184
|
+
const response = await fetch(new Request(request, {headers, cache: 'no-store'}));
|
|
185
|
+
if (response.status === 304 && cached.response) {
|
|
186
|
+
return {
|
|
187
|
+
response: cached.response,
|
|
188
|
+
saved: cached.current ? Promise.resolve() : cached.cache.put(url, cached.response.clone()),
|
|
189
|
+
checked: true,
|
|
190
|
+
error: null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`PWA resource returned HTTP ${response.status} ${response.statusText}: ${url}`);
|
|
195
|
+
}
|
|
196
|
+
if (url === manifestUrl) {
|
|
197
|
+
// Parse the control document before replacing the last usable inventory.
|
|
198
|
+
await readManifest(response.clone());
|
|
199
|
+
}
|
|
200
|
+
return {
|
|
201
|
+
response,
|
|
202
|
+
saved: cached.cache.put(url, response.clone()),
|
|
203
|
+
checked: true,
|
|
204
|
+
error: null
|
|
205
|
+
};
|
|
206
|
+
} catch (cause) {
|
|
207
|
+
const error = resourceError(url, cause);
|
|
208
|
+
if (!cached.response) {
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
return {response: cached.response, saved: Promise.resolve(), checked: false, error};
|
|
116
212
|
}
|
|
117
|
-
throw error;
|
|
118
213
|
}
|
|
119
214
|
|
|
120
|
-
function
|
|
121
|
-
|
|
122
|
-
|
|
215
|
+
function resourceJob(request, url, validate = false) {
|
|
216
|
+
if (resourceJobs.has(url)) {
|
|
217
|
+
return resourceJobs.get(url);
|
|
218
|
+
}
|
|
219
|
+
const job = {result: fetchResource(request, url, validate), done: null, checked: false};
|
|
220
|
+
resourceJobs.set(url, job);
|
|
221
|
+
if (pendingChecks.has(url)) {
|
|
222
|
+
refreshJobs.set(url, job);
|
|
223
|
+
}
|
|
224
|
+
job.done = job.result.then(
|
|
225
|
+
async function finishResource(result) {
|
|
226
|
+
job.checked = result.checked;
|
|
227
|
+
try {
|
|
228
|
+
await result.saved;
|
|
229
|
+
return result.error;
|
|
230
|
+
} catch (cause) {
|
|
231
|
+
return resourceError(url, cause);
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
function missingResource(error) {
|
|
235
|
+
return error;
|
|
236
|
+
}
|
|
237
|
+
).then(
|
|
238
|
+
function releaseResource(error) {
|
|
239
|
+
resourceJobs.delete(url);
|
|
240
|
+
pendingChecks.delete(url);
|
|
241
|
+
if (refreshJobs.get(url) === job) {
|
|
242
|
+
// Retain cycle outcomes without retaining every completed response stream.
|
|
243
|
+
refreshJobs.set(url, {
|
|
244
|
+
result: Promise.resolve({checked: job.checked}),
|
|
245
|
+
done: Promise.resolve(error)
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
return error;
|
|
249
|
+
}
|
|
123
250
|
);
|
|
251
|
+
return job;
|
|
124
252
|
}
|
|
125
253
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
254
|
+
function resourceResponse(result) {
|
|
255
|
+
return result.response.clone();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function populateResources(urls, validate = false) {
|
|
259
|
+
let nextAsset = 0;
|
|
260
|
+
const failures = [];
|
|
261
|
+
let allChecked = true;
|
|
262
|
+
async function fetchAssets() {
|
|
263
|
+
while (nextAsset < urls.length) {
|
|
264
|
+
const url = urls[nextAsset++];
|
|
265
|
+
let job = (validate ? refreshJobs.get(url) : null) ?? resourceJob(new Request(url), url, validate);
|
|
266
|
+
let error = await job.done;
|
|
267
|
+
if (validate && !error && !(await job.result).checked) {
|
|
268
|
+
// A concurrent cache carry-forward is complete, but has not checked this file.
|
|
269
|
+
job = resourceJob(new Request(url), url, true);
|
|
270
|
+
error = await job.done;
|
|
271
|
+
}
|
|
272
|
+
if (error) {
|
|
273
|
+
failures.push(error);
|
|
274
|
+
}
|
|
275
|
+
if (error || !(await job.result).checked) {
|
|
276
|
+
allChecked = false;
|
|
277
|
+
}
|
|
134
278
|
}
|
|
135
279
|
}
|
|
136
|
-
|
|
280
|
+
const workers = [];
|
|
281
|
+
for (let index = 0; index < Math.min(4, urls.length); index += 1) {
|
|
282
|
+
workers.push(fetchAssets());
|
|
283
|
+
}
|
|
284
|
+
await Promise.all(workers);
|
|
285
|
+
return {failures, allChecked};
|
|
137
286
|
}
|
|
138
287
|
|
|
139
|
-
function
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
)
|
|
288
|
+
async function populateCache() {
|
|
289
|
+
await restored;
|
|
290
|
+
const {failures, allChecked} = await populateResources(installationAssets);
|
|
291
|
+
if (failures.length > 0) {
|
|
292
|
+
throw new AggregateError(failures, 'The PWA resource generation could not be installed.');
|
|
293
|
+
}
|
|
294
|
+
if (allChecked) {
|
|
295
|
+
lastChecked = Date.now();
|
|
296
|
+
}
|
|
143
297
|
}
|
|
144
298
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
299
|
+
function onInstall(event) {
|
|
300
|
+
event.waitUntil(populateCache().catch(reportFailureAndReject));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function checkDue() {
|
|
304
|
+
const interval = currentManifest.mode === 'development' ? 120000 : 900000;
|
|
305
|
+
return lastChecked === null || Date.now() - lastChecked > interval;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function refreshResources() {
|
|
309
|
+
await restored;
|
|
310
|
+
if (!checkDue()) {
|
|
311
|
+
return {lastChecked, error: null};
|
|
150
312
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
)
|
|
313
|
+
const failures = [];
|
|
314
|
+
let inventory = resourceJob(new Request(manifestUrl), manifestUrl, true);
|
|
315
|
+
let error = await inventory.done;
|
|
316
|
+
if (!error && !(await inventory.result).checked) {
|
|
317
|
+
inventory = resourceJob(new Request(manifestUrl), manifestUrl, true);
|
|
318
|
+
error = await inventory.done;
|
|
319
|
+
}
|
|
320
|
+
if (error) {
|
|
321
|
+
failures.push(error);
|
|
322
|
+
} else {
|
|
323
|
+
try {
|
|
324
|
+
const result = await inventory.result;
|
|
325
|
+
useManifest(await readManifest(result.response.clone()));
|
|
326
|
+
} catch (cause) {
|
|
327
|
+
failures.push(resourceError(manifestUrl, cause));
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const urls = [...ownedUrls].filter(
|
|
331
|
+
function selectedResource(url) {
|
|
332
|
+
return url !== manifestUrl;
|
|
333
|
+
}
|
|
156
334
|
);
|
|
335
|
+
for (const url of urls) {
|
|
336
|
+
pendingChecks.add(url);
|
|
337
|
+
}
|
|
338
|
+
const checked = await populateResources(urls, true);
|
|
339
|
+
failures.push(...checked.failures);
|
|
340
|
+
if (failures.length === 0) {
|
|
341
|
+
lastChecked = Date.now();
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
lastChecked,
|
|
345
|
+
error: failures.length > 0
|
|
346
|
+
? errorDetails(new AggregateError(failures, 'PWA resources could not all be updated.'))
|
|
347
|
+
: null
|
|
348
|
+
};
|
|
157
349
|
}
|
|
158
350
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
351
|
+
function refresh(checked) {
|
|
352
|
+
if (Number.isFinite(checked)) {
|
|
353
|
+
lastChecked = Math.max(lastChecked ?? 0, checked);
|
|
354
|
+
}
|
|
355
|
+
if (!refreshTask) {
|
|
356
|
+
refreshTask = refreshResources().finally(
|
|
357
|
+
function releaseRefresh() {
|
|
358
|
+
refreshTask = null;
|
|
359
|
+
refreshJobs.clear();
|
|
360
|
+
}
|
|
167
361
|
);
|
|
168
|
-
} catch (error) {
|
|
169
|
-
const cache = await caches.open(cacheName);
|
|
170
|
-
const cached = await cache.match(url);
|
|
171
|
-
if (cached) {
|
|
172
|
-
return {response: cached, fromNetwork: false, url};
|
|
173
|
-
}
|
|
174
|
-
throw error;
|
|
175
362
|
}
|
|
176
|
-
return
|
|
363
|
+
return refreshTask;
|
|
177
364
|
}
|
|
178
365
|
|
|
179
|
-
function
|
|
180
|
-
|
|
366
|
+
function onMessage(event) {
|
|
367
|
+
if (event.data?.type === 'arcane.pwa.capabilities') {
|
|
368
|
+
try {
|
|
369
|
+
event.source?.postMessage({type: 'arcane.pwa.capabilities', refresh: true, cacheName});
|
|
370
|
+
} catch (error) {
|
|
371
|
+
event.waitUntil(reportFailureAndReject(error));
|
|
372
|
+
}
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (event.data?.type !== 'arcane.pwa.refresh') {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
const port = event.ports?.[0];
|
|
379
|
+
event.waitUntil(
|
|
380
|
+
refresh(event.data.lastChecked).then(
|
|
381
|
+
async function completeRefresh(result) {
|
|
382
|
+
port?.postMessage({type: 'arcane.pwa.refreshed', ...result});
|
|
383
|
+
port?.close();
|
|
384
|
+
if (result.error) {
|
|
385
|
+
await reportFailure(result.error);
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
async function failRefresh(error) {
|
|
389
|
+
port?.postMessage({type: 'arcane.pwa.refreshed', lastChecked, error: errorDetails(error)});
|
|
390
|
+
port?.close();
|
|
391
|
+
await reportFailure(error);
|
|
392
|
+
}
|
|
393
|
+
).catch(reportFailureAndReject)
|
|
394
|
+
);
|
|
181
395
|
}
|
|
182
396
|
|
|
183
|
-
async function
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
397
|
+
async function requestedResource(request, url) {
|
|
398
|
+
await restored;
|
|
399
|
+
const redirect = request.mode === 'navigate' ? navigationAliases.get(url) : null;
|
|
400
|
+
if (redirect && cacheUrl(redirect) !== url && ownedUrls.has(cacheUrl(redirect))) {
|
|
401
|
+
return {response: Response.redirect(redirect, 302), done: Promise.resolve(null)};
|
|
402
|
+
}
|
|
403
|
+
if (!ownedUrls.has(url)) {
|
|
404
|
+
return {response: await fetch(request), done: Promise.resolve(null)};
|
|
188
405
|
}
|
|
406
|
+
const cache = await caches.open(cacheName);
|
|
407
|
+
const cached = await cache.match(url);
|
|
408
|
+
if (cached) {
|
|
409
|
+
return {response: cached, done: Promise.resolve(null)};
|
|
410
|
+
}
|
|
411
|
+
const job = resourceJob(request, url, pendingChecks.has(url));
|
|
412
|
+
return {response: resourceResponse(await job.result), done: job.done};
|
|
189
413
|
}
|
|
190
414
|
|
|
191
415
|
function onFetch(event) {
|
|
@@ -193,35 +417,35 @@ function installPwaWorker(manifest) {
|
|
|
193
417
|
if (request.method !== 'GET' || request.headers.has('range')) {
|
|
194
418
|
return;
|
|
195
419
|
}
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
if (redirect && cacheUrl(redirect) !== requestUrl && ownedUrls.has(cacheUrl(redirect))) {
|
|
199
|
-
// Navigation retains its requested document URL unless it follows a redirect.
|
|
200
|
-
event.respondWith(
|
|
201
|
-
Response.redirect(redirect, 302)
|
|
202
|
-
);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
const url = requestUrl;
|
|
206
|
-
if (!ownedUrls.has(url)) {
|
|
420
|
+
const url = cacheUrl(request.url);
|
|
421
|
+
if (new URL(url).origin !== scopeOrigin && !ownedUrls.has(url)) {
|
|
207
422
|
return;
|
|
208
423
|
}
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
event.waitUntil(
|
|
212
|
-
resource.then(saveDevelopmentResource).catch(reportFailureAndReject)
|
|
213
|
-
);
|
|
214
|
-
event.respondWith(
|
|
215
|
-
resource.then(resourceResponse)
|
|
216
|
-
);
|
|
424
|
+
if (manifestRestored && !ownedUrls.has(url)
|
|
425
|
+
&& !(request.mode === 'navigate' && navigationAliases.has(url))) {
|
|
217
426
|
return;
|
|
218
427
|
}
|
|
428
|
+
const resource = requestedResource(request, url);
|
|
219
429
|
event.respondWith(
|
|
220
|
-
|
|
430
|
+
resource.then(
|
|
431
|
+
function requestedResponse(result) {
|
|
432
|
+
return result.response;
|
|
433
|
+
}
|
|
434
|
+
)
|
|
435
|
+
);
|
|
436
|
+
event.waitUntil(
|
|
437
|
+
resource.then(
|
|
438
|
+
async function completeRequestedResource(result) {
|
|
439
|
+
const error = await result.done;
|
|
440
|
+
if (error) {
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
).catch(reportFailureAndReject)
|
|
221
445
|
);
|
|
222
446
|
}
|
|
223
447
|
|
|
224
448
|
self.addEventListener('install', onInstall);
|
|
225
|
-
self.addEventListener('activate', onActivate);
|
|
226
449
|
self.addEventListener('fetch', onFetch);
|
|
450
|
+
self.addEventListener('message', onMessage);
|
|
227
451
|
}
|
package/src/pwa.mjs
CHANGED
|
@@ -277,7 +277,13 @@ export function createPwaArtifacts(
|
|
|
277
277
|
),
|
|
278
278
|
navigationAliases: navigationAliases ?? {[basePath]: app.entry}
|
|
279
279
|
};
|
|
280
|
-
const bootstrap = `import {registerPwa} from ${JSON.stringify(`${runtimeBase}pwa.mjs`)};
|
|
280
|
+
const bootstrap = `import {registerPwa, mountPwaInstallPrompt} from ${JSON.stringify(`${runtimeBase}pwa.mjs`)};
|
|
281
|
+
|
|
282
|
+
mountPwaInstallPrompt({appName: ${JSON.stringify(manifest.name)}}).catch(
|
|
283
|
+
function reportPwaInstallComponentFailure(error) {
|
|
284
|
+
console.error('Arcane PWA install component failed:', error);
|
|
285
|
+
}
|
|
286
|
+
);
|
|
281
287
|
|
|
282
288
|
const controller = registerPwa(
|
|
283
289
|
{
|
|
@@ -301,7 +307,10 @@ controller.ready.catch(
|
|
|
301
307
|
files: [
|
|
302
308
|
{path: PWA_MANIFEST_NAME, content: json(manifest)},
|
|
303
309
|
{path: PWA_OFFLINE_MANIFEST_NAME, content: json(offlineManifest)},
|
|
304
|
-
{
|
|
310
|
+
{
|
|
311
|
+
path: PWA_WORKER_NAME,
|
|
312
|
+
content: createPwaWorkerScript(offlineManifest, `${runtimeBase}pwa.mjs`)
|
|
313
|
+
},
|
|
305
314
|
{path: PWA_BOOTSTRAP_NAME, content: bootstrap}
|
|
306
315
|
],
|
|
307
316
|
entryAssets,
|
package/src/targets/index.mjs
CHANGED
|
@@ -148,7 +148,7 @@ const browserAdapter={
|
|
|
148
148
|
throwIfAborted(signal);
|
|
149
149
|
return {target:'browser',release:await verifyApp({workspaceRoot,appId,signal,onEvent})};
|
|
150
150
|
},
|
|
151
|
-
async run({workspaceRoot,appId,host='127.0.0.1',port=0,signal,onEvent}={}){
|
|
151
|
+
async run({workspaceRoot,appId,host='127.0.0.1',port=0,httpPort=0,tls,certPath,keyPath,signal,onEvent}={}){
|
|
152
152
|
throwIfAborted(signal);
|
|
153
153
|
const releaseRoot=path.join(workspaceRoot,'dist',appId);
|
|
154
154
|
const server=await startDevServer({
|
|
@@ -158,6 +158,10 @@ const browserAdapter={
|
|
|
158
158
|
releaseRoot,
|
|
159
159
|
host,
|
|
160
160
|
port,
|
|
161
|
+
httpPort,
|
|
162
|
+
tls,
|
|
163
|
+
certPath,
|
|
164
|
+
keyPath,
|
|
161
165
|
signal,
|
|
162
166
|
onEvent
|
|
163
167
|
});
|