@lls/offline 24.20.0-debug → 24.22.0-34f32711

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/README.md CHANGED
@@ -3,3 +3,27 @@
3
3
  handles offline as webworker for android/ios and desktop
4
4
 
5
5
  Likely used by core, cocore
6
+
7
+ ## Getting started
8
+
9
+ Deux types de workers pour gérer le offline:
10
+
11
+ - un web worker (WW): offlineWorker.ts
12
+ - un service worker (SW): fetch.worker.ts
13
+
14
+ Le main (useOfflineWorker) instancie les deux workers.
15
+ Dû à la nature du SW qui ne peut pas poster en retour au main, on crèe un lien entre WW et SW via MessageChannel afin qu'ils puissent communiquer entre eux.
16
+
17
+ SW a pour responsabilité d'intercepter les calls (GQL, assets)
18
+ Il demande le contenu à WW.
19
+
20
+ WW a pour responsabilité de
21
+ - fetcher le contenu offline (.zip sur s3)
22
+ - gérer l'opfs (stockage navigateur du contenu)
23
+ - lire dans les archives pour servir SW
24
+
25
+ ## Testing
26
+
27
+ Les WW et SW ont pour interface (offlineWorker.ts, fetch.worker.ts) et délèguent les méthodes métiers aux autres fichiers worker agnostiques (donc testable)
28
+
29
+
@@ -11,12 +11,11 @@ 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) => {
@@ -31,27 +30,54 @@ 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 ${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 () => caches.open(`LLS_OFFLINE_${process.env.CONF_ENV}`);
53
66
 
54
67
  // src/worker/swFetchScreenCall.ts
68
+ var zipSlugName = (str) => str.replace(/[^a-zA-Z0-9-.]/g, "");
69
+ var cocoreFetchPartialPageScreen = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables, knownLinks: knownLinks2 }) => {
70
+ const key = `gqls/${zipSlugName(variables.slugs[0] || "")}.json`;
71
+ const entryId = knownLinks2.get(key);
72
+ if (!entryId) {
73
+ throw new Error(`slug not found ${key}`);
74
+ }
75
+ const gqlPage = await offlineWorkerSync2.loadPageContent({ entryId });
76
+ return { data: gqlPage };
77
+ };
78
+ var cocoreFetchPartialPageSiblingsScreen = async (_ev) => {
79
+ return { data: { viewer: {} } };
80
+ };
55
81
  var coreFetchBookDrawerScreen = async (_ev, { offlineWorkerSync: offlineWorkerSync2, variables }) => {
56
82
  const slug = variables.slugs?.[0];
57
83
  const uris = variables.uris;
@@ -161,30 +187,39 @@ var coreFetchPagesApi = async (ev, { offlineWorkerSync: offlineWorkerSync2, vari
161
187
  }
162
188
  }
163
189
  };
164
- var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2 }) => {
190
+ var handles = {
191
+ coreFetchBookDrawerScreen,
192
+ coreFetchPagesApi,
193
+ cocoreFetchPartialPageScreen,
194
+ cocoreFetchPartialPageSiblingsScreen
195
+ };
196
+ var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).test(url);
197
+ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
165
198
  try {
166
199
  const variables = (
167
- // @ts-ignore TODO: handle variables from body(stream) properly!!
200
+ // @ts-expect-error TODO: handle variables from body(stream) properly!!
168
201
  ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
169
202
  );
170
203
  console.debug("variables are", variables, "vs", ev.request.body);
171
- const handles = {
172
- coreFetchBookDrawerScreen,
173
- coreFetchPagesApi
174
- };
175
204
  const key = ev.request.url.match(Object.keys(handles).join("|"))?.[0];
176
205
  if (!key) {
177
206
  throw new Error("unknown ev");
178
207
  }
179
- const json = await handles[key](ev, { offlineWorkerSync: offlineWorkerSync2, variables });
208
+ const json = await handles[key](ev, { offlineWorkerSync: offlineWorkerSync2, variables, knownLinks: knownLinks2 });
180
209
  return { status: 200, json };
181
210
  } catch (e) {
182
211
  console.error(e);
183
212
  return { status: 500, json: JSON.stringify(e) };
184
213
  }
185
214
  };
215
+ var setKnownLinks = (knownLinks2, { entryNames, bookId }) => {
216
+ entryNames.forEach((entryName) => {
217
+ const link = entryName.replace(new RegExp(`^\\d+/media/|^${bookId}/medias/`), "").replace(new RegExp(`^${bookId}/gqls/`), "gqls/");
218
+ knownLinks2.set(link, { entryName, bookId });
219
+ });
220
+ };
186
221
 
187
- // src/worker/fetch.worker.ts
222
+ // src/worker/fetch-worker.ts
188
223
  var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
189
224
  try {
190
225
  const buf = await offlineWorkerSync2.readZipEntry(entryId);
@@ -212,9 +247,9 @@ var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorke
212
247
  });
213
248
  }
214
249
  };
215
- var fetchScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2 }) => {
250
+ var fetchScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
216
251
  try {
217
- const { status, json } = await handleScreenCall(ev, { offlineWorkerSync: offlineWorkerSync2 });
252
+ const { status, json } = await handleScreenCall(ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 });
218
253
  return new Response(jsonToBuf(json), {
219
254
  headers: {
220
255
  "Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
@@ -250,65 +285,107 @@ var ACCEPTED_EXTENSIONS = {
250
285
  ".m4a": "audio/mp4",
251
286
  ".webp": "image/webp"
252
287
  };
288
+ var putInCache = async (request, response) => {
289
+ const cache = await openCache();
290
+ await cache.put(request, response);
291
+ };
292
+ var onlineFirst = async (request, event) => {
293
+ try {
294
+ const responseFromNetwork = await fetch(request);
295
+ const cached = putInCache(request, responseFromNetwork.clone());
296
+ if (request.url.endsWith(".zip")) {
297
+ await cached;
298
+ event.waitUntil(Promise.resolve());
299
+ } else {
300
+ event.waitUntil(cached);
301
+ }
302
+ return responseFromNetwork;
303
+ } catch (_e) {
304
+ const over = /\/\?story=basic/.test(request.url) ? request.url.replace(/\/\?story=.*$/, "") : request;
305
+ const responseFromCache = await caches.match(over);
306
+ if (!responseFromCache) {
307
+ return new Response("FAILED fetch", {
308
+ // @ts-ignore: poor ts typing
309
+ ok: false,
310
+ status: 500,
311
+ type: "cors",
312
+ url: event.request.url
313
+ });
314
+ }
315
+ return responseFromCache;
316
+ }
317
+ };
318
+ var STORAGE = "cache";
253
319
  var swSelf = self;
254
320
  swSelf.addEventListener("fetch", (ev) => {
255
321
  const url = ev.request.url;
256
322
  const extension = url.match(/\.[^.]+$/)?.[0];
257
323
  const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
258
- const screenCall = /coreFetchPagesApi|coreFetchBookDrawerScreen/.test(url);
259
- if (!mimeType && !screenCall) {
260
- if (!/\.(css|tsx?|jsx?|woff2|fr)/.test(url)) {
261
- console.debug("no ext", url.match(/\.[^.]+$/)?.[0]);
262
- }
324
+ const isValidScreenCall = canHandleScreenCall(url);
325
+ if (STORAGE !== "cache" && url.endsWith(".zip")) {
263
326
  return;
264
327
  }
265
- const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, ""));
266
- if (!entryId && !screenCall) {
267
- if (/lls|lelivrescolaire/.test(url)) {
268
- console.info("no link for", url.replace(/https:\/\/.*?\//, ""));
328
+ if (!mimeType && !isValidScreenCall) {
329
+ if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
330
+ console.debug("no ext", extension || url);
269
331
  }
270
- return;
332
+ return ev.respondWith(onlineFirst(ev.request, ev));
271
333
  }
272
- if (screenCall) {
334
+ const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
335
+ if (isValidScreenCall) {
273
336
  console.debug("screencall!");
274
- return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync }));
337
+ return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
275
338
  }
276
- return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
339
+ if (entryId) {
340
+ return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
341
+ }
342
+ if (/lls|lelivrescolaire/.test(url)) {
343
+ console.info("no link for", url.replace(/https:\/\/.*?\//, ""), `medias/${url.replace(/https:\/\/.*?\//, "")}`);
344
+ }
345
+ return;
277
346
  });
278
347
  swSelf.addEventListener("install", (_event) => {
279
348
  swSelf.skipWaiting();
280
349
  });
281
- swSelf.addEventListener("activate", (_event) => {
282
- return swSelf.clients.claim();
350
+ swSelf.addEventListener("activate", (event) => {
351
+ event.waitUntil(swSelf.clients.claim());
283
352
  });
284
353
  var offlineWorker;
285
354
  var offlineWorkerSync;
286
355
  var initChannel = async (__, { ports }) => {
287
- console.info(`FETCH WORKER innitChannel v${13}`);
356
+ console.info(`FETCH WORKER innitChannel v${/* @__PURE__ */ new Date()}`);
288
357
  offlineWorker = ports[0];
289
- offlineWorkerSync = makeSyncWorker(
358
+ const { handler, ...sender } = makeSyncWorker(
290
359
  offlineWorker,
291
360
  ["readZipEntry", "loadBook", "slugToFileMeta", "loadPageContent", "searchBooks"],
292
361
  "fetchworker"
293
362
  );
294
- offlineWorkerSync.on("setKnownLinks", setKnownLinks);
363
+ offlineWorkerSync = sender;
364
+ offlineWorker.onmessage = handler.handle;
365
+ offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, setKnownLinks));
295
366
  offlineWorker.postMessage({ methodName: "sendAppReady" });
296
367
  };
297
368
  var knownLinks = /* @__PURE__ */ new Map();
298
- var setKnownLinks = ({ entryNames, bookId }) => {
299
- entryNames.forEach((entryName) => {
300
- const link = entryName.replace(/^\d+\/media\//, "");
301
- knownLinks.set(link, { entryName, bookId });
302
- });
369
+ var configure = ({ storage } = {}) => {
370
+ STORAGE = "cache";
371
+ if (typeof storage !== "undefined") {
372
+ STORAGE = storage;
373
+ }
374
+ console.info("sw::configure", { STORAGE });
303
375
  };
304
376
  swSelf.onmessage = async ({
305
377
  data: { methodName, arg } = {},
306
378
  ports
307
379
  }) => {
308
380
  const services = {
309
- initChannel
381
+ initChannel,
382
+ configure
310
383
  };
311
384
  try {
385
+ if (methodName === "configure") {
386
+ await services[methodName](arg);
387
+ return;
388
+ }
312
389
  if (methodName === "initChannel") {
313
390
  if (!ports.length) {
314
391
  throw new Error("fetchworker::missing port!");
@@ -316,7 +393,7 @@ swSelf.onmessage = async ({
316
393
  await services[methodName](arg, { ports });
317
394
  return;
318
395
  }
319
- throw new Error(`fetchworker:invalid methodName ${methodName}`);
396
+ throw new Error(`fetchworker:invalid methodName ${Object.keys(services)} ${methodName}`);
320
397
  } catch (e) {
321
398
  console.error("failed", e);
322
399
  }
@@ -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()}`;
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 ${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 () => caches.open(`LLS_OFFLINE_${process.env.CONF_ENV}`);
19
87
 
20
88
  // src/worker/wwApi.ts
21
89
  var STATE = {
@@ -40,7 +108,9 @@ var slugToFileMeta = async ({ slug, json }) => {
40
108
  return { pageId, chapterId };
41
109
  };
42
110
  var loadPageContent = async ({ chapterId, json }) => {
43
- json.viewer.pages.hits[0].chapter = STATE.chapters[chapterId] || json.viewer.pages.hits[0].chapter;
111
+ if (chapterId) {
112
+ json.viewer.pages.hits[0].chapter = STATE.chapters[chapterId] || json.viewer.pages.hits[0].chapter;
113
+ }
44
114
  return json;
45
115
  };
46
116
  var searchBooks = async ({
@@ -83,14 +153,7 @@ var loadPreload = (json) => {
83
153
  });
84
154
  };
85
155
 
86
- // ../../node_modules/.pnpm/fflate@0.8.2/node_modules/fflate/esm/index.mjs
87
- import { createRequire } from "module";
88
- var require2 = createRequire("/");
89
- var Worker;
90
- try {
91
- Worker = require2("worker_threads").Worker;
92
- } catch (e) {
93
- }
156
+ // ../../node_modules/.pnpm/fflate@0.8.2/node_modules/fflate/esm/browser.js
94
157
  var u8 = Uint8Array;
95
158
  var u16 = Uint16Array;
96
159
  var i32 = Int32Array;
@@ -739,6 +802,9 @@ var loadZip = async (buf, bookId) => {
739
802
  const unzipper = new Unzip();
740
803
  unzipper.register(UnzipInflate);
741
804
  unzipper.onfile = (file) => {
805
+ if (!file.name.includes(".")) {
806
+ return;
807
+ }
742
808
  m.set(file.name, {
743
809
  decoded: null,
744
810
  zipEntry: {
@@ -762,17 +828,39 @@ var loadZip = async (buf, bookId) => {
762
828
  }
763
829
  });
764
830
  };
765
- unzipper.push(buf);
831
+ const chunkSize = 32 * 1024 * 1024;
832
+ const nChunks = Math.max(1, Math.floor(buf.byteLength / chunkSize));
833
+ const sleep = new Promise((resolve) => setTimeout(resolve, 0));
834
+ for (let i = 0; i < nChunks - 1; ++i) {
835
+ unzipper.push(buf.slice(i * chunkSize, (i + 1) * chunkSize));
836
+ await sleep;
837
+ }
838
+ unzipper.push(buf.slice((nChunks - 1) * chunkSize, buf.byteLength), true);
766
839
  BOOKS[bookId] = m;
767
840
  return [...m.keys()];
768
841
  };
769
842
 
770
- // src/worker/offlineWorker.ts
771
- var CHECK_UPDATES = false;
772
- var fetchZipFile = async (zipUrl) => {
773
- const opfsRoot = await navigator.storage.getDirectory();
843
+ // src/worker/offline-worker.ts
844
+ var CHECK_UPDATES = true;
845
+ var STORAGE = "cache";
846
+ var ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
847
+ var LOADED_BOOKS = /* @__PURE__ */ new Map();
848
+ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
774
849
  const fileName = zipUrl.split("/").slice(-1)[0];
775
850
  const readArrayBuffer = async () => {
851
+ if (STORAGE === "native") {
852
+ const back = await sender.nativeReadFile({ fname: fileName });
853
+ return back;
854
+ }
855
+ if (STORAGE === "cache") {
856
+ const response = await caches.match(zipUrl);
857
+ if (!response) {
858
+ throw new Error(`missing zip ${zipUrl}`);
859
+ }
860
+ const buf = await response.arrayBuffer();
861
+ return buf;
862
+ }
863
+ const opfsRoot = await navigator.storage.getDirectory();
776
864
  const handle = await opfsRoot.getFileHandle(fileName);
777
865
  const accessHandle = await handle.createSyncAccessHandle();
778
866
  const size = accessHandle.getSize();
@@ -782,6 +870,9 @@ var fetchZipFile = async (zipUrl) => {
782
870
  return arrayBuffer;
783
871
  };
784
872
  try {
873
+ if (forceRefresh) {
874
+ throw new Error("forceRefresh");
875
+ }
785
876
  const buf = await readArrayBuffer();
786
877
  if (CHECK_UPDATES) {
787
878
  const response = await fetch(zipUrl, { method: "HEAD" });
@@ -795,51 +886,118 @@ var fetchZipFile = async (zipUrl) => {
795
886
  }
796
887
  return buf;
797
888
  } catch (e) {
798
- console.debug("opfs db not found, creating it", e, zipUrl);
799
- const arrayBuffer = await fetch(zipUrl).then((res) => res.arrayBuffer());
889
+ if (e instanceof Error && e.message !== "forceRefresh" && !e.message.includes("refetching")) {
890
+ console.debug(`${STORAGE} zip not found, creating it`, e, zipUrl);
891
+ }
892
+ const response = await fetch(zipUrl);
893
+ const contentLength = +response.headers.get("Content-Length");
894
+ const reader = response.body.getReader();
895
+ let position = 0;
896
+ const arrayBuffer = new Uint8Array(contentLength);
897
+ let prevPosition = 0;
898
+ while (true) {
899
+ const { done, value: chunk } = await reader.read();
900
+ if (done) {
901
+ self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: contentLength } });
902
+ break;
903
+ }
904
+ arrayBuffer.set(chunk, position);
905
+ position += chunk.length;
906
+ if (position - prevPosition > 1e6) {
907
+ prevPosition = position;
908
+ self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: position } });
909
+ }
910
+ }
800
911
  if (arrayBuffer.byteLength < 1e4) {
801
- const error = `likely ${zipUrl} was not fetched`;
912
+ const error = `likely ${zipUrl} was not fetched ${arrayBuffer.byteLength}`;
802
913
  console.debug(error);
803
914
  throw new Error(error);
804
915
  }
805
- const handle = await opfsRoot.getFileHandle(fileName, { create: true });
806
- const accessHandle = await handle.createSyncAccessHandle();
807
- await accessHandle.truncate(0);
808
- await accessHandle.write(arrayBuffer);
809
- await accessHandle.close();
916
+ if (STORAGE === "native") {
917
+ await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
918
+ } else if (STORAGE === "opfs") {
919
+ const opfsRoot = await navigator.storage.getDirectory();
920
+ const handle = await opfsRoot.getFileHandle(fileName, { create: true });
921
+ const accessHandle = await handle.createSyncAccessHandle();
922
+ await accessHandle.truncate(0);
923
+ await accessHandle.write(arrayBuffer);
924
+ await accessHandle.close();
925
+ } else {
926
+ const cache = await openCache();
927
+ const response2 = new Response(arrayBuffer, {
928
+ headers: {
929
+ "Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
930
+ "Content-Type": "application/zip"
931
+ },
932
+ // @ts-ignore: poor ts typing
933
+ ok: true,
934
+ redirected: false,
935
+ status: 200,
936
+ type: "cors",
937
+ url: zipUrl
938
+ });
939
+ await cache.put(zipUrl, response2);
940
+ }
810
941
  return await readArrayBuffer();
811
942
  }
812
943
  };
813
944
  var slugToFileMeta2 = async ({ slug }) => {
814
- console.debug("hereshit");
815
945
  const json = await readZipEntry({ bookId: "preload", entryName: "preload/hashmap_slugToMetaPage.json", asJson: true });
816
- console.debug("hereshit2");
817
946
  return slugToFileMeta({ slug, json });
818
947
  };
819
948
  var PRELOAD_AS_BOOK_ID = "preload";
820
- var loadBook2 = async (bookId) => {
821
- const buf = await fetchZipFile(`https://ci.lls.fr/artefacts/content/production/${bookId}.zip`);
822
- try {
823
- const entryNames = await loadZip(new Uint8Array(buf), bookId);
824
- fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
825
- const json = await readZipEntry({ entryName: `preload/${bookId}.json`, bookId: PRELOAD_AS_BOOK_ID, asJson: true });
826
- loadBook(json);
827
- } catch (e) {
828
- console.error("failed", e);
949
+ var isOldZip = (bookId) => typeof bookId === "number";
950
+ var loadBook2 = async ({ id: bookId, forceRefresh }) => {
951
+ if (LOADED_BOOKS.has(bookId)) {
952
+ await LOADED_BOOKS.get(bookId);
953
+ self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
954
+ return;
829
955
  }
956
+ const p = new Promise(
957
+ tryCatch(async () => {
958
+ const buf = await fetchZipFile(`${ZIP_BASE_URL}/${bookId}.zip`, { forceRefresh });
959
+ try {
960
+ const entryNames = await loadZip(new Uint8Array(buf), bookId);
961
+ fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
962
+ if (isOldZip(bookId)) {
963
+ const json = await readZipEntry({
964
+ entryName: `preload/${bookId}.json`,
965
+ bookId: PRELOAD_AS_BOOK_ID,
966
+ asJson: true
967
+ });
968
+ loadBook(json);
969
+ }
970
+ return true;
971
+ } catch (e) {
972
+ console.error("failed", e);
973
+ return false;
974
+ }
975
+ })
976
+ );
977
+ LOADED_BOOKS.set(bookId, p);
978
+ await p;
830
979
  };
831
980
  var loadPageContent2 = async ({
832
- bookId,
981
+ bookId: iBookId,
833
982
  chapterId,
834
- pageId
983
+ pageId,
984
+ entryId
835
985
  }) => {
836
- const json = await readZipEntry({ entryName: `${chapterId}/${pageId}.json`, bookId, asJson: true });
986
+ const bookId = entryId ? entryId.bookId : iBookId;
987
+ if (!bookId) {
988
+ throw new Error("missing bookId: expect entryId or bookId");
989
+ }
990
+ const json = await readZipEntry({
991
+ entryName: entryId ? entryId.entryName : `${chapterId}/${pageId}.json`,
992
+ bookId,
993
+ asJson: true
994
+ });
837
995
  return await loadPageContent({ chapterId, json });
838
996
  };
839
997
  var fetchWorker;
840
998
  var initChannel = (__, { ports }) => {
841
999
  fetchWorker = ports[0];
842
- const defaultHandler = makeSyncHandler(fetchWorker, {
1000
+ const { canHandle, handle } = makeSyncHandler(fetchWorker, {
843
1001
  readZipEntry,
844
1002
  loadBook: loadBook2,
845
1003
  slugToFileMeta: slugToFileMeta2,
@@ -851,20 +1009,62 @@ var initChannel = (__, { ports }) => {
851
1009
  self.postMessage({ methodName: "ready" });
852
1010
  return;
853
1011
  }
854
- defaultHandler(dataObj);
1012
+ if (canHandle(dataObj.data?.methodName || "")) {
1013
+ return handle(dataObj);
1014
+ }
1015
+ console.error("unknown methodName", dataObj.data?.methodName);
855
1016
  };
856
1017
  fetchWorker.onmessage = fn;
857
1018
  };
858
- var loadBooks = async () => {
859
- const opfsRoot = await navigator.storage.getDirectory();
860
- const entries = await opfsRoot.values();
1019
+ var loadBooks = async ({ primaryOnly = false } = {}) => {
1020
+ let entries = [];
1021
+ if (STORAGE === "native") {
1022
+ entries = await sender.nativeListZips();
1023
+ } else if (STORAGE === "opfs") {
1024
+ const opfsRoot = await navigator.storage.getDirectory();
1025
+ entries = await opfsRoot.values();
1026
+ } else {
1027
+ const cache = await openCache();
1028
+ const keys = await cache.keys();
1029
+ const zips = keys.filter((key2) => key2.url.endsWith(".zip"));
1030
+ entries = zips.map((zip) => ({ name: String(zip.url.split("/").pop()) }));
1031
+ }
861
1032
  for await (const entry of entries) {
1033
+ if (/preload/.test(entry.name)) {
1034
+ continue;
1035
+ }
862
1036
  if (/\d+\.zip$/.test(entry.name)) {
863
- const bookId = entry.name.match(/\d+/)[0];
864
- await loadBook2(Number.parseInt(bookId, 10));
1037
+ if (!primaryOnly) {
1038
+ const bookId = entry.name.match(/\d+/)[0];
1039
+ await loadBook2({ id: Number.parseInt(bookId, 10) });
1040
+ }
1041
+ } else if (/zip$/.test(entry.name)) {
1042
+ const methodUri = entry.name.replace(".zip", "");
1043
+ await loadBook2({ id: methodUri });
1044
+ } else {
1045
+ console.info(`invalid book entry ${entry.name}`);
865
1046
  }
866
1047
  }
867
1048
  };
1049
+ var configure = ({
1050
+ storage,
1051
+ checkUpdates,
1052
+ zipBaseUrl
1053
+ }) => {
1054
+ CHECK_UPDATES = true;
1055
+ STORAGE = "cache";
1056
+ ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
1057
+ if (typeof storage !== "undefined") {
1058
+ STORAGE = storage;
1059
+ }
1060
+ if (typeof checkUpdates !== "undefined") {
1061
+ CHECK_UPDATES = !!checkUpdates;
1062
+ }
1063
+ if (typeof zipBaseUrl !== "undefined") {
1064
+ ZIP_BASE_URL = zipBaseUrl;
1065
+ }
1066
+ console.info("ow::configure", { STORAGE, CHECK_UPDATES, ZIP_BASE_URL });
1067
+ };
868
1068
  var loadPreload2 = async () => {
869
1069
  const zipUrl = "https://lls-ci-fr.s3.eu-west-3.amazonaws.com/artefacts/content/development/preload.zip";
870
1070
  const buf = await fetchZipFile(zipUrl);
@@ -877,9 +1077,13 @@ var loadPreload2 = async () => {
877
1077
  console.error("failed", e);
878
1078
  }
879
1079
  };
880
- self.onmessage = makeSyncHandler(self, {
1080
+ var mainHandle = makeSyncHandler(self, {
881
1081
  initChannel,
882
1082
  loadPreload: loadPreload2,
883
1083
  readZipEntry,
884
- loadBooks
1084
+ loadBooks,
1085
+ loadBook: loadBook2,
1086
+ configure
885
1087
  });
1088
+ var { handler, ...sender } = makeSyncWorker(self, ["nativeWriteFile", "nativeReadFile", "nativeListZips"]);
1089
+ self.onmessage = combineHandlers(mainHandle, handler);
package/lib/index.d.ts ADDED
@@ -0,0 +1 @@
1
+ declare module "@lls/offline"
package/lib/index.js CHANGED
@@ -1,319 +1,19 @@
1
- var __create = Object.create;
2
- var __defProp = Object.defineProperty;
3
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
- var __getOwnPropNames = Object.getOwnPropertyNames;
5
- var __getProtoOf = Object.getPrototypeOf;
6
- var __hasOwnProp = Object.prototype.hasOwnProperty;
7
- var __commonJS = (cb, mod) => function __require() {
8
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
- };
10
- var __copyProps = (to, from, except, desc) => {
11
- if (from && typeof from === "object" || typeof from === "function") {
12
- for (let key of __getOwnPropNames(from))
13
- if (!__hasOwnProp.call(to, key) && key !== except)
14
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
- }
16
- return to;
17
- };
18
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
19
- // If the importer is in node compatibility mode or this is not an ESM
20
- // file that has been converted to a CommonJS file using a Babel-
21
- // compatible transform (i.e. "__esModule" has not been set), then set
22
- // "default" to the CommonJS "module.exports" for node compatibility.
23
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
24
- mod
25
- ));
26
-
27
- // ../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
28
- var require_react_production_min = __commonJS({
29
- "../../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js"(exports) {
30
- "use strict";
31
- var l = Symbol.for("react.element");
32
- var n = Symbol.for("react.portal");
33
- var p = Symbol.for("react.fragment");
34
- var q = Symbol.for("react.strict_mode");
35
- var r = Symbol.for("react.profiler");
36
- var t = Symbol.for("react.provider");
37
- var u = Symbol.for("react.context");
38
- var v = Symbol.for("react.forward_ref");
39
- var w = Symbol.for("react.suspense");
40
- var x = Symbol.for("react.memo");
41
- var y = Symbol.for("react.lazy");
42
- var z = Symbol.iterator;
43
- function A(a2) {
44
- if (null === a2 || "object" !== typeof a2) return null;
45
- a2 = z && a2[z] || a2["@@iterator"];
46
- return "function" === typeof a2 ? a2 : null;
47
- }
48
- var B = { isMounted: function() {
49
- return false;
50
- }, enqueueForceUpdate: function() {
51
- }, enqueueReplaceState: function() {
52
- }, enqueueSetState: function() {
53
- } };
54
- var C = Object.assign;
55
- var D = {};
56
- function E(a2, b, e) {
57
- this.props = a2;
58
- this.context = b;
59
- this.refs = D;
60
- this.updater = e || B;
61
- }
62
- E.prototype.isReactComponent = {};
63
- E.prototype.setState = function(a2, b) {
64
- if ("object" !== typeof a2 && "function" !== typeof a2 && null != a2) throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");
65
- this.updater.enqueueSetState(this, a2, b, "setState");
66
- };
67
- E.prototype.forceUpdate = function(a2) {
68
- this.updater.enqueueForceUpdate(this, a2, "forceUpdate");
69
- };
70
- function F() {
71
- }
72
- F.prototype = E.prototype;
73
- function G(a2, b, e) {
74
- this.props = a2;
75
- this.context = b;
76
- this.refs = D;
77
- this.updater = e || B;
78
- }
79
- var H = G.prototype = new F();
80
- H.constructor = G;
81
- C(H, E.prototype);
82
- H.isPureReactComponent = true;
83
- var I = Array.isArray;
84
- var J = Object.prototype.hasOwnProperty;
85
- var K = { current: null };
86
- var L = { key: true, ref: true, __self: true, __source: true };
87
- function M(a2, b, e) {
88
- var d, c = {}, k = null, h = null;
89
- if (null != b) for (d in void 0 !== b.ref && (h = b.ref), void 0 !== b.key && (k = "" + b.key), b) J.call(b, d) && !L.hasOwnProperty(d) && (c[d] = b[d]);
90
- var g = arguments.length - 2;
91
- if (1 === g) c.children = e;
92
- else if (1 < g) {
93
- for (var f = Array(g), m = 0; m < g; m++) f[m] = arguments[m + 2];
94
- c.children = f;
95
- }
96
- if (a2 && a2.defaultProps) for (d in g = a2.defaultProps, g) void 0 === c[d] && (c[d] = g[d]);
97
- return { $$typeof: l, type: a2, key: k, ref: h, props: c, _owner: K.current };
98
- }
99
- function N(a2, b) {
100
- return { $$typeof: l, type: a2.type, key: b, ref: a2.ref, props: a2.props, _owner: a2._owner };
101
- }
102
- function O(a2) {
103
- return "object" === typeof a2 && null !== a2 && a2.$$typeof === l;
104
- }
105
- function escape(a2) {
106
- var b = { "=": "=0", ":": "=2" };
107
- return "$" + a2.replace(/[=:]/g, function(a3) {
108
- return b[a3];
109
- });
110
- }
111
- var P = /\/+/g;
112
- function Q(a2, b) {
113
- return "object" === typeof a2 && null !== a2 && null != a2.key ? escape("" + a2.key) : b.toString(36);
114
- }
115
- function R(a2, b, e, d, c) {
116
- var k = typeof a2;
117
- if ("undefined" === k || "boolean" === k) a2 = null;
118
- var h = false;
119
- if (null === a2) h = true;
120
- else switch (k) {
121
- case "string":
122
- case "number":
123
- h = true;
124
- break;
125
- case "object":
126
- switch (a2.$$typeof) {
127
- case l:
128
- case n:
129
- h = true;
130
- }
131
- }
132
- if (h) return h = a2, c = c(h), a2 = "" === d ? "." + Q(h, 0) : d, I(c) ? (e = "", null != a2 && (e = a2.replace(P, "$&/") + "/"), R(c, b, e, "", function(a3) {
133
- return a3;
134
- })) : null != c && (O(c) && (c = N(c, e + (!c.key || h && h.key === c.key ? "" : ("" + c.key).replace(P, "$&/") + "/") + a2)), b.push(c)), 1;
135
- h = 0;
136
- d = "" === d ? "." : d + ":";
137
- if (I(a2)) for (var g = 0; g < a2.length; g++) {
138
- k = a2[g];
139
- var f = d + Q(k, g);
140
- h += R(k, b, e, f, c);
141
- }
142
- else if (f = A(a2), "function" === typeof f) for (a2 = f.call(a2), g = 0; !(k = a2.next()).done; ) k = k.value, f = d + Q(k, g++), h += R(k, b, e, f, c);
143
- else if ("object" === k) throw b = String(a2), Error("Objects are not valid as a React child (found: " + ("[object Object]" === b ? "object with keys {" + Object.keys(a2).join(", ") + "}" : b) + "). If you meant to render a collection of children, use an array instead.");
144
- return h;
145
- }
146
- function S(a2, b, e) {
147
- if (null == a2) return a2;
148
- var d = [], c = 0;
149
- R(a2, d, "", "", function(a3) {
150
- return b.call(e, a3, c++);
151
- });
152
- return d;
153
- }
154
- function T(a2) {
155
- if (-1 === a2._status) {
156
- var b = a2._result;
157
- b = b();
158
- b.then(function(b2) {
159
- if (0 === a2._status || -1 === a2._status) a2._status = 1, a2._result = b2;
160
- }, function(b2) {
161
- if (0 === a2._status || -1 === a2._status) a2._status = 2, a2._result = b2;
162
- });
163
- -1 === a2._status && (a2._status = 0, a2._result = b);
164
- }
165
- if (1 === a2._status) return a2._result.default;
166
- throw a2._result;
167
- }
168
- var U = { current: null };
169
- var V = { transition: null };
170
- var W = { ReactCurrentDispatcher: U, ReactCurrentBatchConfig: V, ReactCurrentOwner: K };
171
- function X() {
172
- throw Error("act(...) is not supported in production builds of React.");
173
- }
174
- exports.Children = { map: S, forEach: function(a2, b, e) {
175
- S(a2, function() {
176
- b.apply(this, arguments);
177
- }, e);
178
- }, count: function(a2) {
179
- var b = 0;
180
- S(a2, function() {
181
- b++;
182
- });
183
- return b;
184
- }, toArray: function(a2) {
185
- return S(a2, function(a3) {
186
- return a3;
187
- }) || [];
188
- }, only: function(a2) {
189
- if (!O(a2)) throw Error("React.Children.only expected to receive a single React element child.");
190
- return a2;
191
- } };
192
- exports.Component = E;
193
- exports.Fragment = p;
194
- exports.Profiler = r;
195
- exports.PureComponent = G;
196
- exports.StrictMode = q;
197
- exports.Suspense = w;
198
- exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = W;
199
- exports.act = X;
200
- exports.cloneElement = function(a2, b, e) {
201
- if (null === a2 || void 0 === a2) throw Error("React.cloneElement(...): The argument must be a React element, but you passed " + a2 + ".");
202
- var d = C({}, a2.props), c = a2.key, k = a2.ref, h = a2._owner;
203
- if (null != b) {
204
- void 0 !== b.ref && (k = b.ref, h = K.current);
205
- void 0 !== b.key && (c = "" + b.key);
206
- if (a2.type && a2.type.defaultProps) var g = a2.type.defaultProps;
207
- for (f in b) J.call(b, f) && !L.hasOwnProperty(f) && (d[f] = void 0 === b[f] && void 0 !== g ? g[f] : b[f]);
208
- }
209
- var f = arguments.length - 2;
210
- if (1 === f) d.children = e;
211
- else if (1 < f) {
212
- g = Array(f);
213
- for (var m = 0; m < f; m++) g[m] = arguments[m + 2];
214
- d.children = g;
215
- }
216
- return { $$typeof: l, type: a2.type, key: c, ref: k, props: d, _owner: h };
217
- };
218
- exports.createContext = function(a2) {
219
- a2 = { $$typeof: u, _currentValue: a2, _currentValue2: a2, _threadCount: 0, Provider: null, Consumer: null, _defaultValue: null, _globalName: null };
220
- a2.Provider = { $$typeof: t, _context: a2 };
221
- return a2.Consumer = a2;
222
- };
223
- exports.createElement = M;
224
- exports.createFactory = function(a2) {
225
- var b = M.bind(null, a2);
226
- b.type = a2;
227
- return b;
228
- };
229
- exports.createRef = function() {
230
- return { current: null };
231
- };
232
- exports.forwardRef = function(a2) {
233
- return { $$typeof: v, render: a2 };
234
- };
235
- exports.isValidElement = O;
236
- exports.lazy = function(a2) {
237
- return { $$typeof: y, _payload: { _status: -1, _result: a2 }, _init: T };
238
- };
239
- exports.memo = function(a2, b) {
240
- return { $$typeof: x, type: a2, compare: void 0 === b ? null : b };
241
- };
242
- exports.startTransition = function(a2) {
243
- var b = V.transition;
244
- V.transition = {};
245
- try {
246
- a2();
247
- } finally {
248
- V.transition = b;
249
- }
250
- };
251
- exports.unstable_act = X;
252
- exports.useCallback = function(a2, b) {
253
- return U.current.useCallback(a2, b);
254
- };
255
- exports.useContext = function(a2) {
256
- return U.current.useContext(a2);
257
- };
258
- exports.useDebugValue = function() {
259
- };
260
- exports.useDeferredValue = function(a2) {
261
- return U.current.useDeferredValue(a2);
262
- };
263
- exports.useEffect = function(a2, b) {
264
- return U.current.useEffect(a2, b);
265
- };
266
- exports.useId = function() {
267
- return U.current.useId();
268
- };
269
- exports.useImperativeHandle = function(a2, b, e) {
270
- return U.current.useImperativeHandle(a2, b, e);
271
- };
272
- exports.useInsertionEffect = function(a2, b) {
273
- return U.current.useInsertionEffect(a2, b);
274
- };
275
- exports.useLayoutEffect = function(a2, b) {
276
- return U.current.useLayoutEffect(a2, b);
277
- };
278
- exports.useMemo = function(a2, b) {
279
- return U.current.useMemo(a2, b);
280
- };
281
- exports.useReducer = function(a2, b, e) {
282
- return U.current.useReducer(a2, b, e);
283
- };
284
- exports.useRef = function(a2) {
285
- return U.current.useRef(a2);
286
- };
287
- exports.useState = function(a2) {
288
- return U.current.useState(a2);
289
- };
290
- exports.useSyncExternalStore = function(a2, b, e) {
291
- return U.current.useSyncExternalStore(a2, b, e);
292
- };
293
- exports.useTransition = function() {
294
- return U.current.useTransition();
295
- };
296
- exports.version = "18.3.1";
297
- }
298
- });
299
-
300
- // ../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js
301
- var require_react = __commonJS({
302
- "../../node_modules/.pnpm/react@18.3.1/node_modules/react/index.js"(exports, module) {
303
- "use strict";
304
- if (true) {
305
- module.exports = require_react_production_min();
306
- } else {
307
- module.exports = null;
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);
308
8
  }
309
- }
310
- });
9
+ };
10
+ };
311
11
 
312
12
  // src/utils/worker.ts
313
- var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
13
+ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
314
14
  const dic = {};
315
15
  const cbks = {};
316
- const fn2 = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
16
+ const fn = ({ data: { eventId, methodName, results, error, arg } = {} } = {}) => {
317
17
  if (eventId && eventId in dic) {
318
18
  if (error) {
319
19
  dic[eventId].reject(error);
@@ -322,53 +22,93 @@ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
322
22
  }
323
23
  } else {
324
24
  if (methodName && !methodName.startsWith("async") && !(methodName in cbks)) {
325
- console.error(`syncWorker: ${dbg}`, eventId, methodName, error);
25
+ console.error(`syncWorker: unknown methodName cbk ${dbg}`, eventId, methodName, error);
326
26
  }
327
27
  }
328
28
  methodName && cbks[methodName]?.(arg);
329
29
  };
330
- offlineWorker.onmessage = fn2;
331
30
  const makeFn = (methodName) => {
332
31
  return ({ ports, ...arg } = {}) => {
333
32
  return new Promise((resolve, reject) => {
334
33
  const eventId = `${methodName}:${Date.now()}`;
335
34
  dic[eventId] = { resolve, reject };
336
- offlineWorker.postMessage({ eventId, methodName, arg }, ports);
35
+ offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
337
36
  });
338
37
  };
339
38
  };
340
39
  const obj = Object.assign(Object.fromEntries(methodNames.map((name) => [name, makeFn(name)])), {
341
- on: (key, fn3) => {
342
- cbks[key] = fn3;
40
+ on: (key, fn2) => {
41
+ cbks[key] = fn2;
343
42
  }
344
43
  });
345
- 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
+ };
346
53
  };
347
54
  var a = (a2) => a2;
348
55
  var makeSyncHandler = a(
349
- (sender, services) => async ({ data: { eventId, methodName, arg } = {}, ports }) => {
350
- try {
351
- if (typeof methodName === "string" && methodName in services && services[methodName]) {
352
- const results = await services[methodName](arg, { ports });
353
- sender.postMessage({ eventId, methodName, results });
354
- 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 });
355
71
  }
356
- throw new Error(`invalid methodName ${String(methodName)}`);
357
- } catch (e) {
358
- console.debug("failed", e);
359
- sender.postMessage({ eventId, methodName, error: e });
360
72
  }
361
- }
73
+ })
362
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
+ };
363
85
 
364
86
  // src/hooks/useOfflineWorker.ts
365
- var import_react = __toESM(require_react(), 1);
366
- var offlineWorkerSync = makeSyncWorker(
367
- // @ts-ignore: TODO
368
- new Worker("/src/worker/offlineWorker.js", { type: "module" }),
369
- ["initChannel", "readZipEntry", "loadPreload", "loadBooks"],
87
+ import { useCallback, useEffect, useState } from "react";
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"],
370
92
  "app:offlineWorker"
371
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
+ );
372
112
  var setWorkersReady;
373
113
  var workersReadyPromise = new Promise((resolve) => {
374
114
  setWorkersReady = resolve;
@@ -377,54 +117,83 @@ var setBooksLoaded;
377
117
  var setBooksLoadedPromise = new Promise((resolve) => {
378
118
  setBooksLoaded = resolve;
379
119
  });
380
- var fn = async () => {
381
- let fetchWorker = null;
382
- await navigator.serviceWorker.getRegistrations().then(async (registrations) => {
383
- for (const registration of registrations) {
384
- await registration.unregister();
385
- }
386
- });
387
- await navigator.serviceWorker.register("/fetch.worker.js", { type: "module" }).then((reg) => reg.update());
388
- await navigator.serviceWorker.ready.then(async (reg) => {
389
- console.debug("Service Worker registered:", reg.scope);
390
- fetchWorker = reg.active;
391
- }).catch((err) => console.error("Service Worker registration failed:", err));
392
- if (!fetchWorker) {
393
- return;
120
+ var installWorker = async ({
121
+ skipLoadBooks = false,
122
+ primaryOnly = false,
123
+ nativeReadFile,
124
+ nativeWriteFile,
125
+ nativeListZips,
126
+ storage,
127
+ zipBaseUrl,
128
+ checkUpdates
129
+ } = {}) => {
130
+ const fetchWorker = await fetchWorkerPromise;
131
+ const configureOptions = { storage, zipBaseUrl, checkUpdates };
132
+ if (nativeReadFile) {
133
+ const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
134
+ offlineWorker.onmessage = combineHandlers(handler, wwHandler);
135
+ if (storage !== "native") {
136
+ console.warn("nativeReadFile given, ignoring storage", storage);
137
+ }
138
+ offlineWorkerSync.configure({ ...configureOptions, storage: "native" });
139
+ } else {
140
+ offlineWorker.onmessage = handler.handle;
141
+ offlineWorkerSync.configure(configureOptions);
394
142
  }
395
143
  offlineWorkerSync.on("ready", setWorkersReady);
396
144
  const channel = new MessageChannel();
397
145
  offlineWorkerSync.initChannel({ ports: [channel.port1] });
398
146
  fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
399
- await offlineWorkerSync.loadPreload();
400
- await offlineWorkerSync.loadBooks();
147
+ fetchWorker.postMessage({ methodName: "configure", arg: { storage } });
148
+ offlineWorkerSync.on("opfsProgress", () => {
149
+ });
150
+ if (!primaryOnly) {
151
+ await offlineWorkerSync.loadPreload();
152
+ }
153
+ if (!skipLoadBooks) {
154
+ await offlineWorkerSync.loadBooks({ primaryOnly });
155
+ }
401
156
  setBooksLoaded();
402
157
  };
403
- fn();
404
- var useOfflineWorker_default = () => {
405
- const [ready, setReady] = (0, import_react.useState)(false);
406
- (0, import_react.useEffect)(() => {
407
- const fn2 = async () => {
158
+ var useOfflineWorker_default = ({
159
+ primaryOnly = false,
160
+ nativeReadFile,
161
+ nativeWriteFile,
162
+ nativeListZips,
163
+ checkUpdates,
164
+ storage,
165
+ zipBaseUrl
166
+ } = {}) => {
167
+ const [ready, setReady] = useState(false);
168
+ useEffect(() => {
169
+ const fn = async () => {
170
+ await installWorker({
171
+ primaryOnly,
172
+ nativeReadFile,
173
+ nativeWriteFile,
174
+ nativeListZips,
175
+ storage,
176
+ zipBaseUrl,
177
+ checkUpdates
178
+ });
408
179
  await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
180
+ fetch(window.location.href.replace(window.location.search, ""));
409
181
  setReady(true);
410
182
  };
411
- fn2();
412
- }, []);
413
- return ready;
183
+ fn();
184
+ }, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, storage, zipBaseUrl, checkUpdates]);
185
+ const loadBook = useCallback(
186
+ async ({ id, forceRefresh }, cbk) => {
187
+ offlineWorkerSync.on("opfsProgress", cbk);
188
+ return await offlineWorkerSync.loadBook({ id, forceRefresh });
189
+ },
190
+ []
191
+ );
192
+ return {
193
+ ready,
194
+ loadBook
195
+ };
414
196
  };
415
197
  export {
416
198
  useOfflineWorker_default as useOfflineWorker
417
199
  };
418
- /*! Bundled license information:
419
-
420
- react/cjs/react.production.min.js:
421
- (**
422
- * @license React
423
- * react.production.min.js
424
- *
425
- * Copyright (c) Facebook, Inc. and its affiliates.
426
- *
427
- * This source code is licensed under the MIT license found in the
428
- * LICENSE file in the root directory of this source tree.
429
- *)
430
- */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lls/offline",
3
- "version": "24.20.0-debug",
3
+ "version": "24.22.0-34f32711",
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.20.0"
55
+ "@lls/vitescripts": "24.22.0-34f32711"
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
  }