-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathsw-src.js
More file actions
390 lines (341 loc) · 13.1 KB
/
sw-src.js
File metadata and controls
390 lines (341 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/6.2.0/workbox-sw.js"
);
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST, {
ignoreURLParametersMatching: [/^book/, /^cr_user_id/],
exclude: [/^lang\//],
});
const channel = new BroadcastChannel("cr-message-channel");
let version = 1.6;
// let cachingProgress = 0;
// let cachableAssetsCount = 0;
channel.addEventListener("message", async function (event) {
if (event.data.command === "Cache") {
console.log("Caching request received in the service worker with data: ");
console.log(event.data);
cachingProgress = 0;
const data = event.data.data;
// Route to appropriate caching logic based on book type / prefix
if (data && (data.type === "gdl" || (data.bookName && data.bookName.startsWith("gdl-")))) {
cacheGdlBookAssets(data);
} else {
cacheTheBookJSONAndImages(data);
}
}
});
// Precache static assets during service worker installation
self.addEventListener("install", (event) => {
self.skipWaiting();
});
self.addEventListener("activate", function (event) {
console.log("Service worker activated");
event.waitUntil(self.clients.claim());
channel.postMessage({ command: "Activated", data: {} });
return self.clients.claim();
});
self.registration.addEventListener("updatefound", function (e) {
caches.keys().then((cacheNames) => {
cacheNames.forEach((cacheName) => {
if (cacheName == workbox.core.cacheNames.precache) {
channel.postMessage({ command: "UpdateFound", data: {} });
}
});
});
});
// Serve cached assets when offline or falling back to the network
self.addEventListener("fetch", (event) => {
// const requestURL = new URL(event.request.url);
// if (requestURL.protocol === 'chrome-extension:') {
// return;
// }
// if (requestURL.origin === self.location.origin) {
// event.respondWith(
// caches.match(event.request).then((response) => {
// // If the asset is in the static cache, return it
// if (response) {
// return response;
// }
// // If not in the static cache, fetch it from the network
// return fetch(event.request).then((networkResponse) => {
// // Cache a copy of the response in the static cache for future use
// return networkResponse;
// });
// })
// );
// } else {
// For requests to the BookContent folder, use the Book Content cache
event.respondWith(
caches.match(event.request).then(function (response) {
if (response) {
return response;
}
return fetch(event.request);
})
);
// }
});
var cachingInProgress = false;
async function cacheTheBookJSONAndImages(data) {
console.log("Caching the book JSON and images");
let bookData = data["bookData"];
let bookAudioAndImageFiles = [];
for (let i = 0; i < bookData["pages"].length; i++) {
let page = bookData["pages"][i];
for (let j = 0; j < page["visualElements"].length; j++) {
let visualElement = page["visualElements"][j];
if (visualElement["type"] === "audio") {
bookAudioAndImageFiles.push(
`/BookContent/${data["bookData"]["bookName"]}/content/` +
visualElement["audioSrc"]
);
for (
let k = 0;
k < visualElement["audioTimestamps"]["timestamps"].length;
k++
) {
bookAudioAndImageFiles.push(
`/BookContent/${data["bookData"]["bookName"]}/content/` +
visualElement["audioTimestamps"]["timestamps"][k]["audioSrc"]
);
}
} else if (
visualElement["type"] === "image" &&
visualElement["imageSource"] !== "empty_glow_image"
) {
bookAudioAndImageFiles.push(
`/BookContent/${data["bookData"]["bookName"]}/content/` +
visualElement["imageSource"]
);
}
}
}
bookAudioAndImageFiles.push(data["contentFile"]);
console.log("Book audio files: ", bookAudioAndImageFiles);
if (!cachingInProgress) {
cachingInProgress = true;
await cacheBookAssets(bookData, bookAudioAndImageFiles);
cachingInProgress = false;
}
}
/**
* Extract filename from a path and map to local GDL book structure.
* Maps server-absolute paths to local structure: assets/ for images/lottie, audio/ for mp3.
*/
function mapGdlPathToLocal(serverPath, basePath) {
if (!serverPath || typeof serverPath !== "string") return null;
// Extract just the filename (last part after last /)
const filename = serverPath.split("/").pop();
// Validate filename - must have a name before the extension
if (!filename || filename.startsWith(".") || filename.length < 3) return null;
// Determine directory based on file extension
if (/\.(mp3|wav|ogg|m4a)$/i.test(filename)) {
// Audio files go in audio/ directory
return basePath + "audio/" + filename;
} else if (/\.(png|jpe?g|webp|gif|svg|lottie)$/i.test(filename)) {
// Images and lottie files go in assets/ directory
return basePath + "assets/" + filename;
}
return null;
}
/**
* Recursively walk a JSON object and collect asset-like string values.
* Specifically handles GDL content.json structure with paths, mp3, lottie files, etc.
*/
function collectAssetsFromJson(node, assets, basePath) {
if (!node) return;
// Extended regex to include lottie files and other asset types
const assetRegex = /\.(png|jpe?g|webp|gif|svg|mp3|wav|ogg|m4a|lottie)$/i;
// Known fields that contain asset paths in GDL structure
const assetPathFields = ['path', 'mp3', 'url', 'filename', 'image'];
if (typeof node === "string") {
// Check if it's an asset file
if (assetRegex.test(node)) {
// Skip invalid filenames (like .lottie without name)
const filename = node.split("/").pop();
if (!filename || filename.startsWith(".") || filename.length < 3) {
return;
}
// Handle absolute URLs
if (node.startsWith("http://") || node.startsWith("https://")) {
assets.add(node);
}
// Handle absolute paths starting with / - map to local structure
else if (node.startsWith("/")) {
// Map server-absolute paths to local GDL book structure
const localPath = mapGdlPathToLocal(node, basePath);
if (localPath) {
assets.add(localPath);
}
// Don't add fallback - if mapping fails, the path is invalid
}
// Handle relative paths
else {
assets.add(basePath + node);
}
}
return;
}
if (Array.isArray(node)) {
for (const item of node) {
collectAssetsFromJson(item, assets, basePath);
}
return;
}
if (typeof node === "object") {
// Handle special cases for GDL structure first
// Credits logos array - these are filenames that need to be resolved
if (node.logos && Array.isArray(node.logos)) {
for (const logo of node.logos) {
if (typeof logo === "string") {
// Logo filenames might not have extensions, try common image extensions
// Check if logo already has an extension
if (!/\.(png|jpe?g|webp|gif|svg)$/i.test(logo)) {
assets.add(basePath + "assets/" + logo + ".jpg");
} else {
assets.add(basePath + "assets/" + logo);
}
}
}
}
// Check for known asset path fields - prefer 'path' over 'url' if both exist
// Process 'path' first, then other fields, but skip 'url' and 'filename' if 'path' exists
const hasPath = node.path && typeof node.path === "string" && assetRegex.test(node.path);
for (const field of assetPathFields) {
// Skip 'url' and 'filename' if 'path' exists (to avoid duplicates)
// 'path' is the most reliable field in GDL structure
if (hasPath && (field === "url" || field === "filename")) continue;
if (node[field] && typeof node[field] === "string") {
const value = node[field];
if (assetRegex.test(value)) {
if (value.startsWith("http://") || value.startsWith("https://")) {
assets.add(value);
} else if (value.startsWith("/")) {
// Map server-absolute paths to local GDL book structure
const localPath = mapGdlPathToLocal(value, basePath);
if (localPath) {
assets.add(localPath);
}
// Don't add fallback - if mapping fails, the path is invalid
} else {
// Relative paths - prepend basePath
assets.add(basePath + value);
}
}
}
}
// Recursively process all fields EXCEPT the ones we've already processed
for (const key in node) {
if (Object.prototype.hasOwnProperty.call(node, key)) {
// Skip fields we've already processed to avoid duplicates
if (key === "logos" || assetPathFields.includes(key)) {
continue;
}
// Recursively process everything else
collectAssetsFromJson(node[key], assets, basePath);
}
}
}
}
/**
* Build and cache the asset list for a GDL book.
* Expects data: { bookName, gdlId, basePath, contentFile, ... }
*/
async function cacheGdlBookAssets(data) {
console.log("Caching GDL book assets");
const bookName = data.bookName || data.gdlId;
const basePath = data.basePath || "/";
const contentFile = data.contentFile;
const assetsSet = new Set();
// Always cache the core GDL assets
if (contentFile) {
assetsSet.add(contentFile);
}
assetsSet.add(basePath + "my-lib-style.css");
assetsSet.add(basePath + "gdlplayer.umd.js");
// Try to fetch and inspect the GDL content.json for additional assets
if (contentFile) {
try {
const response = await fetch(contentFile);
if (response.ok) {
const json = await response.json();
console.log("Parsing GDL content.json for assets...");
collectAssetsFromJson(json, assetsSet, basePath);
console.log("Found " + assetsSet.size + " total assets after parsing content.json");
} else {
console.log("Failed to fetch GDL content.json", response.status);
}
} catch (error) {
console.log("Error while fetching GDL content.json", error);
}
}
// Filter out invalid paths and deduplicate by normalizing to correct structure
const validAssets = new Set();
for (const asset of assetsSet) {
// Skip invalid paths (like .lottie without filename)
if (asset.endsWith("/.lottie") || asset.endsWith("/.jpg") || asset.endsWith("/.mp3")) {
continue;
}
// Normalize paths - if it's in the wrong location, try to fix it
// Files should be in assets/ or audio/, not directly in basePath
if (asset.startsWith(basePath) && !asset.includes("/assets/") && !asset.includes("/audio/")) {
const filename = asset.split("/").pop();
if (filename) {
// Determine correct directory based on extension
if (/\.(mp3|wav|ogg|m4a)$/i.test(filename)) {
validAssets.add(basePath + "audio/" + filename);
} else if (/\.(png|jpe?g|webp|gif|svg|lottie)$/i.test(filename)) {
validAssets.add(basePath + "assets/" + filename);
} else {
// Keep as-is for other files (like content.json, .css, .js)
validAssets.add(asset);
}
}
} else {
// Already in correct location or is a core file
validAssets.add(asset);
}
}
const assetArray = Array.from(validAssets);
console.log("GDL assets to cache (" + assetArray.length + " total, filtered from " + assetsSet.size + "): ", assetArray);
if (!cachingInProgress) {
cachingInProgress = true;
await cacheBookAssets({ bookName: bookName }, assetArray);
cachingInProgress = false;
}
}
async function cacheBookAssets(bookData, bookAudioAndImageFiles) {
const cache = await caches.open(bookData["bookName"]);
const batchSize = 5; // Process in batches of 5
let cachingProgress = 0;
for (let i = 0; i < bookAudioAndImageFiles.length; i += batchSize) {
const batch = bookAudioAndImageFiles.slice(i, i + batchSize);
try {
await Promise.all(batch.map(file => cache.add(file)));
} catch (error) {
// Best-effort caching: some assets referenced in content.json might not exist locally.
// Try each file individually and silently skip ones that fail, so caching can continue.
for (const file of batch) {
try {
await cache.add(file);
} catch (fileError) {
// Optional: log at a low level for debugging, but don't treat as a hard error.
// console.log("Skipping missing or unreachable asset:", file);
}
}
}
// Whether or not all files in the batch were cached successfully, count the batch as processed
// so that progress can eventually reach 100% even if some assets are missing.
cachingProgress += batch.length;
// Send progress update after each batch
const progress = Math.round((cachingProgress / bookAudioAndImageFiles.length) * 100);
const clients = await self.clients.matchAll();
if (clients.length > 0) {
await channel.postMessage({
command: "CachingProgress",
data: { progress, bookName: bookData["bookName"] },
});
}
// Introduce a small delay between batches
await new Promise((resolve) => setTimeout(resolve, 100));
}
}