@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.
- package/README.md +1082 -323
- package/package.json +2 -2
- package/src/config-pi/runtime/Client.js +7 -10
- package/src/config-pi/runtime/client/Worker.js +30 -12
- package/src/runtime-pi/Router.js +1 -1
- package/src/runtime-pi/client/Runtime.js +98 -49
- package/src/runtime-pi/client/RuntimeClient.js +12 -40
- package/src/runtime-pi/client/Workport.js +163 -0
- package/src/runtime-pi/client/generate.js +71 -37
- package/src/runtime-pi/client/worker/Worker.js +57 -23
- package/src/runtime-pi/client/worker/Workport.js +80 -0
- package/src/runtime-pi/server/Runtime.js +22 -8
- package/src/runtime-pi/server/RuntimeClient.js +6 -6
- package/src/runtime-pi/util.js +2 -2
- package/test/site/package.json +9 -0
- package/test/site/public/bundle.html +3 -0
- package/test/site/public/bundle.html.json +1 -0
- package/test/site/public/bundle.js +1 -1
- package/test/site/public/bundle.js.gz +0 -0
- package/test/site/public/bundle.webflo.js +8 -8
- package/test/site/public/bundle.webflo.js.gz +0 -0
- package/test/site/public/index.html +5 -5
- package/test/site/public/index1.html +35 -0
- package/test/site/public/page-2/bundle.js +1 -1
- package/test/site/public/page-2/bundle.js.gz +0 -0
- package/test/site/public/page-2/index.html +3 -4
- package/test/site/public/page-3/logo-130x130.png +0 -0
- package/test/site/public/page-4/subpage/bundle.js +1 -1
- package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
- package/test/site/public/sparoots.json +5 -0
- package/test/site/public/worker.js +1 -1
- package/test/site/public/worker.js.gz +0 -0
- package/test/site/server/index.js +14 -6
- package/docker/Dockerfile +0 -26
- package/docker/README.md +0 -77
- 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.
|
|
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
|
|
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
|
-
|
|
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: '
|
|
68
|
-
type: '
|
|
69
|
-
message: '[
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
26
|
+
default_fetching_strategy: 'network-first',
|
|
27
|
+
network_first_urls: [],
|
|
27
28
|
cache_first_urls: [],
|
|
28
29
|
network_only_urls: [],
|
|
29
|
-
|
|
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: '
|
|
55
|
-
type: '
|
|
56
|
-
message: '
|
|
57
|
-
|
|
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: '
|
|
73
|
-
type: 'list',
|
|
74
|
-
message: 'Specify URLs for a "
|
|
75
|
-
initial: (config.
|
|
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',
|
package/src/runtime-pi/Router.js
CHANGED
|
@@ -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.
|
|
189
|
-
return prev && !match(
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
224
|
-
|
|
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
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
75
|
-
let data =
|
|
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({
|
|
65
|
+
window.document.setState({ data }, { update: 'merge' });
|
|
92
66
|
}
|
|
93
67
|
if (window.document.templates) {
|
|
94
|
-
window.document.body.setAttribute('template', '
|
|
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({
|
|
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
|
+
}
|