@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.
@@ -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 = (offlineWorker2, methodNames, dbg) => {
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 && !methodName.startsWith("async") && !(methodName in cbks)) {
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
- dic[eventId] = { resolve, reject };
24
- offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
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 obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
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
- (sender, services) => ({
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
- sender.postMessage({ eventId, methodName, results });
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
- sender.postMessage({ eventId, methodName, error: e });
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
- const variables = (
216
- // @ts-expect-error TODO: handle variables from body(stream) properly!!
217
- ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
218
- );
219
- console.debug("variables are", variables, "vs", ev.request.body);
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 (_ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
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
- const responseFromNetwork = await fetch(request);
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 over = /\/\?story=basic/.test(request.url) ? request.url.replace(/\/\?story=.*$/, "") : request;
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 (STORAGE !== "cache" && url.endsWith(".zip")) {
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?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
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 offlineWorker;
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 = handler.handle;
386
- offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, knownLinks));
387
- offlineWorker.postMessage({ methodName: "sendAppReady" });
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 = async ({
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 }));