@webqit/webflo 0.10.5 → 0.11.2

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.
Files changed (36) hide show
  1. package/README.md +1082 -323
  2. package/package.json +2 -2
  3. package/src/config-pi/runtime/Client.js +7 -10
  4. package/src/config-pi/runtime/client/Worker.js +30 -12
  5. package/src/runtime-pi/Router.js +1 -1
  6. package/src/runtime-pi/client/Runtime.js +98 -49
  7. package/src/runtime-pi/client/RuntimeClient.js +12 -40
  8. package/src/runtime-pi/client/Workport.js +163 -0
  9. package/src/runtime-pi/client/generate.js +71 -37
  10. package/src/runtime-pi/client/worker/Worker.js +57 -23
  11. package/src/runtime-pi/client/worker/Workport.js +80 -0
  12. package/src/runtime-pi/server/Runtime.js +22 -8
  13. package/src/runtime-pi/server/RuntimeClient.js +6 -6
  14. package/src/runtime-pi/util.js +2 -2
  15. package/test/site/package.json +9 -0
  16. package/test/site/public/bundle.html +3 -0
  17. package/test/site/public/bundle.html.json +1 -0
  18. package/test/site/public/bundle.js +1 -1
  19. package/test/site/public/bundle.js.gz +0 -0
  20. package/test/site/public/bundle.webflo.js +8 -8
  21. package/test/site/public/bundle.webflo.js.gz +0 -0
  22. package/test/site/public/index.html +5 -5
  23. package/test/site/public/index1.html +35 -0
  24. package/test/site/public/page-2/bundle.js +1 -1
  25. package/test/site/public/page-2/bundle.js.gz +0 -0
  26. package/test/site/public/page-2/index.html +3 -4
  27. package/test/site/public/page-3/logo-130x130.png +0 -0
  28. package/test/site/public/page-4/subpage/bundle.js +1 -1
  29. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  30. package/test/site/public/sparoots.json +5 -0
  31. package/test/site/public/worker.js +1 -1
  32. package/test/site/public/worker.js.gz +0 -0
  33. package/test/site/server/index.js +14 -6
  34. package/docker/Dockerfile +0 -26
  35. package/docker/README.md +0 -77
  36. package/src/runtime-pi/client/WorkerComm.js +0 -102
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.10.5",
15
+ "version": "0.11.2",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@octokit/webhooks": "^7.15.1",
39
39
  "@webqit/backpack": "^0.1.0",
40
- "@webqit/oohtml-ssr": "^1.0.6",
40
+ "@webqit/oohtml-ssr": "^1.1.0",
41
41
  "@webqit/util": "^0.8.9",
42
42
  "client-sessions": "^0.8.0",
43
43
  "esbuild": "^0.14.38",
@@ -24,7 +24,7 @@ export default class Client extends Dotfile {
24
24
  return _merge(true, {
25
25
  bundle_filename: 'bundle.js',
26
26
  public_base_url: '/',
27
- address_bar_synchrony: 'standard',
27
+ spa_routing: true,
28
28
  oohtml_support: 'full',
29
29
  service_worker_support: true,
30
30
  worker_scope: '/',
@@ -36,10 +36,6 @@ export default class Client extends Dotfile {
36
36
  questions(config, choices = {}) {
37
37
  // Choices
38
38
  const CHOICES = _merge({
39
- address_bar_synchrony: [
40
- {value: 'standard', title: 'standard - on response'},
41
- {value: 'instant', title: 'instant - on request'},
42
- ],
43
39
  oohtml_support: [
44
40
  {value: 'full', title: 'Full'},
45
41
  {value: 'namespacing', title: 'namespacing'},
@@ -64,11 +60,12 @@ export default class Client extends Dotfile {
64
60
  validation: ['important'],
65
61
  },
66
62
  {
67
- name: 'address_bar_synchrony',
68
- type: 'select',
69
- message: '[address_bar_synchrony]: Specify how the address bar synchronizes with navigation',
70
- choices: CHOICES.address_bar_synchrony,
71
- initial: this.indexOfInitial(CHOICES.address_bar_synchrony, config.address_bar_synchrony),
63
+ name: 'spa_routing',
64
+ type: 'toggle',
65
+ message: '[spa_routing]: Enable Single Page Routing Mode',
66
+ active: 'YES',
67
+ inactive: 'NO',
68
+ initial: config.spa_routing,
72
69
  validation: ['important'],
73
70
  },
74
71
  {
@@ -23,10 +23,11 @@ export default class Worker extends Dotfile {
23
23
  withDefaults(config) {
24
24
  return _merge(true, {
25
25
  cache_name: 'cache_v0',
26
- cache_only_urls: [],
26
+ default_fetching_strategy: 'network-first',
27
+ network_first_urls: [],
27
28
  cache_first_urls: [],
28
29
  network_only_urls: [],
29
- network_first_urls: [],
30
+ cache_only_urls: [ '/page-3/{*.json}' ],
30
31
  skip_waiting: false,
31
32
  // -----------------
32
33
  support_push: false,
@@ -42,6 +43,15 @@ export default class Worker extends Dotfile {
42
43
  if (config.cache_name && config.cache_name.indexOf('_v') > -1 && _isNumeric(_after(config.cache_name, '_v'))) {
43
44
  config.cache_name = _before(config.cache_name, '_v') + '_v' + (parseInt(_after(config.cache_name, '_v')) + 1);
44
45
  }
46
+ // Choices
47
+ const CHOICES = _merge({
48
+ default_fetching_strategy: [
49
+ {value: 'network-first', title: 'Network-first (Webflo default)'},
50
+ {value: 'cache-first', title: 'Cache-first'},
51
+ {value: 'network-only', title: 'Network-only'},
52
+ {value: 'cache-only', title: 'Cache-only'},
53
+ ],
54
+ }, choices);
45
55
  // Questions
46
56
  return [
47
57
  {
@@ -51,28 +61,36 @@ export default class Worker extends Dotfile {
51
61
  initial: config.cache_name,
52
62
  },
53
63
  {
54
- name: 'cache_only_urls',
55
- type: 'list',
56
- message: 'Specify URLs for a "cache-only" fetching strategy (comma-separated, globe supported)',
57
- initial: (config.cache_only_urls || []).join(', '),
64
+ name: 'default_fetching_strategy',
65
+ type: 'select',
66
+ message: '[default_fetching_strategy]: Choose the default fetching strategy',
67
+ choices: CHOICES.default_fetching_strategy,
68
+ initial: this.indexOfInitial(CHOICES.default_fetching_strategy, config.default_fetching_strategy),
69
+ validation: ['important'],
70
+ },
71
+ {
72
+ name: 'network_first_urls',
73
+ type: (prev, answers) => answers.default_fetching_strategy === 'network-first' ? null : 'list',
74
+ message: 'Specify URLs for a "network-first-then-cache" fetching strategy (comma-separated, globe supported)',
75
+ initial: (config.network_first_urls || []).join(', '),
58
76
  },
59
77
  {
60
78
  name: 'cache_first_urls',
61
- type: 'list',
79
+ type: (prev, answers) => answers.default_fetching_strategy === 'cache-first' ? null : 'list',
62
80
  message: 'Specify URLs for a "cache-first-then-network" fetching strategy (comma-separated, globe supported)',
63
81
  initial: (config.cache_first_urls || []).join(', '),
64
82
  },
65
83
  {
66
84
  name: 'network_only_urls',
67
- type: 'list',
85
+ type: (prev, answers) => answers.default_fetching_strategy === 'network-only' ? null : 'list',
68
86
  message: 'Specify URLs for a "network-only" fetching strategy (comma-separated, globe supported)',
69
87
  initial: (config.network_only_urls || []).join(', '),
70
88
  },
71
89
  {
72
- name: 'network_first_urls',
73
- type: 'list',
74
- message: 'Specify URLs for a "network-first-then-cache" fetching strategy (comma-separated, globe supported)',
75
- initial: (config.network_first_urls || []).join(', '),
90
+ name: 'cache_only_urls',
91
+ type: (prev, answers) => answers.default_fetching_strategy === 'cache-only' ? null : 'list',
92
+ message: 'Specify URLs for a "cache-only" fetching strategy (comma-separated, globe supported)',
93
+ initial: (config.cache_only_urls || []).join(', '),
76
94
  },
77
95
  {
78
96
  name: 'skip_waiting',
@@ -46,7 +46,7 @@ export default class Router {
46
46
  // The loop
47
47
  // ----------------
48
48
  const next = async function(thisTick) {
49
- const thisContext = {};
49
+ const thisContext = { };
50
50
  if (!thisTick.trail || thisTick.trail.length < thisTick.destination.length) {
51
51
  thisTick = await $this.readTick(thisTick);
52
52
  // -------------
@@ -16,6 +16,7 @@ import xRequest from "../xRequest.js";
16
16
  import xResponse from "../xResponse.js";
17
17
  import xfetch from '../xfetch.js';
18
18
  import xHttpEvent from '../xHttpEvent.js';
19
+ import Workport from './Workport.js';
19
20
 
20
21
  const URL = xURL(whatwag.URL);
21
22
  const FormData = xFormData(whatwag.FormData);
@@ -162,6 +163,27 @@ export default class Runtime {
162
163
  window.addEventListener('online', () => Observer.set(this.network, 'online', navigator.onLine));
163
164
  window.addEventListener('offline', () => Observer.set(this.network, 'online', navigator.onLine));
164
165
 
166
+ // -----------------------
167
+ // Service Worker && COMM
168
+ if (this.cx.service_worker_support) {
169
+ let workport = new Workport(this.cx.worker_filename, { scope: this.cx.worker_scope, startMessages: true });
170
+ Observer.set(this, 'workport', workport);
171
+ workport.messaging.listen(async evt => {
172
+ let responsePort = evt.ports[0];
173
+ let client = this.clients.get('*');
174
+ let response = client.alert && await client.alert(evt);
175
+ if (responsePort) {
176
+ if (response instanceof Promise) {
177
+ response.then(data => {
178
+ responsePort.postMessage(data);
179
+ });
180
+ } else {
181
+ responsePort.postMessage(response);
182
+ }
183
+ }
184
+ });
185
+ }
186
+
165
187
  // ---------------
166
188
  this.go(this.location, {}, { srcType: 'init' });
167
189
  // ---------------
@@ -180,16 +202,37 @@ export default class Runtime {
180
202
  if (url.origin && url.origin !== this.location.origin) return false;
181
203
  if (e && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) return false;
182
204
  if (!this.cx.params.routing) return true;
205
+ if (this.cx.params.routing.targets === false/** explicit false means disabled */) return false;
183
206
  let b = url.pathname.split('/').filter(s => s);
184
207
  const match = a => {
185
208
  a = a.split('/').filter(s => s);
186
209
  return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
187
210
  };
188
- return match(this.cx.params.routing.scope) && this.cx.params.routing.subscopes.reduce((prev, subscope) => {
189
- return prev && !match(subscope);
211
+ return match(this.cx.params.routing.root) && this.cx.params.routing.subroots.reduce((prev, subroot) => {
212
+ return prev && !match(subroot);
190
213
  }, true);
191
214
  }
192
215
 
216
+ // Generates request object
217
+ generateRequest(href, init) {
218
+ return new Request(href, {
219
+ signal: this._abortController.signal,
220
+ ...init,
221
+ headers: {
222
+ 'Accept': 'application/json',
223
+ 'X-Redirect-Policy': 'manual-when-cross-spa',
224
+ 'X-Redirect-Code': this._xRedirectCode,
225
+ 'X-Powered-By': '@webqit/webflo',
226
+ ...(init.headers || {}),
227
+ },
228
+ });
229
+ }
230
+
231
+ // Generates session object
232
+ getSession(e, id = null, persistent = false) {
233
+ return Storage(id, persistent);
234
+ }
235
+
193
236
  /**
194
237
  * Performs a request.
195
238
  *
@@ -206,7 +249,7 @@ export default class Runtime {
206
249
  // Put his forward before instantiating a request and aborting previous
207
250
  // Same-page hash-links clicks on chrome recurse here from histroy popstate
208
251
  if (detail.srcType !== 'init' && (_before(url.href, '#') === _before(init.referrer, '#') && (init.method || 'GET').toUpperCase() === 'GET')) {
209
- return;
252
+ return new Response(null, { status: 304 }); // Not Modified
210
253
  }
211
254
  // ------------
212
255
  if (this._abortController) {
@@ -215,62 +258,77 @@ export default class Runtime {
215
258
  this._abortController = new AbortController();
216
259
  this._xRedirectCode = 200;
217
260
  // ------------
261
+ // States
262
+ // ------------
263
+ Observer.set(this.network, 'error', null);
264
+ Observer.set(this.network, 'requesting', { ...init, ...detail });
218
265
  if (['link', 'form'].includes(detail.srcType)) {
219
- Observer.set(detail.src, 'active', true);
220
- Observer.set(detail.submitter || {}, 'active', true);
266
+ detail.src.state && (detail.src.state.active = true);
267
+ detail.submitter && detail.submitter.state && (detail.submitter.state.active = true);
221
268
  }
222
269
  // ------------
223
- Observer.set(this.network, 'redirecting', null);
224
- if (this.cx.params.address_bar_synchrony === 'instant') {
225
- Observer.set(this.location, url, { detail: { ...init, ...detail }, });
226
- }
227
- // ------------
270
+ // Run
271
+ // ------------
228
272
  // The request object
229
273
  let request = this.generateRequest(url.href, init);
230
274
  // The navigation event
231
275
  let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
232
276
  // Response
233
- let response = await this.clients.get('*').handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
234
- let finalResponse = this.handleResponse(httpEvent, response);
235
- // ------------
236
- if (this.cx.params.address_bar_synchrony !== 'instant') {
237
- Observer.set(this.location, url, { detail: { ...init, ...detail }, });
277
+ let client = this.clients.get('*'), response, finalResponse;
278
+ try {
279
+ // ------------
280
+ // Response
281
+ // ------------
282
+ response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
283
+ finalResponse = this.handleResponse(httpEvent, response);
284
+ // ------------
285
+ // Address bar
286
+ // ------------
287
+ if (response.redirected) {
288
+ Observer.set(this.location, { href: response.url }, { detail: { redirected: true }, });
289
+ } else if (![302, 301].includes(finalResponse.status)) {
290
+ Observer.set(this.location, url);
291
+ }
292
+ // ------------
293
+ // States
294
+ // ------------
295
+ Observer.set(this.network, 'requesting', null);
296
+ if (['link', 'form'].includes(detail.srcType)) {
297
+ detail.src.state && (detail.src.state.active = false);
298
+ detail.submitter && detail.submitter.state && (detail.submitter.state.active = false);
299
+ }
300
+ // ------------
301
+ // Rendering
302
+ // ------------
303
+ if (finalResponse.ok && finalResponse.headers.contentType === 'application/json') {
304
+ client.render && await client.render(httpEvent, finalResponse);
305
+ } else if (!finalResponse.ok) {
306
+ if ([404, 500].includes(finalResponse.status)) {
307
+ Observer.set(this.network, 'error', new Error(finalResponse.statusText, { cause: finalResponse.status }));
308
+ }
309
+ client.unrender && await client.unrender(httpEvent);
310
+ }
311
+ } catch(e) {
312
+ console.error(e);
313
+ Observer.set(this.network, 'error', { ...e, retry: () => this.go(url, init = {}, detail) });
314
+ finalResponse = new Response(null, { status: 500, statusText: e.message });
238
315
  }
239
316
  // ------------
240
317
  // Return value
241
318
  return finalResponse;
242
319
  }
243
320
 
244
- // Generates request object
245
- generateRequest(href, init) {
246
- return new Request(href, {
247
- signal: this._abortController.signal,
248
- ...init,
249
- headers: {
250
- 'Accept': 'application/json',
251
- 'X-Redirect-Policy': 'manual-when-cross-origin',
252
- 'X-Redirect-Code': this._xRedirectCode,
253
- 'X-Powered-By': '@webqit/webflo',
254
- ...(init.headers || {}),
255
- },
256
- });
257
- }
258
-
259
- // Generates session object
260
- getSession(e, id = null, persistent = false) {
261
- return Storage(id, persistent);
262
- }
263
-
264
321
  // Initiates remote fetch and sets the status
265
322
  remoteFetch(request, ...args) {
266
- Observer.set(this.network, 'remote', true);
323
+ let href = typeof request === 'string' ? request : (request.url || request.href);
324
+ Observer.set(this.network, 'remote', href);
267
325
  let _response = fetch(request, ...args);
268
326
  // This catch() is NOT intended to handle failure of the fetch
269
- _response.catch(e => Observer.set(this.network, 'error', e.message));
327
+ _response.catch(e => Observer.set(this.network, 'error', e));
270
328
  // Return xResponse
271
329
  return _response.then(async response => {
272
330
  // Stop loading status
273
- Observer.set(this.network, 'remote', false);
331
+ Observer.set(this.network, 'remote', null);
274
332
  return new Response(response);
275
333
  });
276
334
  }
@@ -278,19 +336,10 @@ export default class Runtime {
278
336
  // Handles response object
279
337
  handleResponse(e, response) {
280
338
  if (!(response instanceof Response)) { response = new Response(response); }
281
- Observer.set(this.network, 'remote', false);
282
- Observer.set(this.network, 'error', null);
283
- if (['link', 'form'].includes(e.detail.srcType)) {
284
- Observer.set(e.detail.src, 'active', false);
285
- Observer.set(e.detail.submitter || {}, 'active', false);
286
- }
287
- if (response.redirected && (new whatwag.URL(response.url)).origin === this.location.origin) {
288
- Observer.set(this.location, { href: response.url }, {
289
- detail: { isRedirect: true },
290
- });
291
- } else {
339
+ if (!response.redirected) {
292
340
  let location = response.headers.get('Location');
293
341
  if (location && response.status === this._xRedirectCode) {
342
+ response.attrs.status = parseInt(response.headers.get('X-Redirect-Code'));
294
343
  Observer.set(this.network, 'redirecting', location);
295
344
  window.location = location;
296
345
  }
@@ -2,8 +2,6 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { Observer } from './Runtime.js';
6
- import WorkerComm from './WorkerComm.js';
7
5
  import Router from './Router.js';
8
6
 
9
7
  export default class RuntimeClient {
@@ -15,12 +13,6 @@ export default class RuntimeClient {
15
13
  */
16
14
  constructor(cx) {
17
15
  this.cx = cx;
18
- if (this.cx.service_worker_support) {
19
- const workerComm = new WorkerComm(this.cx.worker_filename, { scope: this.cx.worker_scope, startMessages: true });
20
- Observer.observe(workerComm, changes => {
21
- //console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
22
- });
23
- }
24
16
  }
25
17
 
26
18
  /**
@@ -35,44 +27,26 @@ export default class RuntimeClient {
35
27
  // The app router
36
28
  const router = new Router(this.cx, httpEvent.url.pathname);
37
29
  const handle = async () => {
38
- if (this.cx.params.address_bar_synchrony === 'instant') {
39
- await this.render(httpEvent, {}, router);
40
- }
41
30
  // --------
42
31
  // ROUTE FOR DATA
43
32
  // --------
44
33
  let httpMethodName = httpEvent.request.method.toLowerCase();
45
- let response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], httpEvent, {}, async event => {
34
+ return router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], httpEvent, {}, async event => {
46
35
  return remoteFetch(event.request);
47
36
  }, remoteFetch);
48
- if (!(response instanceof httpEvent.Response)) {
49
- response = new httpEvent.Response(response);
50
- }
51
-
52
- // --------
53
- // Rendering
54
- // --------
55
- if (response.ok && response.headers.contentType === 'application/json') {
56
- await this.render(httpEvent, response, router);
57
- await this.scrollIntoView(httpEvent);
58
- } else if (!response.ok) {
59
- await this.unrender();
60
- }
61
-
62
- return response;
63
37
  };
64
-
65
38
  // --------
66
39
  // PIPE THROUGH MIDDLEWARES
67
40
  // --------
68
- return (this.cx.middlewares || []).concat(handle).reverse().reduce((next, fn) => {
69
- return () => fn.call(this.cx, httpEvent, router, next);
70
- }, null)();
41
+ return await (this.cx.middlewares || []).concat(handle).reverse().reduce((next, fn) => {
42
+ return () => fn.call(this.cx, httpEvent, router, next);
43
+ }, null)();
71
44
  }
72
45
 
73
46
  // Renderer
74
- async render(httpEvent, response, router) {
75
- let data = response.json ? await response.json() : response;
47
+ async render(httpEvent, response) {
48
+ let data = await response.json();
49
+ const router = new Router(this.cx, httpEvent.url.pathname);
76
50
  return router.route('render', httpEvent, data, async (httpEvent, data) => {
77
51
  // --------
78
52
  // OOHTML would waiting for DOM-ready in order to be initialized
@@ -88,23 +62,21 @@ export default class RuntimeClient {
88
62
  url: this.cx.runtime.location,
89
63
  }, { update: true });
90
64
  }
91
- window.document.setState({ page: data }, { update: 'merge' });
65
+ window.document.setState({ data }, { update: 'merge' });
92
66
  }
93
67
  if (window.document.templates) {
94
- window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
68
+ window.document.body.setAttribute('template', 'routes/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
95
69
  await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
96
70
  }
71
+ await this.scrollIntoView(httpEvent);
97
72
  return window;
98
73
  });
99
74
  }
100
75
 
101
76
  // Unrender
102
- async unrender() {
77
+ async unrender(httpEvent) {
103
78
  if (window.document.state) {
104
- window.document.setState({ page: {} }, { update: 'merge' });
105
- }
106
- if (window.document.templates) {
107
- window.document.body.setAttribute('template', '');
79
+ window.document.setState({ data: {} }, { update: 'merge' });
108
80
  }
109
81
  }
110
82
 
@@ -0,0 +1,163 @@
1
+
2
+
3
+ /**
4
+ * @imports
5
+ */
6
+ import { _isFunction, _isObject } from '@webqit/util/js/index.js';
7
+ import { Observer } from './Runtime.js';
8
+
9
+ export default class Workport {
10
+
11
+ constructor(file, params = {}) {
12
+ this.ready = navigator.serviceWorker.ready;
13
+
14
+ // --------
15
+ // Registration and lifecycle
16
+ // --------
17
+ this.registration = new Promise((resolve, reject) => {
18
+ const register = () => {
19
+ navigator.serviceWorker.register(file, { scope: params.scope || '/' }).then(async registration => {
20
+
21
+ // Helper that updates instance's state
22
+ const state = target => {
23
+ // instance2.state can be any of: "installing", "installed", "activating", "activated", "redundant"
24
+ const equivState = target.state === 'installed' ? 'waiting' :
25
+ (target.state === 'activating' || target.state === 'activated' ? 'active' : target.state)
26
+ Observer.set(this, equivState, target);
27
+ }
28
+
29
+ // We're always installing at first for a new service worker.
30
+ // An existing service would immediately be active
31
+ const worker = registration.active || registration.waiting || registration.installing;
32
+ state(worker);
33
+ worker.addEventListener('statechange', e => state(e.target));
34
+
35
+ // "updatefound" event - a new worker that will control
36
+ // this page is installing somewhere
37
+ registration.addEventListener('updatefound', () => {
38
+ // If updatefound is fired, it means that there's
39
+ // a new service worker being installed.
40
+ state(registration.installing);
41
+ registration.installing.addEventListener('statechange', e => state(e.target));
42
+ });
43
+
44
+ resolve(registration);
45
+ }).catch(e => reject(e));
46
+ };
47
+ if (params.onWondowLoad) {
48
+ window.addEventListener('load', register);
49
+ } else {
50
+ register();
51
+ }
52
+ if (params.startMessages) {
53
+ navigator.serviceWorker.startMessages();
54
+ }
55
+ });
56
+
57
+ // --------
58
+ // Post messaging
59
+ // --------
60
+ const postSendCallback = (message, callback, onAvailability = 1) => {
61
+ if (this.active) {
62
+ if (_isFunction(message)) message = message();
63
+ callback(this.active, message);
64
+ } else if (onAvailability) {
65
+ // Availability Handling
66
+ const availabilityHandler = entry => {
67
+ if (_isFunction(message)) message = message();
68
+ callback(entry.value, message);
69
+ if (onAvailability !== 2) {
70
+ Observer.unobserve(this, 'active', availabilityHandler);
71
+ }
72
+ };
73
+ Observer.observe(this, 'active', availabilityHandler);
74
+ }
75
+ };
76
+ this.messaging = {
77
+ post: (message, onAvailability = 1) => {
78
+ postSendCallback(message, (active, message) => {
79
+ active.postMessage(message);
80
+ }, onAvailability);
81
+ return this.post;
82
+ },
83
+ listen: callback => {
84
+ navigator.serviceWorker.addEventListener('message', callback);
85
+ return this.post;
86
+ },
87
+ request: (message, onAvailability = 1) => {
88
+ return new Promise(res => {
89
+ postSendCallback(message, (active, message) => {
90
+ let messageChannel = new MessageChannel();
91
+ active.postMessage(message, [ messageChannel.port2 ]);
92
+ messageChannel.port1.onmessage = e => res(e.data);
93
+ }, onAvailability);
94
+ });
95
+ },
96
+ channel(channelId) {
97
+ if (!this.channels.has(channelId)) { this.channels.set(channelId, new BroadcastChannel(channel)); }
98
+ let channel = this.channels.get(channelId);
99
+ return {
100
+ broadcast: message => channel.postMessage(message),
101
+ listen: callback => channel.addEventListener('message', callback),
102
+ };
103
+ },
104
+ channels: new Map,
105
+ };
106
+
107
+ // --------
108
+ // Notifications
109
+ // --------
110
+ this.notifications = {
111
+ fire: (title, params = {}) => {
112
+ return new Promise((res, rej) => {
113
+ if (typeof Notification === 'undefined' || Notification.permission !== 'granted') {
114
+ return rej(typeof Notification !== 'undefined' && Notification && Notification.permission);
115
+ }
116
+ notification.addEventListener('error', rej);
117
+ let notification = new Notification(title, params);
118
+ notification.addEventListener('click', res);
119
+ notification.addEventListener('close', res);
120
+ });
121
+ },
122
+ };
123
+
124
+ // --------
125
+ // Push notifications
126
+ // --------
127
+ this.push = {
128
+ getSubscription: async () => {
129
+ return (await this.registration).pushManager.getSubscription();
130
+ },
131
+ subscribe: async (publicKey, params = {}) => {
132
+ var subscription = await this.push.getSubscription();
133
+ return subscription ? subscription : (await this.registration).pushManager.subscribe(
134
+ _isObject(publicKey) ? publicKey : {
135
+ applicationServerKey: urlBase64ToUint8Array(publicKey),
136
+ ...params,
137
+ }
138
+ );
139
+ },
140
+ unsubscribe: async () => {
141
+ var subscription = await this.push.getSubscription();
142
+ return !subscription ? null : subscription.unsubscribe();
143
+ },
144
+ };
145
+ }
146
+
147
+ }
148
+
149
+ // Public base64 to Uint
150
+ function urlBase64ToUint8Array(base64String) {
151
+ var padding = '='.repeat((4 - base64String.length % 4) % 4);
152
+ var base64 = (base64String + padding)
153
+ .replace(/\-/g, '+')
154
+ .replace(/_/g, '/');
155
+
156
+ var rawData = window.atob(base64);
157
+ var outputArray = new Uint8Array(rawData.length);
158
+
159
+ for (var i = 0; i < rawData.length; ++i) {
160
+ outputArray[i] = rawData.charCodeAt(i);
161
+ }
162
+ return outputArray;
163
+ }