@webqit/webflo 0.20.11-next.0 → 0.20.12-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,26 +1,16 @@
1
- <div align="center">
2
-
3
- # Webflo
4
-
5
- _A web-native framework for the next generation of apps_
1
+ # Webflo – _A next-gen, web-native framework_
6
2
 
7
3
  [![npm version][npm-version-src]][npm-version-href]<!--[![npm downloads][npm-downloads-src]][npm-downloads-href]-->
8
4
  [![License][license-src]][license-href]
9
5
 
10
- </div>
11
-
12
- <div align="center">
13
-
14
6
  _Build the full spectrum of modern applications — backends, frontends, offline-first, and realtime apps — on raw platform power._ 🛸<br>
15
7
  [Webflo ↗](https://webflo.netlify.app/overview) is a web-native framework that lets you build absurdlly fast — with the whole sophistication and scale of modern apps solved from the foundation up.
16
8
 
17
- </div>
18
-
19
9
  ---
20
10
 
21
11
  > [!IMPORTANT]
22
12
  > 🚀 **Webflo is in active development and evolving daily.** Current status = **alpha**.<br>
23
- > You’re welcome to experiment, but it’s not yet suited for production workloads.
13
+ > You’re welcome to experiment, but it’s not yet suited for production apps.
24
14
 
25
15
  ---
26
16
 
@@ -38,12 +28,12 @@ For a quick intro, see the docs:
38
28
 
39
29
  | Category | Examples & Notes |
40
30
  | :------------------------- | :--------------------------------------------------------------------------------------------------------------- |
41
- | **Web apps** | From classic MPAs to rich SPAs, hybrid SSR/CSR experiences, and full PWAs — all built on one unified framework. |
42
- | **API backends** | REST endpoints, serverless-style handlers, and webhooks with streaming, partial responses, and live messaging. |
43
- | **Static sites** | Static-first or fully pre-rendered sites that seamlessly upgrade sections to live or interactive behavior. |
44
- | **Mobile experiences** | Installable, offline-capable PWAs with background sync, worker routing, and native-feeling navigation. |
45
- | **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications realtime channels and dialogs available out of the box. |
46
- | **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s live request lifecycle. |
31
+ | **Web apps** | Anything from classic MPAs to rich SPAs with SSR/CSR/hybrid rendering patterns. |
32
+ | **API backends** | REST endpoints and webhooks with streaming, partial responses, and live messaging. |
33
+ | **Static sites** | Static-first or fully pre-rendered sites with the same client-side richness of a Webflo app. |
34
+ | **Mobile experiences** | Installable, offline-capable PWAs with background sync, push notifications, and more. |
35
+ | **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications realtime channels and dialogs available out of the box. |
36
+ | **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s realtime capabilities. |
47
37
 
48
38
  ---
49
39
 
@@ -51,14 +41,12 @@ For a quick intro, see the docs:
51
41
 
52
42
  | Feature | Description |
53
43
  | :------------------------------------ | :-------------------------------------------------------------------------------------------------- |
54
- | 📁 **Folder-based routing** | Filesystem routing across client, worker, and server layers, with seamless interception and flow. |
55
- | 🌍 **Full-stack routing & lifecycle** | Every request flows through browser, worker, and server layers using the same handler model. |
56
- | 🔗 **Internal API composition** | Reuse your own routes as local function calls via `next(path)` — no extra networking required. |
44
+ | 📁 **Folder-based routing** | Filesystem routing across client, service worker, and server layers. |
45
+ | 🌍 **Service Worker routing** | Support for route handlers in the service worker. |
57
46
  | 🔐 **Sessions & auth** | Built-in cookie handling, session utilities, and helpers for gated routes and user-aware flows. |
58
- | ⚡ **Realtime capabilities** | Live responses, incremental updates, dialogs, and background channelsno explicit WebSocket setup. |
47
+ | ⚡ **Realtime capabilities** | Live responses, mutable/differential responses, two-way background messagingall built in. |
59
48
  | 🧠 **Mutation-based reactivity** | State is plain objects and arrays; mutations drive reactivity via the Observer API. |
60
49
  | 🧱 **OOHTML integration** | HTML-native templates, imports, and composition without a component DSL or build-heavy toolchain. |
61
- | 📦 **Offline & worker features** | Worker-side routing, caching, background sync, and offline-first behaviors built in. |
62
50
  | 🧩 **Dev mode & HMR** | Fast development server with fine-grained rebuilds and hot updates for HTML, JS, and CSS. |
63
51
 
64
52
  ---
package/package.json CHANGED
@@ -12,7 +12,7 @@
12
12
  "vanila-javascript"
13
13
  ],
14
14
  "homepage": "https://webqit.io/tooling/webflo",
15
- "version": "0.20.11-next.0",
15
+ "version": "0.20.12-next.0",
16
16
  "license": "MIT",
17
17
  "repository": {
18
18
  "type": "git",
@@ -45,15 +45,15 @@
45
45
  "@octokit/webhooks": "^7.15.1",
46
46
  "@webqit/backpack": "^0.1.12",
47
47
  "@webqit/oohtml-ssr": "^2.2.1",
48
- "@webqit/use-live": "^0.5.41",
48
+ "@webqit/use-live": "^0.5.44",
49
49
  "@webqit/util": "^0.8.11",
50
+ "chokidar": "^4.0.3",
50
51
  "dotenv": "^16.4.7",
51
52
  "esbuild": "^0.14.38",
53
+ "fast-glob": "^3.3.3",
52
54
  "mime-types": "^2.1.33",
53
55
  "simple-git": "^2.20.1",
54
- "urlpattern-polyfill": "^4.0.3",
55
- "chokidar": "^4.0.3",
56
- "fast-glob": "^3.3.3"
56
+ "urlpattern-polyfill": "^4.0.3"
57
57
  },
58
58
  "devDependencies": {
59
59
  "chai": "^4.3.6",
package/site/overview.md CHANGED
@@ -68,25 +68,23 @@ The service worker as a new routing site exposes this underutilized layer of the
68
68
 
69
69
  | Category | Examples & Notes |
70
70
  | :------------------------- | :--------------------------------------------------------------------------------------------------------------- |
71
- | **Web apps** | From classic MPAs to rich SPAs, hybrid SSR/CSR experiences, and full PWAs — all built on one unified framework. |
72
- | **API backends** | REST endpoints, serverless-style handlers, and webhooks with streaming, partial responses, and live messaging. |
73
- | **Static sites** | Static-first or fully pre-rendered sites that seamlessly upgrade sections to live or interactive behavior. |
74
- | **Mobile experiences** | Installable, offline-capable PWAs with background sync, worker routing, and native-feeling navigation. |
75
- | **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications realtime channels and dialogs available out of the box. |
76
- | **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s live request lifecycle. |
71
+ | **Web apps** | Anything from classic MPAs to rich SPAs with SSR/CSR/hybrid rendering patterns. |
72
+ | **API backends** | REST endpoints and webhooks with streaming, partial responses, and live messaging. |
73
+ | **Static sites** | Static-first or fully pre-rendered sites with the same client-side richness of a Webflo app. |
74
+ | **Mobile experiences** | Installable, offline-capable PWAs with background sync, push notifications, and more. |
75
+ | **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications realtime channels and dialogs available out of the box. |
76
+ | **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s realtime capabilities. |
77
77
 
78
78
  ## Features
79
79
 
80
80
  | Feature | Description |
81
81
  | :------------------------------------ | :-------------------------------------------------------------------------------------------------- |
82
- | 📁 **Folder-based routing** | Filesystem routing across client, worker, and server layers, with seamless interception and flow. |
83
- | 🌍 **Full-stack routing & lifecycle** | Every request flows through browser, worker, and server layers using the same handler model. |
84
- | 🔗 **Internal API composition** | Reuse your own routes as local function calls via `next(path)` — no extra networking required. |
82
+ | 📁 **Folder-based routing** | Filesystem routing across client, service worker, and server layers. |
83
+ | 🌍 **Service Worker routing** | Support for route handlers in the service worker. |
85
84
  | 🔐 **Sessions & auth** | Built-in cookie handling, session utilities, and helpers for gated routes and user-aware flows. |
86
- | ⚡ **Realtime capabilities** | Live responses, incremental updates, dialogs, and background channelsno explicit WebSocket setup. |
85
+ | ⚡ **Realtime capabilities** | Live responses, mutable/differential responses, two-way background messagingall built in. |
87
86
  | 🧠 **Mutation-based reactivity** | State is plain objects and arrays; mutations drive reactivity via the Observer API. |
88
87
  | 🧱 **OOHTML integration** | HTML-native templates, imports, and composition without a component DSL or build-heavy toolchain. |
89
- | 📦 **Offline & worker features** | Worker-side routing, caching, background sync, and offline-first behaviors built in. |
90
88
  | 🧩 **Dev mode & HMR** | Fast development server with fine-grained rebuilds and hot updates for HTML, JS, and CSS. |
91
89
 
92
90
  ## Get Started
@@ -2,6 +2,10 @@ import { WebfloRouter } from './webflo-routing/WebfloRouter.js';
2
2
  import { response as responseShim, headers as headersShim } from './webflo-fetch/index.js';
3
3
  import { LiveResponse } from './webflo-fetch/LiveResponse.js';
4
4
  import { AppBootstrap } from './AppBootstrap.js';
5
+ import { HttpEvent } from './webflo-routing/HttpEvent.js';
6
+ import { HttpThread } from './webflo-routing/HttpThread.js';
7
+ import { HttpSession } from './webflo-routing/HttpSession.js';
8
+ import { HttpUser } from './webflo-routing/HttpUser.js';
5
9
  import { _wq } from '../util.js';
6
10
 
7
11
  export class WebfloRuntime {
@@ -11,6 +15,14 @@ export class WebfloRuntime {
11
15
 
12
16
  static get Router() { return WebfloRouter; }
13
17
 
18
+ static get HttpEvent() { return HttpEvent; }
19
+
20
+ static get HttpThread() { return HttpThread; }
21
+
22
+ static get HttpSession() { return HttpSession; }
23
+
24
+ static get HttpUser() { return HttpUser; }
25
+
14
26
  static create(bootstrap) { return new this(bootstrap); }
15
27
 
16
28
  #bootstrap;
@@ -72,34 +84,33 @@ export class WebfloRuntime {
72
84
  return new Request(href, init);
73
85
  }
74
86
 
75
- createHttpCookies({ request, ...rest }) {
76
- return this.constructor.HttpCookies.create({ request, ...rest });
87
+ createHttpThread({ store, threadID, ...rest }) {
88
+ return this.constructor.HttpThread.create({ store, threadID, ...rest });
77
89
  }
78
90
 
79
- createHttpSession({ store, request, ...rest }) {
80
- return this.constructor.HttpSession.create({ store, request, ...rest });
91
+ createHttpCookies({ request, thread, ...rest }) {
92
+ return this.constructor.HttpCookies.create({ request, thread, ...rest });
81
93
  }
82
94
 
83
- createHttpUser({ store, request, session, client, ...rest }) {
84
- return this.constructor.HttpUser.create({ store, request, session, client, ...rest });
95
+ createHttpSession({ store, request, thread, ...rest }) {
96
+ return this.constructor.HttpSession.create({ store, request, thread, ...rest });
85
97
  }
86
98
 
87
- createHttpEvent({ request, cookies, session, user, client, detail, signal, state, ...rest }) {
88
- return this.constructor.HttpEvent.create(null, { request, cookies, session, user, client, detail, signal, state, ...rest });
99
+ createHttpUser({ store, request, thread, client, ...rest }) {
100
+ return this.constructor.HttpUser.create({ store, request, thread, client, ...rest });
101
+ }
102
+
103
+ createHttpEvent({ request, thread, cookies, session, user, client, detail, signal, state, ...rest }) {
104
+ return this.constructor.HttpEvent.create(null, { request, thread, cookies, session, user, client, detail, signal, state, ...rest });
89
105
  }
90
106
 
91
107
  async dispatchNavigationEvent({ httpEvent, crossLayerFetch, clientPortB }) {
92
108
  const { flags: FLAGS, logger: LOGGER } = this.cx;
93
- // Resolve rid before dispatching
94
- if (httpEvent.request.method === 'GET' && httpEvent.url.query['_rid']) {
95
- const requestMeta = _wq(httpEvent.request, 'meta');
96
- requestMeta.set('redirectID', httpEvent.url.query['_rid']);
97
- requestMeta.set('carries', [].concat(await httpEvent.session.get(`carry-store:${requestMeta.get('redirectID')}`) || []));
98
- await httpEvent.session.delete(`carry-store:${requestMeta.get('redirectID')}`);
99
- }
109
+
100
110
  // Dispatch event
101
111
  const router = new this.constructor.Router(this, httpEvent.url.pathname);
102
- await router.route(['SETUP'], httpEvent.extend(false));
112
+ await router.route(['SETUP'], httpEvent);
113
+
103
114
  // Do proper routing for respone
104
115
  const response = await new Promise(async (resolve) => {
105
116
  let autoLiveResponse, response;
@@ -113,6 +124,7 @@ export class WebfloRuntime {
113
124
  return await router.route(routeMethods, httpEvent, crossLayerFetch, remoteFetch);
114
125
  };
115
126
  const fullRoutingPipeline = this.bootstrap.middlewares.concat(route);
127
+
116
128
  try {
117
129
  response = await fullRoutingPipeline.reverse().reduce((next, fn) => {
118
130
  return () => fn.call(this.cx, httpEvent, next);
@@ -121,14 +133,17 @@ export class WebfloRuntime {
121
133
  console.error(e);
122
134
  response = new Response(null, { status: 500, statusText: e.message });
123
135
  }
136
+
124
137
  if (!/Response/.test(LiveResponse.test(response))) {
125
- const isLifecyleComplete = httpEvent.lifeCycleComplete();
138
+ const isLifecyleComplete = httpEvent.lifeCycleComplete() ?? true;
126
139
  response = LiveResponse.test(response) !== 'Default' || !isLifecyleComplete
127
140
  ? await LiveResponse.from(response, { done: isLifecyleComplete })
128
141
  : responseShim.from.value(response);
129
142
  }
143
+
130
144
  // Any "carry" data?
131
145
  await this.handleCarries(httpEvent, response);
146
+
132
147
  // Resolve now...
133
148
  if (autoLiveResponse) {
134
149
  await autoLiveResponse.replaceWith(response, { done: true });
@@ -148,7 +163,7 @@ export class WebfloRuntime {
148
163
 
149
164
  // Send the X-Background-Messaging-Port header
150
165
  // This server's event lifecycle management
151
- if (!httpEvent.lifeCycleComplete()) {
166
+ if (!(httpEvent.lifeCycleComplete() ?? true)) {
152
167
  if (this.isClientSide) {
153
168
  const responseMeta = _wq(response, 'meta');
154
169
  responseMeta.set('background_port', clientPortB);
@@ -191,44 +206,17 @@ export class WebfloRuntime {
191
206
  }
192
207
 
193
208
  async handleCarries(httpEvent, response) {
194
- const requestMeta = _wq(httpEvent.request, 'meta');
195
- const responseMeta = _wq(response, 'meta');
196
- if (response.headers.get('Location')) {
197
- // Don't emit the supposedly incoming "carries"
198
- // Save back to URL
199
- if (requestMeta.get('carries')?.length) {
200
- await httpEvent.session.set(`carry-store:${requestMeta.get('redirectID')}`, requestMeta.get('carries'));
201
- requestMeta.set('carries', []);
202
- }
203
- // Stash current byte of "carry"
204
- if (responseMeta.has('carry')) {
205
- const $url = new URL(response.headers.get('Location'), httpEvent.request.url);
206
- if ($url.searchParams.has('_rid')) {
207
- // If the URL already has a rid, append the new one
208
- const existingRedirectID = $url.searchParams.get('_rid');
209
- const existingData = await httpEvent.session.get(`carry-store:${existingRedirectID}`);
210
- const combinedData = [].concat(responseMeta.get('carry'), existingData || []);
211
- // Save the combined data back to the session
212
- await httpEvent.session.set(`carry-store:${existingRedirectID}`, combinedData);
213
- } else {
214
- // If not, create a new rid
215
- const redirectID = (0 | Math.random() * 9e6).toString(36);
216
- $url.searchParams.set('_rid', redirectID);
217
- await httpEvent.session.set(`carry-store:${redirectID}`, [].concat(responseMeta.get('carry')));
218
- }
219
- }
220
- } else {
209
+ if (!response.headers.get('Location')) {
210
+ const status = await httpEvent.thread.consume('status');
211
+ await httpEvent.thread.clear();
212
+ if (!status) return;
221
213
  // Fire redirect message?
222
- const flashResponses = requestMeta.get('carries')?.filter((c) => ['message', 'error', 'success', 'alert', 'info', 'tip', 'warning'].includes(c.type));
223
- if (flashResponses?.length) {
224
- httpEvent.waitUntil(new Promise((resolve) => {
225
- httpEvent.client.wqLifecycle.open.then(() => {
226
- httpEvent.client.postMessage(flashResponses, { wqEventOptions: { type: 'flash' } });
227
- resolve();
228
- }, { once: true });
229
- }));
230
- }
231
- requestMeta.set('carries', []);
214
+ httpEvent.waitUntil(new Promise((resolve) => {
215
+ httpEvent.client.wqLifecycle.open.then(async () => {
216
+ httpEvent.client.postMessage(status, { wqEventOptions: { type: 'alert' } });
217
+ resolve();
218
+ }, { once: true });
219
+ }));
232
220
  }
233
221
  }
234
222
 
@@ -1,9 +1,10 @@
1
1
  import { HttpCookies } from '../webflo-routing/HttpCookies.js';
2
2
 
3
3
  export class ClientSideCookies extends HttpCookies {
4
- static create({ request }) {
4
+ static create({ request, thread }) {
5
5
  return new this({
6
6
  request,
7
+ thread,
7
8
  entries: document.cookie.split(';').map((c) => c.split('=').map((s) => s.trim()))
8
9
  });
9
10
  }
@@ -7,9 +7,6 @@ import { response as responseShim } from '../webflo-fetch/index.js';
7
7
  import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
8
8
  import { WQStarPort } from '../webflo-messaging/WQStarPort.js';
9
9
  import { ClientSideCookies } from './ClientSideCookies.js';
10
- import { HttpSession } from '../webflo-routing/HttpSession.js';
11
- import { HttpEvent } from '../webflo-routing/HttpEvent.js';
12
- import { HttpUser } from '../webflo-routing/HttpUser.js';
13
10
  import { Url } from '../webflo-url/Url.js';
14
11
  import { _wq } from '../../util.js';
15
12
  import '../webflo-fetch/index.js';
@@ -17,14 +14,8 @@ import '../webflo-url/index.js';
17
14
 
18
15
  export class WebfloClient extends WebfloRuntime {
19
16
 
20
- static get HttpEvent() { return HttpEvent; }
21
-
22
17
  static get HttpCookies() { return ClientSideCookies; }
23
18
 
24
- static get HttpSession() { return HttpSession; }
25
-
26
- static get HttpUser() { return HttpUser; }
27
-
28
19
  #host;
29
20
  get host() { return this.#host; }
30
21
 
@@ -68,19 +59,23 @@ export class WebfloClient extends WebfloRuntime {
68
59
  // Bind prompt handlers
69
60
  const promptsHandler = (e) => {
70
61
  const message = e.data?.message
71
- ? e.data.message + (e.data.details ? `\r\n${e.data.details}` : '')
62
+ ? e.data.message
72
63
  : e.data;
73
64
  const execPromp = () => {
65
+ if (e.defaultPrevented) return;
74
66
  if (e.type === 'confirm') {
75
67
  e.wqRespondWith(confirm(message));
76
68
  } else if (e.type === 'prompt') {
77
69
  e.wqRespondWith(prompt(message));
70
+ } else if (e.type === 'alert') {
71
+ alert(message);
78
72
  }
79
73
  };
80
74
  window.queueMicrotask(execPromp);
81
75
  };
82
76
  this.background.addEventListener('confirm', promptsHandler, { signal: instanceController.signal });
83
77
  this.background.addEventListener('prompt', promptsHandler, { signal: instanceController.signal });
78
+ this.background.addEventListener('alert', promptsHandler, { signal: instanceController.signal });
84
79
  await this.setupCapabilities();
85
80
  this.control();
86
81
  await this.hydrate();
@@ -242,7 +237,8 @@ export class WebfloClient extends WebfloRuntime {
242
237
  #prevEvent;
243
238
  createHttpEvent(init, singleton = true) {
244
239
  if (singleton && this.#prevEvent) {
245
- this.#prevEvent.abort();
240
+ // TODO
241
+ //this.#prevEvent.abort();
246
242
  }
247
243
  const httpEvent = super.createHttpEvent(init);
248
244
  this.$instanceController.signal.addEventListener('abort', () => httpEvent.abort(), { once: true });
@@ -266,20 +262,30 @@ export class WebfloClient extends WebfloRuntime {
266
262
  }
267
263
  // Create and route request
268
264
  scopeObj.request = this.createRequest(scopeObj.url, scopeObj.init);
265
+ scopeObj.thread = this.createHttpThread({
266
+ store: this.createStorage('thread'),
267
+ threadId: scopeObj.url.searchParams.get('_thread'),
268
+ realm: 1
269
+ });
269
270
  scopeObj.cookies = this.createHttpCookies({
270
- request: scopeObj.request
271
+ request: scopeObj.request,
272
+ thread: scopeObj.thread,
273
+ realm: 1
271
274
  });
272
275
  scopeObj.session = this.createHttpSession({
273
- store: this.createStorage('session'),
274
- request: scopeObj.request
275
- });
276
+ store: this.createStorage('session'),
277
+ request: scopeObj.request,
278
+ thread: scopeObj.thread,
279
+ realm: 1
280
+ });
276
281
  const wqMessageChannel = new WQMessageChannel;
277
282
  scopeObj.clientRequestRealtime = wqMessageChannel.port1;
278
283
  scopeObj.user = this.createHttpUser({
279
284
  store: this.createStorage('user'),
280
285
  request: scopeObj.request,
286
+ thread: scopeObj.thread,
281
287
  client: scopeObj.clientRequestRealtime,
282
- session: scopeObj.session,
288
+ realm: 1
283
289
  });
284
290
  if (window.webqit?.oohtml?.configs) {
285
291
  const { BINDINGS_API: { api: bindingsConfig } = {}, } = window.webqit.oohtml.configs;
@@ -287,6 +293,7 @@ export class WebfloClient extends WebfloRuntime {
287
293
  }
288
294
  scopeObj.httpEvent = this.createHttpEvent({
289
295
  request: scopeObj.request,
296
+ thread: scopeObj.thread,
290
297
  client: scopeObj.clientRequestRealtime,
291
298
  cookies: scopeObj.cookies,
292
299
  session: scopeObj.session,
@@ -294,6 +301,7 @@ export class WebfloClient extends WebfloRuntime {
294
301
  detail: scopeObj.detail,
295
302
  signal: init.signal,
296
303
  state: scopeObj.UIState,
304
+ realm: 1
297
305
  }, true);
298
306
  // Set pre-request states
299
307
  Observer.set(this.navigator, {
@@ -434,15 +442,18 @@ export class WebfloClient extends WebfloRuntime {
434
442
  async transitionUI(updateCallback) {
435
443
  if (document.startViewTransition && this.withViewTransitions) {
436
444
  const synthesizeWhile = window.webqit?.realdom?.synthesizeWhile || ((callback) => callback());
437
- await synthesizeWhile(async () => {
438
- Observer.set(this.transition, 'phase', 1);
439
- const viewTransition = document.startViewTransition(updateCallback);
440
- try { await viewTransition.updateCallbackDone; } catch (e) { console.log(e); }
441
- Observer.set(this.transition, 'phase', 2);
442
- try { await viewTransition.ready; } catch (e) { console.log(e); }
443
- Observer.set(this.transition, 'phase', 3);
444
- try { await viewTransition.finished; } catch (e) { console.log(e); }
445
- Observer.set(this.transition, 'phase', 0);
445
+ return new Promise(async (resolve) => {
446
+ await synthesizeWhile(async () => {
447
+ Observer.set(this.transition, 'phase', 1);
448
+ const viewTransition = document.startViewTransition(updateCallback);
449
+ try { await viewTransition.updateCallbackDone; } catch (e) { console.log(e); }
450
+ Observer.set(this.transition, 'phase', 2);
451
+ try { await viewTransition.ready; } catch (e) { console.log(e); }
452
+ Observer.set(this.transition, 'phase', 3);
453
+ try { await viewTransition.finished; } catch (e) { console.log(e); }
454
+ Observer.set(this.transition, 'phase', 0);
455
+ resolve();
456
+ });
446
457
  });
447
458
  } else await updateCallback();
448
459
  }
@@ -48,7 +48,7 @@ export class WebfloRootClient2 extends WebfloRootClient1 {
48
48
  const init = {
49
49
  method: formData && 'POST' || 'GET',
50
50
  body: formData,
51
- signal
51
+ //signal TODO: auto-aborts on a redirect response which thus fails to parse
52
52
  };
53
53
  this.updateCurrentEntry({
54
54
  state: {
@@ -1,4 +1,4 @@
1
- import { Observer, LiveMode } from '@webqit/use-live';
1
+ import { Observer, LiveProgramHandle } from '@webqit/use-live';
2
2
  import { _isObject, _isTypeObject } from '@webqit/util/js/index.js';
3
3
  import { publishMutations, applyMutations } from '../webflo-messaging/wq-message-port.js';
4
4
  import { WQBroadcastChannel } from '../webflo-messaging/WQBroadcastChannel.js';
@@ -22,9 +22,9 @@ export class LiveResponse extends EventTarget {
22
22
  if (isGenerator(data)) {
23
23
  return 'Generator';
24
24
  }
25
- if (data instanceof LiveMode
26
- || data?.[Symbol.toStringTag] === 'LiveMode') {
27
- return 'LiveMode';
25
+ if (data instanceof LiveProgramHandle
26
+ || data?.[Symbol.toStringTag] === 'LiveProgramHandle') {
27
+ return 'LiveProgramHandle';
28
28
  }
29
29
  return 'Default';
30
30
  }
@@ -39,8 +39,8 @@ export class LiveResponse extends EventTarget {
39
39
  if (this.test(data) === 'Generator') {
40
40
  return await this.fromGenerator(data, ...args);
41
41
  }
42
- if (this.test(data) === 'LiveMode') {
43
- return this.fromLiveMode(data, ...args);
42
+ if (this.test(data) === 'LiveProgramHandle') {
43
+ return this.fromLiveProgramHandle(data, ...args);
44
44
  }
45
45
  return new this(data, ...args);
46
46
  }
@@ -137,18 +137,18 @@ export class LiveResponse extends EventTarget {
137
137
  return instance;
138
138
  }
139
139
 
140
- static async fromLiveMode(liveMode, options = {}) {
141
- if (!this.test(liveMode) === 'LiveMode') {
142
- throw new Error('Argument must be a UseLive LiveMode instance.');
140
+ static async fromLiveProgramHandle(liveProgramHandle, options = {}) {
141
+ if (!this.test(liveProgramHandle) === 'LiveProgramHandle') {
142
+ throw new Error('Argument must be a UseLive LiveProgramHandle instance.');
143
143
  }
144
144
  const instance = new this;
145
- await instance.replaceWith(liveMode.value, { done: false, ...options });
145
+ await instance.replaceWith(liveProgramHandle.value, { done: false, ...options });
146
146
  if (instance.#generatorType === 'Default') {
147
- instance.#generator = liveMode;
148
- instance.#generatorType = 'LiveMode';
147
+ instance.#generator = liveProgramHandle;
148
+ instance.#generatorType = 'LiveProgramHandle';
149
149
  }
150
150
  Observer.observe(
151
- liveMode,
151
+ liveProgramHandle,
152
152
  'value',
153
153
  (e) => instance.#replaceWith(e.value),
154
154
  { signal: instance.#abortController.signal }
@@ -446,8 +446,8 @@ export class LiveResponse extends EventTarget {
446
446
  }));
447
447
  }
448
448
 
449
- toLiveMode({ signal: abortSignal } = {}) {
450
- const state = new LiveModeX;
449
+ toLiveProgramHandle({ signal: abortSignal } = {}) {
450
+ const state = new LiveProgramHandleX;
451
451
  const replaceHandler = () => Observer.defineProperty(state, 'value', { value: this.body, enumerable: true, configurable: true });
452
452
  this.addEventListener('replace', replaceHandler, { signal: abortSignal });
453
453
  replaceHandler();
@@ -469,7 +469,7 @@ export const isGenerator = (obj) => {
469
469
  typeof obj?.return === 'function';
470
470
  };
471
471
 
472
- class LiveModeX extends LiveMode {
472
+ class LiveProgramHandleX extends LiveProgramHandle {
473
473
  constructor() { }
474
474
  abort() { }
475
475
  }
@@ -140,40 +140,6 @@ export const response = {
140
140
  return instance;
141
141
  }
142
142
  },
143
- redirect: {
144
- value: function (url, status = 302) {
145
- if (typeof url !== 'string' && !(url instanceof URL)) {
146
- throw new Error('Redirect URL must be a string or URL!');
147
- }
148
- if (typeof status !== 'number') {
149
- throw new Error('Redirect code must be a number!');
150
- }
151
- return new Response(null, { status, headers: { Location: url } });
152
- }
153
- },
154
- redirectWith: {
155
- value: function (url, ...args) {
156
- if (typeof url !== 'string' && !(url instanceof URL)) {
157
- throw new Error('Redirect URL must be a string or URL!');
158
- }
159
- let status = 302;
160
- if (!_isObject(args[0])) {
161
- status = args.shift();
162
- }
163
- if (typeof status !== 'number') {
164
- throw new Error('Redirect code must be a number!');
165
- }
166
- if (args.some((arg) => !_isObject(arg))) {
167
- throw new Error('Redirect arguments must be objects!');
168
- }
169
- const responseInstance = new Response(null, { status, headers: { Location: url } });
170
- if (args.length) {
171
- const responseMeta = _wq(responseInstance, 'meta');
172
- responseMeta.set('carry', args);
173
- }
174
- return responseInstance;
175
- }
176
- },
177
143
  prototype: {
178
144
  status: {
179
145
  get: function () {
@@ -393,6 +359,7 @@ export function renderHttpMessageInit(httpMessageInit) {
393
359
  }
394
360
 
395
361
  export async function parseHttpMessage(httpMessage) {
362
+ if (!httpMessage.body) return null;
396
363
  let result;
397
364
  const contentType = httpMessage.headers.get('Content-Type') || '';
398
365
  if (contentType === 'application/x-www-form-urlencoded' || contentType.startsWith('multipart/form-data')) {
@@ -1,5 +1,5 @@
1
- import { _isObject } from '@webqit/util/js/index.js';
2
1
  import { _even } from '@webqit/util/obj/index.js';
2
+ import { _isObject } from '@webqit/util/js/index.js';
3
3
  import { renderCookieObjToString } from '../webflo-fetch/index.js';
4
4
  import { HttpState } from './HttpState.js';
5
5
 
@@ -7,9 +7,9 @@ export class HttpCookies extends HttpState {
7
7
 
8
8
  #originals;
9
9
 
10
- constructor({ request, entries = [] }) {
10
+ constructor({ request, thread = null, entries = [] }) {
11
11
  entries = [...entries].map(([key, value]) => [key, !_isObject(value) ? { name: key, value } : value]);
12
- super({ store: new Map(entries), request, session: null });
12
+ super({ store: new Map(entries), request, thread });
13
13
  this.#originals = new Map(entries);
14
14
  }
15
15
 
@@ -26,10 +26,14 @@ export class HttpEvent {
26
26
  });
27
27
  }
28
28
 
29
+ get signal() { return this.#abortController.signal; }
30
+
29
31
  get url() { return this.#url; }
30
32
 
31
33
  get request() { return this.#init.request; }
32
34
 
35
+ get thread() { return this.#init.thread; }
36
+
33
37
  get client() { return this.#init.client; }
34
38
 
35
39
  get cookies() { return this.#init.cookies; }
@@ -40,8 +44,6 @@ export class HttpEvent {
40
44
 
41
45
  get detail() { return this.#init.detail; }
42
46
 
43
- get signal() { return this.#abortController.signal; }
44
-
45
47
  get state() { return { ...(this.#init.state || {}) }; }
46
48
 
47
49
  #lifecyclePromises = new Set;
@@ -58,6 +60,9 @@ export class HttpEvent {
58
60
  if (this.#lifecyclePromises.dirty && !this.#lifecyclePromises.size) {
59
61
  throw new Error('Event lifecycle already complete.');
60
62
  }
63
+ if (this.#parentEvent) {
64
+ this.#parentEvent.waitUntil(promise);
65
+ }
61
66
  promise = Promise.resolve(promise);
62
67
  this.#lifecyclePromises.add(promise);
63
68
  this.#lifecyclePromises.dirty = true;
@@ -78,8 +83,11 @@ export class HttpEvent {
78
83
  if (returningThePromise) {
79
84
  return this.#lifeCycleResolutionPromise;
80
85
  }
81
- return this.#lifecyclePromises.dirty // IMPORTANT
82
- && !this.#lifecyclePromises.size;
86
+ if (this.#lifecyclePromises.dirty === undefined) {
87
+ // Hasn't been initialized yet
88
+ return null;
89
+ }
90
+ return !this.#lifecyclePromises.size;
83
91
  }
84
92
 
85
93
  async waitUntil(promise) {
@@ -97,13 +105,49 @@ export class HttpEvent {
97
105
  await this.#internalLiveResponse.replaceWith(data, ...args);
98
106
  }
99
107
 
108
+ async redirect(url, status = 302) {
109
+ if (typeof url !== 'string' && !(url instanceof URL)) {
110
+ throw new Error('Redirect URL must be a string or URL!');
111
+ }
112
+ let options = {};
113
+ if (_isObject(status)) {
114
+ ({ status = 302, ...options } = status);
115
+ }
116
+ if (typeof status !== 'number') {
117
+ throw new Error('Redirect code must be a number!');
118
+ }
119
+ return await this.respondWith(null, { status, ...options, headers: { Location: url } });
120
+ }
121
+
122
+ async redirectWith(url, data, status = 302) {
123
+ if (typeof url !== 'string' && !(url instanceof URL)) {
124
+ throw new Error('Redirect URL must be a string or URL!');
125
+ }
126
+ let options = {};
127
+ if (_isObject(status)) {
128
+ ({ status = 302, ...options } = status);
129
+ }
130
+ if (typeof status !== 'number') {
131
+ throw new Error('Redirect code must be a number!');
132
+ }
133
+ //-----
134
+ const urlRewrite = new URL(url, this.request.url);
135
+ const newThread = this.thread.extend(urlRewrite.searchParams.get('_thread'));
136
+ urlRewrite.searchParams.set('_thread', newThread.threadID);
137
+ await newThread.append('back', this.request.url.replace(urlRewrite.origin, ''));
138
+ for (const [key, value] of Object.entries(data)) {
139
+ await newThread.append(key, value);
140
+ }
141
+ //-----
142
+ return await this.respondWith(null, { status, ...options, headers: { Location: urlRewrite.href } });
143
+ }
144
+
100
145
  clone(init = {}) {
101
146
  return this.constructor.create(this.#parentEvent, { ...this.#init, ...init });
102
147
  }
103
148
 
104
149
  extend(init = {}) {
105
150
  const instance = this.constructor.create(this/*Main difference from clone*/, { ...this.#init, ...(init || {}) });
106
- if (init !== false) this.#extendLifecycle(instance.lifeCycleComplete(true));
107
151
  return instance;
108
152
  }
109
153
 
@@ -1,11 +1,11 @@
1
1
  import { HttpState } from './HttpState.js';
2
2
 
3
3
  export class HttpSession extends HttpState {
4
- static create({ store, request }) {
4
+ static create({ store, request, thread }) {
5
5
  return new this({
6
6
  store,
7
7
  request,
8
- session: true
8
+ thread
9
9
  });
10
10
  }
11
11
  }
@@ -2,16 +2,16 @@ import { _isObject } from '@webqit/util/js/index.js';
2
2
  import { _even } from '@webqit/util/obj/index.js';
3
3
 
4
4
  export class HttpState {
5
-
5
+
6
6
  #store;
7
7
  #request;
8
- #session;
8
+ #thread;
9
9
  #modified = false;
10
10
 
11
- constructor({ store, request, session = null }) {
11
+ constructor({ store, request, thread }) {
12
12
  this.#store = store || new Map;
13
13
  this.#request = request;
14
- this.#session = session === true ? this : session;
14
+ this.#thread = thread === true ? this : thread;
15
15
  }
16
16
 
17
17
  async has(key) { return await this.#store.has(key); }
@@ -59,7 +59,7 @@ export class HttpState {
59
59
 
60
60
  async forEach(callback) { (await this.entries()).forEach(([key, value], i) => callback(value, key, i)); }
61
61
 
62
- [ Symbol.iterator ]() { return this.entries().then((entries) => entries[ Symbol.iterator ]()); }
62
+ [Symbol.iterator]() { return this.entries().then((entries) => entries[Symbol.iterator]()); }
63
63
 
64
64
  get size() { return this.#store.sizs; }
65
65
 
@@ -97,9 +97,15 @@ export class HttpState {
97
97
  handler = { callback: handler };
98
98
  } else if (typeof handler === 'string') {
99
99
  handler = { url: handler };
100
- } else if (typeof handler?.callback !== 'function' && typeof handler?.url !== 'string') {
100
+ } else if (!(_isObject(handler) && (handler = { ...handler }))
101
+ || typeof handler.callback !== 'function' && typeof handler.url !== 'string') {
101
102
  throw new Error(`Handler must be either an URL or a function or an object specifying either an URL (handler.url) or a function (handler.callback)`);
102
103
  }
104
+ if (_isObject(handler.with)) {
105
+ handler.with = { ...handler.with };
106
+ } else if (handler.with) {
107
+ throw new Error(`The "with" parameter must be a valid JSON object`);
108
+ }
103
109
  $handlers.push(handler);
104
110
  }
105
111
  this.#handlers.set(attr, $handlers);
@@ -115,7 +121,7 @@ export class HttpState {
115
121
  if (!handlers) {
116
122
  throw new Error(`No handler defined for the user attribute: ${attr}`);
117
123
  }
118
- for (let i = 0; i < handlers.length; i ++) {
124
+ for (let i = 0; i < handlers.length; i++) {
119
125
  const handler = handlers[i];
120
126
  if (handler.callback) {
121
127
  const returnValue = await handler.callback(this, attr);
@@ -129,20 +135,19 @@ export class HttpState {
129
135
  continue main;
130
136
  }
131
137
  const urlRewrite = new URL(handler.url, this.#request.url);
132
- if (!urlRewrite.searchParams.has('success-redirect')) {
133
- urlRewrite.searchParams.set('success-redirect', this.#request.url.replace(urlRewrite.origin, ''));
134
- }
135
- if (handler.message) {
136
- if (!this.#session) {
137
- throw new Error('Storage type does not support redirect messages');
138
+ const newThread = this.#thread.extend(urlRewrite.searchParams.get('_thread'));
139
+ urlRewrite.searchParams.set('_thread', newThread.threadID);
140
+ await newThread.append('back', this.#request.url.replace(urlRewrite.origin, ''));
141
+ if (handler.with) {
142
+ for (const [key, value] of Object.entries(handler.with)) {
143
+ await newThread.append(key, value);
138
144
  }
139
- const messageID = (0 | Math.random() * 9e6).toString(36);
140
- urlRewrite.searchParams.set('redirect-message', messageID);
141
- await this.#session.set(`redirect-message:${messageID}`, { status: { type: handler.type || 'info', message: handler.message }});
142
145
  }
143
- return new Response(null, { status: 302, headers: {
144
- Location: urlRewrite
145
- }});
146
+ return new Response(null, {
147
+ status: 302, headers: {
148
+ Location: urlRewrite
149
+ }
150
+ });
146
151
  }
147
152
  }
148
153
  entries.push(await this.get(attr));
@@ -0,0 +1,81 @@
1
+ export class HttpThread {
2
+
3
+ static create({ store, threadID, realm }) {
4
+ if (!threadID || !(new RegExp(`^wq\\.${realm}\\.`)).test(threadID)) {
5
+ threadID = `wq.${realm}.${crypto.randomUUID()}`;
6
+ }
7
+ return new this({ store, threadID, realm });
8
+ }
9
+
10
+ #store;
11
+ #threadID;
12
+ #realm;
13
+
14
+ get threadID() { return this.#threadID; }
15
+
16
+ constructor({ store, threadID, realm }) {
17
+ this.#store = store || new Map;
18
+ this.#threadID = threadID;
19
+ this.#realm = realm;
20
+ }
21
+
22
+ extend(_threadID = null) {
23
+ return this.constructor.create({
24
+ store: this.#store,
25
+ threadID: _threadID,
26
+ realm: this.#realm
27
+ });
28
+ }
29
+
30
+ async keys() {
31
+ const thread = await this.#store.get(this.#threadID) || {};
32
+ return Object.keys(thread);
33
+ }
34
+
35
+ async has(key, filter = null) {
36
+ if (filter === true || !filter) return (await this.keys()).includes(key);
37
+ const thread = await this.#store.get(this.#threadID) || {};
38
+ const values = [].concat(thread[key] ?? []);
39
+ return values.findIndex(filter) !== -1;
40
+ }
41
+
42
+ async append(key, value) {
43
+ const thread = await this.#store.get(this.#threadID) || {};
44
+ thread[key] = [].concat(thread[key] ?? []);
45
+ thread[key].push(value);
46
+ await this.#store.set(this.#threadID, thread);
47
+ return this;
48
+ }
49
+
50
+ async consume(key, filter = null) {
51
+ const thread = await this.#store.get(this.#threadID) || {};
52
+ const values = [].concat(thread[key] ?? []);
53
+
54
+ let value;
55
+ if (filter === true) {
56
+ delete thread[key];
57
+ value = values;
58
+ } else if (filter) {
59
+ const i = values.findIndex(filter);
60
+ if (i !== -1) {
61
+ value = values.splice(i, 1)[0];
62
+ }
63
+ } else { value = values.pop(); }
64
+
65
+ if (!values.length) {
66
+ delete thread[key];
67
+ }
68
+ if (!Object.keys(thread).length) {
69
+ await this.#store.delete(this.#threadID);
70
+ } else {
71
+ await this.#store.set(this.#threadID, thread);
72
+ }
73
+
74
+ return value;
75
+ }
76
+
77
+ async clear() {
78
+ await this.#store.delete(this.#threadID);
79
+ return this;
80
+ }
81
+ }
@@ -2,17 +2,17 @@ import { HttpState } from './HttpState.js';
2
2
 
3
3
  export class HttpUser extends HttpState {
4
4
 
5
- static create({ store, request, client, session }) {
6
- return new this({ store, request, client, session });
5
+ static create({ store, request, thread, client }) {
6
+ return new this({ store, request, thread, client });
7
7
  }
8
8
 
9
9
  #client;
10
10
 
11
- constructor({ store, request, client, session }) {
11
+ constructor({ store, request, thread, client }) {
12
12
  super({
13
13
  store,
14
14
  request,
15
- session
15
+ thread
16
16
  });
17
17
  this.#client = client;
18
18
  }
@@ -1,6 +1,7 @@
1
1
  import { _isFunction, _isArray, _isObject } from '@webqit/util/js/index.js';
2
2
  import { _from as _arrFrom } from '@webqit/util/arr/index.js';
3
3
  import { LiveResponse } from '../webflo-fetch/LiveResponse.js';
4
+ import { request as requestShim } from '../webflo-fetch/index.js';
4
5
  import { path as Path } from '../webflo-url/util.js';
5
6
 
6
7
  export class WebfloRouter {
@@ -15,6 +16,13 @@ export class WebfloRouter {
15
16
 
16
17
  async route(method, event, _default = null, remoteFetch = null) {
17
18
  const $this = this;
19
+ const callWebfloDefault = async (thisContext, thisTick) => {
20
+ let returnValue;
21
+ if (_default) {
22
+ returnValue = await _default.call(thisContext, thisTick.event, remoteFetch);
23
+ }
24
+ return returnValue;
25
+ };
18
26
  // ----------------
19
27
  // The loop
20
28
  // ----------------
@@ -33,10 +41,7 @@ export class WebfloRouter {
33
41
  return next(thisTick);
34
42
  }
35
43
  // Exports not found and directory not found
36
- if (_default) {
37
- return await _default.call(thisContext, thisTick.event, remoteFetch);
38
- }
39
- return;
44
+ return callWebfloDefault(thisContext, thisTick);
40
45
  }
41
46
  // -------------
42
47
  // Broadcast any hints exported by handler
@@ -98,7 +103,7 @@ export class WebfloRouter {
98
103
  // Build request inheritance chain
99
104
  const requestInheritanceChain = [url];
100
105
  if (!isFetch && thisTick.event.request instanceof Request) {
101
- const { url: _, ...init } = await Request.copy(thisTick.event.request);
106
+ const { url: _, ...init } = await requestShim.copy.value(thisTick.event.request);
102
107
  requestInheritanceChain.push(init);
103
108
  }
104
109
  const noArg2 = () => {
@@ -106,7 +111,7 @@ export class WebfloRouter {
106
111
  };
107
112
  if (args[0] instanceof Request) {
108
113
  if (args[1]) noArg2();
109
- const { url: _, ...init } = await Request.copy(args[0]);
114
+ const { url: _, ...init } = await requestShim.copy.value(args[0]);
110
115
  requestInheritanceChain.push(init);
111
116
  } else if (!isFetch && _isObject(args[0])) {
112
117
  if (args[1]) noArg2();
@@ -169,7 +174,7 @@ export class WebfloRouter {
169
174
  const returnValue = await handler.call(thisContext, thisTick.event, $next/*next*/, $fetch/*fetch*/);
170
175
 
171
176
  // Handle cleanup on abort
172
- if (LiveResponse.test(returnValue) === 'LiveMode') {
177
+ if (LiveResponse.test(returnValue) === 'LiveProgramHandle') {
173
178
  thisTick.event.signal.addEventListener('abort', () => {
174
179
  returnValue.abort();
175
180
  });
@@ -190,15 +195,7 @@ export class WebfloRouter {
190
195
  }
191
196
  });
192
197
  }
193
- let returnValue;
194
- if (_default) {
195
- returnValue = await _default.call(thisContext, thisTick.event, remoteFetch);
196
- }
197
- try {
198
- // IMPORTANT: Explicitly terminate the event lifecycle if nothing extends it
199
- await thisTick.event.waitUntil();
200
- } catch(e) {}
201
- return returnValue;
198
+ return callWebfloDefault(thisContext, thisTick);
202
199
  };
203
200
 
204
201
  return next({
@@ -2,10 +2,11 @@ import { headers as headersShim } from '../webflo-fetch/index.js';
2
2
  import { HttpCookies } from '../webflo-routing/HttpCookies.js';
3
3
 
4
4
  export class ServerSideCookies extends HttpCookies {
5
- static create({ request }) {
5
+ static create({ request, thread }) {
6
6
  const cookies = headersShim.get.value.call(request.headers, 'Cookie', true);
7
7
  return new this({
8
8
  request,
9
+ thread,
9
10
  entries: cookies.map((c) => [c.name, c])
10
11
  });
11
12
  }
@@ -3,10 +3,11 @@ import { headers as headersShim } from '../webflo-fetch/index.js';
3
3
 
4
4
  export class ServerSideSession extends HttpSession {
5
5
 
6
- static create({ store, request, sessionID, ttl }) {
6
+ static create({ store, request, thread, sessionID, ttl }) {
7
7
  return new this({
8
8
  store,
9
9
  request,
10
+ thread,
10
11
  sessionID,
11
12
  ttl
12
13
  });
@@ -16,14 +17,14 @@ export class ServerSideSession extends HttpSession {
16
17
  get sessionID() { return this.#sessionID; }
17
18
  #ttl;
18
19
 
19
- constructor({ store, request, sessionID, ttl }) {
20
+ constructor({ store, request, thread, sessionID, ttl }) {
20
21
  if (!sessionID) {
21
22
  throw new Error(`sessionID is required`);
22
23
  }
23
24
  super({
24
25
  store,
25
26
  request,
26
- session: true
27
+ thread,
27
28
  });
28
29
  this.#sessionID = sessionID;
29
30
  this.#ttl = ttl;
@@ -20,8 +20,6 @@ import { WebfloRuntime } from '../WebfloRuntime.js';
20
20
  import { WQSockPort } from '../webflo-messaging/WQSockPort.js';
21
21
  import { ServerSideCookies } from './ServerSideCookies.js';
22
22
  import { ServerSideSession } from './ServerSideSession.js';
23
- import { HttpEvent } from '../webflo-routing/HttpEvent.js';
24
- import { HttpUser } from '../webflo-routing/HttpUser.js';
25
23
  import { response as responseShim, headers as headersShim } from '../webflo-fetch/index.js';
26
24
  import { UseLiveTransform } from '../../build-pi/esbuild-plugin-uselive-transform.js';
27
25
  import { createWindow } from '@webqit/oohtml-ssr';
@@ -31,14 +29,10 @@ import '../webflo-url/index.js';
31
29
 
32
30
  export class WebfloServer extends WebfloRuntime {
33
31
 
34
- static get HttpEvent() { return HttpEvent; }
35
-
36
32
  static get HttpCookies() { return ServerSideCookies; }
37
33
 
38
34
  static get HttpSession() { return ServerSideSession; }
39
35
 
40
- static get HttpUser() { return HttpUser; }
41
-
42
36
  static create(bootstrap) {
43
37
  return new this(bootstrap);
44
38
  }
@@ -648,33 +642,45 @@ export class WebfloServer extends WebfloRuntime {
648
642
  // Request processing
649
643
  scopeObj.autoHeaders = HEADERS.entries.filter((entry) => (new URLPattern(entry.url, url.origin)).exec(url.href)) || [];
650
644
  scopeObj.request = this.createRequest(scopeObj.url.href, scopeObj.init, scopeObj.autoHeaders.filter((header) => header.type === 'request'));
651
- scopeObj.cookies = this.createHttpCookies({
652
- request: scopeObj.request
653
- });
654
645
  scopeObj.clientID = this.identifyIncoming(scopeObj.request, true);
655
646
  scopeObj.client = this.#clients.getClient(scopeObj.clientID, true);
656
647
  scopeObj.clientPortID = crypto.randomUUID();
657
648
  scopeObj.clientRequestRealtime = scopeObj.client.createRequestRealtime(scopeObj.clientPortID, scopeObj.request.url);
658
649
  scopeObj.sessionTTL = this.env('SESSION_TTL') || 2592000/*30days*/;
650
+ scopeObj.thread = this.createHttpThread({
651
+ store: this.createStorage(`${scopeObj.url.host}/thread:${scopeObj.clientID}`, scopeObj.sessionTTL),
652
+ threadID: scopeObj.url.searchParams.get('_thread'),
653
+ realm: 3
654
+ });
655
+ scopeObj.cookies = this.createHttpCookies({
656
+ request: scopeObj.request,
657
+ thread: scopeObj.thread,
658
+ realm: 3
659
+ });
659
660
  scopeObj.session = this.createHttpSession({
660
661
  store: this.createStorage(`${scopeObj.url.host}/session:${scopeObj.clientID}`, scopeObj.sessionTTL),
661
662
  request: scopeObj.request,
663
+ thread: scopeObj.thread,
662
664
  sessionID: scopeObj.clientID,
663
- ttl: scopeObj.sessionTTL
665
+ ttl: scopeObj.sessionTTL,
666
+ realm: 3
664
667
  });
665
668
  scopeObj.user = this.createHttpUser({
666
669
  store: this.createStorage(`${scopeObj.url.host}/user:${scopeObj.clientID}`, scopeObj.sessionTTL),
667
670
  request: scopeObj.request,
671
+ thread: scopeObj.thread,
668
672
  client: scopeObj.clientRequestRealtime,
669
- session: scopeObj.session,
673
+ realm: 3
670
674
  });
671
675
  scopeObj.httpEvent = this.createHttpEvent({
672
676
  request: scopeObj.request,
677
+ thread: scopeObj.thread,
673
678
  client: scopeObj.clientRequestRealtime,
674
679
  cookies: scopeObj.cookies,
675
680
  session: scopeObj.session,
676
681
  user: scopeObj.user,
677
682
  detail: scopeObj.detail,
683
+ realm: 3
678
684
  });
679
685
  // Dispatch for response
680
686
  scopeObj.response = await this.dispatchNavigationEvent({
@@ -4,22 +4,13 @@ import { response as responseShim } from '../webflo-fetch/index.js';
4
4
  import { WQBroadcastChannel } from '../webflo-messaging/WQBroadcastChannel.js';
5
5
  import { WorkerSideWorkport } from './WorkerSideWorkport.js';
6
6
  import { WorkerSideCookies } from './WorkerSideCookies.js';
7
- import { HttpSession } from '../webflo-routing/HttpSession.js';
8
- import { HttpEvent } from '../webflo-routing/HttpEvent.js';
9
- import { HttpUser } from '../webflo-routing/HttpUser.js';
10
7
  import '../webflo-fetch/index.js';
11
8
  import '../webflo-url/index.js';
12
9
 
13
10
  export class WebfloWorker extends WebfloRuntime {
14
11
 
15
- static get HttpEvent() { return HttpEvent; }
16
-
17
12
  static get HttpCookies() { return WorkerSideCookies; }
18
13
 
19
- static get HttpSession() { return HttpSession; }
20
-
21
- static get HttpUser() { return HttpUser; }
22
-
23
14
  static get Workport() { return WorkerSideWorkport; }
24
15
 
25
16
  async initialize() {
@@ -106,28 +97,40 @@ export class WebfloWorker extends WebfloRuntime {
106
97
  }
107
98
  // Create and route request
108
99
  scopeObj.request = this.createRequest(scopeObj.url, scopeObj.init);
100
+ scopeObj.thread = this.createHttpThread({
101
+ store: this.createStorage('thread'),
102
+ threadId: scopeObj.url.searchParams.get('_thread'),
103
+ realm: 2
104
+ });
109
105
  scopeObj.cookies = this.createHttpCookies({
110
- request: scopeObj.request
106
+ request: scopeObj.request,
107
+ thread: scopeObj.thread,
108
+ realm: 2
111
109
  });
112
110
  scopeObj.session = this.createHttpSession({
113
111
  store: this.createStorage('session'),
114
- request: scopeObj.request
112
+ request: scopeObj.request,
113
+ thread: scopeObj.thread,
114
+ realm: 2
115
115
  });
116
116
  const requestID = crypto.randomUUID();
117
117
  scopeObj.clientRequestRealtime = new WQBroadcastChannel(requestID);
118
118
  scopeObj.user = this.createHttpUser({
119
119
  store: this.createStorage('user'),
120
120
  request: scopeObj.request,
121
+ thread: scopeObj.thread,
121
122
  client: scopeObj.clientRequestRealtime,
122
- session: scopeObj.session,
123
+ realm: 2
123
124
  });
124
125
  scopeObj.httpEvent = this.createHttpEvent({
125
126
  request: scopeObj.request,
127
+ thread: scopeObj.thread,
126
128
  client: scopeObj.clientRequestRealtime,
127
129
  cookies: scopeObj.cookies,
128
130
  session: scopeObj.session,
129
131
  user: scopeObj.user,
130
132
  detail: scopeObj.detail,
133
+ realm: 2
131
134
  });
132
135
  // Dispatch for response
133
136
  scopeObj.response = await this.dispatchNavigationEvent({
@@ -2,9 +2,10 @@ import { HttpCookies } from '../webflo-routing/HttpCookies.js';
2
2
  import { headers as headersShim } from '../webflo-fetch/index.js';
3
3
 
4
4
  export class WorkerSideCookies extends HttpCookies {
5
- static create({ request }) {
5
+ static create({ request, thread }) {
6
6
  return new this({
7
7
  request,
8
+ thread,
8
9
  entries: headersShim.get.value.call(request.headers, 'Cookie', true).map((c) => [c.name, c])
9
10
  });
10
11
  }