@webqit/webflo 0.8.72-0 → 0.8.74

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/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.8.72-0",
15
+ "version": "0.8.74",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -106,21 +106,25 @@ const _MessageStream = (NativeMessageStream, Headers, FormData) => {
106
106
  // Payload
107
107
  data(force = false) {
108
108
  if (!this._typedDataCache.data || force) {
109
- this._typedDataCache.data = new Promise(async resolve => {
110
- var request = this, data, contentType = request.headers.get('content-type') || '';
109
+ this._typedDataCache.data = new Promise(async (resolve, reject) => {
110
+ var messageInstance = this, data, contentType = messageInstance.headers.get('content-type') || '';
111
111
  var type = contentType === 'application/json' || this._typedDataCache.json ? 'json' : (
112
- contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData || (!contentType && !['get'].includes((request.method || '').toLowerCase())) ? 'formData' : (
112
+ contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/') || this._typedDataCache.formData ? 'formData' : (
113
113
  contentType === 'text/plain' ? 'plain' : 'other'
114
114
  )
115
115
  );
116
- if (type === 'formData') {
117
- data = (await request.formData()).json();
118
- } else {
119
- data = type === 'json' ? await request.json() : (
120
- type === 'plain' ? await request.text() : request.body
121
- );
116
+ try {
117
+ if (type === 'formData') {
118
+ data = (await messageInstance.formData()).json();
119
+ } else {
120
+ data = type === 'json' ? await messageInstance.json() : (
121
+ type === 'plain' ? await messageInstance.text() : messageInstance.body
122
+ );
123
+ }
124
+ resolve(data);
125
+ } catch(e) {
126
+ reject(e);
122
127
  }
123
- resolve(data);
124
128
  });
125
129
  }
126
130
  return this._typedDataCache.data;
@@ -69,6 +69,14 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
69
69
  return value;
70
70
  }
71
71
 
72
+ get location() {
73
+ return this.get('Location');
74
+ }
75
+
76
+ set location(value) {
77
+ return this.set('Location', value);
78
+ }
79
+
72
80
  get redirect() {
73
81
  return this.get('Location');
74
82
  }
@@ -76,6 +84,7 @@ const _ResponseHeaders = NativeHeaders => class extends _Headers(NativeHeaders)
76
84
  set redirect(value) {
77
85
  return this.set('Location', value);
78
86
  }
87
+
79
88
  }
80
89
 
81
90
  export default _ResponseHeaders;
@@ -39,25 +39,105 @@ export default class Http {
39
39
  /**
40
40
  * Performs a request.
41
41
  *
42
- * @param string href
43
- * @param object request
44
- * @param object src
42
+ * @param object|string href
43
+ * @param object options
45
44
  *
46
- * @return UserEvent
45
+ * @return void
47
46
  */
48
- go(href, request = {}, src = null) {
49
- return Observer.set(this.location, 'href', href, {
50
- detail: {request, src,},
51
- });
47
+ async go(url, options = {}) {
48
+ if (this.abortController) {
49
+ this.abortController.abort();
50
+ }
51
+ this.abortController = new AbortController();
52
+ let xRedirectCode = 300;
53
+ // Generates request object
54
+ let generateRequest = (url, options) => {
55
+ return new StdRequest(url, {
56
+ ...options,
57
+ headers: {
58
+ 'Accept': 'application/json',
59
+ 'X-Redirect-Policy': 'manual-when-cross-origin',
60
+ 'X-Redirect-Code': xRedirectCode,
61
+ 'X-Powered-By': '@webqit/webflo',
62
+ ...(options.headers || {}),
63
+ },
64
+ referrer: window.document.location.href,
65
+ signal: this.abortController.signal,
66
+ });
67
+ };
68
+ // Handles response object
69
+ let handleResponse = (response) => {
70
+ if (!response) return;
71
+ if (response.redirected && this.isSameOrigin(response.url)) {
72
+ Observer.set(this.location, { href: response.url }, {
73
+ detail: { isRedirect: true },
74
+ });
75
+ } else if (response.headers.get('Location') && response.status === xRedirectCode) {
76
+ window.location = response.headers.get('Location');
77
+ }
78
+ };
79
+ url = typeof url === 'string' ? { href: url } : url;
80
+ options = { referrer: this.location.href, ...options };
81
+ Observer.set(this.location, url, { detail: options, });
82
+ if (!(_before(url.href, '#') === _before(options.referrer, '#') && (options.method || 'GET').toUpperCase() === 'GET')) {
83
+ handleResponse(await client.call(this, generateRequest(url.href, options)));
84
+ }
85
+ },
86
+
87
+ /**
88
+ * Checks if an URL is same origin.
89
+ *
90
+ * @param object|string url
91
+ *
92
+ * @return Bool
93
+ */
94
+ isSameOrigin(url) {
95
+ if (typeof url === 'string') {
96
+ let href = url;
97
+ url = window.document.createElement('a');
98
+ url.href = href
99
+ }
100
+ return !url.origin || url.origin === this.location.origin;
101
+ },
102
+
103
+ /**
104
+ * History object
105
+ */
106
+ get history() {
107
+ return window.history;
52
108
  }
109
+
53
110
  };
54
111
 
112
+ // -----------------------
113
+ // Initialize network
114
+ Observer.set(instance, 'network', {});
115
+ window.addEventListener('online', () => Observer.set(instance.network, 'online', navigator.onLine));
116
+ window.addEventListener('offline', () => Observer.set(instance.network, 'online', navigator.onLine));
117
+
118
+ // -----------------------
119
+ // Initialize location
120
+ Observer.set(instance, 'location', new Url(window.document.location));
121
+ // -----------------------
122
+ // Syndicate changes to the browser;s location bar
123
+ Observer.observe(instance.location, [[ 'href' ]], ([e]) => {
124
+ if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
125
+ // Already from a "popstate" event as above, so don't push again
126
+ return;
127
+ }
128
+ if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
129
+ instance.history.replaceState(instance.history.state, '', instance.location.href);
130
+ } else {
131
+ try { instance.history.pushState(instance.history.state, '', instance.location.href); } catch(e) {}
132
+ }
133
+ }, { diff: true });
134
+
55
135
  /**
56
136
  * ----------------
57
- * instance.location
137
+ * Navigation Interception
58
138
  * ----------------
59
139
  */
60
- Observer.set(instance, 'location', new Url(window.document.location));
140
+
61
141
  // -----------------------
62
142
  // This event is triggered by
63
143
  // either the browser back button,
@@ -67,10 +147,9 @@ export default class Http {
67
147
  window.addEventListener('popstate', e => {
68
148
  // Needed to allow window.document.location
69
149
  // to update to window.location
150
+ let referrer = window.document.location.href;
70
151
  window.setTimeout(() => {
71
- Observer.set(instance.location, Url.copy(window.document.location), {
72
- detail: { type: 'history', src: window.document.location },
73
- });
152
+ instance.go(Url.copy(window.document.location), { referrer, src: window.document.location, srcType: 'history', });
74
153
  }, 0);
75
154
  });
76
155
 
@@ -78,24 +157,17 @@ export default class Http {
78
157
  // Capture all link-clicks
79
158
  // and fire to this router.
80
159
  window.addEventListener('click', e => {
81
- var anchor, href;
82
- if ((anchor = e.target.closest('a')) && (href = anchor.href)
83
- // And not towards any target nor have a download directive
84
- && !anchor.target && !anchor.download && !href.includes('/my-account')
85
- // Same origin... but...
86
- && (!anchor.origin || anchor.origin === instance.location.origin)) {
87
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
88
- return;
89
- }
160
+ var anchor = e.target.closest('a');
161
+ if (!anchor || !anchor.href) return;
162
+ if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === instance.location.origin)) {
163
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
90
164
  // Publish everything, including hash
91
- Observer.set(instance.location, Url.copy(anchor), {
92
- detail: { type: 'link', src: anchor, },
93
- });
165
+ instance.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
94
166
  // URLs with # will cause a natural navigation
95
167
  // even if pointing to a different page, a natural navigation will still happen
96
168
  // because with the Observer.set() above, window.document.location.href would have become
97
169
  // the destination page, which makes it look like same page navigation
98
- if (!href.includes('#')) {
170
+ if (!anchor.href.includes('#')) {
99
171
  e.preventDefault();
100
172
  }
101
173
  }
@@ -105,25 +177,27 @@ export default class Http {
105
177
  // Capture all form-submit
106
178
  // and fire to this router.
107
179
  window.addEventListener('submit', e => {
108
- var actionEl = window.document.createElement('a'),
109
- form = e.target.closest('form'),
110
- submits = [e.submitter]; //_arrFrom(form.elements).filter(el => el.matches('button,input[type="submit"],input[type="image"]'));
180
+ var form = e.target.closest('form'), submitter = e.submitter;
111
181
  var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
112
- params[prop] = submits.reduce((val, el) => val || (el.hasAttribute(`form${prop.toLowerCase()}`) ? el[`form${_toTitle(prop)}`] : null), null) || form[prop];
182
+ params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
113
183
  return params;
114
184
  }, {});
115
185
  // We support method hacking
186
+ submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
187
+ submitParams.submitter = submitter;
116
188
  // ---------------
117
- submitParams.method = e.submitter.dataset.method || form.dataset.method || submitParams.method;
189
+ var actionEl = window.document.createElement('a');
190
+ actionEl.href = submitParams.action;
118
191
  // ---------------
119
- if ((actionEl.href = submitParams.action) && !submitParams.target
120
- // Same origin... but...
121
- && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
192
+ // If not targeted and same origin...
193
+ if (!submitParams.target && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
194
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
195
+ // Build data
122
196
  var formData = new FormData(form);
123
- if (e.submitter.name) {
124
- formData.set(e.submitter.name, e.submitter.value);
197
+ if ((submitter || {}).name) {
198
+ formData.set(submitter.name, submitter.value);
125
199
  }
126
- if (submitParams.method === 'get') {
200
+ if (submitParams.method.toUpperCase() === 'GET') {
127
201
  var query = wwwFormUnserialize(actionEl.search);
128
202
  Array.from(formData.entries()).forEach(_entry => {
129
203
  wwwFormSet(query, _entry[0], _entry[1], false);
@@ -131,13 +205,7 @@ export default class Http {
131
205
  actionEl.search = wwwFormSerialize(query);
132
206
  formData = null;
133
207
  }
134
- if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) {
135
- return;
136
- }
137
- // Publish everything, including hash
138
- Observer.set(instance.location, Url.copy(actionEl), {
139
- detail: { type: 'form', src: form, submitParams, data: formData },
140
- });
208
+ instance.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
141
209
  // URLs with # will cause a natural navigation
142
210
  // even if pointing to a different page, a natural navigation will still happen
143
211
  // because with the Observer.set() above, window.document.location.href would have become
@@ -148,71 +216,9 @@ export default class Http {
148
216
  }
149
217
  });
150
218
 
151
- /**
152
- * ----------------
153
- * instance.history
154
- * ----------------
155
- */
156
-
157
- instance.history = window.history;
158
219
  // -----------------------
159
- // Syndicate changes to
160
- // the browser;s location bar
161
- Observer.observe(instance.location, [[ 'href' ]], e => {
162
- e = e[0];
163
- if ((e.detail || {}).src === window.document.location) {
164
- // Already from a "popstate" event as above, so don't push again
165
- return;
166
- }
167
- if (e.value === 'http:') return;
168
- if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
169
- instance.history.replaceState(instance.history.state, '', instance.location.href);
170
- } else {
171
- try {
172
- instance.history.pushState(instance.history.state, '', instance.location.href);
173
- } catch(e) {}
174
- }
175
- }, { diff: true });
176
-
177
- // ----------------------------------
178
- const createRequest = (url, referrer, e = {}) => {
179
- var detail = e.detail || {};
180
- var options = {
181
- method: (detail.submitParams || detail.src || {}).method || 'get',
182
- body: detail.data,
183
- headers: { ...(detail.headers || {}), 'X-Powered-By': '@webqit/webflo', },
184
- referrer,
185
- };
186
- return new StdRequest(url, options);
187
- };
188
- const handleResponse = response => {
189
- if (response && response.redirected) {
190
- window.location = response.url;
191
- return;
192
- var actionEl = window.document.createElement('a');
193
- if ((actionEl.href = response.url) && (!actionEl.origin || actionEl.origin === instance.location.origin)) {
194
- Observer.set(instance.location, { href: response.url }, {
195
- detail: { follow: false },
196
- });
197
- }
198
- }
199
- };
200
- // ----------------------------------
201
-
202
- // Observe location and route
203
- Observer.observe(instance.location, [['href']], async e => {
204
- e = e[0];
205
- var detail = e.detail || {};
206
- if (detail.follow === false) return;
207
- var method = (detail.submitParams || detail.src || {}).method;
208
- if ((_before(e.value, '#') !== _before(e.oldValue, '#')) || (method && method.toUpperCase() !== 'GET')) {
209
- return handleResponse(await client.call(instance, createRequest(e.value, e.oldValue, e), e));
210
- }
211
- }, {diff: false /* method might be the difference */});
212
220
  // Startup route
213
-
214
- handleResponse(await client.call(instance, createRequest(window.document.location.href, document.referrer)));
215
-
221
+ instance.go(window.document.location.href, { referrer: document.referrer });
216
222
  return instance;
217
223
  }
218
224
 
@@ -0,0 +1,250 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import _before from '@webqit/util/str/before.js';
6
+ import _toTitle from '@webqit/util/str/toTitle.js';
7
+ import { wwwFormUnserialize, wwwFormSet, wwwFormSerialize } from '../util.js';
8
+ import { Observer } from './Runtime.js';
9
+ import Url from './Url.js';
10
+
11
+ export default class Navigator {
12
+
13
+ constructor(client) {
14
+ this.client = client;
15
+
16
+ /**
17
+ * ----------------
18
+ * Navigator location
19
+ * ----------------
20
+ */
21
+
22
+ // -----------------------
23
+ // Initialize location
24
+ Observer.set(this, 'location', new Url(window.document.location));
25
+ // -----------------------
26
+ // Syndicate changes to the browser;s location bar
27
+ Observer.observe(this.location, [[ 'href' ]], ([e]) => {
28
+ if (e.value === 'http:' || (e.detail || {}).src === window.document.location) {
29
+ // Already from a "popstate" event as above, so don't push again
30
+ return;
31
+ }
32
+ if (e.value === window.document.location.href || e.value + '/' === window.document.location.href) {
33
+ window.history.replaceState(window.history.state, '', this.location.href);
34
+ } else {
35
+ try { window.history.pushState(window.history.state, '', this.location.href); } catch(e) {}
36
+ }
37
+ }, { diff: true });
38
+
39
+ // -----------------------
40
+ // This event is triggered by
41
+ // either the browser back button,
42
+ // the window.history.back(),
43
+ // the window.history.forward(),
44
+ // or the window.history.go() action.
45
+ window.addEventListener('popstate', e => {
46
+ // Needed to allow window.document.location
47
+ // to update to window.location
48
+ window.setTimeout(() => {
49
+ this.go(Url.copy(window.document.location), { src: window.document.location, srcType: 'history', });
50
+ }, 0);
51
+ });
52
+
53
+ // -----------------------
54
+ // Capture all link-clicks
55
+ // and fire to this router.
56
+ window.addEventListener('click', e => {
57
+ var anchor = e.target.closest('a');
58
+ if (!anchor || !anchor.href) return;
59
+ if (!anchor.target && !anchor.download && (!anchor.origin || anchor.origin === this.location.origin)) {
60
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
61
+ // Publish everything, including hash
62
+ this.go(Url.copy(anchor), { src: anchor, srcType: 'link', });
63
+ // URLs with # will cause a natural navigation
64
+ // even if pointing to a different page, a natural navigation will still happen
65
+ // because with the Observer.set() above, window.document.location.href would have become
66
+ // the destination page, which makes it look like same page navigation
67
+ if (!anchor.href.includes('#')) {
68
+ e.preventDefault();
69
+ }
70
+ }
71
+ });
72
+
73
+ // -----------------------
74
+ // Capture all form-submit
75
+ // and fire to this router.
76
+ window.addEventListener('submit', e => {
77
+ var form = e.target.closest('form'), submitter = e.submitter;
78
+ var submitParams = [ 'action', 'enctype', 'method', 'noValidate', 'target' ].reduce((params, prop) => {
79
+ params[prop] = submitter && submitter.hasAttribute(`form${prop.toLowerCase()}`) ? submitter[`form${_toTitle(prop)}`] : form[prop];
80
+ return params;
81
+ }, {});
82
+ // We support method hacking
83
+ submitParams.method = (submitter && submitter.dataset.method) || form.dataset.method || submitParams.method;
84
+ submitParams.submitter = submitter;
85
+ // ---------------
86
+ var actionEl = window.document.createElement('a');
87
+ actionEl.href = submitParams.action;
88
+ // ---------------
89
+ // If not targeted and same origin...
90
+ if (!submitParams.target && (!actionEl.origin || actionEl.origin === this.location.origin)) {
91
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
92
+ // Build data
93
+ var formData = new FormData(form);
94
+ if ((submitter || {}).name) {
95
+ formData.set(submitter.name, submitter.value);
96
+ }
97
+ if (submitParams.method.toUpperCase() === 'GET') {
98
+ var query = wwwFormUnserialize(actionEl.search);
99
+ Array.from(formData.entries()).forEach(_entry => {
100
+ wwwFormSet(query, _entry[0], _entry[1], false);
101
+ });
102
+ actionEl.search = wwwFormSerialize(query);
103
+ formData = null;
104
+ }
105
+ this.go(Url.copy(actionEl), { ...submitParams, body: formData, src: form, srcType: 'form', });
106
+ // URLs with # will cause a natural navigation
107
+ // even if pointing to a different page, a natural navigation will still happen
108
+ // because with the Observer.set() above, window.document.location.href would have become
109
+ // the destination page, which makes it look like same page navigation
110
+ if (!actionEl.hash) {
111
+ e.preventDefault();
112
+ }
113
+ }
114
+ });
115
+
116
+ /**
117
+ * ----------------
118
+ * Navigator network
119
+ * ----------------
120
+ */
121
+
122
+ // -----------------------
123
+ // Initialize network
124
+ Observer.set(this, 'network', {});
125
+ window.addEventListener('online', () => Observer.set(this.network, 'online', navigator.onLine));
126
+ window.addEventListener('offline', () => Observer.set(this.network, 'online', navigator.onLine));
127
+
128
+ /**
129
+ * ----------------
130
+ * Initial navigation
131
+ * ----------------
132
+ */
133
+
134
+ this.go(this.location, { srcType: 'init' });
135
+ }
136
+
137
+ /**
138
+ * History object
139
+ */
140
+ get history() {
141
+ return window.history;
142
+ }
143
+
144
+ /**
145
+ * Performs a request.
146
+ *
147
+ * @param object|string href
148
+ * @param object params
149
+ *
150
+ * @return void
151
+ */
152
+ async go(url, params = {}) {
153
+
154
+ // Generates request object
155
+ const generateRequest = (url, params) => {
156
+ return new Request(url, {
157
+ ...params,
158
+ headers: {
159
+ 'Accept': 'application/json',
160
+ 'Cache-Control': 'no-store',
161
+ 'X-Redirect-Policy': 'manual-when-cross-origin',
162
+ 'X-Redirect-Code': xRedirectCode,
163
+ 'X-Powered-By': '@webqit/webflo',
164
+ ...(params.headers || {}),
165
+ },
166
+ referrer: window.document.location.href,
167
+ signal: this._abortController.signal,
168
+ });
169
+ };
170
+
171
+ // Initiates remote fetch and sets the status
172
+ const remoteRequest = request => {
173
+ Observer.set(this.network, 'remote', true);
174
+ let _response = fetch(request);
175
+ // This catch() is NOT intended to handle failure of the fetch
176
+ _response.catch(e => Observer.set(this.network, 'error', e.message));
177
+ // Save a reference to this
178
+ return _response.then(async response => {
179
+ // Stop loading status
180
+ Observer.set(this.network, 'remote', false);
181
+ return response;
182
+ });
183
+ };
184
+
185
+ // Handles response object
186
+ const handleResponse = async (response, params) => {
187
+ response = await response;
188
+ Observer.set(this.network, 'remote', false);
189
+ Observer.set(this.network, 'error', null);
190
+ if (['link', 'form'].includes(params.srcType)) {
191
+ Observer.set(params.src, 'active', false);
192
+ Observer.set(params.submitter || {}, 'active', false);
193
+ }
194
+ if (!response) return;
195
+ if (response.redirected && this.isSameOrigin(response.url)) {
196
+ Observer.set(this.location, { href: response.url }, {
197
+ detail: { isRedirect: true },
198
+ });
199
+ return;
200
+ }
201
+ let location = response.headers.get('Location');
202
+ if (location && response.status === xRedirectCode) {
203
+ Observer.set(this.network, 'redirecting', location);
204
+ window.location = location;
205
+ }
206
+ };
207
+
208
+ // ------------
209
+ url = typeof url === 'string' ? { href: url } : url;
210
+ params = { referrer: this.location.href, ...params };
211
+ // ------------
212
+ Observer.set(this.location, url, { detail: params, });
213
+ Observer.set(this.network, 'redirecting', null);
214
+ // ------------
215
+ if (['link', 'form'].includes(params.srcType)) {
216
+ Observer.set(params.src, 'active', true);
217
+ Observer.set(params.submitter || {}, 'active', true);
218
+ }
219
+ // ------------
220
+
221
+ if (this._abortController) {
222
+ this._abortController.abort();
223
+ }
224
+ this._abortController = new AbortController();
225
+ let xRedirectCode = 300;
226
+
227
+ if (params.srcType === 'init' || !(_before(url.href, '#') === _before(params.referrer, '#') && (params.method || 'GET').toUpperCase() === 'GET')) {
228
+ handleResponse(this.client.call(this, generateRequest(url.href, params), params, remoteRequest), params);
229
+ }
230
+
231
+ return this._abortController;
232
+ }
233
+
234
+ /**
235
+ * Checks if an URL is same origin.
236
+ *
237
+ * @param object|string url
238
+ *
239
+ * @return Bool
240
+ */
241
+ isSameOrigin(url) {
242
+ if (typeof url === 'string') {
243
+ let href = url;
244
+ url = window.document.createElement('a');
245
+ url.href = href
246
+ }
247
+ return !url.origin || url.origin === this.location.origin;
248
+ }
249
+
250
+ }
@@ -10,7 +10,7 @@ import NavigationEvent from './NavigationEvent.js';
10
10
  import WorkerClient from './WorkerClient.js';
11
11
  import Storage from './Storage.js';
12
12
  import Router from './Router.js';
13
- import Http from './Http.js';
13
+ import Navigator from './Navigator.js';
14
14
 
15
15
  /**
16
16
  * ---------------------------
@@ -21,192 +21,106 @@ import Http from './Http.js';
21
21
  export const { Observer } = window.WebQit;
22
22
  export default function(layout, params) {
23
23
 
24
- const session = Storage();
25
- const workerClient = new WorkerClient('/worker.js', { startMessages: true });
26
- Observer.observe(workerClient, changes => {
27
- console.log('SERVICE_WORKER_STATE', changes[0].name, changes[0].value);
28
- });
29
-
30
- // Copy..
31
24
  layout = {...layout};
32
25
  params = {...params};
33
- window.addEventListener('online', () => Observer.set(networkWatch, 'online', navigator.onLine));
34
- window.addEventListener('offline', () => Observer.set(networkWatch, 'online', navigator.onLine));
35
- var networkProgressOngoing;
36
-
37
- /**
38
- * ----------------
39
- * Apply routing
40
- * ----------------
41
- */
42
-
43
- Http.createClient(async function(request, event = null) {
44
26
 
45
- const httpInstance = this;
46
-
47
- // -------------------
48
- // Resolve canonicity
49
- // -------------------
50
-
51
- // The srvice object
52
- const $context = {
53
- layout,
54
- onHydration: !event && (await window.WebQit.OOHTML.meta.get('isomorphic')),
55
- response: null,
56
- }
27
+ const session = Storage();
28
+ const workerClient = new WorkerClient('/worker.js', { startMessages: true });
29
+ const navigator = new Navigator(async (request, params, remoteFetch) => {
57
30
 
58
- // The app router
59
- const clientNavigationEvent = new NavigationEvent(request, session);
60
- const requestPath = clientNavigationEvent.url.pathname;
61
- const router = new Router(requestPath, layout, $context);
62
- if (networkProgressOngoing) {
63
- networkProgressOngoing.setActive(false);
64
- networkProgressOngoing = null;
65
- }
31
+ // The navigation event
32
+ const clientNavigationEvent = new NavigationEvent(
33
+ new NavigationEvent.Request(request),
34
+ session,
35
+ );
66
36
 
67
- try {
37
+ // The app router
38
+ const router = new Router(clientNavigationEvent.url.pathname, layout, {
39
+ layout,
40
+ onHydration: params.srcType === 'init',
41
+ });
68
42
 
69
- // --------
70
- // ROUTE FOR DATA
71
- // --------
72
- const httpMethodName = clientNavigationEvent.request.method.toLowerCase();
73
- $context.response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, document.state, async function(event) {
74
- // -----------------
75
- var networkProgress = networkProgressOngoing = new RequestHandle();
76
- networkProgress.setActive(true, event.request._method || event.request.method);
77
- // -----------------
78
- const headers = event.request.headers;
79
- if (!headers.get('Accept')) {
80
- headers.set('Accept', 'application/json');
81
- }
82
- if (!headers.get('Cache-Control')) {
83
- headers.set('Cache-Control', 'no-store');
84
- }
85
- // -----------------
86
- // Sync session data to cache to be available to service-worker routers
87
- const response = fetch(event.request, {}, networkProgress.updateProgress.bind(networkProgress));
88
- // -----------------
89
- // -----------------
90
- response.catch(e => networkProgress.throw(e.message));
91
- return response.then(async _response => {
92
- _response = new clientNavigationEvent.Response(_response.body, {
93
- status: _response.status,
94
- statusText: _response.statusText,
95
- headers: _response.headers,
96
- _proxy: {
97
- url: _response.url,
98
- ok: _response.ok,
99
- redirected: _response.redirected
100
- },
101
- });
102
- // Save a reference to this
103
- $context.responseClone = _response;
104
- // Return a promise that never resolves as a new response is underway
105
- if (!networkProgress.active) {
106
- return new Promise(() => {});
107
- }
108
- // Stop loading status
109
- networkProgress.setActive(false);
110
- return _response;
43
+ // --------
44
+ // ROUTE FOR DATA
45
+ // --------
46
+ const httpMethodName = clientNavigationEvent.request.method.toLowerCase();
47
+ const response = await router.route([httpMethodName === 'delete' ? 'del' : httpMethodName, 'default'], clientNavigationEvent, document.state, async function(event) {
48
+ return remoteFetch(event.request).then(response => {
49
+ return new clientNavigationEvent.Response(response.body, {
50
+ status: response.status,
51
+ statusText: response.statusText,
52
+ headers: response.headers,
53
+ _proxy: {
54
+ url: response.url,
55
+ ok: response.ok,
56
+ redirected: response.redirected
57
+ },
111
58
  });
112
59
  });
113
- if ($context.response instanceof clientNavigationEvent.Response) {
114
- $context.response = await $context.response.data();
115
- }
60
+ }).catch(e => {
61
+ window.document.body.setAttribute('template', '');
62
+ throw e;
63
+ });
116
64
 
117
- // --------
118
- // Render
119
- // --------
120
- const rendering = await router.route('render', clientNavigationEvent, $context.response, async function(event, data) {
121
- // --------
122
- // OOHTML would waiting for DOM-ready in order to be initialized
123
- await new Promise(res => window.WebQit.DOM.ready(res));
124
- if (!window.document.state.env) {
125
- window.document.setState({
126
- env: 'client',
127
- onHydration: $context.onHydration,
128
- network: networkWatch,
129
- url: httpInstance.location,
130
- session,
131
- }, { update: true });
132
- }
133
- window.document.setState({ page: data }, { update: 'merge' });
134
- window.document.body.setAttribute('template', 'page/' + requestPath.split('/').filter(a => a).map(a => a + '+-').join('/'));
135
- return new Promise(res => {
136
- window.document.addEventListener('templatesreadystatechange', () => res(window));
137
- if (window.document.templatesReadyState === 'complete') {
138
- res(window);
139
- }
140
- });
141
- });
142
65
 
66
+ // --------
67
+ // Render
68
+ // --------
69
+ const data = response instanceof clientNavigationEvent.Response ? await response.data() : response;
70
+ await router.route('render', clientNavigationEvent, data, async function(event, data) {
143
71
  // --------
144
- // Render...
145
- // --------
146
-
147
- if (/*document.activeElement === document.body && */event && _isObject(event.detail) && (event.detail.src instanceof Element) && /* do only on url path change */ _before(event.value, '?') !== _before(event.oldValue, '?')) {
148
- setTimeout(() => {
149
- let vieportTop;
150
- if (clientNavigationEvent.url.hash && (urlTarget = document.querySelector(clientNavigationEvent.url.hash))) {
151
- urlTarget.scrollIntoView();
152
- } else if (vieportTop = Array.from(document.querySelectorAll('[data-viewport-top]')).pop()) {
153
- vieportTop.focus();
154
- } else {
155
- document.documentElement.classList.add('scroll-reset');
156
- document.body.scrollIntoView();
157
- setTimeout(() => {
158
- document.documentElement.classList.remove('scroll-reset');
159
- }, 600);
160
- }
161
- }, 0);
72
+ // OOHTML would waiting for DOM-ready in order to be initialized
73
+ await new Promise(res => window.WebQit.DOM.ready(res));
74
+ if (!window.document.state.env) {
75
+ window.document.setState({
76
+ env: 'client',
77
+ onHydration: params.srcType === 'init',
78
+ network: navigator.network,
79
+ url: navigator.location,
80
+ session,
81
+ }, { update: true });
162
82
  }
83
+ window.document.setState({ page: data }, { update: 'merge' });
84
+ window.document.body.setAttribute('template', 'page/' + clientNavigationEvent.url.pathname.split('/').filter(a => a).map(a => a + '+-').join('/'));
85
+ return new Promise(res => {
86
+ window.document.addEventListener('templatesreadystatechange', () => res(window));
87
+ if (window.document.templatesReadyState === 'complete') {
88
+ res(window);
89
+ }
90
+ });
91
+ });
163
92
 
164
- } catch(e) {
165
-
166
- window.document.body.setAttribute('template', '');
167
- throw e;
168
-
93
+ // --------
94
+ // Render...
95
+ // --------
96
+
97
+ if (params.src instanceof Element) {
98
+ setTimeout(() => {
99
+ let viewportTop;
100
+ if (clientNavigationEvent.url.hash && (urlTarget = document.querySelector(clientNavigationEvent.url.hash))) {
101
+ urlTarget.scrollIntoView();
102
+ } else if (viewportTop = Array.from(document.querySelectorAll('[data-viewport-top]')).pop()) {
103
+ viewportTop.focus();
104
+ } else {
105
+ document.documentElement.classList.add('scroll-reset');
106
+ document.body.scrollIntoView();
107
+ setTimeout(() => {
108
+ document.documentElement.classList.remove('scroll-reset');
109
+ }, 600);
110
+ }
111
+ }, 0);
169
112
  }
170
113
 
171
- return $context.responseClone;
114
+ return response;
172
115
  });
173
116
 
117
+ Observer.observe(session, changes => {
118
+ //console.log('SESSION_STATE_CHANGE', changes[0].name, changes[0].value);
119
+ });
120
+ Observer.observe(workerClient, changes => {
121
+ //console.log('SERVICE_WORKER_STATE_CHANGE', changes[0].name, changes[0].value);
122
+ });
123
+ Observer.observe(navigator, changes => {
124
+ //console.log('NAVIGATORSTATE_CHANGE', changes[0].name, changes[0].value);
125
+ });
174
126
  };
175
-
176
- const networkWatch = { progress: {}, online: navigator.onLine };
177
- class RequestHandle {
178
- setActive(state, method = '') {
179
- if (this.active === false) {
180
- return;
181
- }
182
- this.active = state;
183
- Observer.set(networkWatch, {
184
- method,
185
- error: '',
186
- progress: {
187
- active: state,
188
- determinate: false,
189
- valuenow: 0,
190
- valuetotal: NaN,
191
- },
192
- });
193
- }
194
- updateProgress(phase, valuenow, valuetotal) {
195
- if (this.active === false) {
196
- return;
197
- }
198
- Observer.set(networkWatch.progress, {
199
- phase,
200
- determinate: !isNaN(valuetotal),
201
- valuenow,
202
- valuetotal,
203
- });
204
- }
205
- throw(message) {
206
- if (this.active === false) {
207
- return;
208
- }
209
- this.error = true;
210
- Observer.set(networkWatch, 'error', message);
211
- }
212
- };
@@ -117,7 +117,7 @@ export default function(layout, params) {
117
117
  }
118
118
  return _response;
119
119
  }
120
-
120
+
121
121
  return defaultFetch(evt);
122
122
  };
123
123
  evt.respondWith(handleFetch(evt));
@@ -144,13 +144,12 @@ export default function(layout, params) {
144
144
  return network_fetch(evt);
145
145
  };
146
146
 
147
- //evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
148
147
  //evt.request.mode navigate evt.request.cache force-cache evt.request.destination document request.headers.get('Accept') text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
149
148
 
150
149
  const getCacheName = request => request.headers.get('Accept') === 'application/json'
151
150
  ? params.cache_name + '_json'
152
151
  : params.cache_name;
153
-
152
+
154
153
  // Caching strategy: cache_first
155
154
  const cache_fetch = (evt, cacheRefresh = false, is_Navigate_ForceCache_Document = false) => {
156
155
 
@@ -162,10 +161,10 @@ export default function(layout, params) {
162
161
  let url = new URL(request.url);
163
162
  url.searchParams.set('$force-cache', '1');
164
163
  request = new Request(url, {
165
- method: request.method,
164
+ method: request.method,
166
165
  headers: request.headers,
167
166
  body: request.body,
168
- mode: request.mode === 'navigate' ? null : request.mode,
167
+ mode: request.mode === 'navigate'/* throws */ ? null : request.mode,
169
168
  credentials: request.credentials,
170
169
  cache: request.cache,
171
170
  redirect: request.redirect,
@@ -17,7 +17,6 @@ import _isArray from '@webqit/util/js/isArray.js';
17
17
  import { _isString, _isPlainObject, _isPlainArray } from '@webqit/util/js/index.js';
18
18
  import _delay from '@webqit/util/js/delay.js';
19
19
  import { slice as _streamSlice } from 'stream-slice';
20
- import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
21
20
  import * as config from '../../config/index.js';
22
21
  import * as services from '../../services/index.js';
23
22
  import NavigationEvent from './NavigationEvent.js';
@@ -209,7 +208,6 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
209
208
  // --------
210
209
 
211
210
  const fullUrl = protocol + '://' + request.headers.host + request.url;
212
- const _request = new NavigationEvent.Request(fullUrl, requestInit);
213
211
  const _sessionFactory = function(id, params = {}, callback = null) {
214
212
  let factory, secret = hostSetup.variables.entries.SESSION_KEY;
215
213
  Sessions({
@@ -230,8 +228,8 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
230
228
  return !callback ? factory : undefined;
231
229
  };
232
230
  const serverNavigationEvent = new NavigationEvent(
233
- _request,
234
- _sessionFactory('_session', {duration: 60 * 60 * 24 * 30}).get(),
231
+ new NavigationEvent.Request(fullUrl, requestInit),
232
+ _sessionFactory('_session', { duration: 60 * 60, activeDuration: 60 * 60 }).get(),
235
233
  _sessionFactory
236
234
  );
237
235
 
@@ -408,12 +406,7 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
408
406
  // -------------------
409
407
  // Chrome needs this for audio elements to play
410
408
  response.setHeader('Accept-Ranges', 'bytes');
411
- /*
412
- if ($context.response.headers.contentLength && !$context.response.headers.contentRange) {
413
- $context.response.headers.contentRange = `bytes 0-${$context.response.headers.contentLength}/${$context.response.headers.contentLength}`;
414
- }
415
409
 
416
- */
417
410
  // -------------------
418
411
  // Automatic response headers
419
412
  // -------------------
@@ -470,7 +463,15 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
470
463
  // Send
471
464
  // -------------------
472
465
  if ($context.response.headers.redirect) {
473
- response.statusCode = $context.response.status;
466
+ let xRedirectPolicy = serverNavigationEvent.request.headers.get('X-Redirect-Policy');
467
+ let xRedirectCode = serverNavigationEvent.request.headers.get('X-Redirect-Code') || 300;
468
+ let isSameOriginRedirect = (new serverNavigationEvent.globals.URL($context.response.headers.location)).origin === serverNavigationEvent.url.origin;
469
+ if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-origin') || (isSameOriginRedirect && xRedirectPolicy === 'manual-when-same-origin')) {
470
+ response.statusCode = xRedirectCode;
471
+ response.setHeader('X-Redirect-Code', $context.response.status);
472
+ } else {
473
+ response.statusCode = $context.response.status;
474
+ }
474
475
  response.end();
475
476
  } else if ($context.response.original !== undefined && $context.response.original !== null) {
476
477
  response.statusCode = $context.response.status;
@@ -551,15 +552,23 @@ export async function run(hostSetup, request, response, Ui, flags = {}, protocol
551
552
  // --------
552
553
 
553
554
  if (flags.logs !== false) {
555
+ let errorCode = [ 404, 500 ].includes(response.statusCode) ? response.statusCode : 0;
556
+ let xRedirectCode = response.getHeader('X-Redirect-Code');
557
+ let redirectCode = xRedirectCode || ((response.statusCode + '').startsWith('3') ? response.statusCode : 0);
558
+ let statusCode = xRedirectCode || response.statusCode;
554
559
  Ui.log(''
555
560
  + '[' + (hostSetup.vh ? Ui.style.keyword(hostSetup.vh.host) + '][' : '') + Ui.style.comment((new Date).toUTCString()) + '] '
556
561
  + Ui.style.keyword(protocol.toUpperCase() + ' ' + serverNavigationEvent.request.method) + ' '
557
562
  + Ui.style.url(serverNavigationEvent.request.url) + ($context.response && ($context.response.meta || {}).autoIndex ? Ui.style.comment((!serverNavigationEvent.request.url.endsWith('/') ? '/' : '') + $context.response.meta.autoIndex) : '') + ' '
558
563
  + (' (' + Ui.style.comment($context.response && ($context.response.headers || {}).contentType ? $context.response.headers.contentType : 'unknown') + ') ')
559
564
  + (
560
- [ 404, 500 ].includes(response.statusCode)
561
- ? Ui.style.err(response.statusCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
562
- : Ui.style.val(response.statusCode) + ((response.statusCode + '').startsWith('3') ? ' - ' + Ui.style.val(response.getHeader('Location')) : ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')')
565
+ errorCode
566
+ ? Ui.style.err(errorCode + ($context.fatal ? ` [ERROR]: ${$context.fatal.error || $context.fatal.toString()}` : ``))
567
+ : Ui.style.val(statusCode) + (
568
+ redirectCode
569
+ ? ' - ' + Ui.style.val(response.getHeader('Location'))
570
+ : ' (' + Ui.style.keyword(response.getHeader('Content-Range') || response.statusMessage) + ')'
571
+ )
563
572
  )
564
573
  );
565
574
  }