@lls/offline 24.27.0-ff4d6c28 → 24.28.0-0854ed80
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/lib/assets/fetch-worker.js +200 -71
- package/lib/assets/offline-worker.js +135 -669
- package/lib/index.js +91 -33
- package/package.json +4 -3
|
@@ -1,8 +1,26 @@
|
|
|
1
|
+
// src/utils/stream.ts
|
|
2
|
+
var parseRange = (range, size) => {
|
|
3
|
+
const parts = range.replace("bytes=", "").split("-");
|
|
4
|
+
let start = Number.parseInt(parts[0], 10);
|
|
5
|
+
let end = Number.parseInt(parts[1], 10);
|
|
6
|
+
if (isNaN(start)) {
|
|
7
|
+
start = size - end;
|
|
8
|
+
end = size - 1;
|
|
9
|
+
} else if (isNaN(end)) {
|
|
10
|
+
end = size - 1;
|
|
11
|
+
}
|
|
12
|
+
if (end > size - 1) {
|
|
13
|
+
end = size - 1;
|
|
14
|
+
}
|
|
15
|
+
const chunkSize = end - start + 1;
|
|
16
|
+
return { start, end, chunkSize };
|
|
17
|
+
};
|
|
18
|
+
|
|
1
19
|
// src/utils/worker.ts
|
|
2
|
-
var makeSyncWorker = (
|
|
20
|
+
var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
|
|
3
21
|
const dic = {};
|
|
4
22
|
const cbks = {};
|
|
5
|
-
const fn = ({ data: { eventId, methodName, results, error, arg } = {} }
|
|
23
|
+
const fn = ({ source, data: { eventId, methodName, results, error, arg } = {} }) => {
|
|
6
24
|
if (eventId && eventId in dic) {
|
|
7
25
|
if (error) {
|
|
8
26
|
dic[eventId].reject(error);
|
|
@@ -10,22 +28,38 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
|
|
|
10
28
|
dic[eventId].resolve(results);
|
|
11
29
|
}
|
|
12
30
|
} else {
|
|
13
|
-
if (methodName && !
|
|
31
|
+
if (methodName && !(methodName in cbks)) {
|
|
14
32
|
console.error(`syncWorker: unknown methodName cbk ${dbg}`, eventId, methodName, error);
|
|
15
33
|
}
|
|
16
34
|
}
|
|
17
|
-
methodName && cbks[methodName]?.(arg);
|
|
35
|
+
methodName && cbks[methodName]?.({ source, ...arg });
|
|
18
36
|
};
|
|
19
37
|
const makeFn = (methodName) => {
|
|
20
38
|
return ({ ports, ...arg } = {}) => {
|
|
21
39
|
return new Promise((resolve, reject) => {
|
|
22
40
|
const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
|
|
23
|
-
|
|
24
|
-
|
|
41
|
+
let resolved = false;
|
|
42
|
+
const timeout = setTimeout(() => {
|
|
43
|
+
console.warn("slow request for", methodName, arg, eventId);
|
|
44
|
+
}, 3e3);
|
|
45
|
+
const doResolve = (arg2) => {
|
|
46
|
+
if (!resolved) {
|
|
47
|
+
resolved = true;
|
|
48
|
+
clearTimeout(timeout);
|
|
49
|
+
resolve(arg2);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
dic[eventId] = { resolve: doResolve, reject };
|
|
53
|
+
if (ports) {
|
|
54
|
+
offlineWorker.postMessage({ eventId, methodName, arg }, ports);
|
|
55
|
+
} else {
|
|
56
|
+
offlineWorker.postMessage({ eventId, methodName, arg });
|
|
57
|
+
}
|
|
25
58
|
});
|
|
26
59
|
};
|
|
27
60
|
};
|
|
28
|
-
const
|
|
61
|
+
const fns = Object.fromEntries(methodNames.map((name) => [name, makeFn(name)]));
|
|
62
|
+
const obj = Object.assign(fns, {
|
|
29
63
|
on: (key, fn2) => {
|
|
30
64
|
cbks[key] = fn2;
|
|
31
65
|
}
|
|
@@ -42,26 +76,38 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
|
|
|
42
76
|
};
|
|
43
77
|
var a = (a2) => a2;
|
|
44
78
|
var makeSyncHandler = a(
|
|
45
|
-
(
|
|
79
|
+
(services) => ({
|
|
46
80
|
canHandle: (methodName) => {
|
|
47
81
|
return methodName in services;
|
|
48
82
|
},
|
|
49
|
-
handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
|
|
83
|
+
handle: async ({ data: { eventId, methodName, arg } = {}, source, ports, target }) => {
|
|
84
|
+
const emitter = source?.postMessage && source || target?.postMessage && target || self;
|
|
50
85
|
try {
|
|
51
86
|
if (typeof methodName === "string" && methodName in services && services[methodName]) {
|
|
52
87
|
const results = await services[methodName](arg, { ports });
|
|
53
|
-
|
|
88
|
+
emitter.postMessage({ eventId, methodName, results });
|
|
54
89
|
return;
|
|
55
90
|
}
|
|
56
91
|
throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
|
|
57
92
|
} catch (e) {
|
|
58
93
|
console.debug("failed", e);
|
|
59
|
-
|
|
94
|
+
emitter.postMessage({ eventId, methodName, error: e });
|
|
60
95
|
}
|
|
61
96
|
}
|
|
62
97
|
})
|
|
63
98
|
);
|
|
64
99
|
var jsonToBuf = (json) => new TextEncoder().encode(JSON.stringify(json));
|
|
100
|
+
var combineHandlers = (...handlers) => {
|
|
101
|
+
return (message) => {
|
|
102
|
+
const methodName = String("methodName" in message.data ? message.data.methodName : "");
|
|
103
|
+
for (const h of handlers) {
|
|
104
|
+
if (h.canHandle(methodName)) {
|
|
105
|
+
return h.handle(message);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
console.error("no handlers matched", methodName);
|
|
109
|
+
};
|
|
110
|
+
};
|
|
65
111
|
var openCache = async (mode) => caches.open(`LLS_OFFLINE_${mode}`);
|
|
66
112
|
var isStoryZip = (name) => /story-.*?\.zip/.test(name);
|
|
67
113
|
var isValidZipName = (name) => {
|
|
@@ -77,7 +123,7 @@ var cocoreFetchPartialPageScreen = async (_ev, { offlineWorkerSync: offlineWorke
|
|
|
77
123
|
const key = `gqls/${zipSlugName(variables.slugs[0] || "")}.json`;
|
|
78
124
|
const entryId = knownLinks2.get(key);
|
|
79
125
|
if (!entryId) {
|
|
80
|
-
throw new Error(`slug not found ${key}`);
|
|
126
|
+
throw new Error(`slug not found ${key}, links size: ${knownLinks2.size}`);
|
|
81
127
|
}
|
|
82
128
|
const gqlPage = await offlineWorkerSync2.loadPageContent({ entryId });
|
|
83
129
|
return { data: gqlPage };
|
|
@@ -202,22 +248,90 @@ var coreFetchPagesApi = async (ev, { offlineWorkerSync: offlineWorkerSync2, vari
|
|
|
202
248
|
}
|
|
203
249
|
}
|
|
204
250
|
};
|
|
251
|
+
var cocoreFetchPrimaryMethod = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables }) => {
|
|
252
|
+
const uri = variables.uris?.[0];
|
|
253
|
+
if (!uri) {
|
|
254
|
+
throw new Error("invalid uri");
|
|
255
|
+
}
|
|
256
|
+
const method = await offlineWorkerSync2.readZipEntry({ bookId: uri, entryName: "gqls/method.json", asJson: true });
|
|
257
|
+
return {
|
|
258
|
+
data: method
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
var cocoreCreateLexicalFlower = async (_ev, { variables }) => {
|
|
262
|
+
const flower = variables.flower;
|
|
263
|
+
if (!flower) {
|
|
264
|
+
throw new Error("invalid flower");
|
|
265
|
+
}
|
|
266
|
+
const back = {
|
|
267
|
+
viewer: {
|
|
268
|
+
createLexicalFlower: {
|
|
269
|
+
...flower,
|
|
270
|
+
id: Date.now(),
|
|
271
|
+
user: null,
|
|
272
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
data: back
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
var cocoreUpdateLexicalFlower = async (_ev, { variables }) => {
|
|
281
|
+
const flower = variables.flower;
|
|
282
|
+
if (!flower) {
|
|
283
|
+
throw new Error("invalid flower");
|
|
284
|
+
}
|
|
285
|
+
const back = {
|
|
286
|
+
viewer: {
|
|
287
|
+
updateLexicalFlower: {
|
|
288
|
+
...flower,
|
|
289
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
data: back
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
var cocoreDeleteLexicalFlower = async (_ev, { variables }) => {
|
|
298
|
+
const flower = variables.flower;
|
|
299
|
+
if (!flower) {
|
|
300
|
+
throw new Error("invalid flower");
|
|
301
|
+
}
|
|
302
|
+
const back = {
|
|
303
|
+
viewer: {
|
|
304
|
+
deleteLexicalFlower: flower
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return {
|
|
308
|
+
data: back
|
|
309
|
+
};
|
|
310
|
+
};
|
|
205
311
|
var handles = {
|
|
206
312
|
coreFetchBookDrawerScreen,
|
|
207
313
|
cocoreFetchBookScreen,
|
|
208
314
|
coreFetchPagesApi,
|
|
209
315
|
cocoreFetchPartialPageScreen,
|
|
210
|
-
cocoreFetchPartialPageSiblingsScreen
|
|
316
|
+
cocoreFetchPartialPageSiblingsScreen,
|
|
317
|
+
cocoreFetchPrimaryMethod,
|
|
318
|
+
cocoreCreateLexicalFlower,
|
|
319
|
+
cocoreUpdateLexicalFlower,
|
|
320
|
+
cocoreDeleteLexicalFlower
|
|
211
321
|
};
|
|
212
322
|
var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).test(url);
|
|
213
323
|
var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
|
|
214
324
|
try {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
325
|
+
let variables;
|
|
326
|
+
const searchParamsVariables = new URLSearchParams(new URL(ev.request.url).search).get("variables");
|
|
327
|
+
if (searchParamsVariables) {
|
|
328
|
+
variables = JSON.parse(searchParamsVariables);
|
|
329
|
+
} else {
|
|
330
|
+
const body = await new Response(ev.request.body).json();
|
|
331
|
+
variables = body.variables;
|
|
332
|
+
}
|
|
220
333
|
const key = ev.request.url.match(Object.keys(handles).join("|"))?.[0];
|
|
334
|
+
console.debug("screencall", key, "qs", variables, "vs body", ev.request.body);
|
|
221
335
|
if (!key) {
|
|
222
336
|
throw new Error("unknown ev");
|
|
223
337
|
}
|
|
@@ -238,17 +352,39 @@ var setKnownLinks = (knownLinks2, { entryNames, bookId }) => {
|
|
|
238
352
|
// src/worker/fetch-worker.ts
|
|
239
353
|
var STORAGE = "cache";
|
|
240
354
|
var CONF_ENV = "development";
|
|
241
|
-
var fetchAsset = async (
|
|
355
|
+
var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
|
|
242
356
|
try {
|
|
243
|
-
const buf = await offlineWorkerSync2.readZipEntry(entryId)
|
|
357
|
+
const [buf, maybeRangeHeaders] = await offlineWorkerSync2.readZipEntry(entryId).then((buf2) => {
|
|
358
|
+
const range = ev.request.headers.get("Range");
|
|
359
|
+
if (!hasStreamingExtension(ev.request.url)) {
|
|
360
|
+
return [buf2, {}];
|
|
361
|
+
}
|
|
362
|
+
if (!range) {
|
|
363
|
+
return [
|
|
364
|
+
buf2,
|
|
365
|
+
{
|
|
366
|
+
"Accept-Ranges": "bytes",
|
|
367
|
+
"Content-Range": `bytes ${0}-${buf2.byteLength}/${buf2.byteLength}`
|
|
368
|
+
}
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
const { start, end, chunkSize } = parseRange(range, buf2.byteLength);
|
|
372
|
+
const rangeHeaders = {
|
|
373
|
+
"Content-Length": chunkSize,
|
|
374
|
+
"Accept-Ranges": "bytes",
|
|
375
|
+
"Content-Range": `bytes ${start}-${end}/${buf2.byteLength}`
|
|
376
|
+
};
|
|
377
|
+
return [buf2.slice(start, end + 1), rangeHeaders];
|
|
378
|
+
});
|
|
244
379
|
return new Response(buf, {
|
|
245
380
|
headers: {
|
|
246
381
|
"Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
|
|
247
382
|
"Content-Length": buf.byteLength,
|
|
248
|
-
"Content-Type": mimeType
|
|
383
|
+
"Content-Type": mimeType,
|
|
384
|
+
...maybeRangeHeaders
|
|
249
385
|
},
|
|
250
386
|
// @ts-ignore: poor ts typing
|
|
251
|
-
status: 200,
|
|
387
|
+
status: maybeRangeHeaders["Content-Range"] ? 206 : 200,
|
|
252
388
|
statusText: "OK"
|
|
253
389
|
});
|
|
254
390
|
} catch (e) {
|
|
@@ -301,27 +437,35 @@ var ACCEPTED_EXTENSIONS = {
|
|
|
301
437
|
".riv": "application/octet-stream",
|
|
302
438
|
".pdf": "application/pdf"
|
|
303
439
|
};
|
|
440
|
+
var hasStreamingExtension = (str) => /\.(mp3|mp4|webm|m4a)$/.test(str);
|
|
304
441
|
var putInCache = async (request, response) => {
|
|
305
442
|
const cache = await openCache(CONF_ENV);
|
|
306
443
|
await cache.put(request, response);
|
|
307
444
|
};
|
|
445
|
+
var tryOnline = async (request, event) => {
|
|
446
|
+
const responseFromNetwork = await fetch(request);
|
|
447
|
+
if (isValidZipName(request.url) && !isValidS3Zip(responseFromNetwork)) {
|
|
448
|
+
throw new Error(`zip is invalid: got ${responseFromNetwork.headers.get("Content-Length")} bytes`);
|
|
449
|
+
}
|
|
450
|
+
const cached = request.method === "GET" || request.method === "OPTIONS" ? putInCache(request, responseFromNetwork.clone()) : Promise.resolve();
|
|
451
|
+
if (request.url.endsWith(".zip")) {
|
|
452
|
+
await cached;
|
|
453
|
+
event.waitUntil(Promise.resolve());
|
|
454
|
+
} else {
|
|
455
|
+
event.waitUntil(cached);
|
|
456
|
+
}
|
|
457
|
+
return responseFromNetwork;
|
|
458
|
+
};
|
|
459
|
+
var tryOffline = async (request, _event) => {
|
|
460
|
+
const over = /\/\?story=basic/.test(request.url) ? request.url.replace(/\/\?story=.*$/, "") : request;
|
|
461
|
+
const responseFromCache = await caches.match(over);
|
|
462
|
+
return responseFromCache;
|
|
463
|
+
};
|
|
308
464
|
var onlineFirst = async (request, event) => {
|
|
309
465
|
try {
|
|
310
|
-
|
|
311
|
-
if (isValidZipName(request.url) && !isValidS3Zip(responseFromNetwork)) {
|
|
312
|
-
throw new Error(`zip is invalid: got ${responseFromNetwork.headers.get("Content-Length")} bytes`);
|
|
313
|
-
}
|
|
314
|
-
const cached = request.method === "HEAD" ? Promise.resolve() : putInCache(request, responseFromNetwork.clone());
|
|
315
|
-
if (request.url.endsWith(".zip")) {
|
|
316
|
-
await cached;
|
|
317
|
-
event.waitUntil(Promise.resolve());
|
|
318
|
-
} else {
|
|
319
|
-
event.waitUntil(cached);
|
|
320
|
-
}
|
|
321
|
-
return responseFromNetwork;
|
|
466
|
+
return await tryOnline(request, event);
|
|
322
467
|
} catch (e) {
|
|
323
|
-
const
|
|
324
|
-
const responseFromCache = await caches.match(over);
|
|
468
|
+
const responseFromCache = await tryOffline(request, event);
|
|
325
469
|
if (!responseFromCache) {
|
|
326
470
|
return new Response(
|
|
327
471
|
`FAILED fetch ${e && typeof e === "object" && "message" in e ? e.message : e?.toString?.()}`,
|
|
@@ -343,18 +487,23 @@ swSelf.addEventListener("fetch", (ev) => {
|
|
|
343
487
|
const extension = url.match(/\.[^.]+$/)?.[0];
|
|
344
488
|
const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
|
|
345
489
|
const isValidScreenCall = canHandleScreenCall(url);
|
|
346
|
-
if (
|
|
490
|
+
if (url.endsWith(".zip")) {
|
|
491
|
+
if (STORAGE === "cache") {
|
|
492
|
+
return ev.respondWith(onlineFirst(ev.request, ev));
|
|
493
|
+
}
|
|
347
494
|
return;
|
|
348
495
|
}
|
|
349
496
|
if (!mimeType && !isValidScreenCall) {
|
|
350
|
-
if (!/\.(css|tsx?|
|
|
497
|
+
if (!/\.(css|tsx?|[mj]sx?|woff2|wasm|fr|(web)?manifest)(\??|$)|@(vite|react|id)|(:6019\/?$)/.test(url)) {
|
|
351
498
|
console.debug("no ext", extension || url);
|
|
352
499
|
}
|
|
353
500
|
return ev.respondWith(onlineFirst(ev.request, ev));
|
|
354
501
|
}
|
|
502
|
+
if (/http:\/\/localhost:\d+\/(favicon\.svg|assets\/)/.test(url)) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
355
505
|
const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
|
|
356
506
|
if (isValidScreenCall) {
|
|
357
|
-
console.debug("screencall!");
|
|
358
507
|
return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
|
|
359
508
|
}
|
|
360
509
|
if (entryId) {
|
|
@@ -363,6 +512,7 @@ swSelf.addEventListener("fetch", (ev) => {
|
|
|
363
512
|
if (/lls|lelivrescolaire/.test(url)) {
|
|
364
513
|
console.info("no link for", url.replace(/https:\/\/.*?\//, ""), `medias/${url.replace(/https:\/\/.*?\//, "")}`);
|
|
365
514
|
}
|
|
515
|
+
console.debug("unintercepted url", url);
|
|
366
516
|
return;
|
|
367
517
|
});
|
|
368
518
|
swSelf.addEventListener("install", (_event) => {
|
|
@@ -371,20 +521,23 @@ swSelf.addEventListener("install", (_event) => {
|
|
|
371
521
|
swSelf.addEventListener("activate", (event) => {
|
|
372
522
|
event.waitUntil(swSelf.clients.claim());
|
|
373
523
|
});
|
|
374
|
-
var
|
|
524
|
+
var setKnownLinks2 = (arg) => setKnownLinks(knownLinks, arg);
|
|
375
525
|
var offlineWorkerSync;
|
|
376
526
|
var initChannel = async (__, { ports }) => {
|
|
377
527
|
console.info(`FETCH WORKER innitChannel v${/* @__PURE__ */ new Date()}`);
|
|
378
|
-
offlineWorker = ports[0];
|
|
379
|
-
const { handler, ...sender } = makeSyncWorker(
|
|
528
|
+
const offlineWorker = ports[0];
|
|
529
|
+
const { handler: syncHandler, ...sender } = makeSyncWorker(
|
|
380
530
|
offlineWorker,
|
|
381
|
-
["readZipEntry", "loadBook", "slugToFileMeta", "loadPageContent", "searchBooks"],
|
|
531
|
+
["readZipEntry", "loadBook", "slugToFileMeta", "loadPageContent", "searchBooks", "ensureAppReady"],
|
|
382
532
|
"fetchworker"
|
|
383
533
|
);
|
|
534
|
+
const onHandler = makeSyncHandler({ setKnownLinks: setKnownLinks2 });
|
|
384
535
|
offlineWorkerSync = sender;
|
|
385
|
-
offlineWorker.onmessage =
|
|
386
|
-
|
|
387
|
-
|
|
536
|
+
offlineWorker.onmessage = combineHandlers(syncHandler, onHandler);
|
|
537
|
+
offlineWorker.onclose = () => {
|
|
538
|
+
console.error("socket to ow has been closed!");
|
|
539
|
+
};
|
|
540
|
+
await offlineWorkerSync.ensureAppReady();
|
|
388
541
|
};
|
|
389
542
|
var knownLinks = /* @__PURE__ */ new Map();
|
|
390
543
|
var configure = ({
|
|
@@ -401,28 +554,4 @@ var configure = ({
|
|
|
401
554
|
}
|
|
402
555
|
console.info("sw::configure", { STORAGE, CONF_ENV });
|
|
403
556
|
};
|
|
404
|
-
swSelf.onmessage =
|
|
405
|
-
data: { methodName, arg } = {},
|
|
406
|
-
ports
|
|
407
|
-
}) => {
|
|
408
|
-
const services = {
|
|
409
|
-
initChannel,
|
|
410
|
-
configure
|
|
411
|
-
};
|
|
412
|
-
try {
|
|
413
|
-
if (methodName === "configure") {
|
|
414
|
-
await services[methodName](arg);
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
if (methodName === "initChannel") {
|
|
418
|
-
if (!ports.length) {
|
|
419
|
-
throw new Error("fetchworker::missing port!");
|
|
420
|
-
}
|
|
421
|
-
await services[methodName](arg, { ports });
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
throw new Error(`fetchworker:invalid methodName ${Object.keys(services)} ${methodName}`);
|
|
425
|
-
} catch (e) {
|
|
426
|
-
console.error("failed", methodName, arg, e);
|
|
427
|
-
}
|
|
428
|
-
};
|
|
557
|
+
swSelf.onmessage = combineHandlers(makeSyncHandler({ initChannel, configure }));
|