@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 +11 -23
- package/package.json +5 -5
- package/site/overview.md +9 -11
- package/src/runtime-pi/WebfloRuntime.js +43 -55
- package/src/runtime-pi/webflo-client/ClientSideCookies.js +2 -1
- package/src/runtime-pi/webflo-client/WebfloClient.js +36 -25
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +1 -1
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +16 -16
- package/src/runtime-pi/webflo-fetch/index.js +1 -34
- package/src/runtime-pi/webflo-routing/HttpCookies.js +3 -3
- package/src/runtime-pi/webflo-routing/HttpEvent.js +49 -5
- package/src/runtime-pi/webflo-routing/HttpSession.js +2 -2
- package/src/runtime-pi/webflo-routing/HttpState.js +24 -19
- package/src/runtime-pi/webflo-routing/HttpThread.js +81 -0
- package/src/runtime-pi/webflo-routing/HttpUser.js +4 -4
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +13 -16
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +2 -1
- package/src/runtime-pi/webflo-server/ServerSideSession.js +4 -3
- package/src/runtime-pi/webflo-server/WebfloServer.js +17 -11
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +15 -12
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +2 -1
package/README.md
CHANGED
|
@@ -1,26 +1,16 @@
|
|
|
1
|
-
|
|
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
|
|
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** |
|
|
42
|
-
| **API backends** | REST endpoints
|
|
43
|
-
| **Static sites** | Static-first or fully pre-rendered sites
|
|
44
|
-
| **Mobile experiences** | Installable, offline-capable PWAs with background sync,
|
|
45
|
-
| **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications
|
|
46
|
-
| **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s
|
|
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
|
|
55
|
-
| 🌍 **
|
|
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,
|
|
47
|
+
| ⚡ **Realtime capabilities** | Live responses, mutable/differential responses, two-way background messaging — all 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.
|
|
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.
|
|
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** |
|
|
72
|
-
| **API backends** | REST endpoints
|
|
73
|
-
| **Static sites** | Static-first or fully pre-rendered sites
|
|
74
|
-
| **Mobile experiences** | Installable, offline-capable PWAs with background sync,
|
|
75
|
-
| **Realtime & multiplayer** | Chats, presence, dashboards, live docs, notifications
|
|
76
|
-
| **AI & agents** | Multi-step AI workflows, background agents, and automation powered by Webflo’s
|
|
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
|
|
83
|
-
| 🌍 **
|
|
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,
|
|
85
|
+
| ⚡ **Realtime capabilities** | Live responses, mutable/differential responses, two-way background messaging — all 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
|
-
|
|
76
|
-
return this.constructor.
|
|
87
|
+
createHttpThread({ store, threadID, ...rest }) {
|
|
88
|
+
return this.constructor.HttpThread.create({ store, threadID, ...rest });
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
|
|
80
|
-
return this.constructor.
|
|
91
|
+
createHttpCookies({ request, thread, ...rest }) {
|
|
92
|
+
return this.constructor.HttpCookies.create({ request, thread, ...rest });
|
|
81
93
|
}
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
return this.constructor.
|
|
95
|
+
createHttpSession({ store, request, thread, ...rest }) {
|
|
96
|
+
return this.constructor.HttpSession.create({ store, request, thread, ...rest });
|
|
85
97
|
}
|
|
86
98
|
|
|
87
|
-
|
|
88
|
-
return this.constructor.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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,
|
|
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
|
|
26
|
-
|| data?.[Symbol.toStringTag] === '
|
|
27
|
-
return '
|
|
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) === '
|
|
43
|
-
return this.
|
|
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
|
|
141
|
-
if (!this.test(
|
|
142
|
-
throw new Error('Argument must be a UseLive
|
|
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(
|
|
145
|
+
await instance.replaceWith(liveProgramHandle.value, { done: false, ...options });
|
|
146
146
|
if (instance.#generatorType === 'Default') {
|
|
147
|
-
instance.#generator =
|
|
148
|
-
instance.#generatorType = '
|
|
147
|
+
instance.#generator = liveProgramHandle;
|
|
148
|
+
instance.#generatorType = 'LiveProgramHandle';
|
|
149
149
|
}
|
|
150
150
|
Observer.observe(
|
|
151
|
-
|
|
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
|
-
|
|
450
|
-
const state = new
|
|
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
|
|
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,
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
8
|
+
#thread;
|
|
9
9
|
#modified = false;
|
|
10
10
|
|
|
11
|
-
constructor({ store, request,
|
|
11
|
+
constructor({ store, request, thread }) {
|
|
12
12
|
this.#store = store || new Map;
|
|
13
13
|
this.#request = request;
|
|
14
|
-
this.#
|
|
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
|
-
[
|
|
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 (
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (handler.
|
|
136
|
-
|
|
137
|
-
|
|
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, {
|
|
144
|
-
|
|
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,
|
|
6
|
-
return new this({ store, request,
|
|
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,
|
|
11
|
+
constructor({ store, request, thread, client }) {
|
|
12
12
|
super({
|
|
13
13
|
store,
|
|
14
14
|
request,
|
|
15
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) === '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|