@lls/offline 24.22.0-debug → 24.22.0-dee67770

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.
@@ -11,16 +11,15 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
11
11
  }
12
12
  } else {
13
13
  if (methodName && !methodName.startsWith("async") && !(methodName in cbks)) {
14
- console.error(`syncWorker: ${dbg}`, eventId, methodName, error);
14
+ console.error(`syncWorker: unknown methodName cbk ${dbg}`, eventId, methodName, error);
15
15
  }
16
16
  }
17
17
  methodName && cbks[methodName]?.(arg);
18
18
  };
19
- offlineWorker2.onmessage = fn;
20
19
  const makeFn = (methodName) => {
21
20
  return ({ ports, ...arg } = {}) => {
22
21
  return new Promise((resolve, reject) => {
23
- const eventId = `${methodName}:${Date.now()}`;
22
+ const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
24
23
  dic[eventId] = { resolve, reject };
25
24
  offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
26
25
  });
@@ -31,25 +30,39 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
31
30
  cbks[key] = fn2;
32
31
  }
33
32
  });
34
- return obj;
33
+ return {
34
+ ...obj,
35
+ handler: {
36
+ canHandle: (methodName) => {
37
+ return methodName in cbks || methodNames.includes(methodName);
38
+ },
39
+ handle: fn
40
+ }
41
+ };
35
42
  };
36
43
  var a = (a2) => a2;
37
44
  var makeSyncHandler = a(
38
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
39
- try {
40
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
41
- const results = await services[methodName](arg, { ports });
42
- sender.postMessage({ eventId, methodName, results });
43
- return;
45
+ (sender, services) => ({
46
+ canHandle: (methodName) => {
47
+ return methodName in services;
48
+ },
49
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
50
+ try {
51
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
52
+ const results = await services[methodName](arg, { ports });
53
+ sender.postMessage({ eventId, methodName, results });
54
+ return;
55
+ }
56
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
57
+ } catch (e) {
58
+ console.debug("failed", e);
59
+ sender.postMessage({ eventId, methodName, error: e });
44
60
  }
45
- throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
46
- } catch (e) {
47
- console.debug("failed", e);
48
- sender.postMessage({ eventId, methodName, error: e });
49
61
  }
50
- }
62
+ })
51
63
  );
52
64
  var jsonToBuf = (json) => new TextEncoder().encode(JSON.stringify(json));
65
+ var openCache = async (mode) => caches.open(`LLS_OFFLINE_${mode}`);
53
66
 
54
67
  // src/worker/swFetchScreenCall.ts
55
68
  var zipSlugName = (str) => str.replace(/[^a-zA-Z0-9-.]/g, "");
@@ -62,6 +75,15 @@ var cocoreFetchPartialPageScreen = async (_ev, { offlineWorkerSync: offlineWorke
62
75
  const gqlPage = await offlineWorkerSync2.loadPageContent({ entryId });
63
76
  return { data: gqlPage };
64
77
  };
78
+ var cocoreFetchBookScreen = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables, knownLinks: knownLinks2 }) => {
79
+ const key = `gqls/${zipSlugName(variables.uris[0] || "")}.json`;
80
+ const entryId = knownLinks2.get(key);
81
+ if (!entryId) {
82
+ throw new Error(`uri not found ${key}`);
83
+ }
84
+ const gqlBook = await offlineWorkerSync2.loadPageContent({ entryId });
85
+ return { data: gqlBook };
86
+ };
65
87
  var cocoreFetchPartialPageSiblingsScreen = async (_ev) => {
66
88
  return { data: { viewer: {} } };
67
89
  };
@@ -176,6 +198,7 @@ var coreFetchPagesApi = async (ev, { offlineWorkerSync: offlineWorkerSync2, vari
176
198
  };
177
199
  var handles = {
178
200
  coreFetchBookDrawerScreen,
201
+ cocoreFetchBookScreen,
179
202
  coreFetchPagesApi,
180
203
  cocoreFetchPartialPageScreen,
181
204
  cocoreFetchPartialPageSiblingsScreen
@@ -184,7 +207,7 @@ var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).te
184
207
  var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
185
208
  try {
186
209
  const variables = (
187
- // @ts-ignore TODO: handle variables from body(stream) properly!!
210
+ // @ts-expect-error TODO: handle variables from body(stream) properly!!
188
211
  ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
189
212
  );
190
213
  console.debug("variables are", variables, "vs", ev.request.body);
@@ -199,23 +222,28 @@ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, known
199
222
  return { status: 500, json: JSON.stringify(e) };
200
223
  }
201
224
  };
225
+ var setKnownLinks = (knownLinks2, { entryNames, bookId }) => {
226
+ entryNames.forEach((entryName) => {
227
+ const link = entryName.replace(new RegExp(`^\\d+/media/|^${bookId}/medias/`), "").replace(new RegExp(`^${bookId}/gqls/`), "gqls/");
228
+ knownLinks2.set(link, { entryName, bookId });
229
+ });
230
+ };
202
231
 
203
232
  // src/worker/fetch-worker.ts
204
- var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
233
+ var STORAGE = "cache";
234
+ var CONF_ENV = "development";
235
+ var fetchAsset = async (_ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
205
236
  try {
206
237
  const buf = await offlineWorkerSync2.readZipEntry(entryId);
207
- const url = ev.request.url;
208
238
  return new Response(buf, {
209
239
  headers: {
210
240
  "Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
241
+ "Content-Length": buf.byteLength,
211
242
  "Content-Type": mimeType
212
243
  },
213
244
  // @ts-ignore: poor ts typing
214
- ok: true,
215
- redirected: false,
216
245
  status: 200,
217
- type: "cors",
218
- url
246
+ statusText: "OK"
219
247
  });
220
248
  } catch (e) {
221
249
  console.error("FAILED fetchAsset", e);
@@ -223,8 +251,7 @@ var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorke
223
251
  // @ts-ignore: poor ts typing
224
252
  ok: false,
225
253
  status: 500,
226
- type: "cors",
227
- url: ev.request.url
254
+ statusText: e?.toString?.() || "fail fetch"
228
255
  });
229
256
  }
230
257
  };
@@ -266,65 +293,113 @@ var ACCEPTED_EXTENSIONS = {
266
293
  ".m4a": "audio/mp4",
267
294
  ".webp": "image/webp"
268
295
  };
296
+ var putInCache = async (request, response) => {
297
+ const cache = await openCache(CONF_ENV);
298
+ await cache.put(request, response);
299
+ };
300
+ var onlineFirst = async (request, event) => {
301
+ try {
302
+ const responseFromNetwork = await fetch(request);
303
+ const cached = putInCache(request, responseFromNetwork.clone());
304
+ if (request.url.endsWith(".zip")) {
305
+ await cached;
306
+ event.waitUntil(Promise.resolve());
307
+ } else {
308
+ event.waitUntil(cached);
309
+ }
310
+ return responseFromNetwork;
311
+ } catch (_e) {
312
+ const over = /\/\?story=basic/.test(request.url) ? request.url.replace(/\/\?story=.*$/, "") : request;
313
+ const responseFromCache = await caches.match(over);
314
+ if (!responseFromCache) {
315
+ return new Response("FAILED fetch", {
316
+ // @ts-ignore: poor ts typing
317
+ ok: false,
318
+ status: 500,
319
+ type: "cors",
320
+ url: event.request.url
321
+ });
322
+ }
323
+ return responseFromCache;
324
+ }
325
+ };
269
326
  var swSelf = self;
270
327
  swSelf.addEventListener("fetch", (ev) => {
271
328
  const url = ev.request.url;
272
329
  const extension = url.match(/\.[^.]+$/)?.[0];
273
330
  const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
274
331
  const isValidScreenCall = canHandleScreenCall(url);
275
- if (!mimeType && !isValidScreenCall) {
276
- if (!/\.(css|tsx?|jsx?|woff2|fr)/.test(url)) {
277
- console.debug("no ext", url.match(/\.[^.]+$/)?.[0]);
278
- }
332
+ if (STORAGE !== "cache" && url.endsWith(".zip")) {
279
333
  return;
280
334
  }
281
- const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
282
- if (!entryId && !isValidScreenCall) {
283
- if (/lls|lelivrescolaire/.test(url)) {
284
- console.info("no link for", url.replace(/https:\/\/.*?\//, ""));
335
+ if (!mimeType && !isValidScreenCall) {
336
+ if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
337
+ console.debug("no ext", extension || url);
285
338
  }
286
- return;
339
+ return ev.respondWith(onlineFirst(ev.request, ev));
287
340
  }
341
+ const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
288
342
  if (isValidScreenCall) {
289
343
  console.debug("screencall!");
290
344
  return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
291
345
  }
292
- return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
346
+ if (entryId) {
347
+ return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
348
+ }
349
+ if (/lls|lelivrescolaire/.test(url)) {
350
+ console.info("no link for", url.replace(/https:\/\/.*?\//, ""), `medias/${url.replace(/https:\/\/.*?\//, "")}`);
351
+ }
352
+ return;
293
353
  });
294
354
  swSelf.addEventListener("install", (_event) => {
295
355
  swSelf.skipWaiting();
296
356
  });
297
- swSelf.addEventListener("activate", (_event) => {
298
- return swSelf.clients.claim();
357
+ swSelf.addEventListener("activate", (event) => {
358
+ event.waitUntil(swSelf.clients.claim());
299
359
  });
300
360
  var offlineWorker;
301
361
  var offlineWorkerSync;
302
362
  var initChannel = async (__, { ports }) => {
303
- console.info(`FETCH WORKER innitChannel v${13}`);
363
+ console.info(`FETCH WORKER innitChannel v${/* @__PURE__ */ new Date()}`);
304
364
  offlineWorker = ports[0];
305
- offlineWorkerSync = makeSyncWorker(
365
+ const { handler, ...sender } = makeSyncWorker(
306
366
  offlineWorker,
307
367
  ["readZipEntry", "loadBook", "slugToFileMeta", "loadPageContent", "searchBooks"],
308
368
  "fetchworker"
309
369
  );
310
- offlineWorkerSync.on("setKnownLinks", setKnownLinks);
370
+ offlineWorkerSync = sender;
371
+ offlineWorker.onmessage = handler.handle;
372
+ offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, knownLinks));
311
373
  offlineWorker.postMessage({ methodName: "sendAppReady" });
312
374
  };
313
375
  var knownLinks = /* @__PURE__ */ new Map();
314
- var setKnownLinks = ({ entryNames, bookId }) => {
315
- entryNames.forEach((entryName) => {
316
- const link = entryName.replace(/^\d+\/media\//, "");
317
- knownLinks.set(link, { entryName, bookId });
318
- });
376
+ var configure = ({
377
+ storage,
378
+ confEnv
379
+ } = {}) => {
380
+ STORAGE = "cache";
381
+ CONF_ENV = "development";
382
+ if (typeof storage !== "undefined") {
383
+ STORAGE = storage;
384
+ }
385
+ if (typeof confEnv !== "undefined") {
386
+ CONF_ENV = confEnv;
387
+ }
388
+ console.info("sw::configure", { STORAGE, CONF_ENV });
319
389
  };
320
390
  swSelf.onmessage = async ({
321
391
  data: { methodName, arg } = {},
322
392
  ports
323
393
  }) => {
324
394
  const services = {
325
- initChannel
395
+ initChannel,
396
+ configure
326
397
  };
327
398
  try {
399
+ if (methodName === "configure") {
400
+ await services[methodName](arg);
401
+ return;
402
+ }
328
403
  if (methodName === "initChannel") {
329
404
  if (!ports.length) {
330
405
  throw new Error("fetchworker::missing port!");
@@ -1,21 +1,89 @@
1
+ // src/utils/error.ts
2
+ var tryCatch = (fn) => {
3
+ return (resolve, reject) => {
4
+ try {
5
+ return fn().then(resolve);
6
+ } catch (e) {
7
+ return reject(e);
8
+ }
9
+ };
10
+ };
11
+
1
12
  // src/utils/worker.ts
13
+ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
14
+ const dic = {};
15
+ const cbks = {};
16
+ const fn = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
17
+ if (eventId && eventId in dic) {
18
+ if (error) {
19
+ dic[eventId].reject(error);
20
+ } else {
21
+ dic[eventId].resolve(results);
22
+ }
23
+ } else {
24
+ if (methodName && !methodName.startsWith("async") && !(methodName in cbks)) {
25
+ console.error(`syncWorker: unknown methodName cbk ${dbg}`, eventId, methodName, error);
26
+ }
27
+ }
28
+ methodName && cbks[methodName]?.(arg);
29
+ };
30
+ const makeFn = (methodName) => {
31
+ return ({ ports, ...arg } = {}) => {
32
+ return new Promise((resolve, reject) => {
33
+ const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
34
+ dic[eventId] = { resolve, reject };
35
+ offlineWorker.postMessage({ eventId, methodName, arg }, ports);
36
+ });
37
+ };
38
+ };
39
+ const obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
40
+ on: (key2, fn2) => {
41
+ cbks[key2] = fn2;
42
+ }
43
+ });
44
+ return {
45
+ ...obj,
46
+ handler: {
47
+ canHandle: (methodName) => {
48
+ return methodName in cbks || methodNames.includes(methodName);
49
+ },
50
+ handle: fn
51
+ }
52
+ };
53
+ };
2
54
  var a = (a2) => a2;
3
55
  var makeSyncHandler = a(
4
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
5
- try {
6
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
7
- const results = await services[methodName](arg, { ports });
8
- sender.postMessage({ eventId, methodName, results });
9
- return;
56
+ (sender2, services) => ({
57
+ canHandle: (methodName) => {
58
+ return methodName in services;
59
+ },
60
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
61
+ try {
62
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
63
+ const results = await services[methodName](arg, { ports });
64
+ sender2.postMessage({ eventId, methodName, results });
65
+ return;
66
+ }
67
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
68
+ } catch (e) {
69
+ console.debug("failed", e);
70
+ sender2.postMessage({ eventId, methodName, error: e });
10
71
  }
11
- throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
12
- } catch (e) {
13
- console.debug("failed", e);
14
- sender.postMessage({ eventId, methodName, error: e });
15
72
  }
16
- }
73
+ })
17
74
  );
18
75
  var bufToJson = (buf) => JSON.parse(new TextDecoder("UTF-8").decode(new Uint8Array(buf)));
76
+ var combineHandlers = (...handlers) => {
77
+ return ({ data: { methodName, ...data } = {}, ports }) => {
78
+ for (const h of handlers) {
79
+ if (h.canHandle(String(methodName))) {
80
+ return h.handle({ data: { methodName, ...data }, ports });
81
+ }
82
+ }
83
+ console.error("no handlers matched", methodName);
84
+ };
85
+ };
86
+ var openCache = async (mode) => caches.open(`LLS_OFFLINE_${mode}`);
19
87
 
20
88
  // src/worker/wwApi.ts
21
89
  var STATE = {
@@ -734,6 +802,9 @@ var loadZip = async (buf, bookId) => {
734
802
  const unzipper = new Unzip();
735
803
  unzipper.register(UnzipInflate);
736
804
  unzipper.onfile = (file) => {
805
+ if (!file.name.includes(".")) {
806
+ return;
807
+ }
737
808
  m.set(file.name, {
738
809
  decoded: null,
739
810
  zipEntry: {
@@ -771,10 +842,26 @@ var loadZip = async (buf, bookId) => {
771
842
 
772
843
  // src/worker/offline-worker.ts
773
844
  var CHECK_UPDATES = true;
774
- var fetchZipFile = async (zipUrl, forceRefresh) => {
775
- const opfsRoot = await navigator.storage.getDirectory();
845
+ var STORAGE = "cache";
846
+ var ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
847
+ var CONF_ENV = "development";
848
+ var LOADED_BOOKS = /* @__PURE__ */ new Map();
849
+ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
776
850
  const fileName = zipUrl.split("/").slice(-1)[0];
777
851
  const readArrayBuffer = async () => {
852
+ if (STORAGE === "native") {
853
+ const back = await sender.nativeReadFile({ fname: fileName });
854
+ return back;
855
+ }
856
+ if (STORAGE === "cache") {
857
+ const response = await caches.match(zipUrl);
858
+ if (!response) {
859
+ throw new Error(`missing zip ${zipUrl}`);
860
+ }
861
+ const buf = await response.arrayBuffer();
862
+ return buf;
863
+ }
864
+ const opfsRoot = await navigator.storage.getDirectory();
778
865
  const handle = await opfsRoot.getFileHandle(fileName);
779
866
  const accessHandle = await handle.createSyncAccessHandle();
780
867
  const size = accessHandle.getSize();
@@ -800,8 +887,8 @@ var fetchZipFile = async (zipUrl, forceRefresh) => {
800
887
  }
801
888
  return buf;
802
889
  } catch (e) {
803
- if (e.message !== "forceRefresh" && !e.message.includes("refetching")) {
804
- console.debug("opfs db not found, creating it", e, zipUrl);
890
+ if (e instanceof Error && e.message !== "forceRefresh" && !e.message.includes("refetching")) {
891
+ console.debug(`${STORAGE} zip not found, creating it`, e, zipUrl);
805
892
  }
806
893
  const response = await fetch(zipUrl);
807
894
  const contentLength = +response.headers.get("Content-Length");
@@ -812,6 +899,7 @@ var fetchZipFile = async (zipUrl, forceRefresh) => {
812
899
  while (true) {
813
900
  const { done, value: chunk } = await reader.read();
814
901
  if (done) {
902
+ self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: contentLength } });
815
903
  break;
816
904
  }
817
905
  arrayBuffer.set(chunk, position);
@@ -826,34 +914,69 @@ var fetchZipFile = async (zipUrl, forceRefresh) => {
826
914
  console.debug(error);
827
915
  throw new Error(error);
828
916
  }
829
- const handle = await opfsRoot.getFileHandle(fileName, { create: true });
830
- const accessHandle = await handle.createSyncAccessHandle();
831
- await accessHandle.truncate(0);
832
- await accessHandle.write(arrayBuffer);
833
- await accessHandle.close();
917
+ if (STORAGE === "native") {
918
+ await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
919
+ } else if (STORAGE === "opfs") {
920
+ const opfsRoot = await navigator.storage.getDirectory();
921
+ const handle = await opfsRoot.getFileHandle(fileName, { create: true });
922
+ const accessHandle = await handle.createSyncAccessHandle();
923
+ await accessHandle.truncate(0);
924
+ await accessHandle.write(arrayBuffer);
925
+ await accessHandle.close();
926
+ } else {
927
+ const cache = await openCache(CONF_ENV);
928
+ const response2 = new Response(arrayBuffer, {
929
+ headers: {
930
+ "Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
931
+ "Content-Type": "application/zip"
932
+ },
933
+ // @ts-ignore: poor ts typing
934
+ ok: true,
935
+ redirected: false,
936
+ status: 200,
937
+ type: "cors",
938
+ url: zipUrl
939
+ });
940
+ await cache.put(zipUrl, response2);
941
+ }
834
942
  return await readArrayBuffer();
835
943
  }
836
944
  };
837
945
  var slugToFileMeta2 = async ({ slug }) => {
838
- console.debug("hereshit");
839
946
  const json = await readZipEntry({ bookId: "preload", entryName: "preload/hashmap_slugToMetaPage.json", asJson: true });
840
- console.debug("hereshit2");
841
947
  return slugToFileMeta({ slug, json });
842
948
  };
843
949
  var PRELOAD_AS_BOOK_ID = "preload";
844
950
  var isOldZip = (bookId) => typeof bookId === "number";
845
951
  var loadBook2 = async ({ id: bookId, forceRefresh }) => {
846
- const buf = await fetchZipFile(`https://ci.lls.fr/artefacts/content/development/${bookId}.zip`, forceRefresh);
847
- try {
848
- const entryNames = await loadZip(new Uint8Array(buf), bookId);
849
- fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
850
- if (isOldZip(bookId)) {
851
- const json = await readZipEntry({ entryName: `preload/${bookId}.json`, bookId: PRELOAD_AS_BOOK_ID, asJson: true });
852
- loadBook(json);
853
- }
854
- } catch (e) {
855
- console.error("failed", e);
952
+ if (LOADED_BOOKS.has(bookId)) {
953
+ await LOADED_BOOKS.get(bookId);
954
+ self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
955
+ return;
856
956
  }
957
+ const p = new Promise(
958
+ tryCatch(async () => {
959
+ const buf = await fetchZipFile(`${ZIP_BASE_URL}/${bookId}.zip`, { forceRefresh });
960
+ try {
961
+ const entryNames = await loadZip(new Uint8Array(buf), bookId);
962
+ fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
963
+ if (isOldZip(bookId)) {
964
+ const json = await readZipEntry({
965
+ entryName: `preload/${bookId}.json`,
966
+ bookId: PRELOAD_AS_BOOK_ID,
967
+ asJson: true
968
+ });
969
+ loadBook(json);
970
+ }
971
+ return true;
972
+ } catch (e) {
973
+ console.error("failed", e);
974
+ return false;
975
+ }
976
+ })
977
+ );
978
+ LOADED_BOOKS.set(bookId, p);
979
+ await p;
857
980
  };
858
981
  var loadPageContent2 = async ({
859
982
  bookId: iBookId,
@@ -875,7 +998,7 @@ var loadPageContent2 = async ({
875
998
  var fetchWorker;
876
999
  var initChannel = (__, { ports }) => {
877
1000
  fetchWorker = ports[0];
878
- const defaultHandler = makeSyncHandler(fetchWorker, {
1001
+ const { canHandle, handle } = makeSyncHandler(fetchWorker, {
879
1002
  readZipEntry,
880
1003
  loadBook: loadBook2,
881
1004
  slugToFileMeta: slugToFileMeta2,
@@ -887,23 +1010,69 @@ var initChannel = (__, { ports }) => {
887
1010
  self.postMessage({ methodName: "ready" });
888
1011
  return;
889
1012
  }
890
- defaultHandler(dataObj);
1013
+ if (canHandle(dataObj.data?.methodName || "")) {
1014
+ return handle(dataObj);
1015
+ }
1016
+ console.error("unknown methodName", dataObj.data?.methodName);
891
1017
  };
892
1018
  fetchWorker.onmessage = fn;
893
1019
  };
894
- var loadBooks = async () => {
895
- const opfsRoot = await navigator.storage.getDirectory();
896
- const entries = await opfsRoot.values();
1020
+ var loadBooks = async ({ primaryOnly = false } = {}) => {
1021
+ let entries = [];
1022
+ if (STORAGE === "native") {
1023
+ entries = await sender.nativeListZips();
1024
+ } else if (STORAGE === "opfs") {
1025
+ const opfsRoot = await navigator.storage.getDirectory();
1026
+ entries = await opfsRoot.values();
1027
+ } else {
1028
+ const cache = await openCache(CONF_ENV);
1029
+ const keys = await cache.keys();
1030
+ const zips = keys.filter((key2) => {
1031
+ return key2.url.match(/lecture-cp\.zip/);
1032
+ });
1033
+ entries = zips.map((zip) => ({ name: String(zip.url.split("/").pop()) }));
1034
+ }
897
1035
  for await (const entry of entries) {
1036
+ if (/preload/.test(entry.name)) {
1037
+ continue;
1038
+ }
898
1039
  if (/\d+\.zip$/.test(entry.name)) {
899
- const bookId = entry.name.match(/\d+/)[0];
900
- await loadBook2({ id: Number.parseInt(bookId, 10) });
1040
+ if (!primaryOnly) {
1041
+ const bookId = entry.name.match(/\d+/)[0];
1042
+ await loadBook2({ id: Number.parseInt(bookId, 10) });
1043
+ }
901
1044
  } else if (/zip$/.test(entry.name)) {
902
1045
  const methodUri = entry.name.replace(".zip", "");
903
1046
  await loadBook2({ id: methodUri });
1047
+ } else {
1048
+ console.info(`invalid book entry ${entry.name}`);
904
1049
  }
905
1050
  }
906
1051
  };
1052
+ var configure = ({
1053
+ storage,
1054
+ checkUpdates,
1055
+ zipBaseUrl,
1056
+ confEnv
1057
+ }) => {
1058
+ CHECK_UPDATES = true;
1059
+ STORAGE = "cache";
1060
+ ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
1061
+ CONF_ENV = "development";
1062
+ if (typeof storage !== "undefined") {
1063
+ STORAGE = storage;
1064
+ }
1065
+ if (typeof checkUpdates !== "undefined") {
1066
+ CHECK_UPDATES = !!checkUpdates;
1067
+ }
1068
+ if (typeof zipBaseUrl !== "undefined") {
1069
+ ZIP_BASE_URL = zipBaseUrl;
1070
+ }
1071
+ if (typeof confEnv !== "undefined") {
1072
+ CONF_ENV = confEnv;
1073
+ }
1074
+ console.info("ow::configure", { STORAGE, CHECK_UPDATES, ZIP_BASE_URL, CONF_ENV });
1075
+ };
907
1076
  var loadPreload2 = async () => {
908
1077
  const zipUrl = "https://lls-ci-fr.s3.eu-west-3.amazonaws.com/artefacts/content/development/preload.zip";
909
1078
  const buf = await fetchZipFile(zipUrl);
@@ -916,10 +1085,13 @@ var loadPreload2 = async () => {
916
1085
  console.error("failed", e);
917
1086
  }
918
1087
  };
919
- self.onmessage = makeSyncHandler(self, {
1088
+ var mainHandle = makeSyncHandler(self, {
920
1089
  initChannel,
921
1090
  loadPreload: loadPreload2,
922
1091
  readZipEntry,
923
1092
  loadBooks,
924
- loadBook: loadBook2
1093
+ loadBook: loadBook2,
1094
+ configure
925
1095
  });
1096
+ var { handler, ...sender } = makeSyncWorker(self, ["nativeWriteFile", "nativeReadFile", "nativeListZips"]);
1097
+ self.onmessage = combineHandlers(mainHandle, handler);
package/lib/index.js CHANGED
@@ -1,8 +1,19 @@
1
+ // src/utils/error.ts
2
+ var tryCatch = (fn) => {
3
+ return (resolve, reject) => {
4
+ try {
5
+ return fn().then(resolve);
6
+ } catch (e) {
7
+ return reject(e);
8
+ }
9
+ };
10
+ };
11
+
1
12
  // src/utils/worker.ts
2
- var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
13
+ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
3
14
  const dic = {};
4
15
  const cbks = {};
5
- const fn2 = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
16
+ const fn = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
6
17
  if (eventId && eventId in dic) {
7
18
  if (error) {
8
19
  dic[eventId].reject(error);
@@ -11,53 +22,93 @@ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
11
22
  }
12
23
  } else {
13
24
  if (methodName && !methodName.startsWith("async") && !(methodName in cbks)) {
14
- console.error(`syncWorker: ${dbg}`, eventId, methodName, error);
25
+ console.error(`syncWorker: unknown methodName cbk ${dbg}`, eventId, methodName, error);
15
26
  }
16
27
  }
17
28
  methodName && cbks[methodName]?.(arg);
18
29
  };
19
- offlineWorker.onmessage = fn2;
20
30
  const makeFn = (methodName) => {
21
31
  return ({ ports, ...arg } = {}) => {
22
32
  return new Promise((resolve, reject) => {
23
- const eventId = `${methodName}:${Date.now()}`;
33
+ const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
24
34
  dic[eventId] = { resolve, reject };
25
- offlineWorker.postMessage({ eventId, methodName, arg }, ports);
35
+ offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
26
36
  });
27
37
  };
28
38
  };
29
39
  const obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
30
- on: (key, fn3) => {
31
- cbks[key] = fn3;
40
+ on: (key, fn2) => {
41
+ cbks[key] = fn2;
32
42
  }
33
43
  });
34
- return obj;
44
+ return {
45
+ ...obj,
46
+ handler: {
47
+ canHandle: (methodName) => {
48
+ return methodName in cbks || methodNames.includes(methodName);
49
+ },
50
+ handle: fn
51
+ }
52
+ };
35
53
  };
36
54
  var a = (a2) => a2;
37
55
  var makeSyncHandler = a(
38
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
39
- try {
40
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
41
- const results = await services[methodName](arg, { ports });
42
- sender.postMessage({ eventId, methodName, results });
43
- return;
56
+ (sender, services) => ({
57
+ canHandle: (methodName) => {
58
+ return methodName in services;
59
+ },
60
+ handle: async ({ data: { eventId, methodName, arg } = {}, ports }) => {
61
+ try {
62
+ if (typeof methodName === "string" && methodName in services && services[methodName]) {
63
+ const results = await services[methodName](arg, { ports });
64
+ sender.postMessage({ eventId, methodName, results });
65
+ return;
66
+ }
67
+ throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
68
+ } catch (e) {
69
+ console.debug("failed", e);
70
+ sender.postMessage({ eventId, methodName, error: e });
44
71
  }
45
- throw new Error(`invalid methodName ${Object.keys(services)} |${String(methodName)}`);
46
- } catch (e) {
47
- console.debug("failed", e);
48
- sender.postMessage({ eventId, methodName, error: e });
49
72
  }
50
- }
73
+ })
51
74
  );
75
+ var combineHandlers = (...handlers) => {
76
+ return ({ data: { methodName, ...data } = {}, ports }) => {
77
+ for (const h of handlers) {
78
+ if (h.canHandle(String(methodName))) {
79
+ return h.handle({ data: { methodName, ...data }, ports });
80
+ }
81
+ }
82
+ console.error("no handlers matched", methodName);
83
+ };
84
+ };
52
85
 
53
86
  // src/hooks/useOfflineWorker.ts
54
87
  import { useCallback, useEffect, useState } from "react";
55
- var offlineWorkerSync = makeSyncWorker(
56
- // @ts-ignore: TODO
57
- new Worker("/src/worker/offline-worker.js", { type: "module" }),
58
- ["initChannel", "readZipEntry", "loadPreload", "loadBooks", "loadBook"],
88
+ var offlineWorker = new Worker("/src/worker/offline-worker.js", { type: "module" });
89
+ var { handler, ...offlineWorkerSync } = makeSyncWorker(
90
+ offlineWorker,
91
+ ["initChannel", "readZipEntry", "configure", "loadPreload", "loadBooks", "loadBook"],
59
92
  "app:offlineWorker"
60
93
  );
94
+ var fetchWorkerPromise = new Promise(
95
+ tryCatch(async () => {
96
+ let fetchWorker = null;
97
+ await navigator.serviceWorker.register("/fetch-worker.js", { type: "module" }).then((reg) => {
98
+ return reg.update().catch((e) => {
99
+ console.debug(e, "likely we are offline");
100
+ });
101
+ });
102
+ await navigator.serviceWorker.ready.then(async (reg) => {
103
+ console.debug("Service Worker registered:", reg.scope);
104
+ fetchWorker = reg.active;
105
+ }).catch((err) => console.error("Service Worker registration failed:", err));
106
+ if (!fetchWorker) {
107
+ console.debug("failed to get fetch worker");
108
+ }
109
+ return fetchWorker;
110
+ })
111
+ );
61
112
  var setWorkersReady;
62
113
  var workersReadyPromise = new Promise((resolve) => {
63
114
  setWorkersReady = resolve;
@@ -66,46 +117,81 @@ var setBooksLoaded;
66
117
  var setBooksLoadedPromise = new Promise((resolve) => {
67
118
  setBooksLoaded = resolve;
68
119
  });
69
- var fn = async () => {
70
- let fetchWorker = null;
71
- await navigator.serviceWorker.getRegistrations().then(async (registrations) => {
72
- for (const registration of registrations) {
73
- await registration.unregister();
120
+ var installWorker = async ({
121
+ skipLoadBooks = false,
122
+ primaryOnly = false,
123
+ nativeReadFile,
124
+ nativeWriteFile,
125
+ nativeListZips,
126
+ storage,
127
+ zipBaseUrl,
128
+ checkUpdates,
129
+ confEnv
130
+ } = {}) => {
131
+ const fetchWorker = await fetchWorkerPromise;
132
+ const configureOptions = { storage, zipBaseUrl, checkUpdates, confEnv };
133
+ if (nativeReadFile) {
134
+ const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
135
+ offlineWorker.onmessage = combineHandlers(handler, wwHandler);
136
+ if (storage !== "native") {
137
+ console.warn("nativeReadFile given, ignoring storage", storage);
74
138
  }
75
- });
76
- await navigator.serviceWorker.register("/fetch-worker.js", { type: "module" }).then((reg) => reg.update());
77
- await navigator.serviceWorker.ready.then(async (reg) => {
78
- console.debug("Service Worker registered:", reg.scope);
79
- fetchWorker = reg.active;
80
- }).catch((err) => console.error("Service Worker registration failed:", err));
81
- if (!fetchWorker) {
82
- console.debug("failed to get fetch worker");
83
- return;
139
+ offlineWorkerSync.configure({ ...configureOptions, storage: "native" });
140
+ } else {
141
+ offlineWorker.onmessage = handler.handle;
142
+ offlineWorkerSync.configure(configureOptions);
84
143
  }
85
144
  offlineWorkerSync.on("ready", setWorkersReady);
86
145
  const channel = new MessageChannel();
87
146
  offlineWorkerSync.initChannel({ ports: [channel.port1] });
88
147
  fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
148
+ fetchWorker.postMessage({ methodName: "configure", arg: { storage } });
89
149
  offlineWorkerSync.on("opfsProgress", () => {
90
150
  });
91
- await offlineWorkerSync.loadPreload();
92
- await offlineWorkerSync.loadBooks();
151
+ if (!primaryOnly) {
152
+ await offlineWorkerSync.loadPreload();
153
+ }
154
+ if (!skipLoadBooks) {
155
+ await offlineWorkerSync.loadBooks({ primaryOnly });
156
+ }
93
157
  setBooksLoaded();
94
158
  };
95
- fn();
96
- var useOfflineWorker_default = () => {
159
+ var useOfflineWorker_default = ({
160
+ primaryOnly = false,
161
+ nativeReadFile,
162
+ nativeWriteFile,
163
+ nativeListZips,
164
+ checkUpdates,
165
+ storage,
166
+ zipBaseUrl,
167
+ confEnv
168
+ } = {}) => {
97
169
  const [ready, setReady] = useState(false);
98
170
  useEffect(() => {
99
- const fn2 = async () => {
171
+ const fn = async () => {
172
+ await installWorker({
173
+ primaryOnly,
174
+ nativeReadFile,
175
+ nativeWriteFile,
176
+ nativeListZips,
177
+ storage,
178
+ zipBaseUrl,
179
+ checkUpdates,
180
+ confEnv
181
+ });
100
182
  await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
183
+ fetch(window.location.href.replace(window.location.search, ""));
101
184
  setReady(true);
102
185
  };
103
- fn2();
104
- }, []);
105
- const loadBook = useCallback(async ({ id, forceRefresh }, cbk) => {
106
- offlineWorkerSync.on("opfsProgress", cbk);
107
- return await offlineWorkerSync.loadBook({ id, forceRefresh });
108
- }, []);
186
+ fn();
187
+ }, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, storage, zipBaseUrl, checkUpdates, confEnv]);
188
+ const loadBook = useCallback(
189
+ async ({ id, forceRefresh }, cbk) => {
190
+ offlineWorkerSync.on("opfsProgress", cbk);
191
+ return await offlineWorkerSync.loadBook({ id, forceRefresh });
192
+ },
193
+ []
194
+ );
109
195
  return {
110
196
  ready,
111
197
  loadBook
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lls/offline",
3
- "version": "24.22.0-debug",
3
+ "version": "24.22.0-dee67770",
4
4
  "description": "offline",
5
5
  "main": "./lib/index.js",
6
6
  "module": "./lib/index.js",
@@ -36,10 +36,12 @@
36
36
  "devDependencies": {
37
37
  "@biomejs/biome": "1.9.4",
38
38
  "@happy-dom/global-registrator": "15.11.0",
39
+ "@ladle/react": "5.0.3",
39
40
  "@testing-library/react": "16.3.0",
40
41
  "@types/react": "18.3.12",
41
42
  "@types/react-dom": "18.3.1",
42
43
  "@vitejs/plugin-react": "4.5.1",
44
+ "@vitejs/plugin-react-swc": "3.10.1",
43
45
  "esbuild": "0.24.0",
44
46
  "happy-dom": "15.11.0",
45
47
  "lodash": "4.17.21",
@@ -48,17 +50,21 @@
48
50
  "react-dom": "18.3.1",
49
51
  "typescript": "5.8.3",
50
52
  "vite": "7.0.0",
53
+ "vite-tsconfig-paths": "5.1.4",
51
54
  "zod": "3.25.50",
52
- "@lls/vitescripts": "24.22.0"
55
+ "@lls/vitescripts": "24.22.0-dee67770"
53
56
  },
54
57
  "dependencies": {
55
58
  "fflate": "0.8.2"
56
59
  },
57
60
  "scripts": {
58
- "start": "echo no devserver",
61
+ "start": "pnpm ladle serve",
59
62
  "dts": "pnpm tsc --project tsconfig.production.json",
60
- "build": "node build",
61
- "test": "bun test --bail",
62
- "lint": "pnpm biome check --apply src __tests__"
63
+ "build": "NODE_ENV=production node build.js",
64
+ "deploy-ladle": "PUBLISH=1 pnpm ladle build",
65
+ "deploy-ladle-pr": "pnpm ladle build && s5cmd sync --delete --acl public-read --destination-region eu-west-1 'dist/*' s3://build.lelivrescolaire.fr/storybook/viewer-pr-$PR_NUMBER/",
66
+ "test": "bun test --bail __tests__/unit __tests__/e2e",
67
+ "lint": "pnpm biome check --apply src __tests__ stories",
68
+ "precommit": "pnpm lint"
63
69
  }
64
70
  }