@webqit/webflo 0.20.25 → 0.20.27

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 (57) hide show
  1. package/package.json +8 -5
  2. package/src/build-pi/index.js +7 -5
  3. package/src/init-pi/index.js +0 -1
  4. package/src/runtime-pi/{WebfloRuntime.js → AppRuntime.js} +57 -113
  5. package/src/runtime-pi/webflo-client/DeviceCapabilities.js +1 -1
  6. package/src/runtime-pi/webflo-client/WebfloClient.js +163 -95
  7. package/src/runtime-pi/webflo-client/{WebfloRootClient1.js → WebfloRootClientA.js} +39 -56
  8. package/src/runtime-pi/webflo-client/{WebfloRootClient2.js → WebfloRootClientB.js} +3 -3
  9. package/src/runtime-pi/webflo-client/WebfloSubClient.js +28 -15
  10. package/src/runtime-pi/webflo-client/index.js +3 -3
  11. package/src/runtime-pi/webflo-messaging/ClientPortMixin.js +13 -0
  12. package/src/runtime-pi/{webflo-server/messaging/ClientRequestRealtime.js → webflo-messaging/ClientRequestPort001.js} +13 -9
  13. package/src/runtime-pi/webflo-messaging/ClientRequestPort010.js +4 -0
  14. package/src/runtime-pi/webflo-messaging/ClientRequestPort100.js +17 -0
  15. package/src/runtime-pi/webflo-messaging/WebfloTenancy001.js +27 -0
  16. package/src/runtime-pi/webflo-messaging/WebfloTenant001.js +27 -0
  17. package/src/runtime-pi/webflo-routing/HttpCookies101.js +53 -0
  18. package/src/runtime-pi/webflo-routing/HttpCookies110.js +3 -0
  19. package/src/runtime-pi/webflo-routing/{HttpEvent.js → HttpEvent111.js} +95 -73
  20. package/src/runtime-pi/webflo-routing/HttpKeyvalInterface.js +120 -0
  21. package/src/runtime-pi/webflo-routing/HttpSession001.js +24 -0
  22. package/src/runtime-pi/webflo-routing/HttpSession110.js +3 -0
  23. package/src/runtime-pi/webflo-routing/{HttpThread.js → HttpThread111.js} +54 -13
  24. package/src/runtime-pi/webflo-routing/{HttpUser.js → HttpUser111.js} +10 -23
  25. package/src/runtime-pi/webflo-routing/KeyvalsFactory001.js +53 -0
  26. package/src/runtime-pi/webflo-routing/KeyvalsFactory110.js +48 -0
  27. package/src/runtime-pi/webflo-routing/KeyvalsFactoryInterface.js +56 -0
  28. package/src/runtime-pi/webflo-routing/{WebfloRouter.js → WebfloRouter111.js} +5 -6
  29. package/src/runtime-pi/webflo-server/WebfloServer.js +262 -266
  30. package/src/runtime-pi/webflo-worker/WebfloWorker.js +97 -44
  31. package/src/util.js +3 -2
  32. package/src/runtime-pi/apis.js +0 -9
  33. package/src/runtime-pi/webflo-client/ClientSideCookies.js +0 -18
  34. package/src/runtime-pi/webflo-fetch/LiveResponse.js +0 -476
  35. package/src/runtime-pi/webflo-fetch/index.js +0 -419
  36. package/src/runtime-pi/webflo-fetch/util.js +0 -28
  37. package/src/runtime-pi/webflo-messaging/WQBroadcastChannel.js +0 -10
  38. package/src/runtime-pi/webflo-messaging/WQMessageChannel.js +0 -26
  39. package/src/runtime-pi/webflo-messaging/WQMessageEvent.js +0 -87
  40. package/src/runtime-pi/webflo-messaging/WQMessagePort.js +0 -38
  41. package/src/runtime-pi/webflo-messaging/WQRelayPort.js +0 -47
  42. package/src/runtime-pi/webflo-messaging/WQSockPort.js +0 -111
  43. package/src/runtime-pi/webflo-messaging/WQStarPort.js +0 -112
  44. package/src/runtime-pi/webflo-messaging/wq-message-port.js +0 -413
  45. package/src/runtime-pi/webflo-routing/HttpCookies.js +0 -43
  46. package/src/runtime-pi/webflo-routing/HttpSession.js +0 -11
  47. package/src/runtime-pi/webflo-routing/HttpState.js +0 -182
  48. package/src/runtime-pi/webflo-server/ServerSideCookies.js +0 -22
  49. package/src/runtime-pi/webflo-server/ServerSideSession.js +0 -40
  50. package/src/runtime-pi/webflo-server/messaging/Client.js +0 -27
  51. package/src/runtime-pi/webflo-server/messaging/Clients.js +0 -25
  52. package/src/runtime-pi/webflo-url/Url.js +0 -156
  53. package/src/runtime-pi/webflo-url/index.js +0 -1
  54. package/src/runtime-pi/webflo-url/urlpattern.js +0 -38
  55. package/src/runtime-pi/webflo-url/util.js +0 -109
  56. package/src/runtime-pi/webflo-url/xURL.js +0 -94
  57. package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +0 -21
@@ -3,42 +3,43 @@ import Url from 'url';
3
3
  import Path from 'path';
4
4
  import Http from 'http';
5
5
  import Https from 'https';
6
- import { WebSocketServer } from 'ws';
7
6
  import Mime from 'mime-types';
8
7
  import crypto from 'crypto';
9
- import 'dotenv/config';
10
8
  import $glob from 'fast-glob';
11
9
  import EsBuild from 'esbuild';
12
10
  import { Readable } from 'stream';
11
+ import { WebSocketServer } from 'ws';
13
12
  import { spawn } from 'child_process';
14
- import { _from as _arrFrom, _any } from '@webqit/util/arr/index.js';
15
- import { _isEmpty, _isObject } from '@webqit/util/js/index.js';
16
- import { _each } from '@webqit/util/obj/index.js';
17
- import { WebfloHMR, openBrowser } from './webflo-devmode.js';
18
- import { Clients } from './messaging/Clients.js';
19
- import { WebfloRuntime } from '../WebfloRuntime.js';
20
- import { WQSockPort } from '../webflo-messaging/WQSockPort.js';
21
- import { ServerSideCookies } from './ServerSideCookies.js';
22
- import { ServerSideSession } from './ServerSideSession.js';
23
- import { response as responseShim, headers as headersShim } from '../webflo-fetch/index.js';
24
- import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
25
13
  import { createWindow } from '@webqit/oohtml-ssr';
26
- import { _wq } from '../../util.js';
27
- import '../webflo-fetch/index.js';
28
- import '../webflo-url/index.js';
29
-
30
- export class WebfloServer extends WebfloRuntime {
31
-
32
- static get HttpCookies() { return ServerSideCookies; }
14
+ import { RequestPlus } from '@webqit/fetch-plus';
15
+ import { HttpThread111 } from '../webflo-routing/HttpThread111.js';
16
+ import { HttpCookies101 } from '../webflo-routing/HttpCookies101.js';
17
+ import { HttpSession001 } from '../webflo-routing/HttpSession001.js';
18
+ import { HttpUser111 } from '../webflo-routing/HttpUser111.js';
19
+ import { HttpEvent111 } from '../webflo-routing/HttpEvent111.js';
20
+ import { WebfloRouter111 } from '../webflo-routing/WebfloRouter111.js';
21
+ import { KeyvalsFactory001 } from '../webflo-routing/KeyvalsFactory001.js';
22
+ import { WebfloTenancy001 } from '../webflo-messaging/WebfloTenancy001.js';
23
+ import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
24
+ import { WebfloHMR, openBrowser } from './webflo-devmode.js';
25
+ import { InMemoryKV } from '@webqit/keyval/inmemory';
26
+ import { URLPatternPlus } from '@webqit/url-plus';
27
+ import { WebSocketPort } from '@webqit/port-plus';
28
+ import { AppRuntime } from '../AppRuntime.js';
29
+ import { _meta } from '../../util.js';
30
+ import 'dotenv/config';
33
31
 
34
- static get HttpSession() { return ServerSideSession; }
32
+ export class WebfloServer extends AppRuntime {
35
33
 
36
34
  static create(bootstrap) {
37
35
  return new this(bootstrap);
38
36
  }
39
37
 
38
+ #keyvals;
39
+ get keyvals() { return this.#keyvals; }
40
+
40
41
  #servers = new Map;
41
- #clients = new Clients;
42
+ #tenancy = new WebfloTenancy001({ handshake: 1, postAwaitsOpen: true, autoClose: false });
42
43
  #hmr;
43
44
 
44
45
  #renderFileCache = new Map;
@@ -61,6 +62,14 @@ export class WebfloServer extends WebfloRuntime {
61
62
  await this.buildRoutes({ server: true });
62
63
  }
63
64
 
65
+ // ----------
66
+ // The keyvals API
67
+ this.#keyvals = new KeyvalsFactory001({
68
+ localDir: this.env('KEYVALS_DIR'),
69
+ redisUrl: this.env('REDIS_URL'),
70
+ redisNamespace: APP_META.name
71
+ });
72
+
64
73
  // ----------
65
74
  // Call default-init
66
75
  const instanceController = await super.initialize();
@@ -72,11 +81,14 @@ export class WebfloServer extends WebfloRuntime {
72
81
  // ----------
73
82
  // Show proxies
74
83
  const { PROXY } = this.config;
84
+
75
85
  if (PROXY.entries.length) {
76
86
  // Show active proxies
77
87
  LOGGER.info(`> Reverse proxies active.`);
88
+
78
89
  for (const proxy of PROXY.entries) {
79
90
  let desc = `> ${proxy.hostnames.join('|')} >>> ${proxy.port || proxy.path}`;
91
+
80
92
  // Start a proxy recursively?
81
93
  if (proxy.path && FLAGS['recursive']) {
82
94
  desc += ` ✅`;
@@ -87,6 +99,7 @@ export class WebfloServer extends WebfloRuntime {
87
99
  shell: true // for Windows compatibility
88
100
  });
89
101
  }
102
+
90
103
  LOGGER.info(desc);
91
104
  }
92
105
  }
@@ -95,6 +108,7 @@ export class WebfloServer extends WebfloRuntime {
95
108
  // Show server details
96
109
  if (this.#servers.size) {
97
110
  LOGGER.info(`> Server running! (${APP_META.title || ''}) ✅`);
111
+
98
112
  for (let [proto, def] of this.#servers) {
99
113
  LOGGER.info(`> ${proto.toUpperCase()} / ${def.hostnames.concat('').join(`:${def.port} / `)}`);
100
114
  }
@@ -109,7 +123,9 @@ export class WebfloServer extends WebfloRuntime {
109
123
  const routeDirs = [...new Set([this.config.LAYOUT.CLIENT_DIR, this.config.LAYOUT.WORKER_DIR, this.config.LAYOUT.SERVER_DIR])];
110
124
  const entryPoints = await $glob(routeDirs.map((d) => `${d}/**/handler{${client ? ',.client' : ''}${worker ? ',.worker' : ''}${server ? ',.server' : ''}}.js`), { absolute: true })
111
125
  .then((files) => files.map((file) => file.replace(/\\/g, '/')));
126
+
112
127
  const initFiles = await $glob(`${process.cwd()}/init.server.js`);
128
+
113
129
  const bundlingConfig = {
114
130
  entryPoints: entryPoints.concat(initFiles),
115
131
  outdir: this.config.RUNTIME_DIR,
@@ -123,11 +139,13 @@ export class WebfloServer extends WebfloRuntime {
123
139
  plugins: [UseLiveTransform()],
124
140
  ...options,
125
141
  };
142
+
126
143
  return await EsBuild.build(bundlingConfig);
127
144
  }
128
145
 
129
146
  async enterDevMode() {
130
147
  const { appMeta, flags: FLAGS } = this.cx;
148
+
131
149
  this.#hmr = WebfloHMR.manage(this, {
132
150
  appMeta,
133
151
  buildScripts: {
@@ -137,8 +155,10 @@ export class WebfloServer extends WebfloRuntime {
137
155
  },
138
156
  buildSensitivity: parseInt(FLAGS['build-sensitivity'] || 0),
139
157
  });
158
+
140
159
  await this.#hmr.buildRoutes(true);
141
160
  await this.#hmr.bundleAssetsIfPending(true);
161
+
142
162
  if (FLAGS['open']) {
143
163
  for (let [proto, def] of this.#servers) {
144
164
  const url = `${proto}://${def.hostnames.find((h) => h !== '*') || 'localhost'}:${def.port}`;
@@ -147,194 +167,41 @@ export class WebfloServer extends WebfloRuntime {
147
167
  }
148
168
  }
149
169
 
150
- async initCreateStorage() {
151
- if (this.bootstrap.init.createStorage
152
- || !this.bootstrap.init.redis) {
153
- return super.initCreateStorage();
154
- }
155
-
156
- // ns -> field -> { value, subscriptions: Set<fn> }
157
- const local = new Map();
158
- const fire = (state, value, key, namespace, scopeReference = false) => {
159
- const returnValues = [];
160
- for (const subscription of state.subscriptions) {
161
- const { callback, options } = subscription;
162
-
163
- const scope = subscription.scopeReference === scopeReference ? 0 : scopeReference ? 1 : 2;
164
- // For options.scope === 0, only include same-request cycle mutations
165
- // For options.scope === 1, only include local mutations
166
- // For options.scope === 2, include remote mutations
167
- if ((options.scope || 0) !== scope) continue;
168
-
169
- returnValues.push(callback(value, scope));
170
- if (options.once) state.subscriptions.delete(subscription);
171
- const ns = local.get(namespace);
172
- if (!state.subscriptions.size) ns.delete(key);
173
- if (!ns.size) local.delete(namespace);
174
- }
175
- return Promise.all(returnValues);
176
- };
177
-
178
- // Initialize storage
179
- const redis = this.bootstrap.init.redis;
180
- this.bootstrap.init.createStorage = (namespace, ttl = null, scopeReference = {}) => {
181
- const cleanups = [];
182
- return {
183
- async has(key) { return await redis.hexists(namespace, key); },
184
- async get(key) {
185
- const jsonValue = await redis.hget(namespace, key);
186
- return jsonValue === null ? undefined : JSON.parse(jsonValue);
187
- },
188
- async set(key, value) {
189
- const jsonValue = JSON.stringify(value);
190
- const returnValue = await redis.hset(namespace, key, jsonValue);
191
- if (!this.ttlApplied && ttl) {
192
- await redis.expire(namespace, ttl);
193
- this.ttlApplied = true;
194
- }
195
- const state = local.get(namespace)?.get(key);
196
- if (state) {
197
- state.jsonValue = jsonValue;
198
- state.initialized = true;
199
- await fire(state, value, key, namespace, scopeReference);
200
- }
201
- return returnValue;
202
- },
203
- async delete(key) {
204
- const returnValue = await redis.hdel(namespace, key);
205
- const state = local.get(namespace)?.get(key);
206
- if (state) {
207
- state.jsonValue = null;
208
- state.initialized = true;
209
- await fire(state, undefined, key, namespace, scopeReference);
210
- }
211
- return returnValue;
212
- },
213
- async clear() {
214
- const returnValue = await redis.del(namespace);
215
- const nsLocal = local.get(namespace);
216
- if (nsLocal) {
217
- for (const [key, state] of nsLocal.entries()) {
218
- state.jsonValue = null;
219
- state.initialized = true;
220
- await fire(state, undefined, key, namespace, scopeReference);
221
- }
222
- }
223
- return returnValue;
224
- },
225
- async keys() { return await redis.hkeys(namespace); },
226
- async values() { return (await redis.hvals(namespace) || []).map((value) => value === null ? undefined : JSON.parse(value)); },
227
- async entries() { return Object.entries(await redis.hgetall(namespace) || {}).map(([key, value]) => [key, value === null ? undefined : JSON.parse(value)]); },
228
- get size() { return redis.hlen(namespace); },
229
- observe(key, callback, options = {}) {
230
- // Prepare local data structure
231
- let ns = local.get(namespace);
232
- if (!ns) {
233
- ns = new Map();
234
- local.set(namespace, ns);
235
- }
236
-
237
- let state = ns.get(key);
238
- if (!state) {
239
- state = { jsonValue: null, initialized: false, subscriptions: new Set() };
240
- ns.set(key, state);
241
- // Prime initial value only once
242
- redis.hget(namespace, key).then((jsonValue) => {
243
- if (state.initialized) return;
244
- state.jsonValue = jsonValue;
245
- state.initialized = true;
246
- });
247
- }
248
-
249
- const subscription = { callback, options, scopeReference };
250
- state.subscriptions.add(subscription);
251
- if (options.signal) {
252
- options.signal.addEventListener('abort', () => {
253
- state.subscriptions.delete(subscription);
254
- if (state.subscriptions.size === 0) ns.delete(key);
255
- if (ns.size === 0) local.delete(namespace);
256
- });
257
- }
258
-
259
- // Unsubscribe logic
260
- const cleanup = () => {
261
- state.subscriptions.delete(subscription);
262
- if (state.subscriptions.size === 0) ns.delete(key);
263
- if (ns.size === 0) local.delete(namespace);
264
- };
265
- cleanups.push(cleanup);
266
- return cleanup;
267
- },
268
- cleanup() {
269
- for (const cleanup of cleanups) cleanup();
270
- cleanups.length = 0;
271
- },
272
- };
273
- };
274
-
275
- // Watch for changes
276
- const redisWatch = this.bootstrap.init.redisWatch;
277
- if (redisWatch) {
278
- // Subscribe to all events
279
- redisWatch.psubscribe('__keyevent@0__:*');
280
- redisWatch.on('pmessage', async (pattern, channel, redisKey) => {
281
- const [, , event] = channel.split(':'); // -> "hset", "hdel", "expire", "del"
282
- const namespace = redisKey;
283
-
284
- const nsLocal = local.get(namespace);
285
- if (!nsLocal) return;
286
-
287
- const emitDiff = async () => {
288
- for (const [field, state] of nsLocal) {
289
- const jsonValue = await redis.hget(namespace, field);
290
- if (jsonValue !== state.jsonValue) {
291
- state.jsonValue = jsonValue;
292
- const value = jsonValue === null ? undefined : JSON.parse(jsonValue);
293
- fire(state, value, field, namespace);
294
- }
295
- }
296
- }
297
-
298
- // Field updates
299
- if (event === 'hset') await emitDiff();
300
- if (event === 'hdel') await emitDiff();
301
-
302
- // Namespace removal (expire or del)
303
- if (event === 'expire' || event === 'del') {
304
- for (const [key, state] of nsLocal) {
305
- if (state.jsonValue !== null) {
306
- state.jsonValue = null;
307
- fire(state, undefined, key, namespace);
308
- }
309
- }
310
- local.delete(namespace);
311
- }
312
- });
313
- }
314
- }
315
-
316
170
  control() {
317
- const { flags: FLAGS } = this.cx;
171
+ const { flags: FLAGS, logger: LOGGER } = this.cx;
318
172
  const { SERVER, PROXY } = this.config;
319
173
  const instanceController = super.control();
320
174
 
321
175
  if (!FLAGS['test-only'] && !FLAGS['https-only'] && SERVER.port) {
322
- const httpServer = Http.createServer((request, response) => this.handleNodeHttpRequest(request, response));
176
+ const httpServer = Http.createServer((request, response) => {
177
+ this.handleNodeHttpRequest(request, response).catch((e) => {
178
+ LOGGER.error(e);
179
+ });
180
+ });
323
181
  httpServer.listen(FLAGS['port'] || SERVER.port);
182
+
324
183
  this.#servers.set('http', {
325
184
  instance: httpServer,
326
185
  hostnames: SERVER.hostnames,
327
186
  port: FLAGS['port'] || SERVER.port,
328
187
  });
188
+
329
189
  // Handle WebSocket connections
330
190
  httpServer.on('upgrade', (request, socket, head) => {
331
- this.handleNodeWsRequest(wss, request, socket, head);
191
+ this.handleNodeWsRequest(wss, request, socket, head).catch((e) => {
192
+ LOGGER.error(e);
193
+ });
332
194
  });
333
195
  }
334
196
 
335
197
  if (!FLAGS['test-only'] && !FLAGS['http-only'] && SERVER.https.port) {
336
- const httpsServer = Https.createServer((request, response) => this.handleNodeHttpRequest(request, response));
198
+ const httpsServer = Https.createServer((request, response) => {
199
+ this.handleNodeHttpRequest(request, response).catch((e) => {
200
+ LOGGER.error(e);
201
+ });
202
+ });
337
203
  httpsServer.listen(SERVER.https.port);
204
+
338
205
  const addSSLContext = (SERVER) => {
339
206
  if (!Fs.existsSync(SERVER.https.keyfile)) return;
340
207
  const cert = {
@@ -344,32 +211,37 @@ export class WebfloServer extends WebfloRuntime {
344
211
  SERVER.https.hostnames.forEach((hostname) => {
345
212
  httpsServer.addContext(hostname, cert);
346
213
  });
347
- }
214
+ };
215
+
348
216
  this.#servers.set('https', {
349
217
  instance: httpsServer,
350
218
  hostnames: SERVER.https.hostnames,
351
219
  port: SERVER.https.port,
352
220
  });
353
- // -------
221
+
354
222
  addSSLContext(SERVER);
223
+
355
224
  for (const proxy of PROXY.entries) {
356
225
  if (proxy.SERVER) {
357
226
  addSSLContext(proxy.SERVER);
358
227
  }
359
228
  }
229
+
360
230
  // Handle WebSocket connections
361
231
  httpsServer.on('upgrade', (request, socket, head) => {
362
- this.handleNodeWsRequest(wss, request, socket, head);
232
+ this.handleNodeWsRequest(wss, request, socket, head).catch((e) => {
233
+ LOGGER.error(e);
234
+ });
363
235
  });
364
236
  }
365
237
 
366
238
  const wss = new WebSocketServer({ noServer: true });
367
239
 
368
240
  process.on('uncaughtException', (err) => {
369
- console.error('Uncaught Exception:', err);
241
+ LOGGER.error('Uncaught Exception:', err);
370
242
  });
371
243
  process.on('unhandledRejection', (reason, promise) => {
372
- console.log('Unhandled Rejection', reason, promise);
244
+ LOGGER.log('Unhandled Rejection', reason, promise);
373
245
  });
374
246
 
375
247
  return instanceController;
@@ -377,32 +249,35 @@ export class WebfloServer extends WebfloRuntime {
377
249
 
378
250
  identifyIncoming(request, autoGenerateID = false) {
379
251
  const secret = this.env('SESSION_KEY');
380
- let clientID = headersShim.get.value.call(request.headers, 'Cookie', true).find((c) => c.name === '__sessid')?.value;
381
- if (clientID?.includes('.')) {
252
+ let tenantID = request.headers.get('Cookie', true).find((c) => c.name === '__sessid')?.value;
253
+ if (tenantID?.includes('.')) {
382
254
  if (secret) {
383
- const [rand, signature] = clientID.split('.');
255
+ const [rand, signature] = tenantID.split('.');
384
256
  const expectedSignature = crypto.createHmac('sha256', secret)
385
257
  .update(rand)
386
258
  .digest('hex');
387
259
  if (signature !== expectedSignature) {
388
- clientID = null;
260
+ tenantID = null;
389
261
  }
390
262
  } else {
391
- clientID = null;
263
+ tenantID = null;
392
264
  }
393
265
  }
394
- if (!clientID && autoGenerateID) {
266
+ if (!tenantID) {
267
+ tenantID = request.headers.get('Authorization')?.replace(/\s+/, '_');
268
+ }
269
+ if (!tenantID && autoGenerateID) {
395
270
  if (secret) {
396
271
  const rand = `${(0 | Math.random() * 9e6).toString(36)}`;
397
272
  const signature = crypto.createHmac('sha256', secret)
398
273
  .update(rand)
399
274
  .digest('hex');
400
- clientID = `${rand}.${signature}`
275
+ tenantID = `${rand}.${signature}`
401
276
  } else {
402
- clientID = crypto.randomUUID();
277
+ tenantID = crypto.randomUUID();
403
278
  }
404
279
  }
405
- return clientID;
280
+ return tenantID;
406
281
  }
407
282
 
408
283
  async preResolveIncoming({ type, nodeRequest, proxy, reject, handle }) {
@@ -459,7 +334,7 @@ export class WebfloServer extends WebfloRuntime {
459
334
  }
460
335
  if (REDIRECTS) {
461
336
  const rejection = REDIRECTS.entries.reduce((_rdr, entry) => {
462
- return _rdr || ((_rdr = (new URLPattern(entry.from, url.origin)).exec(url.href)) && {
337
+ return _rdr || ((_rdr = new URLPatternPlus(entry.from, url.origin).exec(url.href)) && {
463
338
  status: entry.code || 302,
464
339
  statusText: entry.code === 301 ? 'Moved Permanently' : 'Found',
465
340
  headers: { Location: _rdr.render(entry.to) }
@@ -469,10 +344,11 @@ export class WebfloServer extends WebfloRuntime {
469
344
  return reject(rejection);
470
345
  }
471
346
  }
472
- return handle(url);
347
+ return await handle(url);
473
348
  }
474
349
 
475
350
  async handleNodeWsRequest(wss, nodeRequest, socket, head) {
351
+ const { logger: LOGGER } = this.cx;
476
352
  const reject = (rejection) => {
477
353
  const status = rejection.status || 400;
478
354
  const statusText = rejection.statusText || 'Bad Request';
@@ -487,7 +363,7 @@ export class WebfloServer extends WebfloRuntime {
487
363
  `\r\n` +
488
364
  body + `\r\n`
489
365
  );
490
- socket.destroy();
366
+ socket.end();
491
367
  };
492
368
  const proxy = async (destinationURL) => {
493
369
  const isSecure = destinationURL.protocol === 'wss:';
@@ -513,11 +389,11 @@ export class WebfloServer extends WebfloRuntime {
513
389
  });
514
390
  // Handle errors
515
391
  proxySocket.on('error', err => {
516
- console.error('Proxy socket error:', err);
517
- socket.destroy();
392
+ LOGGER.error('Proxy socket error:', err);
393
+ socket.end();
518
394
  });
519
395
  socket.on('error', () => {
520
- proxySocket.destroy();
396
+ proxySocket.end();
521
397
  });
522
398
  };
523
399
  const handle = (requestURL) => {
@@ -527,24 +403,29 @@ export class WebfloServer extends WebfloRuntime {
527
403
  this.#hmr.clients.add(ws);
528
404
  });
529
405
  }
406
+
530
407
  if (requestURL.searchParams.get('rel') === 'background-messaging') {
531
- const request = new Request(requestURL.href, { headers: nodeRequest.headers });
532
- const clientID = this.identifyIncoming(request);
533
- const client = clientID && this.#clients.getClient(clientID);
534
- if (!client) {
535
- return reject({ body: `Lost or invalid clientID` });
408
+ const request = new RequestPlus(requestURL.href, { headers: nodeRequest.headers });
409
+ const tenantID = this.identifyIncoming(request);
410
+
411
+ const tenant = tenantID && this.#tenancy.getTenant(tenantID);
412
+ if (!tenant) {
413
+ return reject({ body: `Lost or invalid tenantID` });
536
414
  }
537
- const clientRequestRealtime = client?.getRequestRealtime(requestURL.pathname.split('/').pop());
538
- if (!clientRequestRealtime) {
415
+
416
+ const clientRequestPort = tenant?.getRequestPort(requestURL.pathname.split('/').pop());
417
+ if (!clientRequestPort) {
539
418
  return reject({ body: `Lost or invalid portID` });
540
419
  }
420
+
541
421
  wss.handleUpgrade(nodeRequest, socket, head, (ws) => {
542
422
  wss.emit('connection', ws, nodeRequest);
543
- const wsw = new WQSockPort(ws);
544
- clientRequestRealtime.addPort(wsw);
423
+ const wsw = new WebSocketPort(ws, { handshake: 1, postAwaitsOpen: true });
424
+ clientRequestPort.addPort(wsw);
545
425
  });
546
426
  }
547
427
  };
428
+
548
429
  return await this.preResolveIncoming({ type: 'ws', nodeRequest, proxy, reject, handle });
549
430
  }
550
431
 
@@ -556,8 +437,10 @@ export class WebfloServer extends WebfloRuntime {
556
437
  if (existing) nodeResponse.setHeader(name, [].concat(existing).concat(value));
557
438
  else nodeResponse.setHeader(name, value);
558
439
  }
559
- nodeResponse.statusCode = responseShim.prototype.status.get.call(response);
440
+
441
+ nodeResponse.statusCode = response.status;
560
442
  nodeResponse.statusMessage = response.statusText;
443
+
561
444
  if (response.body instanceof Readable) {
562
445
  response.body.pipe(nodeResponse);
563
446
  } else if (response.body instanceof ReadableStream) {
@@ -567,6 +450,7 @@ export class WebfloServer extends WebfloRuntime {
567
450
  } else {
568
451
  nodeResponse.end();
569
452
  }
453
+
570
454
  // Logging
571
455
  const { logger: LOGGER } = this.cx;
572
456
  if (LOGGER && requestURL) {
@@ -574,10 +458,12 @@ export class WebfloServer extends WebfloRuntime {
574
458
  LOGGER.log(log);
575
459
  }
576
460
  };
461
+
577
462
  // Reject with error status
578
463
  const reject = async (rejection) => {
579
464
  respondWith(new Response(null, rejection));
580
465
  };
466
+
581
467
  // Proxy request to a remote/local host
582
468
  const proxy = async (destinationURL) => {
583
469
  const requestInit = this.parseNodeRequest(nodeRequest);
@@ -586,6 +472,7 @@ export class WebfloServer extends WebfloRuntime {
586
472
  const response = await fetch(destinationURL, requestInit);
587
473
  respondWith(response, destinationURL);
588
474
  };
475
+
589
476
  // Handle
590
477
  const handle = async (requestURL) => {
591
478
  const requestInit = this.parseNodeRequest(nodeRequest);
@@ -596,6 +483,7 @@ export class WebfloServer extends WebfloRuntime {
596
483
  });
597
484
  respondWith(response, requestURL);
598
485
  };
486
+
599
487
  return await this.preResolveIncoming({ typr: 'http', nodeRequest, reject, proxy, handle });
600
488
  }
601
489
 
@@ -636,29 +524,34 @@ export class WebfloServer extends WebfloRuntime {
636
524
  const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
637
525
  const isSameOriginRedirect = destinationURL.origin === httpEvent.url.origin;
638
526
  let isSameSpaRedirect = true;
527
+
639
528
  if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && $sparoots.length) {
640
529
  // Longest-first sorting
641
530
  const sparoots = $sparoots.sort((a, b) => a.length > b.length ? -1 : 1);
642
531
  const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
643
532
  isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
644
533
  }
534
+
645
535
  if (xRedirectPolicy === 'manual' || (!isSameOriginRedirect && (xRedirectPolicy === 'manual-when-cross-origin' || xRedirectPolicy === 'manual-when-cross-spa')) || (!isSameSpaRedirect && xRedirectPolicy === 'manual-when-cross-spa')) {
646
- response.headers.set('X-Redirect-Code', responseShim.prototype.status.get.call(response));
536
+ response.headers.set('X-Redirect-Code', response.status);
647
537
  response.headers.set('Access-Control-Allow-Origin', '*');
648
538
  response.headers.set('Cache-Control', 'no-store');
649
- const responseMeta = _wq(response, 'meta');
539
+ const responseMeta = _meta(response);
650
540
  responseMeta.set('status', xRedirectCode);
651
541
  }
652
542
  }
653
543
 
654
544
  async remoteFetch(request, ...args) {
655
545
  let href = request;
546
+
656
547
  if (request instanceof Request) {
657
548
  href = request.url;
658
549
  } else if (request instanceof URL) {
659
550
  href = request.href;
660
551
  }
552
+
661
553
  const _response = fetch(request, ...args);
554
+
662
555
  // Save a reference to this
663
556
  return _response.then(async response => {
664
557
  // Stop loading status
@@ -670,6 +563,7 @@ export class WebfloServer extends WebfloRuntime {
670
563
  const { flags: FLAGS } = this.cx;
671
564
  const { RUNTIME_LAYOUT, LAYOUT } = this.config;
672
565
  const scopeObj = {};
566
+
673
567
  if (FLAGS['dev']) {
674
568
  if (httpEvent.url.pathname === '/@hmr') {
675
569
  const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
@@ -690,22 +584,28 @@ export class WebfloServer extends WebfloRuntime {
690
584
  } else {
691
585
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
692
586
  }
587
+
693
588
  scopeObj.ext = Path.parse(scopeObj.filename).ext;
589
+
694
590
  const finalizeResponse = (response) => {
695
591
  // Qualify Service-Worker responses
696
592
  if (httpEvent.request.headers.get('Service-Worker') === 'script') {
697
593
  response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
698
594
  }
699
- const responseMeta = _wq(response, 'meta');
595
+
596
+ const responseMeta = _meta(response);
700
597
  responseMeta.set('filename', scopeObj.filename);
701
598
  responseMeta.set('static', true);
702
599
  responseMeta.set('index', scopeObj.index);
600
+
703
601
  return response;
704
602
  };
603
+
705
604
  // Pre-encoding support?
706
605
  if (scopeObj.preEncodingSupportLevel !== 0) {
707
606
  scopeObj.acceptEncs = [];
708
607
  scopeObj.supportedEncs = { gzip: '.gz', br: '.br' };
608
+
709
609
  if ((scopeObj.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
710
610
  && (scopeObj.enc = scopeObj.acceptEncs.reduce((prev, _enc) => prev || (scopeObj.supportedEncs[_enc] && Fs.existsSync(scopeObj.filename + scopeObj.supportedEncs[_enc]) && _enc), null))) {
711
611
  // Route to a pre-compressed version of the file
@@ -715,6 +615,7 @@ export class WebfloServer extends WebfloRuntime {
715
615
  // TODO: Do dynamic encoding
716
616
  }
717
617
  }
618
+
718
619
  // if is a directory, search for index file matching the extention
719
620
  if (!scopeObj.ext && scopeObj.autoIndexFileSupport !== false && Fs.existsSync(scopeObj.filename) && (scopeObj.stats = Fs.lstatSync(scopeObj.filename)).isDirectory()) {
720
621
  scopeObj.ext = '.html';
@@ -722,6 +623,7 @@ export class WebfloServer extends WebfloRuntime {
722
623
  scopeObj.filename = Path.join(scopeObj.filename, scopeObj.index);
723
624
  scopeObj.stats = null;
724
625
  }
626
+
725
627
  // ------ If we get here, scopeObj.filename has been finalized ------
726
628
  // Do file stats
727
629
  if (!scopeObj.stats) {
@@ -730,9 +632,11 @@ export class WebfloServer extends WebfloRuntime {
730
632
  throw e; // Re-throw other errors
731
633
  }
732
634
  }
635
+
733
636
  // ETag support
734
637
  scopeObj.stats.etag = `W/"${scopeObj.stats.size}-${scopeObj.stats.mtimeMs}"`;
735
638
  const ifNoneMatch = httpEvent.request.headers.get('If-None-Match');
639
+
736
640
  if (scopeObj.stats.etag && ifNoneMatch === scopeObj.stats.etag) {
737
641
  const response = new Response(null, { status: 304, statusText: 'Not Modified' });
738
642
  response.headers.set('ETag', scopeObj.stats.etag);
@@ -740,29 +644,37 @@ export class WebfloServer extends WebfloRuntime {
740
644
  response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
741
645
  return finalizeResponse(response);
742
646
  }
743
- scopeObj.stats.mime = scopeObj.ext && Mime.lookup(scopeObj.ext)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
647
+
648
+ scopeObj.stats.mime = scopeObj.ext && (Mime.lookup(scopeObj.ext) || null)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
649
+
744
650
  // Range support
745
651
  const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
746
652
  scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
747
- const statusCode = responseShim.prototype.status.get.call(scopeObj.response);
653
+ const statusCode = scopeObj.response.status;
748
654
  if (statusCode === 416) return finalizeResponse(scopeObj.response);
655
+
749
656
  // ------ If we get here, it means we're good ------
750
657
  if (scopeObj.enc) {
751
658
  scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
752
659
  }
660
+
753
661
  // 1. Strong cache validators
754
662
  scopeObj.response.headers.set('ETag', scopeObj.stats.etag);
755
663
  scopeObj.response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
664
+
756
665
  // 2. Content presentation and policy
757
666
  scopeObj.response.headers.set('Content-Disposition', `inline; filename="${Path.basename(scopeObj.filename)}"`);
758
667
  scopeObj.response.headers.set('Referrer-Policy', 'no-referrer-when-downgrade');
668
+
759
669
  // 3. Cache-Control
760
670
  scopeObj.response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
761
671
  scopeObj.response.headers.set('Vary', 'Accept-Encoding'); // The header that talks to our support for "Accept-Encoding"
672
+
762
673
  // 4. Security headers
763
674
  scopeObj.response.headers.set('X-Content-Type-Options', 'nosniff');
764
675
  scopeObj.response.headers.set('Access-Control-Allow-Origin', '*');
765
676
  scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
677
+
766
678
  // 5. Partial content support
767
679
  scopeObj.response.headers.set('Accept-Ranges', 'bytes');
768
680
 
@@ -772,63 +684,110 @@ export class WebfloServer extends WebfloRuntime {
772
684
  async navigate(url, init = {}, detail = {}) {
773
685
  const { HEADERS } = this.config;
774
686
  const { flags: FLAGS } = this.cx;
775
- const scopeObj = { url, init, detail };
687
+
688
+ // Scope object
689
+ const scopeObj = {
690
+ url,
691
+ init,
692
+ detail,
693
+ requestID: (0 | Math.random() * 9e6).toString(36),
694
+ sessionTTL: parseInt(this.env('SESSION_TTL')) || 2592000/*30days*/
695
+ };
776
696
  if (typeof scopeObj.url === 'string') {
777
697
  scopeObj.url = new URL(scopeObj.url, 'http://localhost');
778
698
  }
779
- // Request processing
699
+
700
+ // Request
780
701
  scopeObj.autoHeaders = HEADERS.entries.filter((entry) => (new URLPattern(entry.url, url.origin)).exec(url.href)) || [];
781
- scopeObj.request = this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
782
- scopeObj.clientID = this.identifyIncoming(scopeObj.request, true);
783
- scopeObj.client = this.#clients.getClient(scopeObj.clientID, true);
784
- scopeObj.clientPortID = crypto.randomUUID();
785
- scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.clientPortID, scopeObj.request.url);
786
- scopeObj.sessionTTL = this.env('SESSION_TTL') || 2592000/*30days*/;
787
- scopeObj.thread = this.createHttpThread({
788
- store: this.createStorage(`${scopeObj.url.host}/thread:${scopeObj.clientID}`, scopeObj.sessionTTL),
702
+ scopeObj.request = scopeObj.init instanceof Request && scopeObj.init.url === scopeObj.url.href
703
+ ? scopeObj.init
704
+ : this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
705
+ RequestPlus.upgradeInPlace(scopeObj.request);
706
+ scopeObj.tenantID = this.identifyIncoming(scopeObj.request, true);
707
+
708
+ // Origins
709
+ const origins = [scopeObj.requestID];
710
+
711
+ // Thread
712
+ scopeObj.thread = HttpThread111.create({
713
+ context: {},
714
+ store: this.#keyvals.create({ path: ['thread', scopeObj.tenantID], origins }),
789
715
  threadID: scopeObj.url.searchParams.get('_thread'),
790
716
  realm: 3
791
717
  });
792
- scopeObj.cookies = this.createHttpCookies({
793
- request: scopeObj.request,
794
- thread: scopeObj.thread,
718
+
719
+ // Cookies
720
+ const entries = scopeObj.request.headers.get('Cookie', true).map((c) => [c.name, c]);
721
+ const store = InMemoryKV.create({ path: ['cookies', scopeObj.tenantID] });
722
+ entries.forEach(([key, value]) => store.set(key, { value }));
723
+ const initial = Object.fromEntries(entries);
724
+ scopeObj.cookies = HttpCookies101.create({
725
+ context: { handlersRegistry: this.#keyvals.getHandlers('cookies', true) },
726
+ store,
727
+ initial,
795
728
  realm: 3
796
729
  });
797
- scopeObj.session = this.createHttpSession({
798
- store: this.createStorage(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
799
- request: scopeObj.request,
800
- thread: scopeObj.thread,
801
- sessionID: scopeObj.clientID,
730
+
731
+ // Session
732
+ scopeObj.session = HttpSession001.create({
733
+ context: { handlersRegistry: this.#keyvals.getHandlers('session', true) },
734
+ store: this.#keyvals.create({ path: ['session', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
735
+ sessionID: scopeObj.tenantID,
802
736
  ttl: scopeObj.sessionTTL,
803
737
  realm: 3
804
738
  });
805
- scopeObj.user = this.createHttpUser({
806
- store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
807
- request: scopeObj.request,
808
- thread: scopeObj.thread,
809
- client: scopeObj.clientRequestRealtime,
739
+
740
+ // User
741
+ scopeObj.user = HttpUser111.create({
742
+ context: { handlersRegistry: this.#keyvals.getHandlers('user', true) },
743
+ store: this.#keyvals.create({ path: ['user', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
810
744
  realm: 3
811
745
  });
812
- scopeObj.httpEvent = this.createHttpEvent({
746
+
747
+ // Client
748
+ scopeObj.tenant = this.#tenancy.getTenant(scopeObj.tenantID, true);
749
+ scopeObj.clientRequestPort = scopeObj.tenant.createRequestPort(
750
+ crypto.randomUUID(),
751
+ scopeObj.request.url
752
+ );
753
+
754
+ // HttpEvent
755
+ scopeObj.httpEvent = HttpEvent111.create({
756
+ detail: scopeObj.detail,
757
+ signal: init.signal,
813
758
  request: scopeObj.request,
814
759
  thread: scopeObj.thread,
815
- client: scopeObj.clientRequestRealtime,
816
760
  cookies: scopeObj.cookies,
817
761
  session: scopeObj.session,
818
762
  user: scopeObj.user,
819
- detail: scopeObj.detail,
763
+ client: scopeObj.clientRequestPort,
820
764
  realm: 3
821
765
  });
766
+
822
767
  // Dispatch for response
823
768
  scopeObj.response = await this.dispatchNavigationEvent({
824
769
  httpEvent: scopeObj.httpEvent,
825
770
  crossLayerFetch: (event) => this.localFetch(event),
826
- clientPortB: `ws:${scopeObj.httpEvent.client.portID}?rel=background-messaging`
771
+ clientPortB: `socket:///${scopeObj.httpEvent.client.portID}?rel=background-messaging`
827
772
  });
773
+
774
+ // Commit session - expires six months
775
+ if (!scopeObj.response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
776
+ scopeObj.response.headers.append('Set-Cookie', `__sessid=${scopeObj.tenantID}; Path=/; ${!FLAGS['dev'] ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${scopeObj.sessionTTL ? `; Max-Age=${scopeObj.sessionTTL}` : ''}`);
777
+ }
778
+
779
+ // Commit cookies
780
+ for (const cookieStr of await scopeObj.cookies.render()) {
781
+ scopeObj.response.headers.append('Set-Cookie', cookieStr);
782
+ }
783
+ await scopeObj.cookies._commit();
784
+
828
785
  // Reponse handlers
829
786
  if (FLAGS['dev']) {
830
787
  scopeObj.response.headers.set('X-Webflo-Dev-Mode', 'true'); // Must come before satisfyRequestFormat() sp as to be rendered
831
788
  }
789
+
790
+ // Write headers / satisfy request format
832
791
  if (scopeObj.response.headers.get('Location')) {
833
792
  this.writeRedirectHeaders(scopeObj.httpEvent, scopeObj.response);
834
793
  } else {
@@ -838,74 +797,90 @@ export class WebfloServer extends WebfloRuntime {
838
797
  scopeObj.response.headers.set('Cache-Control', 'no-store');
839
798
  }
840
799
  }
800
+
841
801
  return scopeObj.response;
842
802
  }
843
803
 
844
804
  async satisfyRequestFormat(httpEvent, response) {
845
- const statusCode = responseShim.prototype.status.get.call(response);
805
+ const statusCode = response.status;
806
+
846
807
  if (statusCode === 206 || statusCode === 416) {
847
808
  // If the response is a partial content, we don't need to do anything else
848
809
  return response;
849
810
  }
811
+
850
812
  // Satisfy "Accept" header
851
- const requestAccept = headersShim.get.value.call(httpEvent.request.headers, 'Accept', true);
813
+ const requestAccept = httpEvent.request.headers.get('Accept', true);
852
814
  const asHTML = requestAccept?.match('text/html');
853
815
  const asIs = requestAccept?.match(response.headers.get('Content-Type'));
854
- const responseMeta = _wq(response, 'meta');
816
+ const responseMeta = _meta(response);
817
+
855
818
  if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
856
819
  response = await this.render(httpEvent, response);
857
- } else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
820
+ } else if (requestAccept && response.body && response.headers.get('Content-Type') && !asIs) {
858
821
  return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
859
822
  }
823
+
860
824
  // ------- With "exception" responses out of the way,
861
825
  // let's set the header that talks to our support for "Accept"
862
826
  if (!responseMeta.get('static')) {
863
827
  response.headers.append('Vary', 'Accept');
864
828
  }
829
+
865
830
  // Satisfy "Range" header
866
- const requestRange = headersShim.get.value.call(httpEvent.request.headers, 'Range', true);
867
- if (requestRange.length && response.headers.get('Content-Length')) {
831
+ const requestRange = httpEvent.request.headers.get('Range', true);
832
+ if (requestRange.length && response.body && response.headers.get('Content-Length')) {
868
833
  const stats = {
869
834
  size: parseInt(response.headers.get('Content-Length')),
870
835
  mime: response.headers.get('Content-Type') || 'application/octet-stream',
871
836
  };
837
+
872
838
  const headersBefore = response.headers;
873
839
  response = this.createStreamingResponse(
874
840
  httpEvent,
875
841
  (params) => this.streamSlice(response.body, { ...params }),
876
842
  stats
877
843
  );
844
+
878
845
  for (const [name, value] of headersBefore) {
879
846
  if (/Content-Length|Content-Type/i.test(name)) continue;
880
847
  response.headers.append(name, value);
881
848
  }
882
849
  }
850
+
883
851
  return response;
884
852
  }
885
853
 
886
854
  async render(httpEvent, response) {
887
855
  const { LAYOUT } = this.config;
888
856
  const scopeObj = {};
889
- scopeObj.router = new this.constructor.Router(this, httpEvent.url.pathname);
857
+
858
+ scopeObj.router = new WebfloRouter111(this, httpEvent.url.pathname);
890
859
  scopeObj.rendering = await scopeObj.router.route('render', httpEvent, async (httpEvent) => {
891
860
  let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
861
+
892
862
  while ((renderFile = Path.join(LAYOUT.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
893
863
  && (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
894
864
  this.#renderFileCache.set(renderFile, false);
895
865
  pathnameSplit.pop();
896
866
  }
867
+
897
868
  const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(LAYOUT.PUBLIC_DIR)));
898
869
  const instanceParams = /*QueryString.stringify*/({
899
870
  //file: renderFile,
900
871
  url: dirPublic.href,// httpEvent.url.href,
901
872
  });
873
+
902
874
  const { window, document } = createWindow(renderFile, instanceParams);
875
+
903
876
  //const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
904
877
  await new Promise((res) => {
905
878
  if (document.readyState === 'complete') return res(1);
906
879
  document.addEventListener('load', res);
907
880
  });
908
- const data = await responseShim.prototype.parse.value.call(response);
881
+
882
+ const data = await response.any({ to: 'json' });
883
+
909
884
  if (window.webqit?.oohtml?.config) {
910
885
  // Await rendering engine
911
886
  if (window.webqit?.$qCompilerWorker) {
@@ -915,14 +890,17 @@ export class WebfloServer extends WebfloRuntime {
915
890
  setTimeout(() => res(1), 1000);
916
891
  });
917
892
  }
893
+
918
894
  const {
919
895
  HTML_IMPORTS: { attr: modulesContextAttrs } = {},
920
896
  BINDINGS_API: { api: bindingsConfig } = {},
921
897
  } = window.webqit.oohtml.config;
898
+
922
899
  if (modulesContextAttrs) {
923
900
  const newRoute = '/' + `app/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
924
901
  document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
925
902
  }
903
+
926
904
  if (bindingsConfig) {
927
905
  document[bindingsConfig.bind]({
928
906
  state: {},
@@ -935,9 +913,11 @@ export class WebfloServer extends WebfloRuntime {
935
913
  background: null
936
914
  }, { diff: true });
937
915
  }
916
+
938
917
  await new Promise(res => setTimeout(res, 300));
939
918
  }
940
- for (const name of ['X-Background-Messaging-Port', 'X-Live-Response-Message-ID', 'X-Webflo-Dev-Mode']) {
919
+
920
+ for (const name of ['X-Message-Port', 'X-Webflo-Dev-Mode']) {
941
921
  document.querySelector(`meta[name="${name}"]`)?.remove();
942
922
  if (!response.headers.get(name)) continue;
943
923
  const metaElement = document.createElement('meta');
@@ -945,6 +925,7 @@ export class WebfloServer extends WebfloRuntime {
945
925
  metaElement.setAttribute('content', response.headers.get(name));
946
926
  document.head.prepend(metaElement);
947
927
  }
928
+
948
929
  // Append hydration data
949
930
  for (const [rel, content] of [['hydration', data]]) {
950
931
  document.querySelector(`script[rel="${rel}"][type="application/json"]`)?.remove();
@@ -954,18 +935,23 @@ export class WebfloServer extends WebfloRuntime {
954
935
  dataScript.textContent = JSON.stringify(content);
955
936
  document.body.append(dataScript);
956
937
  }
938
+
957
939
  const rendering = window.toString();
958
940
  document.documentElement.remove();
959
941
  document.writeln('');
942
+
960
943
  try { window.close(); } catch (e) { }
944
+
961
945
  return rendering;
962
946
  });
947
+
963
948
  // Validate rendering
964
949
  if (typeof scopeObj.rendering !== 'string' && !(typeof scopeObj.rendering?.toString === 'function')) {
965
950
  throw new Error('render() must return a string response or an object that implements toString()..');
966
951
  }
952
+
967
953
  // Convert back to response
968
- const statusCode = responseShim.prototype.status.get.call(response);
954
+ const statusCode = response.status;
969
955
  scopeObj.response = new Response(scopeObj.rendering, {
970
956
  headers: response.headers,
971
957
  status: statusCode,
@@ -973,32 +959,42 @@ export class WebfloServer extends WebfloRuntime {
973
959
  });
974
960
  scopeObj.response.headers.set('Content-Type', 'text/html');
975
961
  scopeObj.response.headers.set('Content-Length', (new Blob([scopeObj.rendering])).size);
962
+
976
963
  return scopeObj.response;
977
964
  }
978
965
 
979
966
  generateLog(request, response, isproxy = false) {
980
967
  const { logger: LOGGER } = this.cx;
981
968
  const log = [];
969
+
982
970
  // ---------------
983
971
  const style = LOGGER.style || { keyword: (str) => str, comment: (str) => str, url: (str) => str, val: (str) => str, err: (str) => str, };
984
- const statusCode = responseShim.prototype.status.get.call(response);
972
+ const statusCode = response.status;
985
973
  const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
986
974
  const xRedirectCode = response.headers.get('X-Redirect-Code');
987
975
  const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
988
976
  const _statusCode = xRedirectCode && `${xRedirectCode} (${statusCode})` || statusCode;
989
- const responseMeta = _wq(response, 'meta');
977
+ const responseMeta = _meta(response);
990
978
  // ---------------
979
+
991
980
  log.push(`[${style.comment((new Date).toUTCString())}]`);
992
981
  log.push(style.keyword(request.method));
993
982
  if (isproxy) log.push(style.keyword('>>'));
994
983
  log.push(style.url(request.url));
984
+
995
985
  if (responseMeta.has('hint')) log.push(`(${style.comment(responseMeta.get('hint'))})`);
996
986
  const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length') && this.formatBytes(response.headers.get('Content-Length'))].filter((x) => x);
987
+
997
988
  if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
998
989
  if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
999
990
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
1000
991
  else log.push(style.val(`${_statusCode} ${response.statusText}`));
992
+ if (response.headers.get('X-Message-Port')) {
993
+ log.push(style.keyword(`[${style.keyword('L')}]`));
994
+ }
995
+
1001
996
  if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
997
+
1002
998
  return log.join(' ');
1003
999
  }
1004
1000