@webqit/webflo 0.20.26 → 0.20.28

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 +6 -4
  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 -103
  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 -269
  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
 
@@ -632,33 +520,35 @@ export class WebfloServer extends WebfloRuntime {
632
520
  writeRedirectHeaders(httpEvent, response) {
633
521
  const $sparoots = this.bootstrap.$sparoots;
634
522
  const xRedirectPolicy = httpEvent.request.headers.get('X-Redirect-Policy');
635
- const xRedirectCode = httpEvent.request.headers.get('X-Redirect-Code') || 300;
636
523
  const destinationURL = new URL(response.headers.get('Location'), httpEvent.url.origin);
637
524
  const isSameOriginRedirect = destinationURL.origin === httpEvent.url.origin;
638
525
  let isSameSpaRedirect = true;
526
+
639
527
  if (isSameOriginRedirect && xRedirectPolicy === 'manual-when-cross-spa' && $sparoots.length) {
640
528
  // Longest-first sorting
641
529
  const sparoots = $sparoots.sort((a, b) => a.length > b.length ? -1 : 1);
642
530
  const matchRoot = path => sparoots.reduce((prev, root) => prev || (`${path}/`.startsWith(`${root}/`) && root), null);
643
531
  isSameSpaRedirect = matchRoot(destinationURL.pathname) === matchRoot(httpEvent.url.pathname);
644
532
  }
533
+
645
534
  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));
535
+ response.headers.set('X-Redirect-Code', response.status);
647
536
  response.headers.set('Access-Control-Allow-Origin', '*');
648
537
  response.headers.set('Cache-Control', 'no-store');
649
- const responseMeta = _wq(response, 'meta');
650
- responseMeta.set('status', xRedirectCode);
651
538
  }
652
539
  }
653
540
 
654
541
  async remoteFetch(request, ...args) {
655
542
  let href = request;
543
+
656
544
  if (request instanceof Request) {
657
545
  href = request.url;
658
546
  } else if (request instanceof URL) {
659
547
  href = request.href;
660
548
  }
549
+
661
550
  const _response = fetch(request, ...args);
551
+
662
552
  // Save a reference to this
663
553
  return _response.then(async response => {
664
554
  // Stop loading status
@@ -670,6 +560,7 @@ export class WebfloServer extends WebfloRuntime {
670
560
  const { flags: FLAGS } = this.cx;
671
561
  const { RUNTIME_LAYOUT, LAYOUT } = this.config;
672
562
  const scopeObj = {};
563
+
673
564
  if (FLAGS['dev']) {
674
565
  if (httpEvent.url.pathname === '/@hmr') {
675
566
  const filename = httpEvent.url.searchParams.get('src')?.split('?')[0] || '';
@@ -690,22 +581,28 @@ export class WebfloServer extends WebfloRuntime {
690
581
  } else {
691
582
  scopeObj.filename = Path.join(LAYOUT.PUBLIC_DIR, httpEvent.url.pathname.split('?')[0]);
692
583
  }
584
+
693
585
  scopeObj.ext = Path.parse(scopeObj.filename).ext;
586
+
694
587
  const finalizeResponse = (response) => {
695
588
  // Qualify Service-Worker responses
696
589
  if (httpEvent.request.headers.get('Service-Worker') === 'script') {
697
590
  response.headers.set('Service-Worker-Allowed', this.config.WORKER.scope || '/');
698
591
  }
699
- const responseMeta = _wq(response, 'meta');
592
+
593
+ const responseMeta = _meta(response);
700
594
  responseMeta.set('filename', scopeObj.filename);
701
595
  responseMeta.set('static', true);
702
596
  responseMeta.set('index', scopeObj.index);
597
+
703
598
  return response;
704
599
  };
600
+
705
601
  // Pre-encoding support?
706
602
  if (scopeObj.preEncodingSupportLevel !== 0) {
707
603
  scopeObj.acceptEncs = [];
708
604
  scopeObj.supportedEncs = { gzip: '.gz', br: '.br' };
605
+
709
606
  if ((scopeObj.acceptEncs = (httpEvent.request.headers.get('Accept-Encoding') || '').split(',').map((e) => e.trim())).length
710
607
  && (scopeObj.enc = scopeObj.acceptEncs.reduce((prev, _enc) => prev || (scopeObj.supportedEncs[_enc] && Fs.existsSync(scopeObj.filename + scopeObj.supportedEncs[_enc]) && _enc), null))) {
711
608
  // Route to a pre-compressed version of the file
@@ -715,6 +612,7 @@ export class WebfloServer extends WebfloRuntime {
715
612
  // TODO: Do dynamic encoding
716
613
  }
717
614
  }
615
+
718
616
  // if is a directory, search for index file matching the extention
719
617
  if (!scopeObj.ext && scopeObj.autoIndexFileSupport !== false && Fs.existsSync(scopeObj.filename) && (scopeObj.stats = Fs.lstatSync(scopeObj.filename)).isDirectory()) {
720
618
  scopeObj.ext = '.html';
@@ -722,6 +620,7 @@ export class WebfloServer extends WebfloRuntime {
722
620
  scopeObj.filename = Path.join(scopeObj.filename, scopeObj.index);
723
621
  scopeObj.stats = null;
724
622
  }
623
+
725
624
  // ------ If we get here, scopeObj.filename has been finalized ------
726
625
  // Do file stats
727
626
  if (!scopeObj.stats) {
@@ -730,9 +629,11 @@ export class WebfloServer extends WebfloRuntime {
730
629
  throw e; // Re-throw other errors
731
630
  }
732
631
  }
632
+
733
633
  // ETag support
734
634
  scopeObj.stats.etag = `W/"${scopeObj.stats.size}-${scopeObj.stats.mtimeMs}"`;
735
635
  const ifNoneMatch = httpEvent.request.headers.get('If-None-Match');
636
+
736
637
  if (scopeObj.stats.etag && ifNoneMatch === scopeObj.stats.etag) {
737
638
  const response = new Response(null, { status: 304, statusText: 'Not Modified' });
738
639
  response.headers.set('ETag', scopeObj.stats.etag);
@@ -740,29 +641,37 @@ export class WebfloServer extends WebfloRuntime {
740
641
  response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
741
642
  return finalizeResponse(response);
742
643
  }
743
- scopeObj.stats.mime = scopeObj.ext && Mime.lookup(scopeObj.ext)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
644
+
645
+ scopeObj.stats.mime = scopeObj.ext && (Mime.lookup(scopeObj.ext) || null)?.replace('application/javascript', 'text/javascript') || 'application/octet-stream';
646
+
744
647
  // Range support
745
648
  const readStream = (params = {}) => Fs.createReadStream(scopeObj.filename, { ...params });
746
649
  scopeObj.response = this.createStreamingResponse(httpEvent, readStream, scopeObj.stats);
747
- const statusCode = responseShim.prototype.status.get.call(scopeObj.response);
650
+ const statusCode = scopeObj.response.status;
748
651
  if (statusCode === 416) return finalizeResponse(scopeObj.response);
652
+
749
653
  // ------ If we get here, it means we're good ------
750
654
  if (scopeObj.enc) {
751
655
  scopeObj.response.headers.set('Content-Encoding', scopeObj.enc);
752
656
  }
657
+
753
658
  // 1. Strong cache validators
754
659
  scopeObj.response.headers.set('ETag', scopeObj.stats.etag);
755
660
  scopeObj.response.headers.set('Last-Modified', scopeObj.stats.mtime.toUTCString());
661
+
756
662
  // 2. Content presentation and policy
757
663
  scopeObj.response.headers.set('Content-Disposition', `inline; filename="${Path.basename(scopeObj.filename)}"`);
758
664
  scopeObj.response.headers.set('Referrer-Policy', 'no-referrer-when-downgrade');
665
+
759
666
  // 3. Cache-Control
760
667
  scopeObj.response.headers.set('Cache-Control', 'public, max-age=31536000'); // 1 year
761
668
  scopeObj.response.headers.set('Vary', 'Accept-Encoding'); // The header that talks to our support for "Accept-Encoding"
669
+
762
670
  // 4. Security headers
763
671
  scopeObj.response.headers.set('X-Content-Type-Options', 'nosniff');
764
672
  scopeObj.response.headers.set('Access-Control-Allow-Origin', '*');
765
673
  scopeObj.response.headers.set('X-Frame-Options', 'SAMEORIGIN');
674
+
766
675
  // 5. Partial content support
767
676
  scopeObj.response.headers.set('Accept-Ranges', 'bytes');
768
677
 
@@ -772,63 +681,110 @@ export class WebfloServer extends WebfloRuntime {
772
681
  async navigate(url, init = {}, detail = {}) {
773
682
  const { HEADERS } = this.config;
774
683
  const { flags: FLAGS } = this.cx;
775
- const scopeObj = { url, init, detail };
684
+
685
+ // Scope object
686
+ const scopeObj = {
687
+ url,
688
+ init,
689
+ detail,
690
+ requestID: (0 | Math.random() * 9e6).toString(36),
691
+ sessionTTL: parseInt(this.env('SESSION_TTL')) || 2592000/*30days*/
692
+ };
776
693
  if (typeof scopeObj.url === 'string') {
777
694
  scopeObj.url = new URL(scopeObj.url, 'http://localhost');
778
695
  }
779
- // Request processing
696
+
697
+ // Request
780
698
  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),
699
+ scopeObj.request = scopeObj.init instanceof Request && scopeObj.init.url === scopeObj.url.href
700
+ ? scopeObj.init
701
+ : this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
702
+ RequestPlus.upgradeInPlace(scopeObj.request);
703
+ scopeObj.tenantID = this.identifyIncoming(scopeObj.request, true);
704
+
705
+ // Origins
706
+ const origins = [scopeObj.requestID];
707
+
708
+ // Thread
709
+ scopeObj.thread = HttpThread111.create({
710
+ context: {},
711
+ store: this.#keyvals.create({ path: ['thread', scopeObj.tenantID], origins }),
789
712
  threadID: scopeObj.url.searchParams.get('_thread'),
790
713
  realm: 3
791
714
  });
792
- scopeObj.cookies = this.createHttpCookies({
793
- request: scopeObj.request,
794
- thread: scopeObj.thread,
715
+
716
+ // Cookies
717
+ const entries = scopeObj.request.headers.get('Cookie', true).map((c) => [c.name, c]);
718
+ const store = InMemoryKV.create({ path: ['cookies', scopeObj.tenantID] });
719
+ entries.forEach(([key, value]) => store.set(key, { value }));
720
+ const initial = Object.fromEntries(entries);
721
+ scopeObj.cookies = HttpCookies101.create({
722
+ context: { handlersRegistry: this.#keyvals.getHandlers('cookies', true) },
723
+ store,
724
+ initial,
795
725
  realm: 3
796
726
  });
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,
727
+
728
+ // Session
729
+ scopeObj.session = HttpSession001.create({
730
+ context: { handlersRegistry: this.#keyvals.getHandlers('session', true) },
731
+ store: this.#keyvals.create({ path: ['session', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
732
+ sessionID: scopeObj.tenantID,
802
733
  ttl: scopeObj.sessionTTL,
803
734
  realm: 3
804
735
  });
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,
736
+
737
+ // User
738
+ scopeObj.user = HttpUser111.create({
739
+ context: { handlersRegistry: this.#keyvals.getHandlers('user', true) },
740
+ store: this.#keyvals.create({ path: ['user', scopeObj.tenantID], ttl: scopeObj.sessionTTL, origins }),
810
741
  realm: 3
811
742
  });
812
- scopeObj.httpEvent = this.createHttpEvent({
743
+
744
+ // Client
745
+ scopeObj.tenant = this.#tenancy.getTenant(scopeObj.tenantID, true);
746
+ scopeObj.clientRequestPort = scopeObj.tenant.createRequestPort(
747
+ crypto.randomUUID(),
748
+ scopeObj.request.url
749
+ );
750
+
751
+ // HttpEvent
752
+ scopeObj.httpEvent = HttpEvent111.create({
753
+ detail: scopeObj.detail,
754
+ signal: init.signal,
813
755
  request: scopeObj.request,
814
756
  thread: scopeObj.thread,
815
- client: scopeObj.clientRequestRealtime,
816
757
  cookies: scopeObj.cookies,
817
758
  session: scopeObj.session,
818
759
  user: scopeObj.user,
819
- detail: scopeObj.detail,
760
+ client: scopeObj.clientRequestPort,
820
761
  realm: 3
821
762
  });
763
+
822
764
  // Dispatch for response
823
765
  scopeObj.response = await this.dispatchNavigationEvent({
824
766
  httpEvent: scopeObj.httpEvent,
825
767
  crossLayerFetch: (event) => this.localFetch(event),
826
- clientPortB: `ws:${scopeObj.httpEvent.client.portID}?rel=background-messaging`
768
+ clientPortB: `socket:///${scopeObj.httpEvent.client.portID}?rel=background-messaging`
827
769
  });
770
+
771
+ // Commit session - expires six months
772
+ if (!scopeObj.response.headers.get('Set-Cookie', true).find((c) => c.name === '__sessid')) {
773
+ scopeObj.response.headers.append('Set-Cookie', `__sessid=${scopeObj.tenantID}; Path=/; ${!FLAGS['dev'] ? 'Secure; ' : ''}HttpOnly; SameSite=Lax${scopeObj.sessionTTL ? `; Max-Age=${scopeObj.sessionTTL}` : ''}`);
774
+ }
775
+
776
+ // Commit cookies
777
+ for (const cookieStr of await scopeObj.cookies.render()) {
778
+ scopeObj.response.headers.append('Set-Cookie', cookieStr);
779
+ }
780
+ await scopeObj.cookies._commit();
781
+
828
782
  // Reponse handlers
829
783
  if (FLAGS['dev']) {
830
784
  scopeObj.response.headers.set('X-Webflo-Dev-Mode', 'true'); // Must come before satisfyRequestFormat() sp as to be rendered
831
785
  }
786
+
787
+ // Write headers / satisfy request format
832
788
  if (scopeObj.response.headers.get('Location')) {
833
789
  this.writeRedirectHeaders(scopeObj.httpEvent, scopeObj.response);
834
790
  } else {
@@ -838,74 +794,90 @@ export class WebfloServer extends WebfloRuntime {
838
794
  scopeObj.response.headers.set('Cache-Control', 'no-store');
839
795
  }
840
796
  }
797
+
841
798
  return scopeObj.response;
842
799
  }
843
800
 
844
801
  async satisfyRequestFormat(httpEvent, response) {
845
- const statusCode = responseShim.prototype.status.get.call(response);
802
+ const statusCode = response.status;
803
+
846
804
  if (statusCode === 206 || statusCode === 416) {
847
805
  // If the response is a partial content, we don't need to do anything else
848
806
  return response;
849
807
  }
808
+
850
809
  // Satisfy "Accept" header
851
- const requestAccept = headersShim.get.value.call(httpEvent.request.headers, 'Accept', true);
810
+ const requestAccept = httpEvent.request.headers.get('Accept', true);
852
811
  const asHTML = requestAccept?.match('text/html');
853
812
  const asIs = requestAccept?.match(response.headers.get('Content-Type'));
854
- const responseMeta = _wq(response, 'meta');
813
+ const responseMeta = _meta(response);
814
+
855
815
  if (requestAccept && asHTML > asIs && !responseMeta.get('static')) {
856
816
  response = await this.render(httpEvent, response);
857
- } else if (requestAccept && response.headers.get('Content-Type') && !asIs) {
817
+ } else if (requestAccept && response.body && response.headers.get('Content-Type') && !asIs) {
858
818
  return new Response(response.body, { status: 406, statusText: 'Not Acceptable', headers: response.headers });
859
819
  }
820
+
860
821
  // ------- With "exception" responses out of the way,
861
822
  // let's set the header that talks to our support for "Accept"
862
823
  if (!responseMeta.get('static')) {
863
824
  response.headers.append('Vary', 'Accept');
864
825
  }
826
+
865
827
  // Satisfy "Range" header
866
- const requestRange = headersShim.get.value.call(httpEvent.request.headers, 'Range', true);
867
- if (requestRange.length && response.headers.get('Content-Length')) {
828
+ const requestRange = httpEvent.request.headers.get('Range', true);
829
+ if (requestRange.length && response.body && response.headers.get('Content-Length')) {
868
830
  const stats = {
869
831
  size: parseInt(response.headers.get('Content-Length')),
870
832
  mime: response.headers.get('Content-Type') || 'application/octet-stream',
871
833
  };
834
+
872
835
  const headersBefore = response.headers;
873
836
  response = this.createStreamingResponse(
874
837
  httpEvent,
875
838
  (params) => this.streamSlice(response.body, { ...params }),
876
839
  stats
877
840
  );
841
+
878
842
  for (const [name, value] of headersBefore) {
879
843
  if (/Content-Length|Content-Type/i.test(name)) continue;
880
844
  response.headers.append(name, value);
881
845
  }
882
846
  }
847
+
883
848
  return response;
884
849
  }
885
850
 
886
851
  async render(httpEvent, response) {
887
852
  const { LAYOUT } = this.config;
888
853
  const scopeObj = {};
889
- scopeObj.router = new this.constructor.Router(this, httpEvent.url.pathname);
854
+
855
+ scopeObj.router = new WebfloRouter111(this, httpEvent.url.pathname);
890
856
  scopeObj.rendering = await scopeObj.router.route('render', httpEvent, async (httpEvent) => {
891
857
  let renderFile, pathnameSplit = httpEvent.url.pathname.split('/');
858
+
892
859
  while ((renderFile = Path.join(LAYOUT.PUBLIC_DIR, './' + pathnameSplit.join('/'), 'index.html'))
893
860
  && (this.#renderFileCache.get(renderFile) === false/* false on previous runs */ || !Fs.existsSync(renderFile))) {
894
861
  this.#renderFileCache.set(renderFile, false);
895
862
  pathnameSplit.pop();
896
863
  }
864
+
897
865
  const dirPublic = Url.pathToFileURL(Path.resolve(Path.join(LAYOUT.PUBLIC_DIR)));
898
866
  const instanceParams = /*QueryString.stringify*/({
899
867
  //file: renderFile,
900
868
  url: dirPublic.href,// httpEvent.url.href,
901
869
  });
870
+
902
871
  const { window, document } = createWindow(renderFile, instanceParams);
872
+
903
873
  //const { window, document } = await import('@webqit/oohtml-ssr/src/instance.js?' + instanceParams);
904
874
  await new Promise((res) => {
905
875
  if (document.readyState === 'complete') return res(1);
906
876
  document.addEventListener('load', res);
907
877
  });
908
- const data = await responseShim.prototype.parse.value.call(response);
878
+
879
+ const data = await response.any({ to: 'json' });
880
+
909
881
  if (window.webqit?.oohtml?.config) {
910
882
  // Await rendering engine
911
883
  if (window.webqit?.$qCompilerWorker) {
@@ -915,14 +887,17 @@ export class WebfloServer extends WebfloRuntime {
915
887
  setTimeout(() => res(1), 1000);
916
888
  });
917
889
  }
890
+
918
891
  const {
919
892
  HTML_IMPORTS: { attr: modulesContextAttrs } = {},
920
893
  BINDINGS_API: { api: bindingsConfig } = {},
921
894
  } = window.webqit.oohtml.config;
895
+
922
896
  if (modulesContextAttrs) {
923
897
  const newRoute = '/' + `app/${httpEvent.url.pathname}`.split('/').map(a => (a => a.startsWith('$') ? '-' : a)(a.trim())).filter(a => a).join('/');
924
898
  document.body.setAttribute(modulesContextAttrs.importscontext, newRoute);
925
899
  }
900
+
926
901
  if (bindingsConfig) {
927
902
  document[bindingsConfig.bind]({
928
903
  state: {},
@@ -935,9 +910,11 @@ export class WebfloServer extends WebfloRuntime {
935
910
  background: null
936
911
  }, { diff: true });
937
912
  }
913
+
938
914
  await new Promise(res => setTimeout(res, 300));
939
915
  }
940
- for (const name of ['X-Background-Messaging-Port', 'X-Live-Response-Message-ID', 'X-Webflo-Dev-Mode']) {
916
+
917
+ for (const name of ['X-Message-Port', 'X-Webflo-Dev-Mode']) {
941
918
  document.querySelector(`meta[name="${name}"]`)?.remove();
942
919
  if (!response.headers.get(name)) continue;
943
920
  const metaElement = document.createElement('meta');
@@ -945,6 +922,7 @@ export class WebfloServer extends WebfloRuntime {
945
922
  metaElement.setAttribute('content', response.headers.get(name));
946
923
  document.head.prepend(metaElement);
947
924
  }
925
+
948
926
  // Append hydration data
949
927
  for (const [rel, content] of [['hydration', data]]) {
950
928
  document.querySelector(`script[rel="${rel}"][type="application/json"]`)?.remove();
@@ -954,18 +932,23 @@ export class WebfloServer extends WebfloRuntime {
954
932
  dataScript.textContent = JSON.stringify(content);
955
933
  document.body.append(dataScript);
956
934
  }
935
+
957
936
  const rendering = window.toString();
958
937
  document.documentElement.remove();
959
938
  document.writeln('');
939
+
960
940
  try { window.close(); } catch (e) { }
941
+
961
942
  return rendering;
962
943
  });
944
+
963
945
  // Validate rendering
964
946
  if (typeof scopeObj.rendering !== 'string' && !(typeof scopeObj.rendering?.toString === 'function')) {
965
947
  throw new Error('render() must return a string response or an object that implements toString()..');
966
948
  }
949
+
967
950
  // Convert back to response
968
- const statusCode = responseShim.prototype.status.get.call(response);
951
+ const statusCode = response.status;
969
952
  scopeObj.response = new Response(scopeObj.rendering, {
970
953
  headers: response.headers,
971
954
  status: statusCode,
@@ -973,32 +956,42 @@ export class WebfloServer extends WebfloRuntime {
973
956
  });
974
957
  scopeObj.response.headers.set('Content-Type', 'text/html');
975
958
  scopeObj.response.headers.set('Content-Length', (new Blob([scopeObj.rendering])).size);
959
+
976
960
  return scopeObj.response;
977
961
  }
978
962
 
979
963
  generateLog(request, response, isproxy = false) {
980
964
  const { logger: LOGGER } = this.cx;
981
965
  const log = [];
966
+
982
967
  // ---------------
983
968
  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);
969
+ const statusCode = response.status;
985
970
  const errorCode = statusCode >= 400 && statusCode < 500 ? statusCode : 0;
986
971
  const xRedirectCode = response.headers.get('X-Redirect-Code');
987
972
  const isRedirect = (xRedirectCode || statusCode + '').startsWith('3') && (xRedirectCode || statusCode) !== 304;
988
- const _statusCode = xRedirectCode && `${xRedirectCode} (${statusCode})` || statusCode;
989
- const responseMeta = _wq(response, 'meta');
973
+ const _statusCode = xRedirectCode && `${statusCode} (${xRedirectCode})` || statusCode;
974
+ const responseMeta = _meta(response);
990
975
  // ---------------
976
+
991
977
  log.push(`[${style.comment((new Date).toUTCString())}]`);
992
978
  log.push(style.keyword(request.method));
993
979
  if (isproxy) log.push(style.keyword('>>'));
994
980
  log.push(style.url(request.url));
981
+
995
982
  if (responseMeta.has('hint')) log.push(`(${style.comment(responseMeta.get('hint'))})`);
996
983
  const contentInfo = [response.headers.get('Content-Type'), response.headers.get('Content-Length') && this.formatBytes(response.headers.get('Content-Length'))].filter((x) => x);
984
+
997
985
  if (contentInfo.length) log.push(`(${style.comment(contentInfo.join('; '))})`);
998
986
  if (response.headers.get('Content-Encoding')) log.push(`(${style.comment(response.headers.get('Content-Encoding'))})`);
999
987
  if (errorCode) log.push(style.err(`${errorCode} ${response.statusText}`));
1000
988
  else log.push(style.val(`${_statusCode} ${response.statusText}`));
989
+ if (response.headers.get('X-Message-Port')) {
990
+ log.push(style.keyword(`[${style.keyword('L')}]`));
991
+ }
992
+
1001
993
  if (isRedirect) log.push(`- ${style.url(response.headers.get('Location'))}`);
994
+
1002
995
  return log.join(' ');
1003
996
  }
1004
997