@webqit/webflo 0.11.21 → 0.11.24

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 (86) hide show
  1. package/.gitignore +7 -7
  2. package/LICENSE +20 -20
  3. package/README.md +2074 -2071
  4. package/package.json +82 -82
  5. package/src/Context.js +79 -79
  6. package/src/config-pi/deployment/Env.js +69 -69
  7. package/src/config-pi/deployment/Layout.js +65 -65
  8. package/src/config-pi/deployment/Origins.js +133 -133
  9. package/src/config-pi/deployment/Virtualization.js +65 -65
  10. package/src/config-pi/deployment/index.js +17 -17
  11. package/src/config-pi/index.js +15 -15
  12. package/src/config-pi/runtime/Client.js +101 -101
  13. package/src/config-pi/runtime/Server.js +128 -128
  14. package/src/config-pi/runtime/client/Worker.js +135 -135
  15. package/src/config-pi/runtime/client/index.js +11 -11
  16. package/src/config-pi/runtime/index.js +17 -17
  17. package/src/config-pi/runtime/server/Headers.js +77 -77
  18. package/src/config-pi/runtime/server/Redirects.js +73 -73
  19. package/src/config-pi/runtime/server/index.js +13 -13
  20. package/src/config-pi/static/Manifest.js +321 -321
  21. package/src/config-pi/static/Ssg.js +51 -51
  22. package/src/config-pi/static/index.js +13 -13
  23. package/src/deployment-pi/index.js +10 -10
  24. package/src/deployment-pi/origins/index.js +215 -215
  25. package/src/index.js +19 -19
  26. package/src/runtime-pi/Router.js +131 -131
  27. package/src/runtime-pi/client/Context.js +6 -6
  28. package/src/runtime-pi/client/Router.js +47 -47
  29. package/src/runtime-pi/client/Runtime.js +357 -341
  30. package/src/runtime-pi/client/RuntimeClient.js +98 -98
  31. package/src/runtime-pi/client/Storage.js +56 -56
  32. package/src/runtime-pi/client/Url.js +205 -205
  33. package/src/runtime-pi/client/Workport.js +163 -163
  34. package/src/runtime-pi/client/generate.js +467 -467
  35. package/src/runtime-pi/client/index.js +23 -23
  36. package/src/runtime-pi/client/oohtml/full.js +6 -6
  37. package/src/runtime-pi/client/oohtml/namespacing.js +6 -6
  38. package/src/runtime-pi/client/oohtml/scripting.js +7 -7
  39. package/src/runtime-pi/client/oohtml/templating.js +7 -7
  40. package/src/runtime-pi/client/whatwag.js +27 -27
  41. package/src/runtime-pi/client/worker/Context.js +6 -6
  42. package/src/runtime-pi/client/worker/Worker.js +291 -291
  43. package/src/runtime-pi/client/worker/WorkerClient.js +46 -46
  44. package/src/runtime-pi/client/worker/Workport.js +79 -79
  45. package/src/runtime-pi/client/worker/index.js +23 -23
  46. package/src/runtime-pi/index.js +13 -13
  47. package/src/runtime-pi/server/Context.js +15 -15
  48. package/src/runtime-pi/server/Router.js +157 -157
  49. package/src/runtime-pi/server/Runtime.js +547 -547
  50. package/src/runtime-pi/server/RuntimeClient.js +112 -112
  51. package/src/runtime-pi/server/index.js +23 -23
  52. package/src/runtime-pi/server/whatwag.js +35 -35
  53. package/src/runtime-pi/util.js +162 -162
  54. package/src/runtime-pi/xFormData.js +59 -59
  55. package/src/runtime-pi/xHeaders.js +87 -87
  56. package/src/runtime-pi/xHttpEvent.js +92 -92
  57. package/src/runtime-pi/xHttpMessage.js +179 -179
  58. package/src/runtime-pi/xRequest.js +73 -73
  59. package/src/runtime-pi/xRequestHeaders.js +94 -94
  60. package/src/runtime-pi/xResponse.js +68 -68
  61. package/src/runtime-pi/xResponseHeaders.js +109 -109
  62. package/src/runtime-pi/xURL.js +110 -110
  63. package/src/runtime-pi/xfetch.js +6 -6
  64. package/src/services-pi/certbot/http-auth-hook.js +22 -22
  65. package/src/services-pi/certbot/http-cleanup-hook.js +22 -22
  66. package/src/services-pi/certbot/index.js +79 -79
  67. package/src/services-pi/index.js +8 -8
  68. package/src/static-pi/index.js +10 -10
  69. package/src/webflo.js +31 -31
  70. package/test/index.test.js +26 -25
  71. package/test/site/package.json +9 -9
  72. package/test/site/public/bundle.html +5 -5
  73. package/test/site/public/bundle.html.json +3 -3
  74. package/test/site/public/bundle.js +2 -2
  75. package/test/site/public/bundle.webflo.js +15 -15
  76. package/test/site/public/index.html +29 -29
  77. package/test/site/public/index1.html +34 -34
  78. package/test/site/public/page-2/bundle.html +4 -4
  79. package/test/site/public/page-2/bundle.js +2 -2
  80. package/test/site/public/page-2/index.html +45 -45
  81. package/test/site/public/page-2/main.html +2 -2
  82. package/test/site/public/page-4/subpage/bundle.js +2 -2
  83. package/test/site/public/page-4/subpage/index.html +30 -30
  84. package/test/site/public/sparoots.json +4 -4
  85. package/test/site/public/worker.js +3 -3
  86. package/test/site/server/index.js +15 -15
@@ -1,342 +1,358 @@
1
-
2
- /**
3
- * @imports
4
- */
5
- import { _before, _toTitle } from '@webqit/util/str/index.js';
6
- import { Observer } from '@webqit/oohtml-ssr/apis.js';
7
- import Storage from './Storage.js';
8
- import Url from './Url.js';
9
- import { wwwFormUnserialize, wwwFormSet, wwwFormSerialize } from '../util.js';
10
- import * as whatwag from './whatwag.js';
11
- import xURL from '../xURL.js';
12
- import xFormData from "../xFormData.js";
13
- import xRequestHeaders from "../xRequestHeaders.js";
14
- import xResponseHeaders from "../xResponseHeaders.js";
15
- import xRequest from "../xRequest.js";
16
- import xResponse from "../xResponse.js";
17
- import xfetch from '../xfetch.js';
18
- import xHttpEvent from '../xHttpEvent.js';
19
- import Workport from './Workport.js';
20
-
21
- const URL = xURL(whatwag.URL);
22
- const FormData = xFormData(whatwag.FormData);
23
- const ReadableStream = whatwag.ReadableStream;
24
- const RequestHeaders = xRequestHeaders(whatwag.Headers);
25
- const ResponseHeaders = xResponseHeaders(whatwag.Headers);
26
- const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
27
- const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
28
- const fetch = xfetch(whatwag.fetch);
29
- const HttpEvent = xHttpEvent(Request, Response, URL);
30
-
31
- export {
32
- URL,
33
- FormData,
34
- ReadableStream,
35
- RequestHeaders,
36
- ResponseHeaders,
37
- Request,
38
- Response,
39
- fetch,
40
- HttpEvent,
41
- Observer,
42
- }
43
-
44
- export default class Runtime {
45
-
46
- /**
47
- * Runtime
48
- *
49
- * @param Object cx
50
- * @param Function clientCallback
51
- *
52
- * @return void
53
- */
54
- constructor(cx, clientCallback) {
55
-
56
- // ---------------
57
- this.cx = cx;
58
- this.clients = new Map;
59
- // ---------------
60
- this.cx.runtime = this;
61
- let client = clientCallback(this.cx, '*');
62
- if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
63
- this.clients.set('*', client);
64
-
65
- // -----------------------
66
- // Initialize location
67
- Observer.set(this, 'location', new Url(window.document.location));
68
- // -----------------------
69
- // Syndicate changes to the browser;s location bar
70
- Observer.observe(this.location, [[ 'href' ]], ([e]) => {
71
- if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
72
- // Already from a "popstate" event as above, so don't push again
73
- return;
74
- }
75
- if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
76
- window.history.replaceState(window.history.state, '', this.location.href);
77
- } else {
78
- try { window.history.pushState(window.history.state, '', this.location.href); } catch(e) {}
79
- }
80
- }, { diff: true });
81
-
82
- // -----------------------
83
- // This event is triggered by
84
- // either the browser back button,
85
- // the window.history.back(),
86
- // the window.history.forward(),
87
- // or the window.history.go() action.
88
- window.addEventListener('popstate', e => {
89
- // Needed to allow window.document.location
90
- // to update to window.location
91
- window.setTimeout(() => {
92
- this.go(Url.copy(window.document.location), {}, { src: window.document.location, srcType: 'history', });
93
- }, 0);
94
- });
95
-
96
- // -----------------------
97
- // Capture all link-clicks
98
- // and fire to this router.
99
- window.addEventListener('click', e => {
100
- var anchor = e.target.closest('a');
101
- if (!anchor || !anchor.href) return;
102
- if (!anchor.target && !anchor.download && this.isSpaRoute(anchor, e)) {
103
- // Publish everything, including hash
104
- this.go(Url.copy(anchor), {}, { src: anchor, srcType: 'link', });
105
- if (!(_before(window.document.location.href, '#') === _before(anchor.href, '#') && anchor.href.includes('#'))) {
106
- e.preventDefault();
107
- }
108
- }
109
- });
110
-
111
- // -----------------------
112
- // Capture all form-submit
113
- // and fire to this router.
114
- window.addEventListener('submit', e => {
115
- const form = e.target.closest('form'), submitter = e.submitter;
116
- const submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
117
- params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
118
- return params;
119
- }, {});
120
- // We support method hacking
121
- submitParams.method = (submitter && submitter.dataset.formmethod) || form.dataset.method || submitParams.method;
122
- submitParams.submitter = submitter;
123
- // ---------------
124
- var actionEl = window.document.createElement('a');
125
- actionEl.href = submitParams.action;
126
- // ---------------
127
- // If not targeted and same origin...
128
- if (!submitParams.target && this.isSpaRoute(actionEl, e)) {
129
- // Build data
130
- var formData = new FormData(form);
131
- if ((submitter || {}).name) {
132
- formData.set(submitter.name, submitter.value);
133
- }
134
- if (submitParams.method.toUpperCase() === 'GET') {
135
- var query = wwwFormUnserialize(actionEl.search);
136
- Array.from(formData.entries()).forEach(_entry => {
137
- wwwFormSet(query, _entry[0], _entry[1], false);
138
- });
139
- actionEl.search = wwwFormSerialize(query);
140
- formData = null;
141
- }
142
- this.go(Url.copy(actionEl), {
143
- method: submitParams.method,
144
- body: formData,
145
- }, { ...submitParams, src: form, srcType: 'form', });
146
- if (!(_before(window.document.location.href, '#') === _before(actionEl.href, '#') && actionEl.href.includes('#'))) {
147
- e.preventDefault();
148
- }
149
- }
150
- });
151
-
152
- // -----------------------
153
- // Initialize network
154
- Observer.set(this, 'network', {});
155
- window.addEventListener('online', () => Observer.set(this.network, 'connectivity', 'online'));
156
- window.addEventListener('offline', () => Observer.set(this.network, 'connectivity', 'offline'));
157
-
158
- // -----------------------
159
- // Service Worker && COMM
160
- if (this.cx.params.service_worker_support) {
161
- let workport = new Workport(this.cx.params.worker_filename, { scope: this.cx.params.worker_scope, startMessages: true });
162
- Observer.set(this, 'workport', workport);
163
- workport.messaging.listen(async evt => {
164
- let responsePort = evt.ports[0];
165
- let client = this.clients.get('*');
166
- let response = client.alert && await client.alert(evt);
167
- if (responsePort) {
168
- if (response instanceof Promise) {
169
- response.then(data => {
170
- responsePort.postMessage(data);
171
- });
172
- } else {
173
- responsePort.postMessage(response);
174
- }
175
- }
176
- });
177
- }
178
-
179
- // ---------------
180
- this.go(this.location, {}, { srcType: 'init' });
181
- // ---------------
182
- }
183
-
184
- /**
185
- * History object
186
- */
187
- get history() {
188
- return window.history;
189
- }
190
-
191
- // Check is-route
192
- isSpaRoute(url, e) {
193
- url = typeof url === 'string' ? new whatwag.URL(url) : url;
194
- if (url.origin && url.origin !== this.location.origin) return false;
195
- if (e && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) return false;
196
- if (!this.cx.params.routing) return true;
197
- if (this.cx.params.routing.targets === false/** explicit false means disabled */) return false;
198
- let b = url.pathname.split('/').filter(s => s);
199
- const match = a => {
200
- a = a.split('/').filter(s => s);
201
- return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
202
- };
203
- return match(this.cx.params.routing.root) && this.cx.params.routing.subroots.reduce((prev, subroot) => {
204
- return prev && !match(subroot);
205
- }, true);
206
- }
207
-
208
- // Generates request object
209
- generateRequest(href, init) {
210
- return new Request(href, {
211
- signal: this._abortController.signal,
212
- ...init,
213
- headers: {
214
- 'Accept': 'application/json',
215
- 'X-Redirect-Policy': 'manual-when-cross-spa',
216
- 'X-Redirect-Code': this._xRedirectCode,
217
- 'X-Powered-By': '@webqit/webflo',
218
- ...(init.headers || {}),
219
- },
220
- });
221
- }
222
-
223
- // Generates session object
224
- getSession(e, id = null, persistent = false) {
225
- return Storage(id, persistent);
226
- }
227
-
228
- /**
229
- * Performs a request.
230
- *
231
- * @param object|string href
232
- * @param object init
233
- * @param object src
234
- *
235
- * @return Response
236
- */
237
- async go(url, init = {}, detail = {}) {
238
- url = typeof url === 'string' ? new whatwag.URL(url) : url;
239
- init = { referrer: this.location.href, ...init };
240
- // ------------
241
- // Put his forward before instantiating a request and aborting previous
242
- // Same-page hash-links clicks on chrome recurse here from histroy popstate
243
- if (detail.srcType !== 'init' && (_before(url.href, '#') === _before(init.referrer, '#') && (init.method || 'GET').toUpperCase() === 'GET')) {
244
- return new Response(null, { status: 304 }); // Not Modified
245
- }
246
- // ------------
247
- if (this._abortController) {
248
- this._abortController.abort();
249
- }
250
- this._abortController = new AbortController();
251
- this._xRedirectCode = 200;
252
- // ------------
253
- // States
254
- // ------------
255
- Observer.set(this.network, 'error', null);
256
- Observer.set(this.network, 'requesting', { ...init, ...detail });
257
- if (['link', 'form'].includes(detail.srcType)) {
258
- detail.src.state && (detail.src.state.active = true);
259
- detail.submitter && detail.submitter.state && (detail.submitter.state.active = true);
260
- }
261
- // ------------
262
- // Run
263
- // ------------
264
- // The request object
265
- let request = this.generateRequest(url.href, init);
266
- // The navigation event
267
- let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
268
- // Response
269
- let client = this.clients.get('*'), response, finalResponse;
270
- try {
271
- // ------------
272
- // Response
273
- // ------------
274
- response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
275
- finalResponse = this.handleResponse(httpEvent, response);
276
- // ------------
277
- // Address bar
278
- // ------------
279
- if (response.redirected) {
280
- Observer.set(this.location, { href: response.url }, { detail: { redirected: true }, });
281
- } else if (![302, 301].includes(finalResponse.status)) {
282
- Observer.set(this.location, url);
283
- }
284
- // ------------
285
- // States
286
- // ------------
287
- Observer.set(this.network, 'requesting', null);
288
- if (['link', 'form'].includes(detail.srcType)) {
289
- detail.src.state && (detail.src.state.active = false);
290
- detail.submitter && detail.submitter.state && (detail.submitter.state.active = false);
291
- }
292
- // ------------
293
- // Rendering
294
- // ------------
295
- if (finalResponse.ok && finalResponse.headers.contentType === 'application/json') {
296
- client.render && await client.render(httpEvent, finalResponse);
297
- } else if (!finalResponse.ok) {
298
- if ([404, 500].includes(finalResponse.status)) {
299
- Observer.set(this.network, 'error', new Error(finalResponse.statusText, { cause: finalResponse.status }));
300
- }
301
- client.unrender && await client.unrender(httpEvent);
302
- }
303
- } catch(e) {
304
- console.error(e);
305
- Observer.set(this.network, 'error', { ...e, retry: () => this.go(url, init = {}, detail) });
306
- finalResponse = new Response(null, { status: 500, statusText: e.message });
307
- }
308
- // ------------
309
- // Return value
310
- return finalResponse;
311
- }
312
-
313
- // Initiates remote fetch and sets the status
314
- remoteFetch(request, ...args) {
315
- let href = typeof request === 'string' ? request : (request.url || request.href);
316
- Observer.set(this.network, 'remote', href);
317
- let _response = fetch(request, ...args);
318
- // This catch() is NOT intended to handle failure of the fetch
319
- _response.catch(e => Observer.set(this.network, 'error', e));
320
- // Return xResponse
321
- return _response.then(async response => {
322
- // Stop loading status
323
- Observer.set(this.network, 'remote', null);
324
- return new Response(response);
325
- });
326
- }
327
-
328
- // Handles response object
329
- handleResponse(e, response) {
330
- if (!(response instanceof Response)) { response = new Response(response); }
331
- if (!response.redirected) {
332
- let location = response.headers.get('Location');
333
- if (location && response.status === this._xRedirectCode) {
334
- response.attrs.status = parseInt(response.headers.get('X-Redirect-Code'));
335
- Observer.set(this.network, 'redirecting', location);
336
- window.location = location;
337
- }
338
- }
339
- return response;
340
- }
341
-
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import { _before, _toTitle } from '@webqit/util/str/index.js';
6
+ import { Observer } from '@webqit/oohtml-ssr/apis.js';
7
+ import Storage from './Storage.js';
8
+ import Url from './Url.js';
9
+ import { wwwFormUnserialize, wwwFormSet, wwwFormSerialize } from '../util.js';
10
+ import * as whatwag from './whatwag.js';
11
+ import xURL from '../xURL.js';
12
+ import xFormData from "../xFormData.js";
13
+ import xRequestHeaders from "../xRequestHeaders.js";
14
+ import xResponseHeaders from "../xResponseHeaders.js";
15
+ import xRequest from "../xRequest.js";
16
+ import xResponse from "../xResponse.js";
17
+ import xfetch from '../xfetch.js';
18
+ import xHttpEvent from '../xHttpEvent.js';
19
+ import Workport from './Workport.js';
20
+
21
+ const URL = xURL(whatwag.URL);
22
+ const FormData = xFormData(whatwag.FormData);
23
+ const ReadableStream = whatwag.ReadableStream;
24
+ const RequestHeaders = xRequestHeaders(whatwag.Headers);
25
+ const ResponseHeaders = xResponseHeaders(whatwag.Headers);
26
+ const Request = xRequest(whatwag.Request, RequestHeaders, FormData, whatwag.Blob);
27
+ const Response = xResponse(whatwag.Response, ResponseHeaders, FormData, whatwag.Blob);
28
+ const fetch = xfetch(whatwag.fetch);
29
+ const HttpEvent = xHttpEvent(Request, Response, URL);
30
+
31
+ export {
32
+ URL,
33
+ FormData,
34
+ ReadableStream,
35
+ RequestHeaders,
36
+ ResponseHeaders,
37
+ Request,
38
+ Response,
39
+ fetch,
40
+ HttpEvent,
41
+ Observer,
42
+ }
43
+
44
+ export default class Runtime {
45
+
46
+ /**
47
+ * Runtime
48
+ *
49
+ * @param Object cx
50
+ * @param Function clientCallback
51
+ *
52
+ * @return void
53
+ */
54
+ constructor(cx, clientCallback) {
55
+ // ---------------
56
+ this.cx = cx;
57
+ this.clients = new Map;
58
+ // ---------------
59
+ this.cx.runtime = this;
60
+ let client = clientCallback(this.cx, '*');
61
+ if (!client || !client.handle) throw new Error(`Application instance must define a ".handle()" method.`);
62
+ this.clients.set('*', client);
63
+
64
+ // -----------------------
65
+ // Initialize location
66
+ Observer.set(this, 'location', new Url(window.document.location));
67
+ // -----------------------
68
+ // Syndicate changes to the browser;s location bar
69
+ Observer.observe(this.location, [[ 'href' ]], ([e]) => {
70
+ if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
71
+ // Already from a "popstate" event as above, so don't push again
72
+ return;
73
+ }
74
+ if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
75
+ window.history.replaceState(window.history.state, '', this.location.href);
76
+ } else {
77
+ try { window.history.pushState(window.history.state, '', this.location.href); } catch(e) {}
78
+ }
79
+ }, { diff: true });
80
+
81
+ // -----------------------
82
+ // This event is triggered by
83
+ // either the browser back button,
84
+ // the window.history.back(),
85
+ // the window.history.forward(),
86
+ // or the window.history.go() action.
87
+ window.addEventListener('popstate', e => {
88
+ // Needed to allow window.document.location
89
+ // to update to window.location
90
+ window.setTimeout(() => {
91
+ this.go(Url.copy(window.document.location), {}, { src: window.document.location, srcType: 'history', });
92
+ }, 0);
93
+ });
94
+
95
+ // -----------------------
96
+ // Capture all link-clicks
97
+ // and fire to this router.
98
+ window.addEventListener('click', e => {
99
+ var anchorEl = e.target.closest('a');
100
+ if (!anchorEl || !anchorEl.href) return;
101
+ if (!anchorEl.target && !anchorEl.download && this.isSpaRoute(anchorEl, e)) {
102
+ // Publish everything, including hash
103
+ this.go(Url.copy(anchorEl), {}, { src: anchorEl, srcType: 'link', });
104
+ if (!this.isHashAction(anchorEl)) {
105
+ e.preventDefault();
106
+ }
107
+ }
108
+ });
109
+
110
+ // -----------------------
111
+ // Capture all form-submit
112
+ // and fire to this router.
113
+ window.addEventListener('submit', e => {
114
+ const form = e.target.closest('form'), submitter = e.submitter;
115
+ const submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
116
+ params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
117
+ return params;
118
+ }, {});
119
+ // We support method hacking
120
+ submitParams.method = (submitter && submitter.dataset.formmethod) || form.dataset.method || submitParams.method;
121
+ submitParams.submitter = submitter;
122
+ // ---------------
123
+ var actionEl = window.document.createElement('a');
124
+ actionEl.href = submitParams.action;
125
+ // ---------------
126
+ // If not targeted and same origin...
127
+ if (!submitParams.target && this.isSpaRoute(actionEl, e)) {
128
+ // Build data
129
+ var formData = new FormData(form);
130
+ if ((submitter || {}).name) {
131
+ formData.set(submitter.name, submitter.value);
132
+ }
133
+ if (submitParams.method.toUpperCase() === 'GET') {
134
+ var query = wwwFormUnserialize(actionEl.search);
135
+ Array.from(formData.entries()).forEach(_entry => {
136
+ wwwFormSet(query, _entry[0], _entry[1], false);
137
+ });
138
+ actionEl.search = wwwFormSerialize(query);
139
+ formData = null;
140
+ }
141
+ this.go(Url.copy(actionEl), {
142
+ method: submitParams.method,
143
+ body: formData,
144
+ }, { ...submitParams, src: form, srcType: 'form', });
145
+ if (!this.isHashAction(actionEl)) {
146
+ e.preventDefault();
147
+ }
148
+ }
149
+ });
150
+
151
+ // -----------------------
152
+ // Initialize network
153
+ Observer.set(this, 'network', {});
154
+ window.addEventListener('online', () => Observer.set(this.network, 'connectivity', 'online'));
155
+ window.addEventListener('offline', () => Observer.set(this.network, 'connectivity', 'offline'));
156
+
157
+ // -----------------------
158
+ // Service Worker && COMM
159
+ if (this.cx.params.service_worker_support) {
160
+ let workport = new Workport(this.cx.params.worker_filename, { scope: this.cx.params.worker_scope, startMessages: true });
161
+ Observer.set(this, 'workport', workport);
162
+ workport.messaging.listen(async evt => {
163
+ let responsePort = evt.ports[0];
164
+ let client = this.clients.get('*');
165
+ let response = client.alert && await client.alert(evt);
166
+ if (responsePort) {
167
+ if (response instanceof Promise) {
168
+ response.then(data => {
169
+ responsePort.postMessage(data);
170
+ });
171
+ } else {
172
+ responsePort.postMessage(response);
173
+ }
174
+ }
175
+ });
176
+ }
177
+
178
+ // ---------------
179
+ this.go(this.location, {}, { srcType: 'init' });
180
+ // ---------------
181
+ }
182
+
183
+ /**
184
+ * History object
185
+ */
186
+ get history() {
187
+ return window.history;
188
+ }
189
+
190
+ // Check is-hash-action
191
+ isHashAction(urlObj) {
192
+ const isHashNav = _before(window.document.location.href, '#') === _before(urlObj.href, '#') && urlObj.href.includes('#');
193
+ return isHashNav && urlObj.hash.length > 1 && document.querySelector(urlObj.hash);
194
+ }
195
+
196
+ // Check is-spa-route
197
+ isSpaRoute(url, e = undefined) {
198
+ url = typeof url === 'string' ? new whatwag.URL(url) : url;
199
+ if (url.origin && url.origin !== this.location.origin) return false;
200
+ if (e && (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)) return false;
201
+ if (!this.cx.params.routing) return true;
202
+ if (this.cx.params.routing.targets === false/** explicit false means disabled */) return false;
203
+ let b = url.pathname.split('/').filter(s => s);
204
+ const match = a => {
205
+ a = a.split('/').filter(s => s);
206
+ return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
207
+ };
208
+ return match(this.cx.params.routing.root) && this.cx.params.routing.subroots.reduce((prev, subroot) => {
209
+ return prev && !match(subroot);
210
+ }, true);
211
+ }
212
+
213
+ // Generates request object
214
+ generateRequest(href, init) {
215
+ return new Request(href, {
216
+ signal: this._abortController.signal,
217
+ ...init,
218
+ headers: {
219
+ 'Accept': 'application/json',
220
+ 'X-Redirect-Policy': 'manual-when-cross-spa',
221
+ 'X-Redirect-Code': this._xRedirectCode,
222
+ 'X-Powered-By': '@webqit/webflo',
223
+ ...(init.headers || {}),
224
+ },
225
+ });
226
+ }
227
+
228
+ // Generates session object
229
+ getSession(e, id = null, persistent = false) {
230
+ return Storage(id, persistent);
231
+ }
232
+
233
+ /**
234
+ * Performs a request.
235
+ *
236
+ * @param object|string href
237
+ * @param object init
238
+ * @param object src
239
+ *
240
+ * @return Response
241
+ */
242
+ async go(url, init = {}, detail = {}) {
243
+ url = typeof url === 'string' ? new whatwag.URL(url) : url;
244
+ init = { referrer: this.location.href, ...init };
245
+ // ------------
246
+ // Put his forward before instantiating a request and aborting previous
247
+ // Same-page hash-links clicks on chrome recurse here from histroy popstate
248
+ if (detail.srcType !== 'init' && (_before(url.href, '#') === _before(init.referrer, '#') && (init.method || 'GET').toUpperCase() === 'GET')) {
249
+ return new Response(null, { status: 304 }); // Not Modified
250
+ }
251
+ // ------------
252
+ if (this._abortController) {
253
+ this._abortController.abort();
254
+ }
255
+ this._abortController = new AbortController();
256
+ this._xRedirectCode = 200;
257
+ // ------------
258
+ // States
259
+ // ------------
260
+ Observer.set(this.network, 'error', null);
261
+ Observer.set(this.network, 'requesting', { ...init, ...detail });
262
+ if (['link', 'form'].includes(detail.srcType)) {
263
+ detail.src.state && (detail.src.state.active = true);
264
+ detail.submitter && detail.submitter.state && (detail.submitter.state.active = true);
265
+ }
266
+ // ------------
267
+ // Run
268
+ // ------------
269
+ // The request object
270
+ let request = this.generateRequest(url.href, init);
271
+ // The navigation event
272
+ let httpEvent = new HttpEvent(request, detail, (id = null, persistent = false) => this.getSession(httpEvent, id, persistent));
273
+ // Response
274
+ let client = this.clients.get('*'), response, finalResponse;
275
+ try {
276
+ // ------------
277
+ // Response
278
+ // ------------
279
+ response = await client.handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
280
+ finalResponse = this.handleResponse(httpEvent, response);
281
+ // ------------
282
+ // Address bar
283
+ // ------------
284
+ if (response.redirected) {
285
+ Observer.set(this.location, { href: response.url }, { detail: { redirected: true }, });
286
+ Observer.set(this.network, 'requesting', null);
287
+ } else if (![302, 301].includes(finalResponse.status)) {
288
+ Observer.set(this.location, Url.copy(url)/* copy() is important */);
289
+ Observer.set(this.network, 'requesting', null);
290
+ }
291
+ // ------------
292
+ // States
293
+ // ------------
294
+ if (['link', 'form'].includes(detail.srcType)) {
295
+ detail.src.state && (detail.src.state.active = false);
296
+ detail.submitter && detail.submitter.state && (detail.submitter.state.active = false);
297
+ }
298
+ // ------------
299
+ // Rendering
300
+ // ------------
301
+ if (finalResponse.ok && finalResponse.headers.contentType === 'application/json') {
302
+ client.render && await client.render(httpEvent, finalResponse);
303
+ } else if (!finalResponse.ok) {
304
+ if ([404, 500].includes(finalResponse.status)) {
305
+ Observer.set(this.network, 'error', new Error(finalResponse.statusText, { cause: finalResponse.status }));
306
+ }
307
+ client.unrender && await client.unrender(httpEvent);
308
+ }
309
+ } catch(e) {
310
+ console.error(e);
311
+ Observer.set(this.network, 'error', { ...e, retry: () => this.go(url, init = {}, detail) });
312
+ finalResponse = new Response(null, { status: 500, statusText: e.message });
313
+ }
314
+ // ------------
315
+ // Return value
316
+ return finalResponse;
317
+ }
318
+
319
+ // Initiates remote fetch and sets the status
320
+ remoteFetch(request, ...args) {
321
+ let href = typeof request === 'string' ? request : (request.url || request.href);
322
+ Observer.set(this.network, 'remote', href);
323
+ let _response = fetch(request, ...args);
324
+ // This catch() is NOT intended to handle failure of the fetch
325
+ _response.catch(e => Observer.set(this.network, 'error', e));
326
+ // Return xResponse
327
+ return _response.then(async response => {
328
+ // Stop loading status
329
+ Observer.set(this.network, 'remote', null);
330
+ return new Response(response);
331
+ });
332
+ }
333
+
334
+ // Handles response object
335
+ handleResponse(e, response) {
336
+ if (!(response instanceof Response)) { response = new Response(response); }
337
+ if (!response.redirected) {
338
+ let location = response.headers.get('Location');
339
+ if (location) {
340
+ let xActualRedirectCode = parseInt(response.headers.get('X-Redirect-Code'));
341
+ if (xActualRedirectCode && response.status === this._xRedirectCode) {
342
+ response.attrs.status = xActualRedirectCode;
343
+ Observer.set(this.network, 'redirecting', location);
344
+ window.location = location;
345
+ } else if ([302,301].includes(response.status)) {
346
+ if (!this.isSpaRoute(location)) {
347
+ Observer.set(this.network, 'redirecting', location);
348
+ window.location = location;
349
+ } else {
350
+ this.go(location, {}, { srcType: 'rdr' });
351
+ }
352
+ }
353
+ }
354
+ }
355
+ return response;
356
+ }
357
+
342
358
  }