@webqit/webflo 0.11.61 → 1.0.0
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/{Context.js → AbstractContext.js} +1 -9
- package/src/deployment-pi/origins/index.js +1 -1
- package/src/index.js +1 -9
- package/src/runtime-pi/HttpEvent.js +101 -81
- package/src/runtime-pi/HttpUser.js +126 -0
- package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
- package/src/runtime-pi/MessagingOverChannel.js +85 -0
- package/src/runtime-pi/MessagingOverSocket.js +106 -0
- package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
- package/src/runtime-pi/WebfloCookieStorage.js +27 -0
- package/src/runtime-pi/WebfloEventTarget.js +39 -0
- package/src/runtime-pi/WebfloMessageEvent.js +58 -0
- package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
- package/src/runtime-pi/{Router.js → WebfloRouter.js} +3 -34
- package/src/runtime-pi/WebfloRuntime.js +52 -0
- package/src/runtime-pi/WebfloStorage.js +109 -0
- package/src/runtime-pi/client/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/Context.js +2 -6
- package/src/runtime-pi/client/CookieStorage.js +17 -0
- package/src/runtime-pi/client/Router.js +3 -13
- package/src/runtime-pi/client/SessionStorage.js +33 -0
- package/src/runtime-pi/client/Url.js +24 -72
- package/src/runtime-pi/client/WebfloClient.js +544 -0
- package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
- package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
- package/src/runtime-pi/client/WebfloSubClient.js +165 -0
- package/src/runtime-pi/client/Workport.js +89 -161
- package/src/runtime-pi/client/generate.js +1 -1
- package/src/runtime-pi/client/index.js +13 -18
- package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
- package/src/runtime-pi/client/worker/Context.js +2 -6
- package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
- package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
- package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
- package/src/runtime-pi/client/worker/Workport.js +13 -73
- package/src/runtime-pi/client/worker/index.js +7 -18
- package/src/runtime-pi/index.js +1 -8
- package/src/runtime-pi/server/ClientMessaging.js +18 -0
- package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
- package/src/runtime-pi/server/Context.js +2 -6
- package/src/runtime-pi/server/CookieStorage.js +17 -0
- package/src/runtime-pi/server/Router.js +2 -68
- package/src/runtime-pi/server/SessionStorage.js +53 -0
- package/src/runtime-pi/server/WebfloServer.js +755 -0
- package/src/runtime-pi/server/index.js +7 -18
- package/src/runtime-pi/util-http.js +268 -32
- package/src/runtime-pi/xURL.js +25 -22
- package/src/runtime-pi/xfetch.js +2 -2
- package/src/runtime-pi/Application.js +0 -29
- package/src/runtime-pi/Cookies.js +0 -82
- package/src/runtime-pi/Runtime.js +0 -21
- package/src/runtime-pi/client/Application.js +0 -76
- package/src/runtime-pi/client/Runtime.js +0 -525
- package/src/runtime-pi/client/createStorage.js +0 -58
- package/src/runtime-pi/client/worker/Application.js +0 -44
- package/src/runtime-pi/client/worker/Runtime.js +0 -275
- package/src/runtime-pi/server/Application.js +0 -101
- package/src/runtime-pi/server/Runtime.js +0 -558
- package/src/runtime-pi/xFormData.js +0 -24
- package/src/runtime-pi/xHeaders.js +0 -146
- package/src/runtime-pi/xRequest.js +0 -46
- package/src/runtime-pi/xRequestHeaders.js +0 -109
- package/src/runtime-pi/xResponse.js +0 -33
- package/src/runtime-pi/xResponseHeaders.js +0 -117
- package/src/runtime-pi/xxHttpMessage.js +0 -102
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import { _before, _toTitle } from '@webqit/util/str/index.js';
|
|
2
|
+
import { _isObject } from '@webqit/util/js/index.js';
|
|
3
|
+
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
4
|
+
import { MultiportMessagingAPI } from '../MultiportMessagingAPI.js';
|
|
5
|
+
import { MessagingOverBroadcast } from '../MessagingOverBroadcast.js';
|
|
6
|
+
import { MessagingOverChannel } from '../MessagingOverChannel.js';
|
|
7
|
+
import { MessagingOverSocket } from '../MessagingOverSocket.js';
|
|
8
|
+
import { ClientMessaging } from './ClientMessaging.js';
|
|
9
|
+
import { CookieStorage } from './CookieStorage.js';
|
|
10
|
+
import { SessionStorage } from './SessionStorage.js';
|
|
11
|
+
import { HttpEvent } from '../HttpEvent.js';
|
|
12
|
+
import { HttpUser } from '../HttpUser.js';
|
|
13
|
+
import { Router } from './Router.js';
|
|
14
|
+
import { Url } from './Url.js';
|
|
15
|
+
import xfetch from '../xfetch.js';
|
|
16
|
+
import '../util-http.js';
|
|
17
|
+
|
|
18
|
+
const { Observer } = webqit;
|
|
19
|
+
|
|
20
|
+
export class WebfloClient extends WebfloRuntime {
|
|
21
|
+
|
|
22
|
+
static get Router() { return Router; }
|
|
23
|
+
|
|
24
|
+
static get HttpEvent() { return HttpEvent; }
|
|
25
|
+
|
|
26
|
+
static get CookieStorage() { return CookieStorage; }
|
|
27
|
+
|
|
28
|
+
static get SessionStorage() { return SessionStorage; }
|
|
29
|
+
|
|
30
|
+
static get HttpUser() { return HttpUser; }
|
|
31
|
+
|
|
32
|
+
#host;
|
|
33
|
+
get host() { return this.#host; }
|
|
34
|
+
|
|
35
|
+
#network;
|
|
36
|
+
get network() { return this.#network; }
|
|
37
|
+
|
|
38
|
+
#location;
|
|
39
|
+
get location() { return this.#location; }
|
|
40
|
+
|
|
41
|
+
#navigator;
|
|
42
|
+
get navigator() { return this.#navigator; }
|
|
43
|
+
|
|
44
|
+
#transition;
|
|
45
|
+
get transition() { return this.#transition; }
|
|
46
|
+
|
|
47
|
+
#backgroundMessaging;
|
|
48
|
+
get backgroundMessaging() { return this.#backgroundMessaging; }
|
|
49
|
+
|
|
50
|
+
constructor(host) {
|
|
51
|
+
super();
|
|
52
|
+
this.#host = host;
|
|
53
|
+
Object.defineProperty(this.host, 'webfloRuntime', { get: () => this });
|
|
54
|
+
this.#network = { status: window.navigator.onLine };
|
|
55
|
+
this.#location = new Url/*NOT URL*/(this.host.location);
|
|
56
|
+
this.#navigator = {
|
|
57
|
+
requesting: null,
|
|
58
|
+
redirecting: null,
|
|
59
|
+
remotely: false,
|
|
60
|
+
origins: [],
|
|
61
|
+
error: null,
|
|
62
|
+
};
|
|
63
|
+
this.#transition = {
|
|
64
|
+
from: new Url/*NOT URL*/({}),
|
|
65
|
+
to: new Url/*NOT URL*/(this.host.location),
|
|
66
|
+
rel: 'unrelated',
|
|
67
|
+
phase: 0
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
initialize() {
|
|
72
|
+
this.#backgroundMessaging = new MultiportMessagingAPI(this, { runtime: this });
|
|
73
|
+
// Bind response and redirect handlers
|
|
74
|
+
const responseHandler = (e) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
if (e.type === 'response' && _isObject(e.data) && _isObject(e.data.status)) {
|
|
77
|
+
e.originalTarget.fire('status', e.data.status);
|
|
78
|
+
}
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
if (e.defaultPrevented || e.immediatePropagationStopped) return;
|
|
81
|
+
window.queueMicrotask(() => {
|
|
82
|
+
if (e.type === 'response') {
|
|
83
|
+
const httpEvent = this.constructor.HttpEvent.create(null, { url: this.location.href });
|
|
84
|
+
this.render(httpEvent, e.data, true);
|
|
85
|
+
} else if (e.type === 'redirect') {
|
|
86
|
+
this.redirect(e.data);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}, 10);
|
|
90
|
+
};
|
|
91
|
+
this.backgroundMessaging.handleMessages('response', responseHandler);
|
|
92
|
+
this.backgroundMessaging.handleMessages('redirect', responseHandler);
|
|
93
|
+
// Bind network status handlers
|
|
94
|
+
const onlineHandler = () => Observer.set(this.network, 'status', window.navigator.onLine);
|
|
95
|
+
window.addEventListener('online', onlineHandler);
|
|
96
|
+
window.addEventListener('offline', onlineHandler);
|
|
97
|
+
// Start controlling
|
|
98
|
+
const uncontrols = this.control();
|
|
99
|
+
return () => {
|
|
100
|
+
this.#backgroundMessaging.close();
|
|
101
|
+
window.removeEventListener('online', onlineHandler);
|
|
102
|
+
window.removeEventListener('offline', onlineHandler);
|
|
103
|
+
uncontrols();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
controlClassic(locationCallback) {
|
|
108
|
+
const setStates = (url, detail, method = 'GET') => {
|
|
109
|
+
Observer.set(this.navigator, {
|
|
110
|
+
requesting: new Url/*NOT URL*/(url),
|
|
111
|
+
origins: detail.navigationOrigins || [],
|
|
112
|
+
method,
|
|
113
|
+
error: null
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const resetStates = () => {
|
|
117
|
+
Observer.set(this.navigator, {
|
|
118
|
+
requesting: null,
|
|
119
|
+
remotely: false,
|
|
120
|
+
origins: [],
|
|
121
|
+
method: null
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
// -----------------------
|
|
125
|
+
// Capture all link-clicks
|
|
126
|
+
const clickHandler = (e) => {
|
|
127
|
+
if (!this._canIntercept(e)) return;
|
|
128
|
+
var anchorEl = e.target.closest('a');
|
|
129
|
+
if (!anchorEl || !anchorEl.href || (anchorEl.target && !anchorEl.target.startsWith('_webflo:')) || anchorEl.download || !this.isSpaRoute(anchorEl)) return;
|
|
130
|
+
const resolvedUrl = new URL(anchorEl.hasAttribute('href') ? anchorEl.getAttribute('href') : '', this.location.href);
|
|
131
|
+
if (this.isHashChange(resolvedUrl)) {
|
|
132
|
+
Observer.set(this.location, 'href', resolvedUrl.href);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// ---------------
|
|
136
|
+
// Handle now
|
|
137
|
+
e.preventDefault();
|
|
138
|
+
this._abortController?.abort();
|
|
139
|
+
this._abortController = new AbortController();
|
|
140
|
+
// Note the order of calls below
|
|
141
|
+
const detail = {
|
|
142
|
+
navigationType: 'push',
|
|
143
|
+
navigationOrigins: [anchorEl],
|
|
144
|
+
destination: this._asEntry(null),
|
|
145
|
+
source: this.currentEntry(), // this
|
|
146
|
+
userInitiated: true,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
if (anchorEl.target === '_webflo:_parent' && this.superRuntime) {
|
|
150
|
+
setStates(resolvedUrl, detail);
|
|
151
|
+
this.superRuntime.navigate(
|
|
152
|
+
resolvedUrl,
|
|
153
|
+
{
|
|
154
|
+
signal: this._abortController.signal,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
...detail,
|
|
158
|
+
isHoisted: true,
|
|
159
|
+
}
|
|
160
|
+
).then(resetStates);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
locationCallback(resolvedUrl); // this
|
|
164
|
+
this.navigate(
|
|
165
|
+
resolvedUrl,
|
|
166
|
+
{
|
|
167
|
+
signal: this._abortController.signal,
|
|
168
|
+
},
|
|
169
|
+
detail,
|
|
170
|
+
); // this
|
|
171
|
+
};
|
|
172
|
+
// -----------------------
|
|
173
|
+
// Capture all form-submits
|
|
174
|
+
const submitHandler = (e) => {
|
|
175
|
+
if (!this._canIntercept(e)) return;
|
|
176
|
+
// ---------------
|
|
177
|
+
// Declare form submission modifyers
|
|
178
|
+
const form = e.target.closest('form');
|
|
179
|
+
const submitter = e.submitter;
|
|
180
|
+
const _attr = (name) => {
|
|
181
|
+
let value = submitter && submitter.hasAttribute(`form${name.toLowerCase()}`) ? submitter[`form${_toTitle(name)}`] : (form.getAttribute(name) || form[name]);
|
|
182
|
+
if (value && [RadioNodeList, HTMLElement].some((x) => value instanceof x)) {
|
|
183
|
+
value = null;
|
|
184
|
+
}
|
|
185
|
+
return value;
|
|
186
|
+
};
|
|
187
|
+
const submitParams = Object.fromEntries(['method', 'action', 'enctype', 'noValidate', 'target'].map((name) => [name, _attr(name)]));
|
|
188
|
+
submitParams.method = submitParams.method || submitter.dataset.formmethod || 'GET';
|
|
189
|
+
submitParams.action = new URL(form.hasAttribute('action') ? form.getAttribute('action') : (
|
|
190
|
+
submitter?.hasAttribute('formaction') ? submitter.getAttribute('formaction') : ''),
|
|
191
|
+
this.location.href);
|
|
192
|
+
if ((submitParams.target && !submitParams.target.startsWith('_webflo:')) || !this.isSpaRoute(submitParams.action)) return;
|
|
193
|
+
// ---------------
|
|
194
|
+
// Handle now
|
|
195
|
+
let formData = new FormData(form);
|
|
196
|
+
if ((submitter || {}).name) {
|
|
197
|
+
formData.set(submitter.name, submitter.value);
|
|
198
|
+
}
|
|
199
|
+
if (submitParams.method.toUpperCase() === 'GET') {
|
|
200
|
+
Array.from(formData.entries()).forEach((_entry) => {
|
|
201
|
+
submitParams.action.searchParams.set(_entry[0], _entry[1]);
|
|
202
|
+
});
|
|
203
|
+
formData = null;
|
|
204
|
+
}
|
|
205
|
+
if (this.isHashChange(submitParams.action) && submitParams.method.toUpperCase() !== 'POST') {
|
|
206
|
+
Observer.set(this.location, 'href', submitParams.action.href);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
e.preventDefault();
|
|
210
|
+
this._abortController?.abort();
|
|
211
|
+
this._abortController = new AbortController;
|
|
212
|
+
// Note the order of calls below
|
|
213
|
+
const detail = {
|
|
214
|
+
navigationType: 'push',
|
|
215
|
+
navigationOrigins: [submitter, form],
|
|
216
|
+
destination: this._asEntry(null),
|
|
217
|
+
source: this.currentEntry(), // this
|
|
218
|
+
userInitiated: true,
|
|
219
|
+
};
|
|
220
|
+
if (submitParams.target === '_webflo:_parent' && this.superRuntime) {
|
|
221
|
+
setStates(submitParams.action, detail, submitParams.method);
|
|
222
|
+
this.superRuntime.navigate(
|
|
223
|
+
submitParams.action,
|
|
224
|
+
{
|
|
225
|
+
method: submitParams.method,
|
|
226
|
+
body: formData,
|
|
227
|
+
signal: this._abortController.signal,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
...detail,
|
|
231
|
+
isHoisted: true,
|
|
232
|
+
}
|
|
233
|
+
).then(resetStates);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
locationCallback(submitParams.action); // this
|
|
237
|
+
this.navigate(
|
|
238
|
+
submitParams.action,
|
|
239
|
+
{
|
|
240
|
+
method: submitParams.method,
|
|
241
|
+
body: formData,
|
|
242
|
+
signal: this._abortController.signal,
|
|
243
|
+
},
|
|
244
|
+
detail
|
|
245
|
+
); // this
|
|
246
|
+
};
|
|
247
|
+
this.host.addEventListener('click', clickHandler);
|
|
248
|
+
this.host.addEventListener('submit', submitHandler);
|
|
249
|
+
return () => {
|
|
250
|
+
this.host.removeEventListener('click', clickHandler);
|
|
251
|
+
this.host.removeEventListener('submit', submitHandler);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
_asEntry(state) { return { getState() { return state; } }; }
|
|
256
|
+
|
|
257
|
+
_canIntercept(e) { return !(e.metaKey || e.altKey || e.ctrlKey || e.shiftKey); }
|
|
258
|
+
|
|
259
|
+
_xRedirectCode = 200;
|
|
260
|
+
|
|
261
|
+
isHashChange(urlObj) { return _before(this.location.href, '#') === _before(urlObj.href, '#') && (this.location.href.includes('#') || urlObj.href.includes('#')); }
|
|
262
|
+
|
|
263
|
+
isSpaRoute(urlObj) {
|
|
264
|
+
urlObj = typeof urlObj === 'string' ? new URL(urlObj, this.location.origin) : urlObj;
|
|
265
|
+
if (urlObj.origin && urlObj.origin !== this.location.origin) return false;
|
|
266
|
+
if (!this.cx.params.routing) return true;
|
|
267
|
+
if (this.cx.params.routing.targets === false/** explicit false means disabled */) return false;
|
|
268
|
+
let b = urlObj.pathname.split('/').filter(s => s);
|
|
269
|
+
const match = a => {
|
|
270
|
+
a = a.split('/').filter(s => s);
|
|
271
|
+
return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
|
|
272
|
+
};
|
|
273
|
+
return match(this.cx.params.routing.root) && this.cx.params.routing.subroots.reduce((prev, subroot) => {
|
|
274
|
+
return prev && !match(subroot);
|
|
275
|
+
}, true);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async redirect(location, backgroundMessaging) {
|
|
279
|
+
location = typeof location === 'string' ? new URL(location, this.location.origin) : location;
|
|
280
|
+
if (this.isSpaRoute(location)) {
|
|
281
|
+
await this.navigate(location, {}, { navigationType: 'rdr' });
|
|
282
|
+
} else this.hardRedirect(location, backgroundMessaging);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
hardRedirect(location, backgroundMessaging) {
|
|
286
|
+
if (backgroundMessaging) {
|
|
287
|
+
// Redundant as this is a window reload anyways
|
|
288
|
+
backgroundMessaging.close();
|
|
289
|
+
}
|
|
290
|
+
window.location = location;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
createRequest(href, init = {}) {
|
|
294
|
+
return new Request(href, {
|
|
295
|
+
...init,
|
|
296
|
+
headers: {
|
|
297
|
+
'Accept': 'application/json',
|
|
298
|
+
'X-Redirect-Policy': 'manual-when-cross-spa',
|
|
299
|
+
'X-Redirect-Code': this._xRedirectCode,
|
|
300
|
+
'X-Powered-By': '@webqit/webflo',
|
|
301
|
+
...(init.headers || {}),
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async navigate(url, init = {}, detail = {}) {
|
|
307
|
+
// Resolve inputs
|
|
308
|
+
const scope = { url, init, detail };
|
|
309
|
+
if (typeof scope.url === 'string') {
|
|
310
|
+
scope.url = new URL(scope.url, self.location.origin);
|
|
311
|
+
}
|
|
312
|
+
// Ping any existing background process
|
|
313
|
+
this.#backgroundMessaging.postMessage('navigation');
|
|
314
|
+
// Process request...
|
|
315
|
+
scope.response = await new Promise(async (resolveResponse) => {
|
|
316
|
+
scope.handleRespondWith = async (response) => {
|
|
317
|
+
if (scope.finalResponseSeen) {
|
|
318
|
+
throw new Error('Final response already sent');
|
|
319
|
+
}
|
|
320
|
+
if (scope.initialResponseSeen) {
|
|
321
|
+
return await this.execPush(scope.clientMessaging, response);
|
|
322
|
+
}
|
|
323
|
+
response = await this.normalizeResponse(scope.httpEvent, response, true);
|
|
324
|
+
resolveResponse(response);
|
|
325
|
+
};
|
|
326
|
+
// Create and route request
|
|
327
|
+
scope.request = this.createRequest(scope.url, scope.init);
|
|
328
|
+
scope.cookies = this.constructor.CookieStorage.create(scope.request);
|
|
329
|
+
scope.session = this.constructor.SessionStorage.create(scope.request);
|
|
330
|
+
const messageChannel = new MessageChannel;
|
|
331
|
+
this.backgroundMessaging.add(new MessagingOverChannel(null, messageChannel.port1));
|
|
332
|
+
scope.clientMessaging = new ClientMessaging(this, messageChannel.port2);
|
|
333
|
+
scope.user = this.constructor.HttpUser.create(
|
|
334
|
+
scope.request,
|
|
335
|
+
scope.session,
|
|
336
|
+
scope.clientMessaging
|
|
337
|
+
);
|
|
338
|
+
scope.httpEvent = this.constructor.HttpEvent.create(scope.handleRespondWith, {
|
|
339
|
+
request: scope.request,
|
|
340
|
+
detail: scope.detail,
|
|
341
|
+
cookies: scope.cookies,
|
|
342
|
+
session: scope.session,
|
|
343
|
+
user: scope.user,
|
|
344
|
+
client: scope.clientMessaging
|
|
345
|
+
});
|
|
346
|
+
scope.httpEvent.onRequestClone = () => this.createRequest(scope.url, scope.init);
|
|
347
|
+
// Ste pre-request states
|
|
348
|
+
Observer.set(this.navigator, {
|
|
349
|
+
requesting: new Url/*NOT URL*/(scope.url),
|
|
350
|
+
origins: scope.detail.navigationOrigins || [],
|
|
351
|
+
method: scope.request.method,
|
|
352
|
+
error: null
|
|
353
|
+
});
|
|
354
|
+
scope.resetStates = () => {
|
|
355
|
+
Observer.set(this.navigator, {
|
|
356
|
+
requesting: null,
|
|
357
|
+
remotely: false,
|
|
358
|
+
origins: [],
|
|
359
|
+
method: null
|
|
360
|
+
});
|
|
361
|
+
};
|
|
362
|
+
scope.context = {};
|
|
363
|
+
if (window.webqit?.oohtml?.configs) {
|
|
364
|
+
const { BINDINGS_API: { api: bindingsConfig } = {}, } = window.webqit.oohtml.configs;
|
|
365
|
+
scope.context = this.host[bindingsConfig.bindings].data || {};
|
|
366
|
+
}
|
|
367
|
+
// Dispatch for response
|
|
368
|
+
scope.$response = await this.dispatch(scope.httpEvent, scope.context, async (event) => {
|
|
369
|
+
// Was this nexted()? Tell the next layer we're in JSON mode by default
|
|
370
|
+
if (event !== scope.httpEvent && !event.request.headers.has('Accept')) {
|
|
371
|
+
event.request.headers.set('Accept', 'application/json');
|
|
372
|
+
}
|
|
373
|
+
return await this.remoteFetch(event.request);
|
|
374
|
+
});
|
|
375
|
+
// Final reponse!!!
|
|
376
|
+
scope.finalResponseSeen = true;
|
|
377
|
+
if (scope.initialResponseSeen) {
|
|
378
|
+
// Send via background port
|
|
379
|
+
if (typeof scope.$response !== 'undefined') {
|
|
380
|
+
await this.execPush(scope.clientMessaging, scope.$response);
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
// Send normally
|
|
385
|
+
scope.$response = await this.normalizeResponse(scope.httpEvent, scope.$response);
|
|
386
|
+
resolveResponse(scope.$response);
|
|
387
|
+
});
|
|
388
|
+
scope.initialResponseSeen = true;
|
|
389
|
+
scope.finalUrl = scope.response.url || scope.request.url;
|
|
390
|
+
if (scope.response.redirected || scope.detail.navigationType === 'rdr' || scope.detail.isHoisted) {
|
|
391
|
+
const stateData = { ...(this.currentEntry()?.getState() || {}), redirected: true, };
|
|
392
|
+
await this.updateCurrentEntry({ state: stateData }, scope.finalUrl);
|
|
393
|
+
}
|
|
394
|
+
if (scope.response.headers.has('X-Background-Messaging')) {
|
|
395
|
+
scope.backgroundMessaging = this.$createBackgroundMessagingFrom(
|
|
396
|
+
scope.response.headers.get('X-Background-Messaging')
|
|
397
|
+
);
|
|
398
|
+
this.backgroundMessaging.add(scope.backgroundMessaging);
|
|
399
|
+
}
|
|
400
|
+
if (scope.response.headers.has('Location')) {
|
|
401
|
+
// Normalize redirect
|
|
402
|
+
const xActualRedirectCode = parseInt(scope.response.headers.get('X-Redirect-Code'));
|
|
403
|
+
if (xActualRedirectCode && scope.response.status === this._xRedirectCode) {
|
|
404
|
+
scope.response.meta.status = xActualRedirectCode; // @NOTE 1
|
|
405
|
+
}
|
|
406
|
+
// Trigger redirect
|
|
407
|
+
if ([302, 301].includes(scope.response.status)) {
|
|
408
|
+
const location = scope.response.headers.get('Location');
|
|
409
|
+
this.redirect(location, scope.backgroundMessaging);
|
|
410
|
+
if (scope.backgroundMessaging) {
|
|
411
|
+
scope.backgroundMessaging.addEventListener('response', () => {
|
|
412
|
+
scope.resetStates();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Only render now
|
|
419
|
+
if ([202/*Accepted*/, 304/*Not Modified*/].includes(scope.response.status)) {
|
|
420
|
+
if (scope.backgroundMessaging) {
|
|
421
|
+
scope.backgroundMessaging.addEventListener('response', () => {
|
|
422
|
+
scope.resetStates();
|
|
423
|
+
});
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
scope.data = scope.context;
|
|
427
|
+
} else {
|
|
428
|
+
scope.data = await scope.response.parse() || {};
|
|
429
|
+
}
|
|
430
|
+
// Transition UI
|
|
431
|
+
Observer.set(this.transition.from, Url.copy(this.location));
|
|
432
|
+
Observer.set(this.transition.to, 'href', scope.finalUrl);
|
|
433
|
+
Observer.set(this.transition, 'rel', this.transition.from.pathname === this.transition.to.pathname ? 'unchanged' : (`${this.transition.from.pathname}/`.startsWith(`${this.transition.to.pathname}/`) ? 'parent' : (`${this.transition.to.pathname}/`.startsWith(`${this.transition.from.pathname}/`) ? 'child' : 'unrelated')));
|
|
434
|
+
await this.transitionUI(async () => {
|
|
435
|
+
Observer.set(this.location, 'href', scope.finalUrl);
|
|
436
|
+
// Set post-request states
|
|
437
|
+
Observer.set(this.navigator, {
|
|
438
|
+
requesting: null,
|
|
439
|
+
remotely: false,
|
|
440
|
+
origins: [],
|
|
441
|
+
method: null
|
|
442
|
+
});
|
|
443
|
+
// Error?
|
|
444
|
+
if ([404, 500].includes(scope.response.status)) {
|
|
445
|
+
const error = new Error(scope.response.statusText, { code: scope.response.status });
|
|
446
|
+
Object.defineProperty(error, 'retry', { value: async () => await this.navigate(scope.url, scope.init, scope.detail) });
|
|
447
|
+
Observer.set(this.navigator, 'error', error);
|
|
448
|
+
}
|
|
449
|
+
if (_isObject(scope.data) && _isObject(scope.data.status)) {
|
|
450
|
+
scope.httpEvent.client.postMessage(scope.data.status, { messageType: 'status' });
|
|
451
|
+
}
|
|
452
|
+
await this.render(scope.httpEvent, scope.data, !(['GET'].includes(scope.request.method) || scope.response.redirected || scope.detail.navigationType === 'rdr'));
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async dispatch(httpEvent, context, crossLayerFetch, processObj = {}) {
|
|
457
|
+
const response = await super.dispatch(httpEvent, context, crossLayerFetch);
|
|
458
|
+
// Handle "retry" directives
|
|
459
|
+
if (response.headers.has('Retry-After')) {
|
|
460
|
+
if (!processObj.recurseController) {
|
|
461
|
+
// This is start of the process
|
|
462
|
+
processObj.recurseController = new AbortController;
|
|
463
|
+
}
|
|
464
|
+
// Ensure a previous recursion hasn't aborted the process
|
|
465
|
+
if (!processObj.recurseController.signal.aborted) {
|
|
466
|
+
await new Promise((res) => setTimeout(res, parseInt(response.headers.get('Retry-After')) * 1000));
|
|
467
|
+
const eventClone = httpEvent.clone();
|
|
468
|
+
return await this.dispatch(eventClone, context, crossLayerFetch, processObj);
|
|
469
|
+
}
|
|
470
|
+
} else if (processObj.recurseController) {
|
|
471
|
+
// Abort the signal. This is the end of the process
|
|
472
|
+
processObj.recurseController.abort();
|
|
473
|
+
}
|
|
474
|
+
return response;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
$createBackgroundMessagingFrom(uri) {
|
|
478
|
+
const [proto, portID] = uri.split(':');
|
|
479
|
+
let instance;
|
|
480
|
+
if (proto === 'ch') {
|
|
481
|
+
instance = new MessagingOverBroadcast(null, portID);
|
|
482
|
+
} else {
|
|
483
|
+
instance = new MessagingOverSocket(null, portID);
|
|
484
|
+
}
|
|
485
|
+
return instance;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async transitionUI(updateCallback) {
|
|
489
|
+
if (document.startViewTransition) {
|
|
490
|
+
const synthesizeWhile = window.webqit?.realdom?.synthesizeWhile || ((callback) => callback());
|
|
491
|
+
await synthesizeWhile(async () => {
|
|
492
|
+
Observer.set(this.transition, 'phase', 1);
|
|
493
|
+
const viewTransition = document.startViewTransition(updateCallback);
|
|
494
|
+
try { await viewTransition.updateCallbackDone; } catch (e) { console.log(e); }
|
|
495
|
+
Observer.set(this.transition, 'phase', 2);
|
|
496
|
+
try { await viewTransition.ready; } catch (e) { console.log(e); }
|
|
497
|
+
Observer.set(this.transition, 'phase', 3);
|
|
498
|
+
try { await viewTransition.finished; } catch (e) { console.log(e); }
|
|
499
|
+
Observer.set(this.transition, 'phase', 0);
|
|
500
|
+
});
|
|
501
|
+
} else await updateCallback();
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async render(httpEvent, data, merge = false) {
|
|
505
|
+
const router = new this.constructor.Router(this.cx, this.location.pathname);
|
|
506
|
+
await router.route('render', httpEvent, data, async (httpEvent, data) => {
|
|
507
|
+
if (!window.webqit?.oohtml?.configs) return;
|
|
508
|
+
if (window.webqit?.dom) {
|
|
509
|
+
await new Promise(res => window.webqit.dom.ready(res));
|
|
510
|
+
}
|
|
511
|
+
const {
|
|
512
|
+
BINDINGS_API: { api: bindingsConfig } = {},
|
|
513
|
+
HTML_IMPORTS: { attr: modulesContextAttrs } = {},
|
|
514
|
+
} = window.webqit.oohtml.configs;
|
|
515
|
+
if (bindingsConfig) {
|
|
516
|
+
this.host[bindingsConfig.bind]({
|
|
517
|
+
state: {},
|
|
518
|
+
...(!_isObject(data) ? {} : data),
|
|
519
|
+
env: 'client',
|
|
520
|
+
navigator: this.navigator,
|
|
521
|
+
location: this.location,
|
|
522
|
+
network: this.network, // request, redirect, error, status, remote
|
|
523
|
+
transition: this.transition,
|
|
524
|
+
background: null
|
|
525
|
+
}, { diff: true, merge });
|
|
526
|
+
let overridenKeys;
|
|
527
|
+
if (_isObject(data) && (overridenKeys = ['env', 'navigator', 'location', 'network', 'transition', 'background'].filter((k) => k in data)).length) {
|
|
528
|
+
console.error(`The following data properties were overridden: ${overridenKeys.join(', ')}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (modulesContextAttrs) {
|
|
532
|
+
const newRoute = '/' + `routes/${this.location.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
|
|
533
|
+
(this.host === window.document ? window.document.body : this.host).setAttribute(modulesContextAttrs.importscontext, newRoute);
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async remoteFetch(request, ...args) {
|
|
539
|
+
Observer.set(this.#navigator, 'remotely', true);
|
|
540
|
+
const response = await xfetch(request, ...args);
|
|
541
|
+
Observer.set(this.#navigator, 'remotely', false);
|
|
542
|
+
return response;
|
|
543
|
+
}
|
|
544
|
+
}
|