@webqit/webflo 0.10.4 → 0.11.1

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 (51) hide show
  1. package/README.md +1490 -52
  2. package/bundle.html.json +1665 -0
  3. package/package.json +2 -2
  4. package/src/Context.js +1 -1
  5. package/src/config-pi/runtime/Client.js +37 -9
  6. package/src/config-pi/runtime/Server.js +24 -8
  7. package/src/config-pi/runtime/client/Worker.js +30 -12
  8. package/src/runtime-pi/Router.js +1 -1
  9. package/src/runtime-pi/client/Runtime.js +116 -62
  10. package/src/runtime-pi/client/RuntimeClient.js +28 -43
  11. package/src/runtime-pi/client/Workport.js +163 -0
  12. package/src/runtime-pi/client/generate.js +282 -74
  13. package/src/runtime-pi/client/{generate.oohtml.js → oohtml/full.js} +0 -0
  14. package/src/runtime-pi/client/oohtml/namespacing.js +7 -0
  15. package/src/runtime-pi/client/oohtml/scripting.js +8 -0
  16. package/src/runtime-pi/client/oohtml/templating.js +8 -0
  17. package/src/runtime-pi/client/worker/Worker.js +58 -24
  18. package/src/runtime-pi/client/worker/Workport.js +80 -0
  19. package/src/runtime-pi/server/Router.js +2 -2
  20. package/src/runtime-pi/server/Runtime.js +30 -11
  21. package/src/runtime-pi/server/RuntimeClient.js +24 -14
  22. package/src/runtime-pi/util.js +2 -2
  23. package/test/site/package.json +9 -0
  24. package/test/site/public/bundle.html +6 -0
  25. package/test/site/public/bundle.html.json +4 -0
  26. package/test/site/public/bundle.js +2 -0
  27. package/test/site/public/bundle.js.gz +0 -0
  28. package/test/site/public/bundle.webflo.js +15 -0
  29. package/test/site/public/bundle.webflo.js.gz +0 -0
  30. package/test/site/public/index.html +30 -0
  31. package/test/site/public/index1.html +35 -0
  32. package/test/site/public/page-2/bundle.html +5 -0
  33. package/test/site/public/page-2/bundle.html.json +1 -0
  34. package/test/site/public/page-2/bundle.js +2 -0
  35. package/test/site/public/page-2/bundle.js.gz +0 -0
  36. package/test/site/public/page-2/index.html +46 -0
  37. package/test/site/public/page-2/logo-130x130.png +0 -0
  38. package/test/site/public/page-2/main.html +3 -0
  39. package/test/site/public/page-3/logo-130x130.png +0 -0
  40. package/test/site/public/page-4/subpage/bundle.html +0 -0
  41. package/test/site/public/page-4/subpage/bundle.html.json +1 -0
  42. package/test/site/public/page-4/subpage/bundle.js +2 -0
  43. package/test/site/public/page-4/subpage/bundle.js.gz +0 -0
  44. package/test/site/public/page-4/subpage/index.html +31 -0
  45. package/test/site/public/sparoots.json +5 -0
  46. package/test/site/public/worker.js +3 -0
  47. package/test/site/public/worker.js.gz +0 -0
  48. package/test/site/server/index.js +16 -0
  49. package/docker/Dockerfile +0 -26
  50. package/docker/README.md +0 -77
  51. 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.4",
15
+ "version": "0.11.1",
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.3",
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",
package/src/Context.js CHANGED
@@ -17,7 +17,7 @@ export default class Context {
17
17
  this[prop] = this.dict[prop];
18
18
  }
19
19
  if (arguments.length > 1) {
20
- this.dict.CWD = CD;
20
+ Object.defineProperty(this.dict, 'CWD', { get: () => CD });
21
21
  }
22
22
  }
23
23
 
@@ -23,8 +23,10 @@ export default class Client extends Dotfile {
23
23
  withDefaults(config) {
24
24
  return _merge(true, {
25
25
  bundle_filename: 'bundle.js',
26
- support_oohtml: true,
27
- support_service_worker: true,
26
+ public_base_url: '/',
27
+ spa_routing: true,
28
+ oohtml_support: 'full',
29
+ service_worker_support: true,
28
30
  worker_scope: '/',
29
31
  worker_filename: 'worker.js',
30
32
  }, config);
@@ -32,6 +34,16 @@ export default class Client extends Dotfile {
32
34
 
33
35
  // Questions generator
34
36
  questions(config, choices = {}) {
37
+ // Choices
38
+ const CHOICES = _merge({
39
+ oohtml_support: [
40
+ {value: 'full', title: 'Full'},
41
+ {value: 'namespacing', title: 'namespacing'},
42
+ {value: 'scripting', title: 'scripting'},
43
+ {value: 'templating', title: 'templating'},
44
+ {value: 'none', title: 'none'},
45
+ ],
46
+ }, choices);
35
47
  // Questions
36
48
  return [
37
49
  {
@@ -41,30 +53,46 @@ export default class Client extends Dotfile {
41
53
  initial: config.bundle_filename,
42
54
  },
43
55
  {
44
- name: 'support_oohtml',
56
+ name: 'public_base_url',
57
+ type: 'text',
58
+ message: '[public_base_url]: Enter the base-URL for public resource URLs',
59
+ initial: DATA.public_base_url,
60
+ validation: ['important'],
61
+ },
62
+ {
63
+ name: 'spa_routing',
45
64
  type: 'toggle',
46
- message: 'Support rendering with OOHTML? (Adds OOHTML to your app\'s bundle.)',
65
+ message: '[spa_routing]: Enable Single Page Routing Mode',
47
66
  active: 'YES',
48
67
  inactive: 'NO',
49
- initial: config.support_oohtml,
68
+ initial: config.spa_routing,
69
+ validation: ['important'],
70
+ },
71
+ {
72
+ name: 'oohtml_support',
73
+ type: 'select',
74
+ message: '[oohtml_support]: (Adds OOHTML to your app\'s bundle.) Specify OOHTML support level',
75
+ choices: CHOICES.oohtml_support,
76
+ initial: this.indexOfInitial(CHOICES.oohtml_support, config.oohtml_support),
77
+ validation: ['important'],
50
78
  },
51
79
  {
52
- name: 'support_service_worker',
80
+ name: 'service_worker_support',
53
81
  type: 'toggle',
54
82
  message: 'Support Service Worker?',
55
83
  active: 'YES',
56
84
  inactive: 'NO',
57
- initial: config.support_service_worker,
85
+ initial: config.service_worker_support,
58
86
  },
59
87
  {
60
88
  name: 'worker_scope',
61
- type: (prev, answers) => answers.support_service_worker ? 'text' : null,
89
+ type: (prev, answers) => answers.service_worker_support ? 'text' : null,
62
90
  message: 'Specify the Service Worker scope',
63
91
  initial: config.worker_scope,
64
92
  },
65
93
  {
66
94
  name: 'worker_filename',
67
- type: (prev, answers) => answers.support_service_worker ? 'text' : null,
95
+ type: (prev, answers) => answers.service_worker_support ? 'text' : null,
68
96
  message: 'Specify the Service Worker filename',
69
97
  initial: config.worker_filename,
70
98
  },
@@ -29,6 +29,7 @@ export default class Server extends Dotfile {
29
29
  force: false,
30
30
  },
31
31
  force_www: '',
32
+ oohtml_support: 'full',
32
33
  shared: false,
33
34
  }, config);
34
35
  }
@@ -42,13 +43,20 @@ export default class Server extends Dotfile {
42
43
  {value: 'add',},
43
44
  {value: 'remove',},
44
45
  ],
46
+ oohtml_support: [
47
+ {value: 'full', title: 'full'},
48
+ {value: 'namespacing', title: 'namespacing'},
49
+ {value: 'scripting', title: 'scripting'},
50
+ {value: 'templating', title: 'templating'},
51
+ {value: 'none', title: 'none'},
52
+ ],
45
53
  }, choices);
46
54
  // Questions
47
55
  return [
48
56
  {
49
57
  name: 'port',
50
58
  type: 'number',
51
- message: 'Enter port number',
59
+ message: '[port]: Enter port number',
52
60
  initial: config.port,
53
61
  validation: ['important'],
54
62
  },
@@ -62,31 +70,31 @@ export default class Server extends Dotfile {
62
70
  {
63
71
  name: 'port',
64
72
  type: 'number',
65
- message: 'Enter HTTPS port number',
73
+ message: '[port]: Enter HTTPS port number',
66
74
  validation: ['important'],
67
75
  },
68
76
  {
69
77
  name: 'keyfile',
70
78
  type: 'text',
71
- message: 'Enter SSL KEY file',
79
+ message: '[keyfile]: Enter SSL KEY file',
72
80
  validation: ['important'],
73
81
  },
74
82
  {
75
83
  name: 'certfile',
76
84
  type: 'text',
77
- message: 'Enter SSL CERT file',
85
+ message: '[certfile]: Enter SSL CERT file',
78
86
  validation: ['important'],
79
87
  },
80
88
  {
81
89
  name: 'certdoms',
82
90
  type: 'list',
83
- message: 'Enter the CERT domains (comma-separated)',
91
+ message: '[certdoms]: Enter the CERT domains (comma-separated)',
84
92
  validation: ['important'],
85
93
  },
86
94
  {
87
95
  name: 'force',
88
96
  type: 'toggle',
89
- message: 'Force HTTPS?',
97
+ message: '[force]: Force HTTPS?',
90
98
  active: 'YES',
91
99
  inactive: 'NO',
92
100
  },
@@ -95,14 +103,22 @@ export default class Server extends Dotfile {
95
103
  {
96
104
  name: 'force_www',
97
105
  type: 'select',
98
- message: 'Force add/remove "www" on hostname?',
106
+ message: '[force_www]: Force add/remove "www" on hostname?',
99
107
  choices: CHOICES.force_www,
100
108
  initial: this.indexOfInitial(CHOICES.force_www, config.force_www),
101
109
  },
110
+ {
111
+ name: 'oohtml_support',
112
+ type: 'select',
113
+ message: '[oohtml_support]: Specify OOHTML support level',
114
+ choices: CHOICES.oohtml_support,
115
+ initial: this.indexOfInitial(CHOICES.oohtml_support, config.oohtml_support),
116
+ validation: ['important'],
117
+ },
102
118
  {
103
119
  name: 'shared',
104
120
  type: 'toggle',
105
- message: 'Shared server?',
121
+ message: '[shared]: Shared server?',
106
122
  active: 'YES',
107
123
  inactive: 'NO',
108
124
  initial: config.shared,
@@ -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);
@@ -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 && (!anchor.origin || anchor.origin === this.location.origin)) {
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 && (!actionEl.origin || actionEl.origin === this.location.origin)) {
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
- Observer.set(detail.src, 'active', true);
206
- 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);
207
268
  }
208
269
  // ------------
209
- Observer.set(this.location, url, { detail: { ...init, ...detail }, });
210
- Observer.set(this.network, 'redirecting', null);
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 response = await this.clients.get('*').handle(httpEvent, ( ...args ) => this.remoteFetch( ...args ));
218
- let finalResponse = this.handleResponse(httpEvent, response);
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
- Observer.set(this.network, 'remote', true);
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.message));
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', false);
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
- Observer.set(this.network, 'remote', false);
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
- 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 => {
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
- return (this.cx.middlewares || []).concat(handle).reverse().reduce((next, fn) => {
66
- return () => fn.call(this.cx, httpEvent, router, next);
67
- }, null)();
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, router) {
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
- await new Promise(res => window.WebQit.DOM.ready(res));
77
- if (!window.document.state.env) {
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.setState({ page: data }, { update: 'merge' });
86
- window.document.body.setAttribute('template', 'page/' + httpEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
87
- await new Promise(res => (window.document.templatesReadyState === 'complete' && res(), window.document.addEventListener('templatesreadystatechange', res)));
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.setState({ page: {} }, { update: 'merge' });
95
- window.document.body.setAttribute('template', '');
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