@webqit/webflo 0.10.3 → 0.11.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/README.md +1509 -3
- package/bundle.html.json +1665 -0
- package/package.json +3 -3
- package/src/Context.js +8 -4
- package/src/config-pi/deployment/Env.js +2 -2
- package/src/config-pi/deployment/Layout.js +2 -2
- package/src/config-pi/deployment/Origins.js +2 -2
- package/src/config-pi/deployment/Virtualization.js +2 -2
- package/src/config-pi/runtime/Client.js +39 -11
- package/src/config-pi/runtime/Server.js +26 -10
- package/src/config-pi/runtime/client/Worker.js +32 -14
- package/src/config-pi/runtime/server/Headers.js +2 -2
- package/src/config-pi/runtime/server/Redirects.js +2 -2
- package/src/config-pi/static/Manifest.js +2 -2
- package/src/config-pi/static/Ssg.js +2 -2
- package/src/runtime-pi/Router.js +1 -1
- package/src/runtime-pi/client/Runtime.js +116 -62
- package/src/runtime-pi/client/RuntimeClient.js +28 -43
- package/src/runtime-pi/client/Workport.js +163 -0
- package/src/runtime-pi/client/generate.js +285 -77
- package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
- package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
- package/src/runtime-pi/client/oohtml/scripting.js +8 -0
- package/src/runtime-pi/client/oohtml/templating.js +8 -0
- package/src/runtime-pi/client/worker/Worker.js +58 -24
- package/src/runtime-pi/client/worker/Workport.js +80 -0
- package/src/runtime-pi/server/Router.js +2 -2
- package/src/runtime-pi/server/Runtime.js +30 -11
- package/src/runtime-pi/server/RuntimeClient.js +24 -14
- package/src/runtime-pi/util.js +2 -2
- package/src/webflo.js +7 -9
- package/test/site/package.json +9 -0
- package/test/site/public/bundle.html +6 -0
- package/test/site/public/bundle.html.json +4 -0
- package/test/site/public/bundle.js +2 -0
- package/test/site/public/bundle.js.gz +0 -0
- package/test/site/public/bundle.webflo.js +15 -0
- package/test/site/public/bundle.webflo.js.gz +0 -0
- package/test/site/public/index.html +30 -0
- package/test/site/public/index1.html +35 -0
- package/test/site/public/page-2/bundle.html +5 -0
- package/test/site/public/page-2/bundle.html.json +1 -0
- package/test/site/public/page-2/bundle.js +2 -0
- package/test/site/public/page-2/bundle.js.gz +0 -0
- package/test/site/public/page-2/index.html +46 -0
- package/test/site/public/page-2/logo-130x130.png +0 -0
- package/test/site/public/page-2/main.html +3 -0
- package/test/site/public/page-3/logo-130x130.png +0 -0
- package/test/site/public/page-4/subpage/bundle.html +0 -0
- package/test/site/public/page-4/subpage/bundle.html.json +1 -0
- package/test/site/public/page-4/subpage/bundle.js +2 -0
- package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
- package/test/site/public/page-4/subpage/index.html +31 -0
- package/test/site/public/sparoots.json +5 -0
- package/test/site/public/worker.js +3 -0
- package/test/site/public/worker.js.gz +0 -0
- package/test/site/server/index.js +16 -0
- package/src/Cli.js +0 -131
- package/src/Configurator.js +0 -97
- package/src/runtime-pi/client/WorkerComm.js +0 -102
|
@@ -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);
|
|
@@ -98,8 +99,7 @@ export default class Runtime {
|
|
|
98
99
|
window.addEventListener('click', e => {
|
|
99
100
|
var anchor = e.target.closest('a');
|
|
100
101
|
if (!anchor || !anchor.href) return;
|
|
101
|
-
if (!anchor.target && !anchor.download && (
|
|
102
|
-
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
102
|
+
if (!anchor.target && !anchor.download && this.isSpaRoute(anchor, e)) {
|
|
103
103
|
// Publish everything, including hash
|
|
104
104
|
this.go(Url.copy(anchor), {}, { src: anchor, srcType: 'link', });
|
|
105
105
|
// URLs with # will cause a natural navigation
|
|
@@ -129,8 +129,7 @@ export default class Runtime {
|
|
|
129
129
|
actionEl.href = submitParams.action;
|
|
130
130
|
// ---------------
|
|
131
131
|
// If not targeted and same origin...
|
|
132
|
-
if (!submitParams.target && (
|
|
133
|
-
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
|
|
132
|
+
if (!submitParams.target && this.isSpaRoute(actionEl, e)) {
|
|
134
133
|
// Build data
|
|
135
134
|
var formData = new FormData(form);
|
|
136
135
|
if ((submitter || {}).name) {
|
|
@@ -164,6 +163,27 @@ export default class Runtime {
|
|
|
164
163
|
window.addEventListener('online', () => Observer.set(this.network, 'online', navigator.onLine));
|
|
165
164
|
window.addEventListener('offline', () => Observer.set(this.network, 'online', navigator.onLine));
|
|
166
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
|
+
|
|
167
187
|
// ---------------
|
|
168
188
|
this.go(this.location, {}, { srcType: 'init' });
|
|
169
189
|
// ---------------
|
|
@@ -176,6 +196,43 @@ export default class Runtime {
|
|
|
176
196
|
return window.history;
|
|
177
197
|
}
|
|
178
198
|
|
|
199
|
+
// Check is-route
|
|
200
|
+
isSpaRoute(url, e) {
|
|
201
|
+
url = typeof url === 'string' ? new whatwag.URL(url) : url;
|
|
202
|
+
if (url.origin && url.origin !== this.location.origin) return false;
|
|
203
|
+
if (e && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) return false;
|
|
204
|
+
if (!this.cx.params.routing) return true;
|
|
205
|
+
if (this.cx.params.routing.targets === false/** explicit false means disabled */) return false;
|
|
206
|
+
let b = url.pathname.split('/').filter(s => s);
|
|
207
|
+
const match = a => {
|
|
208
|
+
a = a.split('/').filter(s => s);
|
|
209
|
+
return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
|
|
210
|
+
};
|
|
211
|
+
return match(this.cx.params.routing.root) && this.cx.params.routing.subroots.reduce((prev, subroot) => {
|
|
212
|
+
return prev && !match(subroot);
|
|
213
|
+
}, true);
|
|
214
|
+
}
|
|
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
|
+
|
|
179
236
|
/**
|
|
180
237
|
* Performs a request.
|
|
181
238
|
*
|
|
@@ -192,7 +249,7 @@ export default class Runtime {
|
|
|
192
249
|
// Put his forward before instantiating a request and aborting previous
|
|
193
250
|
// Same-page hash-links clicks on chrome recurse here from histroy popstate
|
|
194
251
|
if (detail.srcType !== 'init' && (_before(url.href, '#') === _before(init.referrer, '#') && (init.method || 'GET').toUpperCase() === 'GET')) {
|
|
195
|
-
return;
|
|
252
|
+
return new Response(null, { status: 304 }); // Not Modified
|
|
196
253
|
}
|
|
197
254
|
// ------------
|
|
198
255
|
if (this._abortController) {
|
|
@@ -201,55 +258,77 @@ export default class Runtime {
|
|
|
201
258
|
this._abortController = new AbortController();
|
|
202
259
|
this._xRedirectCode = 200;
|
|
203
260
|
// ------------
|
|
261
|
+
// States
|
|
262
|
+
// ------------
|
|
263
|
+
Observer.set(this.network, 'error', null);
|
|
264
|
+
Observer.set(this.network, 'requesting', { ...init, ...detail });
|
|
204
265
|
if (['link', 'form'].includes(detail.srcType)) {
|
|
205
|
-
|
|
206
|
-
|
|
266
|
+
detail.src.state && (detail.src.state.active = true);
|
|
267
|
+
detail.submitter && detail.submitter.state && (detail.submitter.state.active = true);
|
|
207
268
|
}
|
|
208
269
|
// ------------
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// ------------
|
|
270
|
+
// Run
|
|
271
|
+
// ------------
|
|
212
272
|
// The request object
|
|
213
273
|
let request = this.generateRequest(url.href, init);
|
|
214
274
|
// The navigation event
|
|
215
275
|
let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
|
|
216
276
|
// Response
|
|
217
|
-
let
|
|
218
|
-
|
|
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 });
|
|
315
|
+
}
|
|
316
|
+
// ------------
|
|
219
317
|
// Return value
|
|
220
318
|
return finalResponse;
|
|
221
319
|
}
|
|
222
320
|
|
|
223
|
-
// Generates request object
|
|
224
|
-
generateRequest(href, init) {
|
|
225
|
-
return new Request(href, {
|
|
226
|
-
signal: this._abortController.signal,
|
|
227
|
-
...init,
|
|
228
|
-
headers: {
|
|
229
|
-
'Accept': 'application/json',
|
|
230
|
-
'X-Redirect-Policy': 'manual-when-cross-origin',
|
|
231
|
-
'X-Redirect-Code': this._xRedirectCode,
|
|
232
|
-
'X-Powered-By': '@webqit/webflo',
|
|
233
|
-
...(init.headers || {}),
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Generates session object
|
|
239
|
-
getSession(e, id = null, persistent = false) {
|
|
240
|
-
return Storage(id, persistent);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
321
|
// Initiates remote fetch and sets the status
|
|
244
322
|
remoteFetch(request, ...args) {
|
|
245
|
-
|
|
323
|
+
let href = typeof request === 'string' ? request : (request.url || request.href);
|
|
324
|
+
Observer.set(this.network, 'remote', href);
|
|
246
325
|
let _response = fetch(request, ...args);
|
|
247
326
|
// This catch() is NOT intended to handle failure of the fetch
|
|
248
|
-
_response.catch(e => Observer.set(this.network, 'error', e
|
|
327
|
+
_response.catch(e => Observer.set(this.network, 'error', e));
|
|
249
328
|
// Return xResponse
|
|
250
329
|
return _response.then(async response => {
|
|
251
330
|
// Stop loading status
|
|
252
|
-
Observer.set(this.network, 'remote',
|
|
331
|
+
Observer.set(this.network, 'remote', null);
|
|
253
332
|
return new Response(response);
|
|
254
333
|
});
|
|
255
334
|
}
|
|
@@ -257,19 +336,10 @@ export default class Runtime {
|
|
|
257
336
|
// Handles response object
|
|
258
337
|
handleResponse(e, response) {
|
|
259
338
|
if (!(response instanceof Response)) { response = new Response(response); }
|
|
260
|
-
|
|
261
|
-
Observer.set(this.network, 'error', null);
|
|
262
|
-
if (['link', 'form'].includes(e.detail.srcType)) {
|
|
263
|
-
Observer.set(e.detail.src, 'active', false);
|
|
264
|
-
Observer.set(e.detail.submitter || {}, 'active', false);
|
|
265
|
-
}
|
|
266
|
-
if (response.redirected && this.isSameOrigin(response.url)) {
|
|
267
|
-
Observer.set(this.location, { href: response.url }, {
|
|
268
|
-
detail: { isRedirect: true },
|
|
269
|
-
});
|
|
270
|
-
} else {
|
|
339
|
+
if (!response.redirected) {
|
|
271
340
|
let location = response.headers.get('Location');
|
|
272
341
|
if (location && response.status === this._xRedirectCode) {
|
|
342
|
+
response.attrs.status = parseInt(response.headers.get('X-Redirect-Code'));
|
|
273
343
|
Observer.set(this.network, 'redirecting', location);
|
|
274
344
|
window.location = location;
|
|
275
345
|
}
|
|
@@ -277,20 +347,4 @@ export default class Runtime {
|
|
|
277
347
|
return response;
|
|
278
348
|
}
|
|
279
349
|
|
|
280
|
-
/**
|
|
281
|
-
* Checks if an URL is same origin.
|
|
282
|
-
*
|
|
283
|
-
* @param object|string url
|
|
284
|
-
*
|
|
285
|
-
* @return Bool
|
|
286
|
-
*/
|
|
287
|
-
isSameOrigin(url) {
|
|
288
|
-
if (typeof url === 'string') {
|
|
289
|
-
let href = url;
|
|
290
|
-
url = window.document.createElement('a');
|
|
291
|
-
url.href = href
|
|
292
|
-
}
|
|
293
|
-
return !url.origin || url.origin === this.location.origin;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
350
|
}
|
|
@@ -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.support_service_worker) {
|
|
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
|
/**
|
|
@@ -39,60 +31,53 @@ export default class RuntimeClient {
|
|
|
39
31
|
// ROUTE FOR DATA
|
|
40
32
|
// --------
|
|
41
33
|
let httpMethodName = httpEvent.request.method.toLowerCase();
|
|
42
|
-
|
|
34
|
+
return router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], httpEvent, {}, async event => {
|
|
43
35
|
return remoteFetch(event.request);
|
|
44
36
|
}, remoteFetch);
|
|
45
|
-
if (!(response instanceof httpEvent.Response)) {
|
|
46
|
-
response = new httpEvent.Response(response);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// --------
|
|
50
|
-
// Rendering
|
|
51
|
-
// --------
|
|
52
|
-
if (response.ok && response.headers.contentType === 'application/json') {
|
|
53
|
-
await this.render(httpEvent, response, router);
|
|
54
|
-
await this.scrollIntoView(httpEvent);
|
|
55
|
-
} else if (!response.ok) {
|
|
56
|
-
await this.unrender();
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return response;
|
|
60
37
|
};
|
|
61
|
-
|
|
62
38
|
// --------
|
|
63
39
|
// PIPE THROUGH MIDDLEWARES
|
|
64
40
|
// --------
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
41
|
+
return await (this.cx.middlewares || []).concat(handle).reverse().reduce((next, fn) => {
|
|
42
|
+
return () => fn.call(this.cx, httpEvent, router, next);
|
|
43
|
+
}, null)();
|
|
68
44
|
}
|
|
69
45
|
|
|
70
46
|
// Renderer
|
|
71
|
-
async render(httpEvent, response
|
|
47
|
+
async render(httpEvent, response) {
|
|
72
48
|
let data = await response.json();
|
|
49
|
+
const router = new Router(this.cx, httpEvent.url.pathname);
|
|
73
50
|
return router.route('render', httpEvent, data, async (httpEvent, data) => {
|
|
74
51
|
// --------
|
|
75
52
|
// OOHTML would waiting for DOM-ready in order to be initialized
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
window.document.setState({
|
|
79
|
-
env: 'client',
|
|
80
|
-
onHydration: (httpEvent.detail || {}).srcType === 'init',
|
|
81
|
-
network: this.cx.runtime.network,
|
|
82
|
-
url: this.cx.runtime.location,
|
|
83
|
-
}, { update: true });
|
|
53
|
+
if (window.WebQit.DOM) {
|
|
54
|
+
await new Promise(res => window.WebQit.DOM.ready(res));
|
|
84
55
|
}
|
|
85
|
-
window.document.
|
|
86
|
-
|
|
87
|
-
|
|
56
|
+
if (window.document.state) {
|
|
57
|
+
if (!window.document.state.env) {
|
|
58
|
+
window.document.setState({
|
|
59
|
+
env: 'client',
|
|
60
|
+
onHydration: (httpEvent.detail || {}).srcType === 'init',
|
|
61
|
+
network: this.cx.runtime.network,
|
|
62
|
+
url: this.cx.runtime.location,
|
|
63
|
+
}, { update: true });
|
|
64
|
+
}
|
|
65
|
+
window.document.setState({ data }, { update: 'merge' });
|
|
66
|
+
}
|
|
67
|
+
if (window.document.templates) {
|
|
68
|
+
window.document.body.setAttribute('template', 'routes/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
|
|
69
|
+
await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
|
|
70
|
+
}
|
|
71
|
+
await this.scrollIntoView(httpEvent);
|
|
88
72
|
return window;
|
|
89
73
|
});
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
// Unrender
|
|
93
|
-
async unrender() {
|
|
94
|
-
window.document.
|
|
95
|
-
|
|
77
|
+
async unrender(httpEvent) {
|
|
78
|
+
if (window.document.state) {
|
|
79
|
+
window.document.setState({ data: {} }, { update: 'merge' });
|
|
80
|
+
}
|
|
96
81
|
}
|
|
97
82
|
|
|
98
83
|
// Normalize scroll position
|
|
@@ -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
|
+
}
|