textbrowser 0.47.0-beta.2 → 0.48.0

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/sw-sample.js CHANGED
@@ -1,310 +1,8 @@
1
- /* eslint-env browser, serviceworker -- Service worker */
1
+ /* eslint-env serviceworker -- Service worker */
2
2
 
3
- import {getJSON} from './node_modules/simple-get-json/dist/index-es.js';
4
- import activateCallback from './node_modules/textbrowser/dist/activateCallback-es.js';
5
- import {getWorkFiles} from './node_modules/textbrowser/dist/WorkInfo-es.js';
3
+ import swHelper from './node_modules/textbrowser/dist/sw-helper.js';
6
4
 
7
- const CURRENT_CACHES = {
8
- prefetch: 'prefetch-cache-v'
9
- };
10
- const minutes = 60 * 1000;
5
+ // IMPORTANT: Keep this comment and increment this number to trigger
6
+ // a worker change: 1
11
7
 
12
- // Changeable text to trigger worker change: 1
13
-
14
- // Utilities
15
-
16
- /**
17
- *
18
- * @param {PlainObject} args
19
- * @param {"log"|"error"|"beginInstall"|"finishedInstall"|"beginActivate"|"finishedActivate"} args.type
20
- * @param {string} [args.message=type]
21
- * @returns {Promise<void>}
22
- */
23
- async function post ({type, message = type}) {
24
- const clients = await self.clients.matchAll({
25
- // Are there any uncontrolled within activate anyways?
26
- includeUncontrolled: true,
27
- type: 'window'
28
- }) || [];
29
- if (message.includes('Posting finished')) {
30
- message += ` (count: ${clients.length})`;
31
- }
32
- clients.forEach((client) => {
33
- // Although we only need one client to which to send
34
- // arguments, we want to signal phase complete to all
35
- // eslint-disable-next-line unicorn/require-post-message-target-origin -- In worker
36
- client.postMessage({message, type});
37
- });
38
- }
39
-
40
- /**
41
- *
42
- * @param {string[]} messages
43
- * @returns {Promise<void>}
44
- */
45
- function log (...messages) {
46
- const message = messages.join(' ');
47
- console.log(message);
48
- return post({message, type: 'log'});
49
- }
50
-
51
- /**
52
- *
53
- * @param {Error} error
54
- * @param {string[]} messages
55
- * @returns {Promise<void>}
56
- */
57
- function logError (error, ...messages) {
58
- const message = messages.join(' ');
59
- console.error(error, message);
60
- return post({message, errorType: error.type, name: error.name, type: 'error'});
61
- }
62
-
63
- /**
64
- * @callback DelayCallback
65
- * @param {Float} time
66
- * @returns {void}
67
- */
68
-
69
- /**
70
- *
71
- * @param {DelayCallback} cb
72
- * @param {PositiveInteger} timeout
73
- * @param {string} errMessage
74
- * @param {PositiveInteger} [time=0]
75
- * @returns {Promise<void>}
76
- */
77
- async function tryAndRetry (cb, timeout, errMessage, time = 0) {
78
- time++;
79
- try {
80
- // eslint-disable-next-line n/callback-return
81
- await cb(time);
82
- return undefined;
83
- } catch (err) {
84
- console.log('errrr', err);
85
- logError(err, err.message || errMessage);
86
- return new Promise((resolve, reject) => {
87
- setTimeout(() => {
88
- resolve(tryAndRetry(cb, timeout, errMessage, time));
89
- }, timeout);
90
- });
91
- }
92
- }
93
-
94
- /**
95
- * @typedef {PlainObject} ConfigObject
96
- * @property {string} namespace
97
- * @property {string} basePath
98
- * @property {string} languages
99
- * @property {string} files
100
- * @property {string[]} userStaticFiles
101
- */
102
-
103
- /**
104
- *
105
- * @param {ConfigObject} args
106
- * @returns {ConfigObject}
107
- * @todo Since some of these reused, move to external file (or
108
- * use `setServiceWorkerDefaults`?)
109
- */
110
- function getConfigDefaults (args) {
111
- return {
112
- namespace: 'textbrowser',
113
- basePath: '',
114
- languages: new URL(
115
- '../appdata/languages.json',
116
- // Todo: Substitute with `import.meta.url` once implemented in
117
- // service workers
118
- new URL('node_modules/textbrowser/resources/index.js', location)
119
- ).href,
120
- files: 'files.json',
121
- userStaticFiles: defaultUserStaticFiles,
122
- // Opportunity to override
123
- ...args
124
- };
125
- }
126
-
127
- const defaultUserStaticFiles = [
128
- '/', // Needs a separate entry from `index.html` (at least in Chrome)
129
- 'index.html',
130
- 'files.json',
131
- 'site.json',
132
- 'resources/user.js'
133
- // We do not put the user.json here as that is obtained live with this
134
- // service worker via `pathToUserJSON`
135
- ];
136
- // Todo: We could supply `new URL(fileName, moduleURL).href` to
137
- // get these as reliable full paths without hard-coding or needing to
138
- // actually be in `node_modules/textbrowser`; see `resources/index.js`
139
- const textbrowserStaticResourceFiles = [
140
- 'node_modules/textbrowser/appdata/languages.json',
141
-
142
- /*
143
- // Only needed atm for browser validation
144
- 'node_modules/textbrowser/general-schemas/files.jsonschema',
145
- 'node_modules/textbrowser/general-schemas/languages.jsonschema',
146
- 'node_modules/textbrowser-data-schemas/schemas/locale.jsonschema',
147
- 'node_modules/textbrowser-data-schemas/schemas/metadata.jsonschema',
148
- 'node_modules/textbrowser-data-schemas/schemas/table.jsonschema', // Not currently using for validation or meta-data
149
- */
150
-
151
- 'node_modules/textbrowser/dist/index-es.js'
152
- ];
153
-
154
- const params = new URL(location).searchParams;
155
- const pathToUserJSON = params.get('pathToUserJSON');
156
- const stylesheets = JSON.parse(params.get('stylesheets') || []);
157
-
158
- console.log('sw info', pathToUserJSON);
159
- console.log('sw stylesheets', stylesheets);
160
-
161
- /**
162
- *
163
- * @param {PositiveInteger} time
164
- * @throws {Error} (This is actually caught internally.)
165
- * @returns {Promise<void>}
166
- */
167
- async function install (time) {
168
- post({type: 'beginInstall'});
169
- log(`Install: Trying, attempt ${time}`);
170
- const now = Date.now();
171
- const response = await fetch(pathToUserJSON);
172
- const json = await response.json();
173
-
174
- const {
175
- namespace, languages, files, userStaticFiles
176
- } = getConfigDefaults(json);
177
-
178
- const {version} = await getJSON('./package.json');
179
-
180
- console.log('opening cache', namespace + CURRENT_CACHES.prefetch + version);
181
- const [
182
- cache,
183
- userDataFiles,
184
- {languages: langs}
185
- ] = await Promise.all([
186
- caches.open(namespace + CURRENT_CACHES.prefetch + version),
187
- getWorkFiles(files),
188
- getJSON(languages)
189
- ]);
190
- log('Install: Retrieved dependency values');
191
-
192
- const langPathParts = languages.split('/');
193
-
194
- // Todo: Ought to make these locales only conditionally required and
195
- // then only show those specified in languages menu or go directly
196
- // to work selection
197
- // Todo: We might give option to only download
198
- // one locale and avoid language splash page
199
- const localeFiles = langs.map(
200
- ({locale: {$ref}}) => {
201
- return (langPathParts.length > 1
202
- ? langPathParts.slice(0, -1).join('/') + '/'
203
- : ''
204
- ) + $ref;
205
- }
206
- );
207
-
208
- const urlsToPrefetch = [
209
- ...textbrowserStaticResourceFiles,
210
- ...localeFiles,
211
- ...userStaticFiles,
212
- ...userDataFiles,
213
- ...stylesheets
214
- ];
215
- // .map((url) => url === 'index.html' ? new Request(url, {cache: 'reload'}) : url)
216
- try {
217
- const cachePromises = urlsToPrefetch.map(async (urlToPrefetch) => {
218
- // This constructs a new URL object using the service worker's script location as the base
219
- // for relative URLs.
220
- const url = new URL(urlToPrefetch, location.href);
221
- url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
222
- const request = new Request(url, {mode: 'no-cors'});
223
- try {
224
- const response = await fetch(request);
225
- if (response.status >= 400) {
226
- throw new Error('request for ' + urlToPrefetch +
227
- ' failed with status ' + response.statusText);
228
- }
229
- return cache.put(urlToPrefetch, response);
230
- } catch (error) {
231
- logError(error, 'Not caching ' + urlToPrefetch + ' due to ' + error);
232
- throw error;
233
- }
234
- });
235
- await Promise.all(cachePromises);
236
- log('Install: Pre-fetching complete.');
237
- } catch (error) {
238
- logError(error, `Install: Pre-fetching failed: ${error}`);
239
- // Failing gives chance for a new client to re-trigger install?
240
- throw error;
241
- }
242
-
243
- // An install update event will not be reported until controlled,
244
- // so we need to inform the clients
245
- log(`Install: Posting finished message to clients`);
246
-
247
- // Although we only need one client to which to send
248
- // arguments, we want to signal phase complete to all
249
- post({type: 'finishedInstall'});
250
- }
251
-
252
- /**
253
- *
254
- * @param {PositiveInteger} time
255
- * @returns {Promise<void>}
256
- */
257
- async function activate (time) {
258
- post({type: 'beginActivate'});
259
- log(`Activate: Trying, attempt ${time}`);
260
- const [json, cacheNames] = await Promise.all([
261
- (await fetch(pathToUserJSON)).json(),
262
- caches.keys()
263
- ]);
264
- const {namespace, files, basePath} = getConfigDefaults(json);
265
- const {version} = await getJSON('./package.json');
266
-
267
- const expectedCacheNames = Object.values(CURRENT_CACHES).map((n) => namespace + n + version);
268
- cacheNames.map(async (cacheName) => {
269
- if (!expectedCacheNames.includes(cacheName)) {
270
- log('Activate: Deleting out of date cache:', cacheName);
271
- await caches.delete(cacheName);
272
- }
273
- });
274
-
275
- await activateCallback({namespace, files, basePath, log});
276
- log('Activate: Database changes completed');
277
-
278
- log(`Activate: Posting finished message to clients`);
279
- // Signal phase complete to all clients
280
- post({type: 'finishedActivate'});
281
- }
282
-
283
- self.addEventListener('install', (e) => {
284
- e.waitUntil(tryAndRetry(install, 5 * minutes, 'Error installing'));
285
- });
286
-
287
- self.addEventListener('activate', (e) => {
288
- // Erring is of no present use here:
289
- // https://github.com/w3c/ServiceWorker/issues/659#issuecomment-384919053
290
- e.waitUntil(tryAndRetry(activate, 5 * minutes, 'Error activating'));
291
- });
292
-
293
- // We cannot make this async as `e.respondWith` must be called synchronously
294
- self.addEventListener('fetch', (e) => {
295
- // DevTools opening will trigger these o-i-c requests
296
- const {request} = e;
297
- const {cache, mode, url} = request;
298
- if (cache === 'only-if-cached' &&
299
- mode !== 'same-origin') {
300
- return;
301
- }
302
- console.log('fetching', url);
303
- e.respondWith((async () => {
304
- const cached = await caches.match(request);
305
- if (!cached) {
306
- console.log('no cached found', url);
307
- }
308
- return cached || fetch(request);
309
- })());
310
- });
8
+ swHelper(self);