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.
@@ -0,0 +1,310 @@
1
+ import activateCallback from './activateCallback-es.js';
2
+ import {getWorkFiles} from './WorkInfo-es.js';
3
+
4
+ // VERSION 0.1.0
5
+
6
+ const CURRENT_CACHES = {
7
+ prefetch: 'prefetch-cache-v'
8
+ };
9
+ const minutes = 60 * 1000;
10
+
11
+ // UTILITIES
12
+
13
+ async function getJSON (url) {
14
+ const resp = await fetch(url);
15
+ return await resp.json();
16
+ }
17
+
18
+ function swHelper (self) {
19
+ // These could be made configurable by argument too
20
+ const defaultUserStaticFiles = [
21
+ '/', // Needs a separate entry from `index.html` (at least in Chrome)
22
+ 'index.html',
23
+ 'files.json',
24
+ 'site.json',
25
+ 'resources/user.js'
26
+ // We do not put the user.json here as that is obtained live with this
27
+ // service worker via `pathToUserJSON`
28
+ ];
29
+ // Todo: We could supply `new URL(fileName, moduleURL).href` to
30
+ // get these as reliable full paths without hard-coding or needing to
31
+ // actually be in `node_modules/textbrowser`; see `resources/index.js`
32
+ const textbrowserStaticResourceFiles = [
33
+ 'node_modules/textbrowser/appdata/languages.json',
34
+
35
+ /*
36
+ // Only needed atm for browser validation
37
+ 'node_modules/textbrowser/general-schemas/files.jsonschema',
38
+ 'node_modules/textbrowser/general-schemas/languages.jsonschema',
39
+ 'node_modules/textbrowser-data-schemas/schemas/locale.jsonschema',
40
+ 'node_modules/textbrowser-data-schemas/schemas/metadata.jsonschema',
41
+ 'node_modules/textbrowser-data-schemas/schemas/table.jsonschema', // Not currently using for validation or meta-data
42
+ */
43
+
44
+ 'node_modules/textbrowser/dist/index-es.js'
45
+ ];
46
+
47
+ /**
48
+ * @typedef {PlainObject} ConfigObject
49
+ * @property {string} namespace
50
+ * @property {string} basePath
51
+ * @property {string} languages
52
+ * @property {string} files
53
+ * @property {string[]} userStaticFiles
54
+ */
55
+
56
+ /**
57
+ *
58
+ * @param {ConfigObject} args
59
+ * @returns {ConfigObject}
60
+ * @todo Since some of these reused, move to external file (or
61
+ * use `setServiceWorkerDefaults`?)
62
+ */
63
+ function getConfigDefaults (args) {
64
+ return {
65
+ namespace: 'textbrowser',
66
+ basePath: '',
67
+ languages: new URL('../appdata/languages.json', import.meta.url).href,
68
+ files: 'files.json',
69
+ userStaticFiles: defaultUserStaticFiles,
70
+ // Opportunity to override
71
+ ...args
72
+ };
73
+ }
74
+
75
+ /**
76
+ *
77
+ * @param {string[]} messages
78
+ * @returns {Promise<void>}
79
+ */
80
+ function log (...messages) {
81
+ const message = messages.join(' ');
82
+ console.log(message);
83
+ return post({message, type: 'log'});
84
+ }
85
+
86
+ /**
87
+ *
88
+ * @param {Error} error
89
+ * @param {string[]} messages
90
+ * @returns {Promise<void>} Resolves to `undefined`
91
+ */
92
+ function logError (error, ...messages) {
93
+ const message = messages.join(' ');
94
+ console.error(error, message);
95
+ return post({message, errorType: error.type, name: error.name, type: 'error'});
96
+ }
97
+
98
+ /**
99
+ * @callback DelayCallback
100
+ * @param {Float} time
101
+ * @returns {void}
102
+ */
103
+
104
+ /**
105
+ *
106
+ * @param {DelayCallback} cb
107
+ * @param {PositiveInteger} timeout
108
+ * @param {string} errMessage
109
+ * @param {PositiveInteger} [time=0]
110
+ * @returns {Promise<void>}
111
+ */
112
+ async function tryAndRetry (cb, timeout, errMessage, time = 0) {
113
+ time++;
114
+ try {
115
+ await cb(time); // eslint-disable-line n/callback-return -- Needed for retries
116
+ return undefined;
117
+ } catch (err) {
118
+ console.log('errrr', err);
119
+ logError(err, err.message || errMessage);
120
+ return new Promise((resolve, reject) => {
121
+ setTimeout(() => {
122
+ resolve(tryAndRetry(cb, timeout, errMessage, time));
123
+ }, timeout);
124
+ });
125
+ }
126
+ }
127
+
128
+ /**
129
+ *
130
+ * @param {PlainObject} args
131
+ * @param {"log"|"error"|"beginInstall"|"finishedInstall"|"beginActivate"|"finishedActivate"} args.type
132
+ * @param {string} [args.message=type]
133
+ * @returns {Promise<void>}
134
+ */
135
+ async function post ({type, message = type}) {
136
+ const clients = await self.clients.matchAll({
137
+ // Are there any uncontrolled within activate anyways?
138
+ includeUncontrolled: true,
139
+ type: 'window'
140
+ }) || [];
141
+ if (message.includes('Posting finished')) {
142
+ message += ` (count: ${clients.length})`;
143
+ }
144
+ clients.forEach((client) => {
145
+ // Although we only need one client to which to send
146
+ // arguments, we want to signal phase complete to all
147
+ // eslint-disable-next-line unicorn/require-post-message-target-origin -- In worker
148
+ client.postMessage({message, type});
149
+ });
150
+ }
151
+
152
+ /**
153
+ *
154
+ * @param {PositiveInteger} time
155
+ * @throws {Error}
156
+ * @returns {Promise<void>}
157
+ */
158
+ async function install (time) {
159
+ post({type: 'beginInstall'});
160
+ log(`Install: Trying, attempt ${time}`);
161
+ const now = Date.now();
162
+ const json = await getJSON(pathToUserJSON);
163
+
164
+ const {
165
+ namespace, languages, files, userStaticFiles
166
+ } = getConfigDefaults(json);
167
+
168
+ const {version} = await getJSON('./package.json');
169
+
170
+ console.log('opening cache', namespace + CURRENT_CACHES.prefetch + version);
171
+ const [
172
+ cache,
173
+ userDataFiles,
174
+ {languages: langs}
175
+ ] = await Promise.all([
176
+ caches.open(namespace + CURRENT_CACHES.prefetch + version),
177
+ getWorkFiles(files),
178
+ getJSON(languages)
179
+ ]);
180
+ log('Install: Retrieved dependency values');
181
+
182
+ const langPathParts = languages.split('/');
183
+
184
+ // Todo: Ought to make these locales only conditionally required and
185
+ // then only show those specified in languages menu or go directly
186
+ // to work selection
187
+ // Todo: We might give option to only download
188
+ // one locale and avoid language splash page
189
+ const localeFiles = langs.map(
190
+ ({locale: {$ref}}) => {
191
+ return (langPathParts.length > 1
192
+ ? langPathParts.slice(0, -1).join('/') + '/'
193
+ : ''
194
+ ) + $ref;
195
+ }
196
+ );
197
+
198
+ const urlsToPrefetch = [
199
+ ...textbrowserStaticResourceFiles,
200
+ ...localeFiles,
201
+ ...userStaticFiles,
202
+ ...userDataFiles,
203
+ ...stylesheets
204
+ ];
205
+ // .map((url) => url === 'index.html' ? new Request(url, {cache: 'reload'}) : url)
206
+ try {
207
+ const cachePromises = urlsToPrefetch.map(async (urlToPrefetch) => {
208
+ // This constructs a new URL object using the service worker's script location as the base
209
+ // for relative URLs.
210
+ const url = new URL(urlToPrefetch, location.href);
211
+ url.search += (url.search ? '&' : '?') + 'cache-bust=' + now;
212
+ const request = new Request(url, {mode: 'no-cors'});
213
+ try {
214
+ const response = await fetch(request);
215
+ if (response.status >= 400) {
216
+ throw new Error('request for ' + urlToPrefetch +
217
+ ' failed with status ' + response.statusText);
218
+ }
219
+ return cache.put(urlToPrefetch, response);
220
+ } catch (error) {
221
+ logError(error, 'Not caching ' + urlToPrefetch + ' due to ' + error);
222
+ throw error;
223
+ }
224
+ });
225
+ await Promise.all(cachePromises);
226
+ log('Install: Pre-fetching complete.');
227
+ } catch (error) {
228
+ logError(error, `Install: Pre-fetching failed: ${error}`);
229
+ // Failing gives chance for a new client to re-trigger install?
230
+ throw error;
231
+ }
232
+
233
+ // An install update event will not be reported until controlled,
234
+ // so we need to inform the clients
235
+ log(`Install: Posting finished message to clients`);
236
+
237
+ // Although we only need one client to which to send
238
+ // arguments, we want to signal phase complete to all
239
+ post({type: 'finishedInstall'});
240
+ }
241
+
242
+ /**
243
+ *
244
+ * @param {PositiveInteger} time
245
+ * @returns {Promise<void>}
246
+ */
247
+ async function activate (time) {
248
+ post({type: 'beginActivate'});
249
+ log(`Activate: Trying, attempt ${time}`);
250
+ const [json, cacheNames] = await Promise.all([
251
+ getJSON(pathToUserJSON),
252
+ caches.keys()
253
+ ]);
254
+ const {namespace, files, basePath} = getConfigDefaults(json);
255
+ const {version} = await getJSON('./package.json');
256
+
257
+ const expectedCacheNames = Object.values(CURRENT_CACHES).map((n) => namespace + n + version);
258
+ cacheNames.map(async (cacheName) => {
259
+ if (!expectedCacheNames.includes(cacheName)) {
260
+ log('Activate: Deleting out of date cache:', cacheName);
261
+ await caches.delete(cacheName);
262
+ }
263
+ });
264
+
265
+ await activateCallback({namespace, files, basePath, log});
266
+ log('Activate: Database changes completed');
267
+
268
+ log(`Activate: Posting finished message to clients`);
269
+ // Signal phase complete to all clients
270
+ post({type: 'finishedActivate'});
271
+ }
272
+
273
+ const params = new URL(location).searchParams;
274
+ const pathToUserJSON = params.get('pathToUserJSON');
275
+ const stylesheets = JSON.parse(params.get('stylesheets') || []);
276
+
277
+ console.log('sw info', pathToUserJSON);
278
+ console.log('sw stylesheets', stylesheets);
279
+
280
+ self.addEventListener('install', (e) => {
281
+ e.waitUntil(tryAndRetry(install, 5 * minutes, 'Error installing'));
282
+ });
283
+
284
+ self.addEventListener('activate', (e) => {
285
+ // Erring is of no present use here:
286
+ // https://github.com/w3c/ServiceWorker/issues/659#issuecomment-384919053
287
+ e.waitUntil(tryAndRetry(activate, 5 * minutes, 'Error activating'));
288
+ });
289
+
290
+ // We cannot make this async as `e.respondWith` must be called synchronously
291
+ self.addEventListener('fetch', (e) => {
292
+ // DevTools opening will trigger these o-i-c requests
293
+ const {request} = e;
294
+ const {cache, mode, url} = request;
295
+ if (cache === 'only-if-cached' &&
296
+ mode !== 'same-origin') {
297
+ return;
298
+ }
299
+ console.log('fetching', url);
300
+ e.respondWith((async () => {
301
+ const cached = await caches.match(request);
302
+ if (!cached) {
303
+ console.log('no cached found', url);
304
+ }
305
+ return cached || fetch(request);
306
+ })());
307
+ });
308
+ }
309
+
310
+ export default swHelper;
package/files.json CHANGED
@@ -237,8 +237,8 @@
237
237
  "id": "writings",
238
238
  "name": {"localeKey": "writings"},
239
239
  "directions": {"localeKey": "choosewritings_bahai_writingsdb"},
240
- "schemaBaseDirectory": "data/writings/schemas",
241
- "metadataBaseDirectory": "data/writings/metadata",
240
+ "schemaBaseDirectory": "node_modules/bahaiwritings/data/writings/schemas",
241
+ "metadataBaseDirectory": "node_modules/bahaiwritings/data/writings/metadata",
242
242
  "files": [
243
243
  {"name": "aqdas", "file": {"$ref": "node_modules/bahaiwritings/data/writings/aqdas.json"}, "schemaFile": "aqdas.jsonschema", "metadataFile": "aqdas.metadata"},
244
244
  {"name": "bible", "file": {"$ref": "node_modules/bahaiwritings/data/writings/Bible.json"}, "schemaFile": "Bible.jsonschema", "metadataFile": "Bible.metadata"},
@@ -256,8 +256,8 @@
256
256
  "id": "other-writings",
257
257
  "name": {"localeKey": "other-writings"},
258
258
  "directions": {"localeKey": "choosewritings_bahai_writingsotherdb"},
259
- "schemaBaseDirectory": "data/other-works/schemas",
260
- "metadataBaseDirectory": "data/other-works/metadata",
259
+ "schemaBaseDirectory": "node_modules/bahaiwritings/data/other-works/schemas",
260
+ "metadataBaseDirectory": "node_modules/bahaiwritings/data/other-works/metadata",
261
261
  "files": [
262
262
  {"name": "lights", "file": {"$ref": "node_modules/bahaiwritings/data/other-works/lights.json"}, "schemaFile": "lights.jsonschema", "metadataFile": "lights.metadata"}
263
263
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "textbrowser",
3
- "version": "0.47.0-beta.2",
3
+ "version": "0.48.0",
4
4
  "description": "Multilinear text browser",
5
5
  "type": "module",
6
6
  "main": "dist/index-es.min.js",
@@ -9,18 +9,6 @@
9
9
  "bin": {
10
10
  "textbrowser": "./server/main.js"
11
11
  },
12
- "scripts": {
13
- "server": "./server/main.js --allowPlugins --namespace=test",
14
- "start": "static -p 8081",
15
- "rollup": "rollup -c",
16
- "lint": "npm run eslint",
17
- "eslint": "eslint --ext=js,md,html .",
18
- "mocha": "mocha --require test/bootstrap/node.js --require chai/register-assert.js test/textbrowserTests.js",
19
- "node": "npm run eslint && npm run rollup && npm run mocha",
20
- "open-test": "open-cli http://127.0.0.1:8081/test/",
21
- "start-open-test": "run-p start open-test",
22
- "test": "npm run eslint && npm run rollup && npm run start-open-test"
23
- },
24
12
  "browserslist": [
25
13
  "cover 100%"
26
14
  ],
@@ -69,6 +57,7 @@
69
57
  "@rollup/plugin-node-resolve": "^15.0.1",
70
58
  "@rollup/plugin-terser": "^0.1.0",
71
59
  "@stadtlandnetz/rollup-plugin-postprocess": "^1.1.0",
60
+ "@web/rollup-plugin-import-meta-assets": "^1.0.7",
72
61
  "ajv": "8.11.2",
73
62
  "babel-plugin-dynamic-import-node": "^2.3.3",
74
63
  "chai": "^4.3.7",
@@ -96,5 +85,17 @@
96
85
  "rollup": "^3.4.0",
97
86
  "rollup-plugin-re": "^1.0.7",
98
87
  "textbrowser-data-schemas": "^0.2.0"
88
+ },
89
+ "scripts": {
90
+ "server": "./server/main.js --allowPlugins --namespace=test",
91
+ "start": "static -p 8081",
92
+ "rollup": "rollup -c",
93
+ "lint": "npm run eslint",
94
+ "eslint": "eslint --ext=js,md,html .",
95
+ "mocha": "mocha --require test/bootstrap/node.js --require chai/register-assert.js test/textbrowserTests.js",
96
+ "node": "npm run eslint && npm run rollup && npm run mocha",
97
+ "open-test": "open-cli http://127.0.0.1:8081/test/",
98
+ "start-open-test": "run-p start open-test",
99
+ "test": "npm run eslint && npm run rollup && npm run start-open-test"
99
100
  }
100
- }
101
+ }
@@ -1,3 +1,7 @@
1
+ /**
2
+ * @file Note that this should be kept as a polyglot client-server file
3
+ * (besides the server, it is to be invoked by the project service worker).
4
+ */
1
5
  /* eslint-env worker */
2
6
 
3
7
  const {ceil} = Math;
@@ -8,7 +8,7 @@ import {serialize as formSerialize} from 'form-serialization';
8
8
  import getLocaleFallbackResults from './utils/getLocaleFallbackResults.js';
9
9
  import {dialogs} from './utils/dialogs.js';
10
10
  import {getFieldNameAndValueAliases, getBrowseFieldData} from './utils/Metadata.js';
11
- import {getWorkFiles, getWorkData} from './utils/WorkInfo.js';
11
+ import {getWorkData} from './utils/WorkInfo.js';
12
12
  import {
13
13
  respondToState, listenForWorkerUpdate, registerServiceWorker, setServiceWorkerDefaults
14
14
  } from './utils/ServiceWorker.js';
@@ -165,10 +165,6 @@ async function requestPermissions (langs, l) {
165
165
 
166
166
  class TextBrowser {
167
167
  constructor (options) {
168
- // Todo: Replace the `languages` default with `import.meta.url`
169
- // (`new URL('../appdata/languages.json', import.meta.url).href`?)
170
- // https://github.com/tc39/proposal-import-meta
171
- const moduleURL = new URL('node_modules/textbrowser/resources/index.js', location);
172
168
  this.site = options.site || 'site.json';
173
169
  const stylesheets = options.stylesheets || ['@builtin'];
174
170
  const builtinIndex = stylesheets.indexOf('@builtin');
@@ -176,7 +172,7 @@ class TextBrowser {
176
172
  stylesheets.splice(
177
173
  builtinIndex,
178
174
  1,
179
- new URL('index.css', moduleURL).href
175
+ new URL('index.css', import.meta.url).href
180
176
  );
181
177
  }
182
178
  this.stylesheets = stylesheets;
@@ -203,10 +199,7 @@ class TextBrowser {
203
199
 
204
200
  async init () {
205
201
  this._stylesheetElements = await loadStylesheets(this.stylesheets);
206
- return this.displayLanguages();
207
- }
208
202
 
209
- async displayLanguages () {
210
203
  // We use getJSON instead of JsonRefs as we do not need to resolve the locales here
211
204
  try {
212
205
  const [langData, siteData] = await getJSON([this.languages, this.site]);
@@ -587,6 +580,5 @@ class TextBrowser {
587
580
  // Todo: Definable as public fields?
588
581
  TextBrowser.prototype.workDisplay = workDisplay;
589
582
  TextBrowser.prototype.resultsDisplayClient = resultsDisplayClient;
590
- TextBrowser.prototype.getWorkFiles = getWorkFiles;
591
583
 
592
584
  export default TextBrowser;
@@ -10,7 +10,6 @@ import {
10
10
  } from './utils/Metadata.js';
11
11
  import {Languages} from './utils/Languages.js';
12
12
  import {getWorkData} from './utils/WorkInfo.js';
13
- // Keep this as the last import for Rollup
14
13
 
15
14
  const {getLangDir} = rtlDetect;
16
15
  const fieldValueAliasRegex = /^.* \((.*?)\)$/;
@@ -117,6 +116,11 @@ export const resultsDisplayClient = async function resultsDisplayClient (args) {
117
116
  });
118
117
  };
119
118
 
119
+ /**
120
+ * This is server-only code, but kept here as the function is similar.
121
+ * @param {{serverOutput: "jamilih"|"html"|"json"}} args
122
+ * @returns {Promise<Array|string>}
123
+ */
120
124
  export const resultsDisplayServer = async function resultsDisplayServer (args) {
121
125
  const {
122
126
  templateArgs
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @file Note that this should be kept as a polyglot client-server file.
3
+ */
1
4
  /**
2
5
  *
3
6
  * @param {string} param
@@ -20,6 +20,9 @@ export const getPreferredLanguages = ({namespace, preferredLocale}) => {
20
20
  return langArr;
21
21
  };
22
22
 
23
+ /**
24
+ * @classdesc Note that this should be kept as a polyglot client-server class.
25
+ */
23
26
  export class Languages {
24
27
  constructor ({langData}) {
25
28
  this.langData = langData;
@@ -2,12 +2,23 @@
2
2
  // import {escapeHTML} from './sanitize.js';
3
3
  import {dialogs} from './dialogs.js';
4
4
 
5
+ /**
6
+ * Note that this function be kept as a polyglot client-server file.
7
+ * @param {PlainObject} target
8
+ * @param {PlainObject} source
9
+ * @returns {{
10
+ * userJSON: string,
11
+ * languages: string,
12
+ * serviceWorkerPath: string,
13
+ * files: string,
14
+ * namespace: string
15
+ * }}
16
+ */
5
17
  export const setServiceWorkerDefaults = (target, source) => {
6
18
  target.userJSON = source.userJSON || 'resources/user.json';
7
19
  target.languages = source.languages || new URL(
8
- '../appdata/languages.json',
9
- // Todo: Substitute with `import.meta.url`
10
- new URL('node_modules/textbrowser/resources/index.js', location)
20
+ '../../appdata/languages.json',
21
+ import.meta.url
11
22
  ).href;
12
23
  target.serviceWorkerPath = source.serviceWorkerPath ||
13
24
  `sw.js?pathToUserJSON=${
@@ -3,7 +3,12 @@ import {i18n} from 'intl-dom';
3
3
  import {getMetaProp, getMetadata, Metadata} from './Metadata.js';
4
4
  import {PluginsForWork, escapePlugin} from './Plugin.js';
5
5
 
6
- export const getWorkFiles = async function getWorkFiles (files = this.files) {
6
+ /**
7
+ * Imported by the `dist/sw-helper.js`
8
+ * @param {string} files The files.json file path
9
+ * @returns {PlainObject}
10
+ */
11
+ export const getWorkFiles = async function getWorkFiles (files) {
7
12
  const filesObj = await getJSON(files);
8
13
  const dataFiles = [];
9
14
  filesObj.groups.forEach((fileGroup) => {
@@ -1,3 +1,6 @@
1
+ /**
2
+ * @file Note that this should be kept as a polyglot client-server file.
3
+ */
1
4
  import {i18n} from 'intl-dom';
2
5
 
3
6
  export default async function getLocaleFallbackResults ({
package/server/main.js CHANGED
@@ -15,7 +15,6 @@ import IntlURLSearchParams from '../resources/utils/IntlURLSearchParams.js';
15
15
  import {resultsDisplayServer} from '../resources/resultsDisplay.js';
16
16
  import getLocaleFallbackResults from '../resources/utils/getLocaleFallbackResults.js';
17
17
  import {setServiceWorkerDefaults} from '../resources/utils/ServiceWorker.js';
18
- // import setGlobalVars from 'indexeddbshim/src/node-UnicodeIdentifiers.js';
19
18
  import {Languages} from '../resources/utils/Languages.js';
20
19
  import activateCallback from '../resources/activateCallback.js';
21
20