@webqit/webflo 0.11.61-0 → 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.
Files changed (118) hide show
  1. package/.gitignore +7 -7
  2. package/LICENSE +20 -20
  3. package/README.md +2079 -2074
  4. package/docker/Dockerfile +42 -42
  5. package/docker/README.md +91 -91
  6. package/docker/package.json +2 -2
  7. package/package.json +80 -81
  8. package/src/{Context.js → AbstractContext.js} +71 -79
  9. package/src/config-pi/deployment/Env.js +68 -68
  10. package/src/config-pi/deployment/Layout.js +63 -63
  11. package/src/config-pi/deployment/Origins.js +139 -139
  12. package/src/config-pi/deployment/Proxy.js +74 -74
  13. package/src/config-pi/deployment/index.js +17 -17
  14. package/src/config-pi/index.js +15 -15
  15. package/src/config-pi/runtime/Client.js +116 -98
  16. package/src/config-pi/runtime/Server.js +125 -125
  17. package/src/config-pi/runtime/client/Worker.js +109 -134
  18. package/src/config-pi/runtime/client/index.js +11 -11
  19. package/src/config-pi/runtime/index.js +17 -17
  20. package/src/config-pi/runtime/server/Headers.js +74 -74
  21. package/src/config-pi/runtime/server/Redirects.js +69 -69
  22. package/src/config-pi/runtime/server/index.js +13 -13
  23. package/src/config-pi/static/Manifest.js +319 -319
  24. package/src/config-pi/static/Ssg.js +49 -49
  25. package/src/config-pi/static/index.js +13 -13
  26. package/src/deployment-pi/index.js +10 -10
  27. package/src/deployment-pi/origins/index.js +216 -216
  28. package/src/index.js +11 -19
  29. package/src/runtime-pi/HttpEvent.js +126 -106
  30. package/src/runtime-pi/HttpUser.js +126 -0
  31. package/src/runtime-pi/MessagingOverBroadcast.js +9 -0
  32. package/src/runtime-pi/MessagingOverChannel.js +85 -0
  33. package/src/runtime-pi/MessagingOverSocket.js +106 -0
  34. package/src/runtime-pi/MultiportMessagingAPI.js +81 -0
  35. package/src/runtime-pi/WebfloCookieStorage.js +27 -0
  36. package/src/runtime-pi/WebfloEventTarget.js +39 -0
  37. package/src/runtime-pi/WebfloMessageEvent.js +58 -0
  38. package/src/runtime-pi/WebfloMessagingAPI.js +69 -0
  39. package/src/runtime-pi/{Router.js → WebfloRouter.js} +99 -130
  40. package/src/runtime-pi/WebfloRuntime.js +52 -0
  41. package/src/runtime-pi/WebfloStorage.js +109 -0
  42. package/src/runtime-pi/client/ClientMessaging.js +5 -0
  43. package/src/runtime-pi/client/Context.js +3 -7
  44. package/src/runtime-pi/client/CookieStorage.js +17 -0
  45. package/src/runtime-pi/client/Router.js +38 -48
  46. package/src/runtime-pi/client/SessionStorage.js +33 -0
  47. package/src/runtime-pi/client/Url.js +156 -205
  48. package/src/runtime-pi/client/WebfloClient.js +544 -0
  49. package/src/runtime-pi/client/WebfloRootClient1.js +179 -0
  50. package/src/runtime-pi/client/WebfloRootClient2.js +109 -0
  51. package/src/runtime-pi/client/WebfloSubClient.js +165 -0
  52. package/src/runtime-pi/client/Workport.js +118 -178
  53. package/src/runtime-pi/client/generate.js +480 -471
  54. package/src/runtime-pi/client/index.js +16 -21
  55. package/src/runtime-pi/client/worker/ClientMessaging.js +5 -0
  56. package/src/runtime-pi/client/worker/Context.js +3 -7
  57. package/src/runtime-pi/client/worker/CookieStorage.js +17 -0
  58. package/src/runtime-pi/client/worker/SessionStorage.js +13 -0
  59. package/src/runtime-pi/client/worker/WebfloWorker.js +294 -0
  60. package/src/runtime-pi/client/worker/Workport.js +17 -85
  61. package/src/runtime-pi/client/worker/index.js +10 -21
  62. package/src/runtime-pi/index.js +6 -13
  63. package/src/runtime-pi/server/ClientMessaging.js +18 -0
  64. package/src/runtime-pi/server/ClientMessagingRegistry.js +57 -0
  65. package/src/runtime-pi/server/Context.js +11 -15
  66. package/src/runtime-pi/server/CookieStorage.js +17 -0
  67. package/src/runtime-pi/server/Router.js +93 -159
  68. package/src/runtime-pi/server/SessionStorage.js +53 -0
  69. package/src/runtime-pi/server/WebfloServer.js +755 -0
  70. package/src/runtime-pi/server/index.js +10 -21
  71. package/src/runtime-pi/util-http.js +322 -86
  72. package/src/runtime-pi/util-url.js +146 -146
  73. package/src/runtime-pi/xURL.js +108 -105
  74. package/src/runtime-pi/xfetch.js +22 -22
  75. package/src/services-pi/cert/http-auth-hook.js +22 -22
  76. package/src/services-pi/cert/http-cleanup-hook.js +22 -22
  77. package/src/services-pi/cert/index.js +79 -79
  78. package/src/services-pi/index.js +8 -8
  79. package/src/static-pi/index.js +10 -10
  80. package/src/webflo.js +30 -30
  81. package/test/index.test.js +26 -26
  82. package/test/site/package.json +9 -9
  83. package/test/site/public/bundle.html +5 -5
  84. package/test/site/public/bundle.html.json +3 -3
  85. package/test/site/public/bundle.js +2 -2
  86. package/test/site/public/bundle.webflo.js +15 -15
  87. package/test/site/public/index.html +29 -29
  88. package/test/site/public/index1.html +34 -34
  89. package/test/site/public/page-2/bundle.html +4 -4
  90. package/test/site/public/page-2/bundle.js +2 -2
  91. package/test/site/public/page-2/index.html +45 -45
  92. package/test/site/public/page-2/main.html +2 -2
  93. package/test/site/public/page-4/subpage/bundle.js +2 -2
  94. package/test/site/public/page-4/subpage/index.html +30 -30
  95. package/test/site/public/sparoots.json +4 -4
  96. package/test/site/public/worker.js +3 -3
  97. package/test/site/server/index.js +15 -15
  98. package/src/runtime-pi/Application.js +0 -29
  99. package/src/runtime-pi/Cookies.js +0 -82
  100. package/src/runtime-pi/Runtime.js +0 -21
  101. package/src/runtime-pi/client/Application.js +0 -100
  102. package/src/runtime-pi/client/Runtime.js +0 -332
  103. package/src/runtime-pi/client/createStorage.js +0 -57
  104. package/src/runtime-pi/client/oohtml/full.js +0 -7
  105. package/src/runtime-pi/client/oohtml/namespacing.js +0 -7
  106. package/src/runtime-pi/client/oohtml/scripting.js +0 -8
  107. package/src/runtime-pi/client/oohtml/templating.js +0 -8
  108. package/src/runtime-pi/client/worker/Application.js +0 -44
  109. package/src/runtime-pi/client/worker/Runtime.js +0 -269
  110. package/src/runtime-pi/server/Application.js +0 -116
  111. package/src/runtime-pi/server/Runtime.js +0 -557
  112. package/src/runtime-pi/xFormData.js +0 -24
  113. package/src/runtime-pi/xHeaders.js +0 -146
  114. package/src/runtime-pi/xRequest.js +0 -46
  115. package/src/runtime-pi/xRequestHeaders.js +0 -109
  116. package/src/runtime-pi/xResponse.js +0 -33
  117. package/src/runtime-pi/xResponseHeaders.js +0 -117
  118. 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
+ }