@lls/offline 24.20.0-debug2 → 24.22.0-4a703cc1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/assets/fetch-worker.js +89 -28
- package/lib/assets/offline-worker.js +114 -44
- package/lib/index.js +80 -33
- package/package.json +2 -2
|
@@ -11,7 +11,7 @@ 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);
|
|
@@ -19,7 +19,7 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
|
|
|
19
19
|
const makeFn = (methodName) => {
|
|
20
20
|
return ({ ports, ...arg } = {}) => {
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
|
-
const eventId = `${methodName}:${Date.now()}`;
|
|
22
|
+
const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
|
|
23
23
|
dic[eventId] = { resolve, reject };
|
|
24
24
|
offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
|
|
25
25
|
});
|
|
@@ -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 (mode) => caches.open(`LLS_OFFLINE_${mode}`);
|
|
65
66
|
|
|
66
67
|
// src/worker/swFetchScreenCall.ts
|
|
67
68
|
var zipSlugName = (str) => str.replace(/[^a-zA-Z0-9-.]/g, "");
|
|
@@ -74,6 +75,15 @@ var cocoreFetchPartialPageScreen = async (_ev, { offlineWorkerSync: offlineWorke
|
|
|
74
75
|
const gqlPage = await offlineWorkerSync2.loadPageContent({ entryId });
|
|
75
76
|
return { data: gqlPage };
|
|
76
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
|
+
};
|
|
77
87
|
var cocoreFetchPartialPageSiblingsScreen = async (_ev) => {
|
|
78
88
|
return { data: { viewer: {} } };
|
|
79
89
|
};
|
|
@@ -188,6 +198,7 @@ var coreFetchPagesApi = async (ev, { offlineWorkerSync: offlineWorkerSync2, vari
|
|
|
188
198
|
};
|
|
189
199
|
var handles = {
|
|
190
200
|
coreFetchBookDrawerScreen,
|
|
201
|
+
cocoreFetchBookScreen,
|
|
191
202
|
coreFetchPagesApi,
|
|
192
203
|
cocoreFetchPartialPageScreen,
|
|
193
204
|
cocoreFetchPartialPageSiblingsScreen
|
|
@@ -196,7 +207,7 @@ var canHandleScreenCall = (url) => new RegExp(Object.keys(handles).join("|")).te
|
|
|
196
207
|
var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, knownLinks: knownLinks2 }) => {
|
|
197
208
|
try {
|
|
198
209
|
const variables = (
|
|
199
|
-
// @ts-
|
|
210
|
+
// @ts-expect-error TODO: handle variables from body(stream) properly!!
|
|
200
211
|
ev.request.body?.variables || JSON.parse(new URLSearchParams(new URL(ev.request.url).search).get("variables") || "{}")
|
|
201
212
|
);
|
|
202
213
|
console.debug("variables are", variables, "vs", ev.request.body);
|
|
@@ -211,23 +222,28 @@ var handleScreenCall = async (ev, { offlineWorkerSync: offlineWorkerSync2, known
|
|
|
211
222
|
return { status: 500, json: JSON.stringify(e) };
|
|
212
223
|
}
|
|
213
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
|
+
};
|
|
214
231
|
|
|
215
232
|
// src/worker/fetch-worker.ts
|
|
216
|
-
var
|
|
233
|
+
var STORAGE = "cache";
|
|
234
|
+
var CONF_ENV = "development";
|
|
235
|
+
var fetchAsset = async (_ev, { entryId, mimeType, offlineWorkerSync: offlineWorkerSync2 }) => {
|
|
217
236
|
try {
|
|
218
237
|
const buf = await offlineWorkerSync2.readZipEntry(entryId);
|
|
219
|
-
const url = ev.request.url;
|
|
220
238
|
return new Response(buf, {
|
|
221
239
|
headers: {
|
|
222
240
|
"Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
|
|
241
|
+
"Content-Length": buf.byteLength,
|
|
223
242
|
"Content-Type": mimeType
|
|
224
243
|
},
|
|
225
244
|
// @ts-ignore: poor ts typing
|
|
226
|
-
ok: true,
|
|
227
|
-
redirected: false,
|
|
228
245
|
status: 200,
|
|
229
|
-
|
|
230
|
-
url
|
|
246
|
+
statusText: "OK"
|
|
231
247
|
});
|
|
232
248
|
} catch (e) {
|
|
233
249
|
console.error("FAILED fetchAsset", e);
|
|
@@ -235,8 +251,7 @@ var fetchAsset = async (ev, { entryId, mimeType, offlineWorkerSync: offlineWorke
|
|
|
235
251
|
// @ts-ignore: poor ts typing
|
|
236
252
|
ok: false,
|
|
237
253
|
status: 500,
|
|
238
|
-
|
|
239
|
-
url: ev.request.url
|
|
254
|
+
statusText: e?.toString?.() || "fail fetch"
|
|
240
255
|
});
|
|
241
256
|
}
|
|
242
257
|
};
|
|
@@ -278,36 +293,69 @@ var ACCEPTED_EXTENSIONS = {
|
|
|
278
293
|
".m4a": "audio/mp4",
|
|
279
294
|
".webp": "image/webp"
|
|
280
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
|
+
};
|
|
281
326
|
var swSelf = self;
|
|
282
327
|
swSelf.addEventListener("fetch", (ev) => {
|
|
283
328
|
const url = ev.request.url;
|
|
284
329
|
const extension = url.match(/\.[^.]+$/)?.[0];
|
|
285
330
|
const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
|
|
286
331
|
const isValidScreenCall = canHandleScreenCall(url);
|
|
332
|
+
if (STORAGE !== "cache" && url.endsWith(".zip")) {
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
287
335
|
if (!mimeType && !isValidScreenCall) {
|
|
288
336
|
if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
|
|
289
337
|
console.debug("no ext", extension || url);
|
|
290
338
|
}
|
|
291
|
-
return;
|
|
339
|
+
return ev.respondWith(onlineFirst(ev.request, ev));
|
|
292
340
|
}
|
|
293
341
|
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
342
|
if (isValidScreenCall) {
|
|
301
343
|
console.debug("screencall!");
|
|
302
344
|
return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
|
|
303
345
|
}
|
|
304
|
-
|
|
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;
|
|
305
353
|
});
|
|
306
354
|
swSelf.addEventListener("install", (_event) => {
|
|
307
355
|
swSelf.skipWaiting();
|
|
308
356
|
});
|
|
309
|
-
swSelf.addEventListener("activate", (
|
|
310
|
-
|
|
357
|
+
swSelf.addEventListener("activate", (event) => {
|
|
358
|
+
event.waitUntil(swSelf.clients.claim());
|
|
311
359
|
});
|
|
312
360
|
var offlineWorker;
|
|
313
361
|
var offlineWorkerSync;
|
|
@@ -321,24 +369,37 @@ var initChannel = async (__, { ports }) => {
|
|
|
321
369
|
);
|
|
322
370
|
offlineWorkerSync = sender;
|
|
323
371
|
offlineWorker.onmessage = handler.handle;
|
|
324
|
-
offlineWorkerSync.on("setKnownLinks", setKnownLinks);
|
|
372
|
+
offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, knownLinks));
|
|
325
373
|
offlineWorker.postMessage({ methodName: "sendAppReady" });
|
|
326
374
|
};
|
|
327
375
|
var knownLinks = /* @__PURE__ */ new Map();
|
|
328
|
-
var
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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 });
|
|
333
389
|
};
|
|
334
390
|
swSelf.onmessage = async ({
|
|
335
391
|
data: { methodName, arg } = {},
|
|
336
392
|
ports
|
|
337
393
|
}) => {
|
|
338
394
|
const services = {
|
|
339
|
-
initChannel
|
|
395
|
+
initChannel,
|
|
396
|
+
configure
|
|
340
397
|
};
|
|
341
398
|
try {
|
|
399
|
+
if (methodName === "configure") {
|
|
400
|
+
await services[methodName](arg);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
342
403
|
if (methodName === "initChannel") {
|
|
343
404
|
if (!ports.length) {
|
|
344
405
|
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 = {};
|
|
@@ -11,7 +22,7 @@ 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);
|
|
@@ -19,7 +30,7 @@ var makeSyncWorker = (offlineWorker, methodNames, dbg) => {
|
|
|
19
30
|
const makeFn = (methodName) => {
|
|
20
31
|
return ({ ports, ...arg } = {}) => {
|
|
21
32
|
return new Promise((resolve, reject) => {
|
|
22
|
-
const eventId = `${methodName}:${Date.now()}`;
|
|
33
|
+
const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
|
|
23
34
|
dic[eventId] = { resolve, reject };
|
|
24
35
|
offlineWorker.postMessage({ eventId, methodName, arg }, ports);
|
|
25
36
|
});
|
|
@@ -63,15 +74,16 @@ var makeSyncHandler = a(
|
|
|
63
74
|
);
|
|
64
75
|
var bufToJson = (buf) => JSON.parse(new TextDecoder("UTF-8").decode(new Uint8Array(buf)));
|
|
65
76
|
var combineHandlers = (...handlers) => {
|
|
66
|
-
return ({ data: { methodName, ...data }, ports }) => {
|
|
77
|
+
return ({ data: { methodName, ...data } = {}, ports }) => {
|
|
67
78
|
for (const h of handlers) {
|
|
68
|
-
if (h.canHandle(methodName)) {
|
|
79
|
+
if (h.canHandle(String(methodName))) {
|
|
69
80
|
return h.handle({ data: { methodName, ...data }, ports });
|
|
70
81
|
}
|
|
71
82
|
}
|
|
72
83
|
console.error("no handlers matched", methodName);
|
|
73
84
|
};
|
|
74
85
|
};
|
|
86
|
+
var openCache = async (mode) => caches.open(`LLS_OFFLINE_${mode}`);
|
|
75
87
|
|
|
76
88
|
// src/worker/wwApi.ts
|
|
77
89
|
var STATE = {
|
|
@@ -830,17 +842,25 @@ var loadZip = async (buf, bookId) => {
|
|
|
830
842
|
|
|
831
843
|
// src/worker/offline-worker.ts
|
|
832
844
|
var CHECK_UPDATES = true;
|
|
833
|
-
var
|
|
834
|
-
var
|
|
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();
|
|
835
849
|
var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
836
850
|
const fileName = zipUrl.split("/").slice(-1)[0];
|
|
837
851
|
const readArrayBuffer = async () => {
|
|
838
|
-
if (
|
|
839
|
-
console.debug("try reading native", fileName);
|
|
852
|
+
if (STORAGE === "native") {
|
|
840
853
|
const back = await sender.nativeReadFile({ fname: fileName });
|
|
841
|
-
console.debug("done readnig");
|
|
842
854
|
return back;
|
|
843
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
|
+
}
|
|
844
864
|
const opfsRoot = await navigator.storage.getDirectory();
|
|
845
865
|
const handle = await opfsRoot.getFileHandle(fileName);
|
|
846
866
|
const accessHandle = await handle.createSyncAccessHandle();
|
|
@@ -867,8 +887,8 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
867
887
|
}
|
|
868
888
|
return buf;
|
|
869
889
|
} catch (e) {
|
|
870
|
-
if (e.message !== "forceRefresh" && !e.message.includes("refetching")) {
|
|
871
|
-
console.debug(
|
|
890
|
+
if (e instanceof Error && e.message !== "forceRefresh" && !e.message.includes("refetching")) {
|
|
891
|
+
console.debug(`${STORAGE} zip not found, creating it`, e, zipUrl);
|
|
872
892
|
}
|
|
873
893
|
const response = await fetch(zipUrl);
|
|
874
894
|
const contentLength = +response.headers.get("Content-Length");
|
|
@@ -879,6 +899,7 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
879
899
|
while (true) {
|
|
880
900
|
const { done, value: chunk } = await reader.read();
|
|
881
901
|
if (done) {
|
|
902
|
+
self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: contentLength } });
|
|
882
903
|
break;
|
|
883
904
|
}
|
|
884
905
|
arrayBuffer.set(chunk, position);
|
|
@@ -893,44 +914,69 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
893
914
|
console.debug(error);
|
|
894
915
|
throw new Error(error);
|
|
895
916
|
}
|
|
896
|
-
if (
|
|
917
|
+
if (STORAGE === "native") {
|
|
897
918
|
await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
|
|
898
|
-
|
|
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);
|
|
899
941
|
}
|
|
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
942
|
return await readArrayBuffer();
|
|
907
943
|
}
|
|
908
944
|
};
|
|
909
945
|
var slugToFileMeta2 = async ({ slug }) => {
|
|
910
|
-
console.debug("hereshit");
|
|
911
946
|
const json = await readZipEntry({ bookId: "preload", entryName: "preload/hashmap_slugToMetaPage.json", asJson: true });
|
|
912
|
-
console.debug("hereshit2");
|
|
913
947
|
return slugToFileMeta({ slug, json });
|
|
914
948
|
};
|
|
915
949
|
var PRELOAD_AS_BOOK_ID = "preload";
|
|
916
950
|
var isOldZip = (bookId) => typeof bookId === "number";
|
|
917
951
|
var loadBook2 = async ({ id: bookId, forceRefresh }) => {
|
|
918
952
|
if (LOADED_BOOKS.has(bookId)) {
|
|
953
|
+
await LOADED_BOOKS.get(bookId);
|
|
919
954
|
self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
|
|
920
955
|
return;
|
|
921
956
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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;
|
|
934
980
|
};
|
|
935
981
|
var loadPageContent2 = async ({
|
|
936
982
|
bookId: iBookId,
|
|
@@ -964,28 +1010,37 @@ var initChannel = (__, { ports }) => {
|
|
|
964
1010
|
self.postMessage({ methodName: "ready" });
|
|
965
1011
|
return;
|
|
966
1012
|
}
|
|
967
|
-
if (canHandle(dataObj.data?.methodName)) {
|
|
1013
|
+
if (canHandle(dataObj.data?.methodName || "")) {
|
|
968
1014
|
return handle(dataObj);
|
|
969
1015
|
}
|
|
970
1016
|
console.error("unknown methodName", dataObj.data?.methodName);
|
|
971
1017
|
};
|
|
972
1018
|
fetchWorker.onmessage = fn;
|
|
973
1019
|
};
|
|
974
|
-
var loadBooks = async () => {
|
|
1020
|
+
var loadBooks = async ({ primaryOnly = false } = {}) => {
|
|
975
1021
|
let entries = [];
|
|
976
|
-
if (
|
|
1022
|
+
if (STORAGE === "native") {
|
|
977
1023
|
entries = await sender.nativeListZips();
|
|
978
|
-
} else {
|
|
1024
|
+
} else if (STORAGE === "opfs") {
|
|
979
1025
|
const opfsRoot = await navigator.storage.getDirectory();
|
|
980
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()) }));
|
|
981
1034
|
}
|
|
982
1035
|
for await (const entry of entries) {
|
|
983
1036
|
if (/preload/.test(entry.name)) {
|
|
984
1037
|
continue;
|
|
985
1038
|
}
|
|
986
1039
|
if (/\d+\.zip$/.test(entry.name)) {
|
|
987
|
-
|
|
988
|
-
|
|
1040
|
+
if (!primaryOnly) {
|
|
1041
|
+
const bookId = entry.name.match(/\d+/)[0];
|
|
1042
|
+
await loadBook2({ id: Number.parseInt(bookId, 10) });
|
|
1043
|
+
}
|
|
989
1044
|
} else if (/zip$/.test(entry.name)) {
|
|
990
1045
|
const methodUri = entry.name.replace(".zip", "");
|
|
991
1046
|
await loadBook2({ id: methodUri });
|
|
@@ -994,14 +1049,29 @@ var loadBooks = async () => {
|
|
|
994
1049
|
}
|
|
995
1050
|
}
|
|
996
1051
|
};
|
|
997
|
-
var configure = ({
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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;
|
|
1001
1064
|
}
|
|
1002
|
-
if (typeof
|
|
1065
|
+
if (typeof checkUpdates !== "undefined") {
|
|
1003
1066
|
CHECK_UPDATES = !!checkUpdates;
|
|
1004
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 });
|
|
1005
1075
|
};
|
|
1006
1076
|
var loadPreload2 = async () => {
|
|
1007
1077
|
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 = {};
|
|
@@ -11,7 +22,7 @@ var makeSyncWorker = (offlineWorker2, 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);
|
|
@@ -19,7 +30,7 @@ var makeSyncWorker = (offlineWorker2, methodNames, dbg) => {
|
|
|
19
30
|
const makeFn = (methodName) => {
|
|
20
31
|
return ({ ports, ...arg } = {}) => {
|
|
21
32
|
return new Promise((resolve, reject) => {
|
|
22
|
-
const eventId = `${methodName}:${Date.now()}`;
|
|
33
|
+
const eventId = `${methodName}:${Date.now()}-${Math.random().toFixed(5)}`;
|
|
23
34
|
dic[eventId] = { resolve, reject };
|
|
24
35
|
offlineWorker2.postMessage({ eventId, methodName, arg }, ports);
|
|
25
36
|
});
|
|
@@ -62,9 +73,9 @@ var makeSyncHandler = a(
|
|
|
62
73
|
})
|
|
63
74
|
);
|
|
64
75
|
var combineHandlers = (...handlers) => {
|
|
65
|
-
return ({ data: { methodName, ...data }, ports }) => {
|
|
76
|
+
return ({ data: { methodName, ...data } = {}, ports }) => {
|
|
66
77
|
for (const h of handlers) {
|
|
67
|
-
if (h.canHandle(methodName)) {
|
|
78
|
+
if (h.canHandle(String(methodName))) {
|
|
68
79
|
return h.handle({ data: { methodName, ...data }, ports });
|
|
69
80
|
}
|
|
70
81
|
}
|
|
@@ -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,61 +118,80 @@ var setBooksLoadedPromise = new Promise((resolve) => {
|
|
|
90
118
|
setBooksLoaded = resolve;
|
|
91
119
|
});
|
|
92
120
|
var installWorker = async ({
|
|
93
|
-
|
|
121
|
+
skipLoadBooks = false,
|
|
122
|
+
primaryOnly = false,
|
|
94
123
|
nativeReadFile,
|
|
95
124
|
nativeWriteFile,
|
|
96
125
|
nativeListZips,
|
|
97
|
-
|
|
126
|
+
storage,
|
|
127
|
+
zipBaseUrl,
|
|
128
|
+
checkUpdates,
|
|
129
|
+
confEnv
|
|
98
130
|
} = {}) => {
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
}
|
|
131
|
+
const fetchWorker = await fetchWorkerPromise;
|
|
132
|
+
const configureOptions = { storage, zipBaseUrl, checkUpdates, confEnv };
|
|
114
133
|
if (nativeReadFile) {
|
|
115
134
|
const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
|
|
116
135
|
offlineWorker.onmessage = combineHandlers(handler, wwHandler);
|
|
117
|
-
|
|
136
|
+
if (storage !== "native") {
|
|
137
|
+
console.warn("nativeReadFile given, ignoring storage", storage);
|
|
138
|
+
}
|
|
139
|
+
offlineWorkerSync.configure({ ...configureOptions, storage: "native" });
|
|
118
140
|
} else {
|
|
119
|
-
offlineWorkerSync.configure({ checkUpdates });
|
|
120
141
|
offlineWorker.onmessage = handler.handle;
|
|
142
|
+
offlineWorkerSync.configure(configureOptions);
|
|
121
143
|
}
|
|
122
144
|
offlineWorkerSync.on("ready", setWorkersReady);
|
|
123
145
|
const channel = new MessageChannel();
|
|
124
146
|
offlineWorkerSync.initChannel({ ports: [channel.port1] });
|
|
125
147
|
fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
|
|
148
|
+
fetchWorker.postMessage({ methodName: "configure", arg: { storage } });
|
|
126
149
|
offlineWorkerSync.on("opfsProgress", () => {
|
|
127
150
|
});
|
|
128
|
-
if (!
|
|
151
|
+
if (!primaryOnly) {
|
|
129
152
|
await offlineWorkerSync.loadPreload();
|
|
130
153
|
}
|
|
131
|
-
|
|
154
|
+
if (!skipLoadBooks) {
|
|
155
|
+
await offlineWorkerSync.loadBooks({ primaryOnly });
|
|
156
|
+
}
|
|
132
157
|
setBooksLoaded();
|
|
133
158
|
};
|
|
134
|
-
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
|
+
} = {}) => {
|
|
135
169
|
const [ready, setReady] = useState(false);
|
|
136
170
|
useEffect(() => {
|
|
137
171
|
const fn = async () => {
|
|
138
|
-
await installWorker({
|
|
172
|
+
await installWorker({
|
|
173
|
+
primaryOnly,
|
|
174
|
+
nativeReadFile,
|
|
175
|
+
nativeWriteFile,
|
|
176
|
+
nativeListZips,
|
|
177
|
+
storage,
|
|
178
|
+
zipBaseUrl,
|
|
179
|
+
checkUpdates,
|
|
180
|
+
confEnv
|
|
181
|
+
});
|
|
139
182
|
await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
|
|
183
|
+
fetch(window.location.href.replace(window.location.search, ""));
|
|
140
184
|
setReady(true);
|
|
141
185
|
};
|
|
142
186
|
fn();
|
|
143
|
-
}, [
|
|
144
|
-
const loadBook = useCallback(
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
);
|
|
148
195
|
return {
|
|
149
196
|
ready,
|
|
150
197
|
loadBook
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lls/offline",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.22.0-4a703cc1",
|
|
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"
|
|
55
|
+
"@lls/vitescripts": "24.22.0-4a703cc1"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"fflate": "0.8.2"
|