@webqit/webflo 0.20.4-next.1 → 0.20.4-next.3
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 +5 -20
- package/site/.vitepress/config.ts +1 -1
- package/site/docs/concepts/realtime.md +57 -53
- package/site/docs/concepts/{request-response.md → requests-responses.md} +1 -1
- package/site/docs/concepts/state.md +1 -1
- package/site/docs/getting-started.md +40 -40
- package/src/{Context.js → CLIContext.js} +9 -8
- package/src/build-pi/esbuild-plugin-livejs-transform.js +35 -0
- package/src/{runtime-pi/webflo-client/webflo-codegen.js → build-pi/index.js} +145 -141
- package/src/index.js +3 -1
- package/src/init-pi/index.js +6 -3
- package/src/init-pi/templates/pwa/package.json +2 -2
- package/src/init-pi/templates/web/package.json +2 -2
- package/src/runtime-pi/AppBootstrap.js +38 -0
- package/src/runtime-pi/WebfloRuntime.js +50 -47
- package/src/runtime-pi/apis.js +9 -0
- package/src/runtime-pi/index.js +2 -4
- package/src/runtime-pi/webflo-client/WebfloClient.js +31 -35
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +16 -14
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +13 -13
- package/src/runtime-pi/webflo-client/bootstrap.js +37 -0
- package/src/runtime-pi/webflo-client/index.js +2 -8
- package/src/runtime-pi/webflo-client/webflo-devmode.js +3 -3
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +127 -96
- package/src/runtime-pi/webflo-fetch/index.js +435 -5
- package/src/runtime-pi/webflo-routing/HttpCookies.js +1 -1
- package/src/runtime-pi/webflo-routing/HttpEvent.js +5 -6
- package/src/runtime-pi/webflo-routing/HttpUser.js +7 -7
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +3 -1
- package/src/runtime-pi/webflo-server/ServerSideSession.js +2 -1
- package/src/runtime-pi/webflo-server/WebfloServer.js +98 -195
- package/src/runtime-pi/webflo-server/bootstrap.js +59 -0
- package/src/runtime-pi/webflo-server/index.js +2 -6
- package/src/runtime-pi/webflo-server/webflo-devmode.js +13 -24
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +11 -15
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
- package/src/runtime-pi/webflo-worker/bootstrap.js +38 -0
- package/src/runtime-pi/webflo-worker/index.js +3 -7
- package/src/webflo-cli.js +1 -2
- package/src/runtime-pi/webflo-fetch/cookies.js +0 -10
- package/src/runtime-pi/webflo-fetch/fetch.js +0 -16
- package/src/runtime-pi/webflo-fetch/formdata.js +0 -54
- package/src/runtime-pi/webflo-fetch/headers.js +0 -151
- package/src/runtime-pi/webflo-fetch/message.js +0 -49
- package/src/runtime-pi/webflo-fetch/request.js +0 -62
- package/src/runtime-pi/webflo-fetch/response.js +0 -110
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Context } from '../Context.js';
|
|
2
1
|
import { WebfloRouter } from './webflo-routing/WebfloRouter.js';
|
|
2
|
+
import { LiveResponse, response as responseShim, headers as headersShim } from './webflo-fetch/index.js';
|
|
3
|
+
import { AppBootstrap } from './AppBootstrap.js';
|
|
3
4
|
import { _wq } from '../util.js';
|
|
4
5
|
|
|
5
6
|
export class WebfloRuntime {
|
|
@@ -7,55 +8,57 @@ export class WebfloRuntime {
|
|
|
7
8
|
#instanceController = new AbortController;
|
|
8
9
|
get $instanceController() { return this.#instanceController; }
|
|
9
10
|
|
|
10
|
-
static get Context() { return Context; }
|
|
11
|
-
|
|
12
11
|
static get Router() { return WebfloRouter; }
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
get cx() { return this.#cx; }
|
|
13
|
+
static create(bootstrap) { return new this(bootstrap); }
|
|
16
14
|
|
|
17
|
-
#
|
|
18
|
-
get config() { return this.#config; }
|
|
15
|
+
#bootstrap;
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
get
|
|
17
|
+
get bootstrap() { return this.#bootstrap; }
|
|
18
|
+
get cx() { return this.bootstrap.cx; }
|
|
19
|
+
get config() { return this.bootstrap.config; }
|
|
20
|
+
get routes() { return this.bootstrap.routes; }
|
|
22
21
|
|
|
23
|
-
constructor(
|
|
24
|
-
|
|
25
|
-
throw new Error('Argument #1 must be a Webflo Context instance');
|
|
26
|
-
}
|
|
27
|
-
this.#cx = cx;
|
|
28
|
-
this.#config = this.#cx.config;
|
|
29
|
-
this.#routes = this.#cx.routes;
|
|
22
|
+
constructor(bootstrap) {
|
|
23
|
+
this.#bootstrap = new AppBootstrap(bootstrap);
|
|
30
24
|
}
|
|
31
25
|
|
|
32
26
|
env(key) {
|
|
33
27
|
const { ENV } = this.config;
|
|
34
28
|
return key in ENV.mappings
|
|
35
|
-
? ENV.data[ENV.mappings[key]]
|
|
36
|
-
: ENV.data[key];
|
|
29
|
+
? ENV.data?.[ENV.mappings[key]]
|
|
30
|
+
: ENV.data?.[key];
|
|
37
31
|
}
|
|
38
32
|
|
|
39
33
|
async initialize() {
|
|
40
|
-
|
|
34
|
+
if (!this.bootstrap.init.createStorage) {
|
|
35
|
+
const inmemSessionRegistry = new Map;
|
|
36
|
+
this.bootstrap.init.createStorage = (namespace) => {
|
|
37
|
+
if (!inmemSessionRegistry.has(namespace)) {
|
|
38
|
+
inmemSessionRegistry.set(namespace, new Map);
|
|
39
|
+
}
|
|
40
|
+
return inmemSessionRegistry.get(namespace);
|
|
41
|
+
};
|
|
42
|
+
}
|
|
41
43
|
return this.#instanceController;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
async setupCapabilities() {
|
|
45
|
-
// Do init work
|
|
46
47
|
return this.#instanceController;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
async hydrate() {
|
|
50
|
-
// Do hydration work
|
|
51
51
|
return this.#instanceController;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
control() {
|
|
55
|
-
// Do control work
|
|
56
55
|
return this.#instanceController;
|
|
57
56
|
}
|
|
58
57
|
|
|
58
|
+
async createStorage(namespace, ttl) {
|
|
59
|
+
return await this.bootstrap.init.createStorage(namespace, ttl);
|
|
60
|
+
}
|
|
61
|
+
|
|
59
62
|
createRequest(href, init = {}) {
|
|
60
63
|
return new Request(href, init);
|
|
61
64
|
}
|
|
@@ -68,16 +71,16 @@ export class WebfloRuntime {
|
|
|
68
71
|
return this.constructor.HttpSession.create({ store, request, ...rest });
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
createHttpUser({ store, request, session,
|
|
72
|
-
return this.constructor.HttpUser.create({ store, request, session,
|
|
74
|
+
createHttpUser({ store, request, session, client, ...rest }) {
|
|
75
|
+
return this.constructor.HttpUser.create({ store, request, session, client, ...rest });
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
createHttpEvent({ request, cookies, session, user,
|
|
76
|
-
return this.constructor.HttpEvent.create(null, { request, cookies, session, user,
|
|
78
|
+
createHttpEvent({ request, cookies, session, user, client, detail, signal, state, ...rest }) {
|
|
79
|
+
return this.constructor.HttpEvent.create(null, { request, cookies, session, user, client, detail, signal, state, ...rest });
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
async dispatchNavigationEvent({ httpEvent, crossLayerFetch,
|
|
80
|
-
const { flags: FLAGS } = this.cx;
|
|
82
|
+
async dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB }) {
|
|
83
|
+
const { flags: FLAGS, logger: LOGGER } = this.cx;
|
|
81
84
|
// Resolve rid before dispatching
|
|
82
85
|
if (httpEvent.request.method === 'GET' && httpEvent.url.query['_rid']) {
|
|
83
86
|
const requestMeta = _wq(httpEvent.request, 'meta');
|
|
@@ -91,7 +94,7 @@ export class WebfloRuntime {
|
|
|
91
94
|
// Do proper routing for respone
|
|
92
95
|
const response = await new Promise(async (resolve) => {
|
|
93
96
|
let autoLiveResponse, response;
|
|
94
|
-
httpEvent.
|
|
97
|
+
httpEvent.client.wqLifecycle.messaging.then(() => {
|
|
95
98
|
autoLiveResponse = new LiveResponse(null, { status: 202, statusText: 'Accepted', done: false });
|
|
96
99
|
resolve(autoLiveResponse);
|
|
97
100
|
});
|
|
@@ -100,10 +103,10 @@ export class WebfloRuntime {
|
|
|
100
103
|
const remoteFetch = (...args) => this.remoteFetch(...args);
|
|
101
104
|
return await router.route(routeMethods, httpEvent, crossLayerFetch, remoteFetch);
|
|
102
105
|
};
|
|
103
|
-
const fullRoutingPipeline =
|
|
106
|
+
const fullRoutingPipeline = this.bootstrap.middlewares.concat(route);
|
|
104
107
|
try {
|
|
105
108
|
response = await fullRoutingPipeline.reverse().reduce((next, fn) => {
|
|
106
|
-
return () => fn.call(this.cx, httpEvent,
|
|
109
|
+
return () => fn.call(this.cx, httpEvent, next);
|
|
107
110
|
}, null)()/*immediately calling the first*/;
|
|
108
111
|
} catch (e) {
|
|
109
112
|
console.error(e);
|
|
@@ -111,7 +114,7 @@ export class WebfloRuntime {
|
|
|
111
114
|
}
|
|
112
115
|
if (!(response instanceof LiveResponse) && !(response instanceof Response)) {
|
|
113
116
|
response = LiveResponse.test(response) === 'Default'
|
|
114
|
-
?
|
|
117
|
+
? responseShim.from.value(response)
|
|
115
118
|
: await LiveResponse.from(response, { responsesOK: true });
|
|
116
119
|
}
|
|
117
120
|
// Any "carry" data?
|
|
@@ -139,42 +142,42 @@ export class WebfloRuntime {
|
|
|
139
142
|
if (!httpEvent.lifeCycleComplete()) {
|
|
140
143
|
if (this.isClientSide) {
|
|
141
144
|
const responseMeta = _wq(response, 'meta');
|
|
142
|
-
responseMeta.set('
|
|
145
|
+
responseMeta.set('background_port', clientPortB);
|
|
143
146
|
} else {
|
|
144
|
-
const
|
|
145
|
-
response.headers.set('X-Background-Messaging-Port',
|
|
147
|
+
const upstreamBackgroundPort = response.headers.get('X-Background-Messaging-Port');
|
|
148
|
+
response.headers.set('X-Background-Messaging-Port', clientPortB);
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
// On navigation:
|
|
149
|
-
// Abort httpEvent.
|
|
150
|
-
httpEvent.
|
|
152
|
+
// Abort httpEvent.client and httpEvent itself
|
|
153
|
+
httpEvent.client.addEventListener('navigate', (e) => {
|
|
151
154
|
setTimeout(() => { // Allow for global handlers to see the events
|
|
152
155
|
if (e.defaultPrevented) {
|
|
153
|
-
|
|
156
|
+
LOGGER.log(`Client Messaging Port on ${httpEvent.request.url} not auto-closed on user navigation.`);
|
|
154
157
|
} else {
|
|
155
|
-
httpEvent.
|
|
158
|
+
httpEvent.client.close();
|
|
156
159
|
httpEvent.abort();
|
|
157
160
|
}
|
|
158
161
|
}, 0);
|
|
159
162
|
});
|
|
160
163
|
// On close:
|
|
161
164
|
// Abort httpEvent itself
|
|
162
|
-
httpEvent.
|
|
165
|
+
httpEvent.client.wqLifecycle.close.then(() => {
|
|
163
166
|
httpEvent.abort();
|
|
164
167
|
});
|
|
165
168
|
|
|
166
169
|
// On ROOT event complete:
|
|
167
|
-
// Close httpEvent.
|
|
170
|
+
// Close httpEvent.client
|
|
168
171
|
httpEvent.lifeCycleComplete(true).then(() => {
|
|
169
|
-
httpEvent.
|
|
172
|
+
httpEvent.client.close();
|
|
170
173
|
});
|
|
171
174
|
}
|
|
172
175
|
|
|
173
176
|
if (!this.isClientSide && response instanceof LiveResponse) {
|
|
174
177
|
// Must convert to Response on the server-side before returning
|
|
175
|
-
return response.toResponse({
|
|
178
|
+
return response.toResponse({ client: httpEvent.client });
|
|
176
179
|
}
|
|
177
|
-
|
|
180
|
+
|
|
178
181
|
return response;
|
|
179
182
|
}
|
|
180
183
|
|
|
@@ -210,8 +213,8 @@ export class WebfloRuntime {
|
|
|
210
213
|
const flashResponses = requestMeta.get('carries')?.map((c) => c.response).filter((r) => r);
|
|
211
214
|
if (flashResponses?.length) {
|
|
212
215
|
httpEvent.waitUntil(new Promise((resolve) => {
|
|
213
|
-
httpEvent.
|
|
214
|
-
httpEvent.
|
|
216
|
+
httpEvent.client.wqLifecycle.open.then(() => {
|
|
217
|
+
httpEvent.client.postMessage(flashResponses, { wqEventOptions: { type: 'flash' } });
|
|
215
218
|
resolve();
|
|
216
219
|
}, { once: true });
|
|
217
220
|
}));
|
|
@@ -315,7 +318,7 @@ export class WebfloRuntime {
|
|
|
315
318
|
|
|
316
319
|
createStreamingResponse(httpEvent, readStream, stats) {
|
|
317
320
|
let response;
|
|
318
|
-
const requestRange = httpEvent.request.headers
|
|
321
|
+
const requestRange = headersShim.get.value.call(httpEvent.request.headers, 'Range', true); // Parses the Range header
|
|
319
322
|
if (requestRange.length) {
|
|
320
323
|
const streams = requestRange.reduce((streams, range) => {
|
|
321
324
|
if (!streams) return;
|
package/src/runtime-pi/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import { _isObject } from '@webqit/util/js/index.js';
|
|
|
3
3
|
import { Observer } from '@webqit/quantum-js';
|
|
4
4
|
import { WebfloRuntime } from '../WebfloRuntime.js';
|
|
5
5
|
import { WQMessageChannel } from '../webflo-messaging/WQMessageChannel.js';
|
|
6
|
+
import { LiveResponse, response as responseShim } from '../webflo-fetch/index.js';
|
|
6
7
|
import { WQStarPort } from '../webflo-messaging/WQStarPort.js';
|
|
7
8
|
import { ClientSideCookies } from './ClientSideCookies.js';
|
|
8
9
|
import { HttpSession } from '../webflo-routing/HttpSession.js';
|
|
@@ -38,13 +39,10 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
38
39
|
#background;
|
|
39
40
|
get background() { return this.#background; }
|
|
40
41
|
|
|
41
|
-
#sdk = {};
|
|
42
|
-
get sdk() { return this.#sdk; }
|
|
43
|
-
|
|
44
42
|
get isClientSide() { return true; }
|
|
45
43
|
|
|
46
|
-
constructor(
|
|
47
|
-
super(
|
|
44
|
+
constructor(bootstrap, host) {
|
|
45
|
+
super(bootstrap);
|
|
48
46
|
this.#host = host;
|
|
49
47
|
Object.defineProperty(this.host, 'webfloRuntime', { get: () => this });
|
|
50
48
|
this.#location = new Url/*NOT URL*/(this.host.location);
|
|
@@ -88,12 +86,6 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
88
86
|
return instanceController;
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
async setupCapabilities() {
|
|
92
|
-
const instanceController = await super.setupCapabilities();
|
|
93
|
-
this.#sdk.Observer = Observer;
|
|
94
|
-
return instanceController;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
89
|
controlSuper() {
|
|
98
90
|
return super.control();
|
|
99
91
|
}
|
|
@@ -241,7 +233,7 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
241
233
|
a = a.split('/').filter(s => s);
|
|
242
234
|
return a.reduce((prev, s, i) => prev && (s === b[i] || [s, b[i]].includes('-')), true);
|
|
243
235
|
};
|
|
244
|
-
return match(this.
|
|
236
|
+
return match(this.bootstrap.$root) && this.bootstrap.$sparoots.reduce((prev, subroot) => {
|
|
245
237
|
return prev && !match(subroot);
|
|
246
238
|
}, true);
|
|
247
239
|
}
|
|
@@ -277,15 +269,15 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
277
269
|
request: scopeObj.request
|
|
278
270
|
});
|
|
279
271
|
scopeObj.session = this.createHttpSession({
|
|
280
|
-
store: this
|
|
272
|
+
store: this.createStorage('session'),
|
|
281
273
|
request: scopeObj.request
|
|
282
274
|
});
|
|
283
275
|
const wqMessageChannel = new WQMessageChannel;
|
|
284
276
|
scopeObj.clientRequestRealtime = wqMessageChannel.port1;
|
|
285
277
|
scopeObj.user = this.createHttpUser({
|
|
286
|
-
store: this
|
|
278
|
+
store: this.createStorage('user'),
|
|
287
279
|
request: scopeObj.request,
|
|
288
|
-
|
|
280
|
+
client: scopeObj.clientRequestRealtime,
|
|
289
281
|
session: scopeObj.session,
|
|
290
282
|
});
|
|
291
283
|
if (window.webqit?.oohtml?.configs) {
|
|
@@ -294,11 +286,10 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
294
286
|
}
|
|
295
287
|
scopeObj.httpEvent = this.createHttpEvent({
|
|
296
288
|
request: scopeObj.request,
|
|
297
|
-
|
|
289
|
+
client: scopeObj.clientRequestRealtime,
|
|
298
290
|
cookies: scopeObj.cookies,
|
|
299
291
|
session: scopeObj.session,
|
|
300
292
|
user: scopeObj.user,
|
|
301
|
-
sdk: this.#sdk,
|
|
302
293
|
detail: scopeObj.detail,
|
|
303
294
|
signal: init.signal,
|
|
304
295
|
state: scopeObj.UIState,
|
|
@@ -335,7 +326,7 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
335
326
|
}
|
|
336
327
|
return await this.remoteFetch(event.request);
|
|
337
328
|
},
|
|
338
|
-
|
|
329
|
+
clientPortB: wqMessageChannel.port2,
|
|
339
330
|
originalRequestInit: scopeObj.init
|
|
340
331
|
});
|
|
341
332
|
// Decode response
|
|
@@ -357,8 +348,9 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
357
348
|
Observer.set(this.location, 'href', scopeObj.finalUrl);
|
|
358
349
|
scopeObj.resetStates();
|
|
359
350
|
// Error?
|
|
360
|
-
|
|
361
|
-
|
|
351
|
+
const statusCode = responseShim.prototype.status.get.call(scopeObj.response);
|
|
352
|
+
if ([404, 500].includes(statusCode)) {
|
|
353
|
+
const error = new Error(scopeObj.response.statusText, { code: statusCode });
|
|
362
354
|
Object.defineProperty(error, 'retry', { value: async () => await this.navigate(scopeObj.url, scopeObj.init, scopeObj.detail) });
|
|
363
355
|
Observer.set(this.navigator, 'error', error);
|
|
364
356
|
}
|
|
@@ -372,16 +364,18 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
372
364
|
});
|
|
373
365
|
}
|
|
374
366
|
|
|
375
|
-
async dispatchNavigationEvent({ httpEvent, crossLayerFetch,
|
|
376
|
-
const response = await super.dispatchNavigationEvent({ httpEvent, crossLayerFetch,
|
|
377
|
-
// Obtain and connect
|
|
378
|
-
|
|
379
|
-
|
|
367
|
+
async dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB, originalRequestInit, processObj = {} }) {
|
|
368
|
+
const response = await super.dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB });
|
|
369
|
+
// Obtain and connect clientPortB as first thing
|
|
370
|
+
const backgroundPort = LiveResponse.getBackground(response);
|
|
371
|
+
if (backgroundPort) {
|
|
372
|
+
this.background.addPort(backgroundPort);
|
|
380
373
|
}
|
|
381
374
|
// Await a response with an "Accepted" or redirect status
|
|
382
|
-
|
|
375
|
+
const statusCode = responseShim.prototype.status.get.call(response);
|
|
376
|
+
if (statusCode === 202 || (response.headers.get('Location') && this.processRedirect(response))) {
|
|
383
377
|
return new Promise(async (resolve) => {
|
|
384
|
-
if (
|
|
378
|
+
if (LiveResponse.hasBackground(response)) {
|
|
385
379
|
const liveResponse = await LiveResponse.from(response);
|
|
386
380
|
liveResponse.addEventListener('replace', () => resolve(liveResponse), { once: true, signal: httpEvent.signal });
|
|
387
381
|
} // Never resolves otherwise
|
|
@@ -398,7 +392,7 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
398
392
|
if (!processObj.recurseController.signal.aborted) {
|
|
399
393
|
await new Promise((res) => setTimeout(res, parseInt(response.headers.get('Retry-After')) * 1000));
|
|
400
394
|
const eventClone = httpEvent.cloneWith({ request: this.createRequest(httpEvent.url, originalRequestInit) });
|
|
401
|
-
return await this.dispatchNavigationEvent({ httpEvent: eventClone, crossLayerFetch,
|
|
395
|
+
return await this.dispatchNavigationEvent({ httpEvent: eventClone, crossLayerFetch, clientPortB, originalRequestInit, processObj });
|
|
402
396
|
}
|
|
403
397
|
} else if (processObj.recurseController) {
|
|
404
398
|
// Abort the signal. This is the end of the loop
|
|
@@ -409,27 +403,29 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
409
403
|
|
|
410
404
|
processRedirect(response) {
|
|
411
405
|
// Normalize redirect
|
|
406
|
+
let statusCode = responseShim.prototype.status.get.call(response);
|
|
412
407
|
const xActualRedirectCode = parseInt(response.headers.get('X-Redirect-Code'));
|
|
413
|
-
if (xActualRedirectCode &&
|
|
408
|
+
if (xActualRedirectCode && statusCode === this.#xRedirectCode) {
|
|
414
409
|
const responseMeta = _wq(response, 'meta');
|
|
415
410
|
responseMeta.set('status', xActualRedirectCode); // @NOTE 1
|
|
411
|
+
statusCode = xActualRedirectCode;
|
|
416
412
|
}
|
|
417
413
|
// Trigger redirect
|
|
418
|
-
if ([302, 301].includes(
|
|
414
|
+
if ([302, 301].includes(statusCode)) {
|
|
419
415
|
const location = new URL(response.headers.get('Location'), this.location.origin);
|
|
420
416
|
if (this.isSpaRoute(location)) {
|
|
421
417
|
this.navigate(location, {}, { navigationType: 'rdr' });
|
|
422
418
|
} else {
|
|
423
|
-
this.redirect(location, response
|
|
419
|
+
this.redirect(location, LiveResponse.getBackground(response));
|
|
424
420
|
}
|
|
425
421
|
return true;
|
|
426
422
|
}
|
|
427
423
|
}
|
|
428
424
|
|
|
429
|
-
redirect(location,
|
|
430
|
-
if (
|
|
425
|
+
redirect(location, responseBackground) {
|
|
426
|
+
if (responseBackground) {
|
|
431
427
|
// Redundant as this is a window reload anyways
|
|
432
|
-
|
|
428
|
+
responseBackground.close();
|
|
433
429
|
}
|
|
434
430
|
window.location = location;
|
|
435
431
|
}
|
|
@@ -470,7 +466,7 @@ export class WebfloClient extends WebfloRuntime {
|
|
|
470
466
|
navigator: this.navigator,
|
|
471
467
|
location: this.location,
|
|
472
468
|
network: this.network, // request, redirect, error, status, remote
|
|
473
|
-
capabilities: this.
|
|
469
|
+
capabilities: this.capabilities,
|
|
474
470
|
transition: this.transition,
|
|
475
471
|
}, { diff: true, merge });
|
|
476
472
|
$response.addEventListener('replace', (e) => {
|
|
@@ -2,6 +2,7 @@ import { Observer } from '@webqit/quantum-js';
|
|
|
2
2
|
import { WebfloClient } from './WebfloClient.js';
|
|
3
3
|
import { ClientSideWorkport } from './ClientSideWorkport.js';
|
|
4
4
|
import { DeviceCapabilities } from './DeviceCapabilities.js';
|
|
5
|
+
import { LiveResponse, response as responseShim } from '../webflo-fetch/index.js';
|
|
5
6
|
import { WebfloHMR } from './webflo-devmode.js';
|
|
6
7
|
|
|
7
8
|
export class WebfloRootClient1 extends WebfloClient {
|
|
@@ -10,8 +11,8 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
10
11
|
|
|
11
12
|
static get DeviceCapabilities() { return DeviceCapabilities; }
|
|
12
13
|
|
|
13
|
-
static create(
|
|
14
|
-
return new this(
|
|
14
|
+
static create(bootstrap, host) {
|
|
15
|
+
return new this(bootstrap, host);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
#network;
|
|
@@ -20,8 +21,8 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
20
21
|
#workport;
|
|
21
22
|
get workport() { return this.#workport; }
|
|
22
23
|
|
|
23
|
-
#
|
|
24
|
-
get
|
|
24
|
+
#capabilities;
|
|
25
|
+
get capabilities() { return this.#capabilities; }
|
|
25
26
|
|
|
26
27
|
#hmr;
|
|
27
28
|
|
|
@@ -29,11 +30,11 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
29
30
|
return document.querySelector('meta[name="webflo:viewtransitions"]')?.value;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
constructor(
|
|
33
|
+
constructor(bootstrap, host) {
|
|
33
34
|
if (!(host instanceof Document)) {
|
|
34
35
|
throw new Error('Argument #1 must be a Document instance');
|
|
35
36
|
}
|
|
36
|
-
super(
|
|
37
|
+
super(bootstrap, host);
|
|
37
38
|
this.#network = { status: window.navigator.onLine };
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -87,10 +88,10 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
87
88
|
// Service Worker && Capabilities
|
|
88
89
|
const cleanups = [];
|
|
89
90
|
instanceController.signal.addEventListener('abort', () => cleanups.forEach((c) => c()), { once: true });
|
|
90
|
-
this.#
|
|
91
|
-
cleanups.push(() => this.#
|
|
92
|
-
if (this.config.CLIENT.capabilities?.service_worker
|
|
93
|
-
const {
|
|
91
|
+
this.#capabilities = await this.constructor.DeviceCapabilities.initialize(this, this.config.CLIENT.capabilities);
|
|
92
|
+
cleanups.push(() => this.#capabilities.close());
|
|
93
|
+
if (this.config.CLIENT.capabilities?.service_worker) {
|
|
94
|
+
const { filename, ...restServiceWorkerParams } = this.config.WORKER;
|
|
94
95
|
this.#workport = await this.constructor.Workport.initialize(null, (this.config.CLIENT.public_base_url || '') + filename, restServiceWorkerParams);
|
|
95
96
|
cleanups.push(() => this.#workport.close());
|
|
96
97
|
}
|
|
@@ -101,16 +102,17 @@ export class WebfloRootClient1 extends WebfloClient {
|
|
|
101
102
|
const instanceController = await super.hydrate();
|
|
102
103
|
const scopeObj = {};
|
|
103
104
|
scopeObj.data = this.host.querySelector(`script[rel="hydration"][type="application/json"]`)?.textContent?.trim() || null;
|
|
104
|
-
scopeObj.response =
|
|
105
|
+
scopeObj.response = responseShim.from.value(scopeObj.data, { headers: { 'Content-Type': 'application/json' } });
|
|
105
106
|
for (const name of ['X-Background-Messaging-Port', 'X-Live-Response-Message-ID', 'X-Webflo-Dev-Mode']) {
|
|
106
107
|
const metaElement = this.host.querySelector(`meta[name="${name}"]`);
|
|
107
108
|
if (!metaElement) continue;
|
|
108
109
|
scopeObj.response.headers.set(name, metaElement.content?.trim() || '');
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const backgroundPort = LiveResponse.getBackground(scopeObj.response);
|
|
112
|
+
if (backgroundPort) {
|
|
113
|
+
this.background.addPort(backgroundPort);
|
|
112
114
|
}
|
|
113
|
-
if (scopeObj.response.body ||
|
|
115
|
+
if (scopeObj.response.body || backgroundPort) {
|
|
114
116
|
const httpEvent = this.createHttpEvent({ request: this.createRequest(this.location.href) }, true);
|
|
115
117
|
await this.render(httpEvent, scopeObj.response);
|
|
116
118
|
} else {
|
|
@@ -21,7 +21,7 @@ export class WebfloSubClient extends WebfloClient {
|
|
|
21
21
|
|
|
22
22
|
get workport() { return this.#superRuntime.workport; }
|
|
23
23
|
|
|
24
|
-
get
|
|
24
|
+
get capabilities() { return this.#superRuntime.capabilities; }
|
|
25
25
|
|
|
26
26
|
get withViewTransitions() { return this.host.hasAttribute('viewtransitions'); }
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ export class WebfloSubClient extends WebfloClient {
|
|
|
32
32
|
if (!(host instanceof HTMLElement)) {
|
|
33
33
|
throw new Error('Argument #1 must be a HTMLElement instance');
|
|
34
34
|
}
|
|
35
|
-
super(superRuntime.
|
|
35
|
+
super(superRuntime.bootstrap, host);
|
|
36
36
|
this.#superRuntime = superRuntime;
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -65,19 +65,19 @@ export class WebfloSubClient extends WebfloClient {
|
|
|
65
65
|
return super.controlClassic/*IMPORTANT*/(locationCallback);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
reload(params) {}
|
|
68
|
+
reload(params) { }
|
|
69
69
|
|
|
70
|
-
back() {}
|
|
70
|
+
back() { }
|
|
71
71
|
|
|
72
|
-
forward() {}
|
|
72
|
+
forward() { }
|
|
73
73
|
|
|
74
|
-
traverseTo(...args) {}
|
|
74
|
+
traverseTo(...args) { }
|
|
75
75
|
|
|
76
|
-
async push(url, state = {}) {}
|
|
76
|
+
async push(url, state = {}) { }
|
|
77
77
|
|
|
78
|
-
entries() {}
|
|
78
|
+
entries() { }
|
|
79
79
|
|
|
80
|
-
currentEntry() {}
|
|
80
|
+
currentEntry() { }
|
|
81
81
|
|
|
82
82
|
async updateCurrentEntry(params, url = null) {
|
|
83
83
|
this.host.reflectLocation(url);
|
|
@@ -90,16 +90,16 @@ export class WebfloSubClient extends WebfloClient {
|
|
|
90
90
|
(this.host.querySelector('[autofocus]') || this.host).focus();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
-
redirect(location,
|
|
93
|
+
redirect(location, responseBackground = null) {
|
|
94
94
|
location = typeof location === 'string' ? new URL(location, this.location.origin) : location;
|
|
95
95
|
const width = Math.min(800, window.innerWidth);
|
|
96
96
|
const height = Math.min(600, window.innerHeight);
|
|
97
97
|
const left = (window.outerWidth - width) / 2;
|
|
98
98
|
const top = (window.outerHeight - height) / 2;
|
|
99
99
|
const popup = window.open(location, '_blank', `popup=true,width=${width},height=${height},left=${left},top=${top}`);
|
|
100
|
-
if (
|
|
100
|
+
if (responseBackground) {
|
|
101
101
|
Observer.set(this.navigator, 'redirecting', new Url/*NOT URL*/(location), { diff: true });
|
|
102
|
-
|
|
102
|
+
responseBackground.addEventListener('close', (e) => {
|
|
103
103
|
window.removeEventListener('message', windowMessageHandler);
|
|
104
104
|
Observer.set(this.navigator, 'redirecting', null);
|
|
105
105
|
popup.postMessage('timeout:5');
|
|
@@ -109,7 +109,7 @@ export class WebfloSubClient extends WebfloClient {
|
|
|
109
109
|
}, { once: true });
|
|
110
110
|
const windowMessageHandler = (e) => {
|
|
111
111
|
if (e.source === popup && e.data === 'close') {
|
|
112
|
-
|
|
112
|
+
responseBackground.close();
|
|
113
113
|
}
|
|
114
114
|
};
|
|
115
115
|
window.addEventListener('message', windowMessageHandler);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Fs from 'fs';
|
|
2
|
+
import Path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
readLayoutConfig,
|
|
5
|
+
readEnvConfig,
|
|
6
|
+
readClientConfig,
|
|
7
|
+
readWorkerConfig,
|
|
8
|
+
scanRoots,
|
|
9
|
+
scanRouteHandlers,
|
|
10
|
+
} from '../../deployment-pi/util.js';
|
|
11
|
+
|
|
12
|
+
export async function bootstrap(cx, offset = '') {
|
|
13
|
+
const $init = Fs.existsSync('./init.client.js')
|
|
14
|
+
? Path.resolve('./init.client.js')
|
|
15
|
+
: null;
|
|
16
|
+
const config = {
|
|
17
|
+
LAYOUT: await readLayoutConfig(cx),
|
|
18
|
+
ENV: await readEnvConfig(cx),
|
|
19
|
+
CLIENT: await readClientConfig(cx),
|
|
20
|
+
WORKER: await readWorkerConfig(cx),
|
|
21
|
+
};
|
|
22
|
+
if (config.CLIENT.copy_public_variables) {
|
|
23
|
+
const publicEnvPattern = /(?:^|_)PUBLIC(?:_|$)/;
|
|
24
|
+
for (const key in process.env) {
|
|
25
|
+
if (publicEnvPattern.test(key)) {
|
|
26
|
+
config.ENV.data[key] = process.env[key];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const routes = {};
|
|
31
|
+
const $roots = Fs.existsSync(config.LAYOUT.PUBLIC_DIR) ? scanRoots(config.LAYOUT.PUBLIC_DIR, 'index.html') : [];
|
|
32
|
+
scanRouteHandlers(config.LAYOUT, 'client', (file, route) => {
|
|
33
|
+
routes[route] = file;
|
|
34
|
+
}, offset, $roots);
|
|
35
|
+
const outdir = Path.join(config.LAYOUT.PUBLIC_DIR, offset);
|
|
36
|
+
return { $init, config, routes, $roots, $sparoots: $roots, outdir, offset };
|
|
37
|
+
}
|
|
@@ -2,16 +2,10 @@ import { WebfloRootClient1 } from './WebfloRootClient1.js';
|
|
|
2
2
|
import { WebfloRootClient2 } from './WebfloRootClient2.js';
|
|
3
3
|
import { WebfloSubClient } from './WebfloSubClient.js';
|
|
4
4
|
|
|
5
|
-
export async function start() {
|
|
5
|
+
export async function start(bootstrap) {
|
|
6
6
|
const WebfloRootClient = window.navigation ? WebfloRootClient2 : WebfloRootClient1;
|
|
7
|
-
const instance = WebfloRootClient.create(
|
|
7
|
+
const instance = WebfloRootClient.create(bootstrap, document);
|
|
8
8
|
await instance.initialize();
|
|
9
9
|
WebfloSubClient.defineElement();
|
|
10
10
|
return instance;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
export {
|
|
14
|
-
WebfloRootClient1,
|
|
15
|
-
WebfloRootClient2,
|
|
16
|
-
WebfloSubClient
|
|
17
|
-
}
|
|
@@ -45,7 +45,7 @@ export class WebfloHMR {
|
|
|
45
45
|
if (event.actionableEffect === 'unlink') {
|
|
46
46
|
delete this.#app.routes[event.affectedRoute];
|
|
47
47
|
} else {
|
|
48
|
-
this.#app.routes[event.affectedRoute] = `/@
|
|
48
|
+
this.#app.routes[event.affectedRoute] = `/@hmr?src=${event.affectedHandler}&t=${Date.now()}`;
|
|
49
49
|
}
|
|
50
50
|
statuses.routesAffected.add(event.affectedRoute);
|
|
51
51
|
} else if (event.realm === 'worker') {
|
|
@@ -203,7 +203,7 @@ export class WebfloHMR {
|
|
|
203
203
|
}
|
|
204
204
|
const $url = node.$url;
|
|
205
205
|
const url = encodeURIComponent($url.href.replace(`${$url.origin}/`, '')); // preserving origin query strings
|
|
206
|
-
const urlRewrite = `/@
|
|
206
|
+
const urlRewrite = `/@hmr?src=${url}&t=${Date.now()}`;
|
|
207
207
|
if (node.matches(this.#selectors.remoteStyleSheet)) {
|
|
208
208
|
node.setAttribute('href', urlRewrite);
|
|
209
209
|
return 1;
|
|
@@ -216,7 +216,7 @@ export class WebfloHMR {
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
async loadHTMLModule(url) {
|
|
219
|
-
const urlRewrite = `/@
|
|
219
|
+
const urlRewrite = `/@hmr?src=${url}?t=${Date.now()}`;
|
|
220
220
|
const fileContents = await fetch(urlRewrite).then((res) => res.text()).catch(() => null);
|
|
221
221
|
if (fileContents === null) return null;
|
|
222
222
|
const temp = document.createElement('template');
|