@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 +1 -1
- package/src/runtime/_MessageStream.js +14 -10
- package/src/runtime/_ResponseHeaders.js +9 -0
- package/src/runtime/client/Http.js +113 -107
- package/src/runtime/client/Navigator.js +250 -0
- package/src/runtime/client/Runtime.js +86 -172
- package/src/runtime/client/Worker.js +4 -5
- package/src/runtime/server/Runtime.js +22 -13
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type === '
|
|
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
|
|
44
|
-
* @param object src
|
|
42
|
+
* @param object|string href
|
|
43
|
+
* @param object options
|
|
45
44
|
*
|
|
46
|
-
* @return
|
|
45
|
+
* @return void
|
|
47
46
|
*/
|
|
48
|
-
go(
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
*
|
|
137
|
+
* Navigation Interception
|
|
58
138
|
* ----------------
|
|
59
139
|
*/
|
|
60
|
-
|
|
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
|
-
|
|
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
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
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] =
|
|
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
|
-
|
|
189
|
+
var actionEl = window.document.createElement('a');
|
|
190
|
+
actionEl.href = submitParams.action;
|
|
118
191
|
// ---------------
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 (
|
|
124
|
-
formData.set(
|
|
197
|
+
if ((submitter || {}).name) {
|
|
198
|
+
formData.set(submitter.name, submitter.value);
|
|
125
199
|
}
|
|
126
|
-
if (submitParams.method === '
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
59
|
-
const clientNavigationEvent = new NavigationEvent(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
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
|
-
|
|
234
|
-
_sessionFactory('_session', {duration: 60 * 60
|
|
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
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
}
|