@lls/offline 24.22.0-eaf69c80 → 24.23.0-0a61fc97
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 +94 -28
- package/lib/assets/offline-worker.js +143 -50
- package/lib/index.js +65 -24
- package/package.json +2 -2
|
@@ -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
|
};
|
|
@@ -276,7 +291,42 @@ var ACCEPTED_EXTENSIONS = {
|
|
|
276
291
|
".gif": "image/gif",
|
|
277
292
|
".webm": "video/webm",
|
|
278
293
|
".m4a": "audio/mp4",
|
|
279
|
-
".webp": "image/webp"
|
|
294
|
+
".webp": "image/webp",
|
|
295
|
+
".riv": "application/octet-stream",
|
|
296
|
+
".pdf": "application/pdf"
|
|
297
|
+
};
|
|
298
|
+
var putInCache = async (request, response) => {
|
|
299
|
+
const cache = await openCache(CONF_ENV);
|
|
300
|
+
await cache.put(request, response);
|
|
301
|
+
};
|
|
302
|
+
var onlineFirst = async (request, event) => {
|
|
303
|
+
try {
|
|
304
|
+
const responseFromNetwork = await fetch(request);
|
|
305
|
+
const cached = putInCache(request, responseFromNetwork.clone());
|
|
306
|
+
if (request.url.endsWith(".zip")) {
|
|
307
|
+
await cached;
|
|
308
|
+
event.waitUntil(Promise.resolve());
|
|
309
|
+
} else {
|
|
310
|
+
event.waitUntil(cached);
|
|
311
|
+
}
|
|
312
|
+
return responseFromNetwork;
|
|
313
|
+
} catch (e) {
|
|
314
|
+
const over = /\/\?story=basic/.test(request.url) ? request.url.replace(/\/\?story=.*$/, "") : request;
|
|
315
|
+
const responseFromCache = await caches.match(over);
|
|
316
|
+
if (!responseFromCache) {
|
|
317
|
+
return new Response(
|
|
318
|
+
`FAILED fetch ${e && typeof e === "object" && "message" in e ? e.message : e?.toString?.()}`,
|
|
319
|
+
{
|
|
320
|
+
// @ts-ignore: poor ts typing
|
|
321
|
+
ok: false,
|
|
322
|
+
status: 500,
|
|
323
|
+
type: "cors",
|
|
324
|
+
url: event.request.url
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return responseFromCache;
|
|
329
|
+
}
|
|
280
330
|
};
|
|
281
331
|
var swSelf = self;
|
|
282
332
|
swSelf.addEventListener("fetch", (ev) => {
|
|
@@ -284,30 +334,33 @@ swSelf.addEventListener("fetch", (ev) => {
|
|
|
284
334
|
const extension = url.match(/\.[^.]+$/)?.[0];
|
|
285
335
|
const mimeType = extension && extension in ACCEPTED_EXTENSIONS ? ACCEPTED_EXTENSIONS[extension] : "";
|
|
286
336
|
const isValidScreenCall = canHandleScreenCall(url);
|
|
337
|
+
if (STORAGE !== "cache" && url.endsWith(".zip")) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
287
340
|
if (!mimeType && !isValidScreenCall) {
|
|
288
341
|
if (!/\.(css|tsx?|jsx?|woff2|fr|manifest)(\??|$)|@(vite|react|id)/.test(url)) {
|
|
289
342
|
console.debug("no ext", extension || url);
|
|
290
343
|
}
|
|
291
|
-
return;
|
|
344
|
+
return ev.respondWith(onlineFirst(ev.request, ev));
|
|
292
345
|
}
|
|
293
346
|
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
347
|
if (isValidScreenCall) {
|
|
301
348
|
console.debug("screencall!");
|
|
302
349
|
return ev.respondWith(fetchScreenCall(ev, { offlineWorkerSync, knownLinks }));
|
|
303
350
|
}
|
|
304
|
-
|
|
351
|
+
if (entryId) {
|
|
352
|
+
return ev.respondWith(fetchAsset(ev, { entryId, mimeType, offlineWorkerSync }));
|
|
353
|
+
}
|
|
354
|
+
if (/lls|lelivrescolaire/.test(url)) {
|
|
355
|
+
console.info("no link for", url.replace(/https:\/\/.*?\//, ""), `medias/${url.replace(/https:\/\/.*?\//, "")}`);
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
305
358
|
});
|
|
306
359
|
swSelf.addEventListener("install", (_event) => {
|
|
307
360
|
swSelf.skipWaiting();
|
|
308
361
|
});
|
|
309
|
-
swSelf.addEventListener("activate", (
|
|
310
|
-
|
|
362
|
+
swSelf.addEventListener("activate", (event) => {
|
|
363
|
+
event.waitUntil(swSelf.clients.claim());
|
|
311
364
|
});
|
|
312
365
|
var offlineWorker;
|
|
313
366
|
var offlineWorkerSync;
|
|
@@ -321,24 +374,37 @@ var initChannel = async (__, { ports }) => {
|
|
|
321
374
|
);
|
|
322
375
|
offlineWorkerSync = sender;
|
|
323
376
|
offlineWorker.onmessage = handler.handle;
|
|
324
|
-
offlineWorkerSync.on("setKnownLinks", setKnownLinks);
|
|
377
|
+
offlineWorkerSync.on("setKnownLinks", setKnownLinks.bind(null, knownLinks));
|
|
325
378
|
offlineWorker.postMessage({ methodName: "sendAppReady" });
|
|
326
379
|
};
|
|
327
380
|
var knownLinks = /* @__PURE__ */ new Map();
|
|
328
|
-
var
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
381
|
+
var configure = ({
|
|
382
|
+
storage,
|
|
383
|
+
confEnv
|
|
384
|
+
} = {}) => {
|
|
385
|
+
STORAGE = "cache";
|
|
386
|
+
CONF_ENV = "development";
|
|
387
|
+
if (typeof storage !== "undefined") {
|
|
388
|
+
STORAGE = storage;
|
|
389
|
+
}
|
|
390
|
+
if (typeof confEnv !== "undefined") {
|
|
391
|
+
CONF_ENV = confEnv;
|
|
392
|
+
}
|
|
393
|
+
console.info("sw::configure", { STORAGE, CONF_ENV });
|
|
333
394
|
};
|
|
334
395
|
swSelf.onmessage = async ({
|
|
335
396
|
data: { methodName, arg } = {},
|
|
336
397
|
ports
|
|
337
398
|
}) => {
|
|
338
399
|
const services = {
|
|
339
|
-
initChannel
|
|
400
|
+
initChannel,
|
|
401
|
+
configure
|
|
340
402
|
};
|
|
341
403
|
try {
|
|
404
|
+
if (methodName === "configure") {
|
|
405
|
+
await services[methodName](arg);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
342
408
|
if (methodName === "initChannel") {
|
|
343
409
|
if (!ports.length) {
|
|
344
410
|
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 = {};
|
|
@@ -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
|
});
|
|
@@ -72,6 +83,17 @@ var combineHandlers = (...handlers) => {
|
|
|
72
83
|
console.error("no handlers matched", methodName);
|
|
73
84
|
};
|
|
74
85
|
};
|
|
86
|
+
var openCache = async (mode) => caches.open(`LLS_OFFLINE_${mode}`);
|
|
87
|
+
var concatArrayBuffers = (buffers) => {
|
|
88
|
+
const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
|
|
89
|
+
const concatenated = new Uint8Array(totalLength);
|
|
90
|
+
let offset = 0;
|
|
91
|
+
for (const buffer of buffers) {
|
|
92
|
+
concatenated.set(new Uint8Array(buffer), offset);
|
|
93
|
+
offset += buffer.byteLength;
|
|
94
|
+
}
|
|
95
|
+
return concatenated.buffer;
|
|
96
|
+
};
|
|
75
97
|
|
|
76
98
|
// src/worker/wwApi.ts
|
|
77
99
|
var STATE = {
|
|
@@ -753,6 +775,9 @@ async function decryptData(encryptedData, key2, iv2) {
|
|
|
753
775
|
return decryptedData;
|
|
754
776
|
}
|
|
755
777
|
var BOOKS = {};
|
|
778
|
+
var zipNameInMedias = (zipName) => {
|
|
779
|
+
return /^(pages|book)/.test(zipName);
|
|
780
|
+
};
|
|
756
781
|
var readZipEntry = async ({
|
|
757
782
|
bookId,
|
|
758
783
|
entryName,
|
|
@@ -762,9 +787,20 @@ var readZipEntry = async ({
|
|
|
762
787
|
console.trace("no bookId", bookId, "found");
|
|
763
788
|
return null;
|
|
764
789
|
}
|
|
765
|
-
const
|
|
790
|
+
const manifest = BOOKS[bookId].manifest;
|
|
791
|
+
let overridenName = entryName;
|
|
792
|
+
if (manifest && !entryName.includes("gqls/")) {
|
|
793
|
+
const zipName = entryName.replace("medias/", "");
|
|
794
|
+
if (zipNameInMedias(zipName)) {
|
|
795
|
+
const originalName = manifest.medias[zipName];
|
|
796
|
+
overridenName = `medias/${originalName}`;
|
|
797
|
+
} else {
|
|
798
|
+
console.error("any media should exist in manifest", zipName, entryName, bookId);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const bookEntry = BOOKS[bookId].map.get(overridenName);
|
|
766
802
|
if (!bookEntry) {
|
|
767
|
-
console.info("no bookEntry", entryName, "found");
|
|
803
|
+
console.info("no bookEntry", entryName, "found", overridenName);
|
|
768
804
|
return null;
|
|
769
805
|
}
|
|
770
806
|
if (!bookEntry.decoded) {
|
|
@@ -775,16 +811,6 @@ var readZipEntry = async ({
|
|
|
775
811
|
var hasZip = (bookId) => {
|
|
776
812
|
return !!BOOKS[bookId];
|
|
777
813
|
};
|
|
778
|
-
function concatArrayBuffers(buffers) {
|
|
779
|
-
const totalLength = buffers.reduce((sum, buf) => sum + buf.byteLength, 0);
|
|
780
|
-
const concatenated = new Uint8Array(totalLength);
|
|
781
|
-
let offset = 0;
|
|
782
|
-
for (const buffer of buffers) {
|
|
783
|
-
concatenated.set(new Uint8Array(buffer), offset);
|
|
784
|
-
offset += buffer.byteLength;
|
|
785
|
-
}
|
|
786
|
-
return concatenated.buffer;
|
|
787
|
-
}
|
|
788
814
|
var loadZip = async (buf, bookId) => {
|
|
789
815
|
const m = /* @__PURE__ */ new Map();
|
|
790
816
|
const unzipper = new Unzip();
|
|
@@ -824,23 +850,42 @@ var loadZip = async (buf, bookId) => {
|
|
|
824
850
|
await sleep;
|
|
825
851
|
}
|
|
826
852
|
unzipper.push(buf.slice((nChunks - 1) * chunkSize, buf.byteLength), true);
|
|
827
|
-
BOOKS[bookId] =
|
|
828
|
-
|
|
853
|
+
BOOKS[bookId] = {
|
|
854
|
+
map: m,
|
|
855
|
+
manifest: null
|
|
856
|
+
};
|
|
857
|
+
const manifestEntry = m.get("manifest.json");
|
|
858
|
+
const keys = [...m.keys()];
|
|
859
|
+
if (manifestEntry) {
|
|
860
|
+
manifestEntry.decoded = await decryptData(await manifestEntry.zipEntry.getData(), key, iv);
|
|
861
|
+
BOOKS[bookId].manifest = bufToJson(manifestEntry.decoded);
|
|
862
|
+
const { medias } = BOOKS[bookId].manifest;
|
|
863
|
+
keys.push(...Object.keys(medias).map((entryName) => `medias/${entryName}`));
|
|
864
|
+
}
|
|
865
|
+
return keys;
|
|
829
866
|
};
|
|
830
867
|
|
|
831
868
|
// src/worker/offline-worker.ts
|
|
832
869
|
var CHECK_UPDATES = true;
|
|
833
|
-
var
|
|
834
|
-
var
|
|
870
|
+
var STORAGE = "cache";
|
|
871
|
+
var ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
|
|
872
|
+
var CONF_ENV = "development";
|
|
873
|
+
var LOADED_BOOKS = /* @__PURE__ */ new Map();
|
|
835
874
|
var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
836
875
|
const fileName = zipUrl.split("/").slice(-1)[0];
|
|
837
876
|
const readArrayBuffer = async () => {
|
|
838
|
-
if (
|
|
839
|
-
console.debug("try reading native", fileName);
|
|
877
|
+
if (STORAGE === "native") {
|
|
840
878
|
const back = await sender.nativeReadFile({ fname: fileName });
|
|
841
|
-
console.debug("done readnig");
|
|
842
879
|
return back;
|
|
843
880
|
}
|
|
881
|
+
if (STORAGE === "cache") {
|
|
882
|
+
const response = await caches.match(zipUrl);
|
|
883
|
+
if (!response) {
|
|
884
|
+
throw new Error(`missing zip ${zipUrl}`);
|
|
885
|
+
}
|
|
886
|
+
const buf = await response.arrayBuffer();
|
|
887
|
+
return buf;
|
|
888
|
+
}
|
|
844
889
|
const opfsRoot = await navigator.storage.getDirectory();
|
|
845
890
|
const handle = await opfsRoot.getFileHandle(fileName);
|
|
846
891
|
const accessHandle = await handle.createSyncAccessHandle();
|
|
@@ -868,7 +913,7 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
868
913
|
return buf;
|
|
869
914
|
} catch (e) {
|
|
870
915
|
if (e instanceof Error && e.message !== "forceRefresh" && !e.message.includes("refetching")) {
|
|
871
|
-
console.debug(
|
|
916
|
+
console.debug(`${STORAGE} zip not found, creating it`, e, zipUrl);
|
|
872
917
|
}
|
|
873
918
|
const response = await fetch(zipUrl);
|
|
874
919
|
const contentLength = +response.headers.get("Content-Length");
|
|
@@ -879,6 +924,7 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
879
924
|
while (true) {
|
|
880
925
|
const { done, value: chunk } = await reader.read();
|
|
881
926
|
if (done) {
|
|
927
|
+
self.postMessage({ methodName: "opfsProgress", arg: { total: contentLength, bytes: contentLength } });
|
|
882
928
|
break;
|
|
883
929
|
}
|
|
884
930
|
arrayBuffer.set(chunk, position);
|
|
@@ -893,44 +939,69 @@ var fetchZipFile = async (zipUrl, { forceRefresh } = {}) => {
|
|
|
893
939
|
console.debug(error);
|
|
894
940
|
throw new Error(error);
|
|
895
941
|
}
|
|
896
|
-
if (
|
|
942
|
+
if (STORAGE === "native") {
|
|
897
943
|
await sender.nativeWriteFile({ fname: fileName, buffer: arrayBuffer });
|
|
898
|
-
|
|
944
|
+
} else if (STORAGE === "opfs") {
|
|
945
|
+
const opfsRoot = await navigator.storage.getDirectory();
|
|
946
|
+
const handle = await opfsRoot.getFileHandle(fileName, { create: true });
|
|
947
|
+
const accessHandle = await handle.createSyncAccessHandle();
|
|
948
|
+
await accessHandle.truncate(0);
|
|
949
|
+
await accessHandle.write(arrayBuffer);
|
|
950
|
+
await accessHandle.close();
|
|
951
|
+
} else {
|
|
952
|
+
const cache = await openCache(CONF_ENV);
|
|
953
|
+
const response2 = new Response(arrayBuffer, {
|
|
954
|
+
headers: {
|
|
955
|
+
"Cache-Control": "max-age=600, s-maxage=600, public, proxy-revalidate",
|
|
956
|
+
"Content-Type": "application/zip"
|
|
957
|
+
},
|
|
958
|
+
// @ts-ignore: poor ts typing
|
|
959
|
+
ok: true,
|
|
960
|
+
redirected: false,
|
|
961
|
+
status: 200,
|
|
962
|
+
type: "cors",
|
|
963
|
+
url: zipUrl
|
|
964
|
+
});
|
|
965
|
+
await cache.put(zipUrl, response2);
|
|
899
966
|
}
|
|
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
967
|
return await readArrayBuffer();
|
|
907
968
|
}
|
|
908
969
|
};
|
|
909
970
|
var slugToFileMeta2 = async ({ slug }) => {
|
|
910
|
-
console.debug("hereshit");
|
|
911
971
|
const json = await readZipEntry({ bookId: "preload", entryName: "preload/hashmap_slugToMetaPage.json", asJson: true });
|
|
912
|
-
console.debug("hereshit2");
|
|
913
972
|
return slugToFileMeta({ slug, json });
|
|
914
973
|
};
|
|
915
974
|
var PRELOAD_AS_BOOK_ID = "preload";
|
|
916
975
|
var isOldZip = (bookId) => typeof bookId === "number";
|
|
917
976
|
var loadBook2 = async ({ id: bookId, forceRefresh }) => {
|
|
918
977
|
if (LOADED_BOOKS.has(bookId)) {
|
|
978
|
+
await LOADED_BOOKS.get(bookId);
|
|
919
979
|
self.postMessage({ methodName: "opfsProgress", arg: { total: 1, bytes: 1 } });
|
|
920
980
|
return;
|
|
921
981
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
982
|
+
const p = new Promise(
|
|
983
|
+
tryCatch(async () => {
|
|
984
|
+
const buf = await fetchZipFile(`${ZIP_BASE_URL}/${bookId}.zip`, { forceRefresh });
|
|
985
|
+
try {
|
|
986
|
+
const entryNames = await loadZip(new Uint8Array(buf), bookId);
|
|
987
|
+
fetchWorker.postMessage({ methodName: "setKnownLinks", arg: { entryNames, bookId } });
|
|
988
|
+
if (isOldZip(bookId)) {
|
|
989
|
+
const json = await readZipEntry({
|
|
990
|
+
entryName: `preload/${bookId}.json`,
|
|
991
|
+
bookId: PRELOAD_AS_BOOK_ID,
|
|
992
|
+
asJson: true
|
|
993
|
+
});
|
|
994
|
+
loadBook(json);
|
|
995
|
+
}
|
|
996
|
+
return true;
|
|
997
|
+
} catch (e) {
|
|
998
|
+
console.error("failed", e);
|
|
999
|
+
return false;
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
1002
|
+
);
|
|
1003
|
+
LOADED_BOOKS.set(bookId, p);
|
|
1004
|
+
await p;
|
|
934
1005
|
};
|
|
935
1006
|
var loadPageContent2 = async ({
|
|
936
1007
|
bookId: iBookId,
|
|
@@ -973,11 +1044,18 @@ var initChannel = (__, { ports }) => {
|
|
|
973
1044
|
};
|
|
974
1045
|
var loadBooks = async ({ primaryOnly = false } = {}) => {
|
|
975
1046
|
let entries = [];
|
|
976
|
-
if (
|
|
1047
|
+
if (STORAGE === "native") {
|
|
977
1048
|
entries = await sender.nativeListZips();
|
|
978
|
-
} else {
|
|
1049
|
+
} else if (STORAGE === "opfs") {
|
|
979
1050
|
const opfsRoot = await navigator.storage.getDirectory();
|
|
980
1051
|
entries = await opfsRoot.values();
|
|
1052
|
+
} else {
|
|
1053
|
+
const cache = await openCache(CONF_ENV);
|
|
1054
|
+
const keys = await cache.keys();
|
|
1055
|
+
const zips = keys.filter((key2) => {
|
|
1056
|
+
return key2.url.match(/lecture-cp\.zip|story-.*?\.zip/);
|
|
1057
|
+
});
|
|
1058
|
+
entries = zips.map((zip) => ({ name: String(zip.url.split("/").pop()) }));
|
|
981
1059
|
}
|
|
982
1060
|
for await (const entry of entries) {
|
|
983
1061
|
if (/preload/.test(entry.name)) {
|
|
@@ -996,14 +1074,29 @@ var loadBooks = async ({ primaryOnly = false } = {}) => {
|
|
|
996
1074
|
}
|
|
997
1075
|
}
|
|
998
1076
|
};
|
|
999
|
-
var configure = ({
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1077
|
+
var configure = ({
|
|
1078
|
+
storage,
|
|
1079
|
+
checkUpdates,
|
|
1080
|
+
zipBaseUrl,
|
|
1081
|
+
confEnv
|
|
1082
|
+
}) => {
|
|
1083
|
+
CHECK_UPDATES = true;
|
|
1084
|
+
STORAGE = "cache";
|
|
1085
|
+
ZIP_BASE_URL = "https://ci.lls.fr/artefacts/content/development";
|
|
1086
|
+
CONF_ENV = "development";
|
|
1087
|
+
if (typeof storage !== "undefined") {
|
|
1088
|
+
STORAGE = storage;
|
|
1003
1089
|
}
|
|
1004
|
-
if (typeof
|
|
1090
|
+
if (typeof checkUpdates !== "undefined") {
|
|
1005
1091
|
CHECK_UPDATES = !!checkUpdates;
|
|
1006
1092
|
}
|
|
1093
|
+
if (typeof zipBaseUrl !== "undefined") {
|
|
1094
|
+
ZIP_BASE_URL = zipBaseUrl;
|
|
1095
|
+
}
|
|
1096
|
+
if (typeof confEnv !== "undefined") {
|
|
1097
|
+
CONF_ENV = confEnv;
|
|
1098
|
+
}
|
|
1099
|
+
console.info("ow::configure", { STORAGE, CHECK_UPDATES, ZIP_BASE_URL, CONF_ENV });
|
|
1007
1100
|
};
|
|
1008
1101
|
var loadPreload2 = async () => {
|
|
1009
1102
|
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 = {};
|
|
@@ -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
|
});
|
|
@@ -76,11 +87,31 @@ 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.getRegistrations().then((registrations) => {
|
|
98
|
+
return Promise.all(registrations.map((reg) => reg.unregister()));
|
|
99
|
+
});
|
|
100
|
+
await navigator.serviceWorker.register("/fetch-worker.js", { type: "module" }).then((reg) => {
|
|
101
|
+
return reg.update().catch((e) => {
|
|
102
|
+
console.debug(e, "likely we are offline");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
await navigator.serviceWorker.ready.then(async (reg) => {
|
|
106
|
+
console.debug("Service Worker registered:", reg.scope);
|
|
107
|
+
fetchWorker = reg.active;
|
|
108
|
+
}).catch((err) => console.error("Service Worker registration failed:", err));
|
|
109
|
+
if (!fetchWorker) {
|
|
110
|
+
console.debug("failed to get fetch worker");
|
|
111
|
+
}
|
|
112
|
+
return fetchWorker;
|
|
113
|
+
})
|
|
114
|
+
);
|
|
84
115
|
var setWorkersReady;
|
|
85
116
|
var workersReadyPromise = new Promise((resolve) => {
|
|
86
117
|
setWorkersReady = resolve;
|
|
@@ -90,45 +121,42 @@ var setBooksLoadedPromise = new Promise((resolve) => {
|
|
|
90
121
|
setBooksLoaded = resolve;
|
|
91
122
|
});
|
|
92
123
|
var installWorker = async ({
|
|
124
|
+
skipLoadBooks = false,
|
|
93
125
|
primaryOnly = false,
|
|
94
126
|
nativeReadFile,
|
|
95
127
|
nativeWriteFile,
|
|
96
128
|
nativeListZips,
|
|
97
|
-
|
|
129
|
+
storage,
|
|
130
|
+
zipBaseUrl,
|
|
131
|
+
checkUpdates,
|
|
132
|
+
confEnv
|
|
98
133
|
} = {}) => {
|
|
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
|
-
}
|
|
134
|
+
const fetchWorker = await fetchWorkerPromise;
|
|
135
|
+
const configureOptions = { storage, zipBaseUrl, checkUpdates, confEnv };
|
|
114
136
|
if (nativeReadFile) {
|
|
115
137
|
const wwHandler = makeSyncHandler(offlineWorker, { nativeReadFile, nativeWriteFile, nativeListZips });
|
|
116
138
|
offlineWorker.onmessage = combineHandlers(handler, wwHandler);
|
|
117
|
-
|
|
139
|
+
if (storage !== "native") {
|
|
140
|
+
console.warn("nativeReadFile given, ignoring storage", storage);
|
|
141
|
+
}
|
|
142
|
+
offlineWorkerSync.configure({ ...configureOptions, storage: "native" });
|
|
118
143
|
} else {
|
|
119
|
-
offlineWorkerSync.configure({ native: false, checkUpdates });
|
|
120
144
|
offlineWorker.onmessage = handler.handle;
|
|
145
|
+
offlineWorkerSync.configure(configureOptions);
|
|
121
146
|
}
|
|
122
147
|
offlineWorkerSync.on("ready", setWorkersReady);
|
|
123
148
|
const channel = new MessageChannel();
|
|
124
149
|
offlineWorkerSync.initChannel({ ports: [channel.port1] });
|
|
125
150
|
fetchWorker.postMessage({ methodName: "initChannel" }, [channel.port2]);
|
|
151
|
+
fetchWorker.postMessage({ methodName: "configure", arg: { storage, confEnv } });
|
|
126
152
|
offlineWorkerSync.on("opfsProgress", () => {
|
|
127
153
|
});
|
|
128
154
|
if (!primaryOnly) {
|
|
129
155
|
await offlineWorkerSync.loadPreload();
|
|
130
156
|
}
|
|
131
|
-
|
|
157
|
+
if (!skipLoadBooks) {
|
|
158
|
+
await offlineWorkerSync.loadBooks({ primaryOnly });
|
|
159
|
+
}
|
|
132
160
|
setBooksLoaded();
|
|
133
161
|
};
|
|
134
162
|
var useOfflineWorker_default = ({
|
|
@@ -136,17 +164,30 @@ var useOfflineWorker_default = ({
|
|
|
136
164
|
nativeReadFile,
|
|
137
165
|
nativeWriteFile,
|
|
138
166
|
nativeListZips,
|
|
139
|
-
checkUpdates
|
|
167
|
+
checkUpdates,
|
|
168
|
+
storage,
|
|
169
|
+
zipBaseUrl,
|
|
170
|
+
confEnv
|
|
140
171
|
} = {}) => {
|
|
141
172
|
const [ready, setReady] = useState(false);
|
|
142
173
|
useEffect(() => {
|
|
143
174
|
const fn = async () => {
|
|
144
|
-
await installWorker({
|
|
175
|
+
await installWorker({
|
|
176
|
+
primaryOnly,
|
|
177
|
+
nativeReadFile,
|
|
178
|
+
nativeWriteFile,
|
|
179
|
+
nativeListZips,
|
|
180
|
+
storage,
|
|
181
|
+
zipBaseUrl,
|
|
182
|
+
checkUpdates,
|
|
183
|
+
confEnv
|
|
184
|
+
});
|
|
145
185
|
await Promise.all([workersReadyPromise, setBooksLoadedPromise]);
|
|
186
|
+
fetch(window.location.href.replace(window.location.search, ""));
|
|
146
187
|
setReady(true);
|
|
147
188
|
};
|
|
148
189
|
fn();
|
|
149
|
-
}, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, checkUpdates]);
|
|
190
|
+
}, [primaryOnly, nativeReadFile, nativeWriteFile, nativeListZips, storage, zipBaseUrl, checkUpdates, confEnv]);
|
|
150
191
|
const loadBook = useCallback(
|
|
151
192
|
async ({ id, forceRefresh }, cbk) => {
|
|
152
193
|
offlineWorkerSync.on("opfsProgress", cbk);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lls/offline",
|
|
3
|
-
"version": "24.
|
|
3
|
+
"version": "24.23.0-0a61fc97",
|
|
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.
|
|
55
|
+
"@lls/vitescripts": "24.23.0-0a61fc97"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"fflate": "0.8.2"
|