@lls/offline 24.22.0-5bf1d034 → 24.22.0-5c0809f5

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.
@@ -62,6 +62,7 @@ var makeSyncHandler = a(
62
62
  })
63
63
  );
64
64
  var jsonToBuf = (json) => new TextEncoder().encode(JSON.stringify(json));
65
+ var openCache = async () => caches.open(`LLS_OFFLINE_${process.env.CONF_ENV}`);
65
66
 
66
67
  // src/worker/swFetchScreenCall.ts
67
68
  var zipSlugName = (str) => str.replace(/[^a-zA-Z0-9-.]/g, "");
@@ -196,7 +197,7 @@ var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).te
196
197
  var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
197
198
  try {
198
199
  const variables = (
199
- // @ts-ignore TODO: handle variables from body(stream) properly!!
200
+ // @ts-expect-error TODO: handle variables from body(stream) properly!!
200
201
  ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
201
202
  );
202
203
  console.debug("variables are", variables, "vs", ev.request.body);
@@ -211,6 +212,12 @@ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, known
211
212
  return { status: 500, json: JSON.stringify(e) };
212
213
  }
213
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
+ };
214
221
 
215
222
  // src/worker/fetch-worker.ts
216
223
  var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
@@ -278,36 +285,70 @@ var ACCEPTED_EXTENSIONS = {
278
285
  ".m4a": "audio/mp4",
279
286
  ".webp": "image/webp"
280
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";
281
319
  var swSelf = self;
282
320
  swSelf.addEventListener("fetch", (ev) => {
283
321
  const url = ev.request.url;
284
322
  const extension = url.match(/\.[^.]+$/)?.[0];
285
323
  const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
286
324
  const isValidScreenCall = canHandleScreenCall(url);
325
+ if (STORAGE !== "cache" && url.endsWith(".zip")) {
326
+ return;
327
+ }
287
328
  if (!mimeType && !isValidScreenCall) {
288
329
  if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
289
330
  console.debug("no ext", extension || url);
290
331
  }
291
- return;
332
+ return ev.respondWith(onlineFirst(ev.request, ev));
292
333
  }
293
334
  const entryId = knownLinks.get(url.replace(/https:\/\/.*?\//, "")) || knownLinks.get(`medias/${url.replace(/https:\/\/.*?\//, "")}`);
294
- if (!entryId && !isValidScreenCall) {
295
- if (/lls|lelivrescolaire/.test(url)) {
296
- console.info("no link for", url.replace(/https:\/\/.*?\//, ""));
297
- }
298
- return;
299
- }
300
335
  if (isValidScreenCall) {
301
336
  console.debug("screencall!");
302
337
  return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
303
338
  }
304
- 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;
305
346
  });
306
347
  swSelf.addEventListener("install", (_event) => {
307
348
  swSelf.skipWaiting();
308
349
  });
309
- swSelf.addEventListener("activate", (_event) => {
310
- return swSelf.clients.claim();
350
+ swSelf.addEventListener("activate", (event) => {
351
+ event.waitUntil(swSelf.clients.claim());
311
352
  });
312
353
  var offlineWorker;
313
354
  var offlineWorkerSync;
@@ -321,24 +362,30 @@ var initChannel = async (__, { ports }) => {
321
362
  );
322
363
  offlineWorkerSync = sender;
323
364
  offlineWorker.onmessage = handler.handle;
324
- offlineWorkerSync.on("setKnownLinks", setKnownLinks);
365
+ offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, setKnownLinks));
325
366
  offlineWorker.postMessage({ methodName: "sendAppReady" });
326
367
  };
327
368
  var knownLinks = /* @__PURE__ */ new Map();
328
- var setKnownLinks = ({ entryNames, bookId }) => {
329
- entryNames.forEach((entryName) => {
330
- const link = entryName.replace(/^\d+\/media\//, "");
331
- knownLinks.set(link, { entryName, bookId });
332
- });
369
+ var configure = ({ storage } = {}) => {
370
+ STORAGE = "cache";
371
+ if (typeof storage !== "undefined") {
372
+ STORAGE = storage;
373
+ }
374
+ console.info("sw::configure", { STORAGE });
333
375
  };
334
376
  swSelf.onmessage = async ({
335
377
  data: { methodName, arg } = {},
336
378
  ports
337
379
  }) => {
338
380
  const services = {
339
- initChannel
381
+ initChannel,
382
+ configure
340
383
  };
341
384
  try {
385
+ if (methodName === "configure") {
386
+ await services[methodName](arg);
387
+ return;
388
+ }
342
389
  if (methodName === "initChannel") {
343
390
  if (!ports.length) {
344
391
  throw new Error("fetchworker::missing port!");
@@ -1,3 +1,14 @@
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
13
  var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
3
14
  const dic = {};
@@ -72,6 +83,7 @@ var combineHandlers = (...handlers) => {
72
83
  console.error("no handlers matched", methodName);
73
84
  };
74
85
  };
86
+ var openCache = async () => caches.open(`LLS_OFFLINE_${process.env.CONF_ENV}`);
75
87
 
76
88
  // src/worker/wwApi.ts
77
89
  var STATE = {
@@ -830,17 +842,24 @@ var loadZip = async (buf, bookId) => {
830
842
 
831
843
  // src/worker/offline-worker.ts
832
844
  var CHECK_UPDATES = true;
833
- var NATIVE = false;
834
- var LOADED_BOOKS = /* @__PURE__ */ new Set();
845
+ var STORAGE = "cache";
846
+ var ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
847
+ var LOADED_BOOKS = /* @__PURE__ */ new Map();
835
848
  var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
836
849
  const fileName = zipUrl.split("/").slice(-1)[0];
837
850
  const readArrayBuffer = async () => {
838
- if (NATIVE) {
839
- console.debug("try reading native", fileName);
851
+ if (STORAGE === "native") {
840
852
  const back = await sender.nativeReadFile({ fname: fileName });
841
- console.debug("done readnig");
842
853
  return back;
843
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
+ }
844
863
  const opfsRoot = await navigator.storage.getDirectory();
845
864
  const handle = await opfsRoot.getFileHandle(fileName);
846
865
  const accessHandle = await handle.createSyncAccessHandle();
@@ -868,7 +887,7 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
868
887
  return buf;
869
888
  } catch (e) {
870
889
  if (e instanceof Error && e.message !== "forceRefresh" && !e.message.includes("refetching")) {
871
- console.debug("opfs db not found, creating it", e, zipUrl);
890
+ console.debug(`${STORAGE} zip not found, creating it`, e, zipUrl);
872
891
  }
873
892
  const response = await fetch(zipUrl);
874
893
  const contentLength = +response.headers.get("Content-Length");
@@ -879,6 +898,7 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
879
898
  while (true) {
880
899
  const { done, value: chunk } = await reader.read();
881
900
  if (done) {
901
+ self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: contentLength } });
882
902
  break;
883
903
  }
884
904
  arrayBuffer.set(chunk, position);
@@ -893,44 +913,69 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
893
913
  console.debug(error);
894
914
  throw new Error(error);
895
915
  }
896
- if (NATIVE) {
916
+ if (STORAGE === "native") {
897
917
  await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
898
- return await readArrayBuffer();
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);
899
940
  }
900
- const opfsRoot = await navigator.storage.getDirectory();
901
- const handle = await opfsRoot.getFileHandle(fileName, { create: true });
902
- const accessHandle = await handle.createSyncAccessHandle();
903
- await accessHandle.truncate(0);
904
- await accessHandle.write(arrayBuffer);
905
- await accessHandle.close();
906
941
  return await readArrayBuffer();
907
942
  }
908
943
  };
909
944
  var slugToFileMeta2 = async ({ slug }) => {
910
- console.debug("hereshit");
911
945
  const json = await readZipEntry({ bookId: "preload", entryName: "preload/hashmap_slugToMetaPage.json", asJson: true });
912
- console.debug("hereshit2");
913
946
  return slugToFileMeta({ slug, json });
914
947
  };
915
948
  var PRELOAD_AS_BOOK_ID = "preload";
916
949
  var isOldZip = (bookId) => typeof bookId === "number";
917
950
  var loadBook2 = async ({ id: bookId, forceRefresh }) => {
918
951
  if (LOADED_BOOKS.has(bookId)) {
952
+ await LOADED_BOOKS.get(bookId);
919
953
  self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
920
954
  return;
921
955
  }
922
- LOADED_BOOKS.add(bookId);
923
- const buf = await fetchZipFile(`https://ci.lls.fr/artefacts/content/development/${bookId}.zip`, { forceRefresh });
924
- try {
925
- const entryNames = await loadZip(new Uint8Array(buf), bookId);
926
- fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
927
- if (isOldZip(bookId)) {
928
- const json = await readZipEntry({ entryName: `preload/${bookId}.json`, bookId: PRELOAD_AS_BOOK_ID, asJson: true });
929
- loadBook(json);
930
- }
931
- } catch (e) {
932
- console.error("failed", e);
933
- }
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;
934
979
  };
935
980
  var loadPageContent2 = async ({
936
981
  bookId: iBookId,
@@ -973,11 +1018,16 @@ var initChannel = (__, { ports }) => {
973
1018
  };
974
1019
  var loadBooks = async ({ primaryOnly = false } = {}) => {
975
1020
  let entries = [];
976
- if (NATIVE) {
1021
+ if (STORAGE === "native") {
977
1022
  entries = await sender.nativeListZips();
978
- } else {
1023
+ } else if (STORAGE === "opfs") {
979
1024
  const opfsRoot = await navigator.storage.getDirectory();
980
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()) }));
981
1031
  }
982
1032
  for await (const entry of entries) {
983
1033
  if (/preload/.test(entry.name)) {
@@ -996,14 +1046,24 @@ var loadBooks = async ({ primaryOnly = false } = {}) => {
996
1046
  }
997
1047
  }
998
1048
  };
999
- var configure = ({ native, checkUpdates }) => {
1000
- console.info("configuring!!", native, checkUpdates);
1001
- if (typeof native !== "undefined") {
1002
- NATIVE = !!native;
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;
1003
1059
  }
1004
- if (typeof native !== "undefined") {
1060
+ if (typeof checkUpdates !== "undefined") {
1005
1061
  CHECK_UPDATES = !!checkUpdates;
1006
1062
  }
1063
+ if (typeof zipBaseUrl !== "undefined") {
1064
+ ZIP_BASE_URL = zipBaseUrl;
1065
+ }
1066
+ console.info("ow::configure", { STORAGE, CHECK_UPDATES, ZIP_BASE_URL });
1007
1067
  };
1008
1068
  var loadPreload2 = async () => {
1009
1069
  const zipUrl = "https://lls-ci-fr.s3.eu-west-3.amazonaws.com/artefacts/content/development/preload.zip";
package/lib/index.js CHANGED
@@ -1,3 +1,14 @@
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
13
  var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
3
14
  const dic = {};
@@ -76,11 +87,28 @@ var combineHandlers = (...handlers) => {
76
87
  import { useCallback, useEffect, useState } from "react";
77
88
  var offlineWorker = new Worker("/src/worker/offline-worker.js", { type: "module" });
78
89
  var { handler, ...offlineWorkerSync } = makeSyncWorker(
79
- // @ts-ignore: TODO
80
90
  offlineWorker,
81
91
  ["initChannel", "readZipEntry", "configure", "loadPreload", "loadBooks", "loadBook"],
82
92
  "app:offlineWorker"
83
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
+ );
84
112
  var setWorkersReady;
85
113
  var workersReadyPromise = new Promise((resolve) => {
86
114
  setWorkersReady = resolve;
@@ -90,45 +118,41 @@ var setBooksLoadedPromise = new Promise((resolve) => {
90
118
  setBooksLoaded = resolve;
91
119
  });
92
120
  var installWorker = async ({
121
+ skipLoadBooks = false,
93
122
  primaryOnly = false,
94
123
  nativeReadFile,
95
124
  nativeWriteFile,
96
125
  nativeListZips,
126
+ storage,
127
+ zipBaseUrl,
97
128
  checkUpdates
98
129
  } = {}) => {
99
- let fetchWorker = null;
100
- await navigator.serviceWorker.getRegistrations().then(async (registrations) => {
101
- for (const registration of registrations) {
102
- await registration.unregister();
103
- }
104
- });
105
- await navigator.serviceWorker.register("/fetch-worker.js", { type: "module" }).then((reg) => reg.update());
106
- await navigator.serviceWorker.ready.then(async (reg) => {
107
- console.debug("Service Worker registered:", reg.scope);
108
- fetchWorker = reg.active;
109
- }).catch((err) => console.error("Service Worker registration failed:", err));
110
- if (!fetchWorker) {
111
- console.debug("failed to get fetch worker");
112
- return;
113
- }
130
+ const fetchWorker = await fetchWorkerPromise;
131
+ const configureOptions = { storage, zipBaseUrl, checkUpdates };
114
132
  if (nativeReadFile) {
115
133
  const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
116
134
  offlineWorker.onmessage = combineHandlers(handler, wwHandler);
117
- offlineWorkerSync.configure({ native: true, checkUpdates });
135
+ if (storage !== "native") {
136
+ console.warn("nativeReadFile given, ignoring storage", storage);
137
+ }
138
+ offlineWorkerSync.configure({ ...configureOptions, storage: "native" });
118
139
  } else {
119
- offlineWorkerSync.configure({ native: false, checkUpdates });
120
140
  offlineWorker.onmessage = handler.handle;
141
+ offlineWorkerSync.configure(configureOptions);
121
142
  }
122
143
  offlineWorkerSync.on("ready", setWorkersReady);
123
144
  const channel = new MessageChannel();
124
145
  offlineWorkerSync.initChannel({ ports: [channel.port1] });
125
146
  fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
147
+ fetchWorker.postMessage({ methodName: "configure", arg: { storage } });
126
148
  offlineWorkerSync.on("opfsProgress", () => {
127
149
  });
128
150
  if (!primaryOnly) {
129
151
  await offlineWorkerSync.loadPreload();
130
152
  }
131
- await offlineWorkerSync.loadBooks();
153
+ if (!skipLoadBooks) {
154
+ await offlineWorkerSync.loadBooks({ primaryOnly });
155
+ }
132
156
  setBooksLoaded();
133
157
  };
134
158
  var useOfflineWorker_default = ({
@@ -136,17 +160,28 @@ var useOfflineWorker_default = ({
136
160
  nativeReadFile,
137
161
  nativeWriteFile,
138
162
  nativeListZips,
139
- checkUpdates
163
+ checkUpdates,
164
+ storage,
165
+ zipBaseUrl
140
166
  } = {}) => {
141
167
  const [ready, setReady] = useState(false);
142
168
  useEffect(() => {
143
169
  const fn = async () => {
144
- await installWorker({ primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates });
170
+ await installWorker({
171
+ primaryOnly,
172
+ nativeReadFile,
173
+ nativeWriteFile,
174
+ nativeListZips,
175
+ storage,
176
+ zipBaseUrl,
177
+ checkUpdates
178
+ });
145
179
  await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
180
+ fetch(window.location.href.replace(window.location.search, ""));
146
181
  setReady(true);
147
182
  };
148
183
  fn();
149
- }, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates]);
184
+ }, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, storage, zipBaseUrl, checkUpdates]);
150
185
  const loadBook = useCallback(
151
186
  async ({ id, forceRefresh }, cbk) => {
152
187
  offlineWorkerSync.on("opfsProgress", cbk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lls/offline",
3
- "version": "24.22.0-5bf1d034",
3
+ "version": "24.22.0-5c0809f5",
4
4
  "description": "offline",
5
5
  "main": "./lib/index.js",
6
6
  "module": "./lib/index.js",
@@ -52,7 +52,7 @@
52
52
  "vite": "7.0.0",
53
53
  "vite-tsconfig-paths": "5.1.4",
54
54
  "zod": "3.25.50",
55
- "@lls/vitescripts": "24.22.0-5bf1d034"
55
+ "@lls/vitescripts": "24.22.0-5c0809f5"
56
56
  },
57
57
  "dependencies": {
58
58
  "fflate": "0.8.2"