@webqit/webflo 0.11.61 → 0.20.2-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/.github/FUNDING.yml +12 -0
- package/.github/workflows/publish.yml +48 -0
- package/.gitignore +2 -0
- package/LICENSE +2 -2
- package/README.md +71 -2050
- package/package.json +28 -13
- package/site/-/_.md +139 -0
- package/site/-/docs.old.md +2010 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url 2.js +93 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js +93 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js 2.map +7 -0
- package/site/.vitepress/cache/deps/@braintree_sanitize-url.js.map +7 -0
- package/site/.vitepress/cache/deps/_metadata 2.json +85 -0
- package/site/.vitepress/cache/deps/_metadata.json +85 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4 2.js +9 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js +9 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-BUSYA2B4.js.map +7 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK 2.js +9719 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js +9719 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-Q2AYPHVK.js.map +7 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7 2.js +12705 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js +12705 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js 2.map +7 -0
- package/site/.vitepress/cache/deps/chunk-QAXAIFA7.js.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape 2.js +30278 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent 2.js +4710 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js +4710 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js 2.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape-cose-bilkent.js.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape.js +30278 -0
- package/site/.vitepress/cache/deps/cytoscape.js 2.map +7 -0
- package/site/.vitepress/cache/deps/cytoscape.js.map +7 -0
- package/site/.vitepress/cache/deps/dayjs 2.js +285 -0
- package/site/.vitepress/cache/deps/dayjs.js +285 -0
- package/site/.vitepress/cache/deps/dayjs.js 2.map +7 -0
- package/site/.vitepress/cache/deps/dayjs.js.map +7 -0
- package/site/.vitepress/cache/deps/debug 2.js +453 -0
- package/site/.vitepress/cache/deps/debug.js +453 -0
- package/site/.vitepress/cache/deps/debug.js 2.map +7 -0
- package/site/.vitepress/cache/deps/debug.js.map +7 -0
- package/site/.vitepress/cache/deps/package 2.json +3 -0
- package/site/.vitepress/cache/deps/package.json +3 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api 2.js +4507 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js +4507 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core 2.js +584 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js +584 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_core.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap 2.js +1166 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js +1166 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___@vueuse_integrations_useFocusTrap.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js 2.js +1667 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js +1667 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___mark__js_src_vanilla__js.js.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch 2.js +1815 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js +1815 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vitepress___minisearch.js.map +7 -0
- package/site/.vitepress/cache/deps/vue 2.js +344 -0
- package/site/.vitepress/cache/deps/vue.js +344 -0
- package/site/.vitepress/cache/deps/vue.js 2.map +7 -0
- package/site/.vitepress/cache/deps/vue.js.map +7 -0
- package/site/.vitepress/config.ts +147 -0
- package/site/.vitepress/theme/custom.css +50 -0
- package/site/.vitepress/theme/index.ts +6 -0
- package/site/api/webflo-fetch/FormData.md +0 -0
- package/site/api/webflo-fetch/Headers.md +0 -0
- package/site/api/webflo-fetch/LiveResponse.md +0 -0
- package/site/api/webflo-fetch/Request.md +0 -0
- package/site/api/webflo-fetch/Response.md +0 -0
- package/site/api/webflo-fetch/fetch.md +0 -0
- package/site/api/webflo-routing/HttpCookies.md +0 -0
- package/site/api/webflo-routing/HttpEvent/respondWith.md +1 -0
- package/site/api/webflo-routing/HttpEvent/waitUntil.md +1 -0
- package/site/api/webflo-routing/HttpEvent/waitUntilNavigate.md +1 -0
- package/site/api/webflo-routing/HttpEvent.md +30 -0
- package/site/api/webflo-routing/HttpSession.md +0 -0
- package/site/api/webflo-routing/HttpState.md +0 -0
- package/site/api/webflo-routing/HttpUser.md +0 -0
- package/site/api/webflo-routing/handler/fetch.md +42 -0
- package/site/api/webflo-routing/handler/next.md +54 -0
- package/site/api/webflo-routing/handler.md +119 -0
- package/site/api.md +26 -0
- package/site/contributing.md +16 -0
- package/site/docs/advanced/lifecycles.md +20 -0
- package/site/docs/advanced/redirects.md +0 -0
- package/site/docs/advanced/routing.md +1 -0
- package/site/docs/advanced.md +9 -0
- package/site/docs/concepts/realtime.md +637 -0
- package/site/docs/concepts/rendering.md +60 -0
- package/site/docs/concepts/request-response.md +47 -0
- package/site/docs/concepts/routing.md +656 -0
- package/site/docs/concepts/state.md +44 -0
- package/site/docs/concepts/templates.md +48 -0
- package/site/docs/concepts.md +97 -0
- package/site/docs/getting-started.md +378 -0
- package/site/docs/tech-stack.md +56 -0
- package/site/docs.md +100 -0
- package/site/examples/pwa.md +10 -0
- package/site/examples/web.md +11 -0
- package/site/examples.md +10 -0
- package/site/faq.md +13 -0
- package/site/guides/guide-auth.md +13 -0
- package/site/guides/guide-file-upload.md +11 -0
- package/site/guides/guide-service-worker.md +10 -0
- package/site/guides/tutorial-1-todo.md +24 -0
- package/site/guides.md +15 -0
- package/site/index.md +39 -0
- package/site/public/img/brand/logo-670x670.png +0 -0
- package/site/recipes/realtime.md +11 -0
- package/site/recipes/streaming.md +15 -0
- package/site/reference/cli.md +11 -0
- package/site/reference/config.md +13 -0
- package/site/reference/tools.md +9 -0
- package/src/Context.js +3 -11
- package/src/config-pi/deployment/Env.js +6 -19
- package/src/config-pi/deployment/Layout.js +11 -3
- package/src/config-pi/runtime/Client.js +40 -48
- package/src/config-pi/runtime/Server.js +52 -20
- package/src/config-pi/runtime/client/Worker.js +22 -20
- package/src/config-pi/static/Init.js +57 -0
- package/src/config-pi/static/index.js +2 -0
- package/src/deployment-pi/origins/index.js +1 -1
- package/src/deployment-pi/util.js +161 -0
- package/src/index.js +3 -9
- package/src/init-pi/index.js +117 -0
- package/src/init-pi/templates/pwa/app/handler.server.js +8 -0
- package/src/init-pi/templates/pwa/app/page.html +7 -0
- package/src/init-pi/templates/pwa/package.json +19 -0
- package/src/init-pi/templates/pwa/public/assets/app.css +16 -0
- package/src/init-pi/templates/pwa/public/index.html +39 -0
- package/src/init-pi/templates/pwa/public/manifest.json +29 -0
- package/src/init-pi/templates/web/app/handler.server.js +8 -0
- package/src/init-pi/templates/web/app/page.html +7 -0
- package/src/init-pi/templates/web/package.json +19 -0
- package/src/init-pi/templates/web/public/assets/app.css +16 -0
- package/src/init-pi/templates/web/public/index.html +39 -0
- package/src/runtime-pi/WebfloRuntime.js +350 -0
- package/src/runtime-pi/index.js +3 -10
- package/src/runtime-pi/webflo-client/ClientSideCookies.js +17 -0
- package/src/runtime-pi/webflo-client/ClientSideWorkport.js +63 -0
- package/src/runtime-pi/webflo-client/DeviceCapabilities.js +213 -0
- package/src/runtime-pi/webflo-client/WebfloClient.js +500 -0
- package/src/runtime-pi/webflo-client/WebfloRootClient1.js +206 -0
- package/src/runtime-pi/webflo-client/WebfloRootClient2.js +113 -0
- package/src/runtime-pi/webflo-client/WebfloSubClient.js +118 -0
- package/src/runtime-pi/webflo-client/index.js +17 -0
- package/src/runtime-pi/webflo-client/webflo-codegen.js +469 -0
- package/src/runtime-pi/webflo-client/webflo-devmode.js +243 -0
- package/src/runtime-pi/webflo-client/webflo-embedded.js +50 -0
- package/src/runtime-pi/webflo-fetch/LiveResponse.js +437 -0
- package/src/runtime-pi/webflo-fetch/cookies.js +10 -0
- package/src/runtime-pi/webflo-fetch/fetch.js +16 -0
- package/src/runtime-pi/webflo-fetch/formdata.js +54 -0
- package/src/runtime-pi/webflo-fetch/headers.js +151 -0
- package/src/runtime-pi/webflo-fetch/index.js +5 -0
- package/src/runtime-pi/webflo-fetch/message.js +49 -0
- package/src/runtime-pi/webflo-fetch/request.js +62 -0
- package/src/runtime-pi/webflo-fetch/response.js +110 -0
- package/src/runtime-pi/webflo-fetch/util.js +28 -0
- package/src/runtime-pi/webflo-messaging/WQBroadcastChannel.js +10 -0
- package/src/runtime-pi/webflo-messaging/WQMessageChannel.js +26 -0
- package/src/runtime-pi/webflo-messaging/WQMessageEvent.js +87 -0
- package/src/runtime-pi/webflo-messaging/WQMessagePort.js +38 -0
- package/src/runtime-pi/webflo-messaging/WQRelayPort.js +47 -0
- package/src/runtime-pi/webflo-messaging/WQSockPort.js +113 -0
- package/src/runtime-pi/webflo-messaging/WQStarPort.js +104 -0
- package/src/runtime-pi/webflo-messaging/wq-message-port.js +404 -0
- package/src/runtime-pi/webflo-routing/HttpCookies.js +42 -0
- package/src/runtime-pi/webflo-routing/HttpEvent.js +112 -0
- package/src/runtime-pi/webflo-routing/HttpSession.js +11 -0
- package/src/runtime-pi/webflo-routing/HttpState.js +153 -0
- package/src/runtime-pi/webflo-routing/HttpUser.js +54 -0
- package/src/runtime-pi/webflo-routing/WebfloRouter.js +245 -0
- package/src/runtime-pi/webflo-server/ServerSideCookies.js +19 -0
- package/src/runtime-pi/webflo-server/ServerSideSession.js +38 -0
- package/src/runtime-pi/webflo-server/WebfloServer.js +937 -0
- package/src/runtime-pi/webflo-server/index.js +11 -0
- package/src/runtime-pi/webflo-server/messaging/Client.js +27 -0
- package/src/runtime-pi/webflo-server/messaging/ClientRequestRealtime.js +50 -0
- package/src/runtime-pi/webflo-server/messaging/Clients.js +25 -0
- package/src/runtime-pi/webflo-server/webflo-devmode.js +326 -0
- package/src/runtime-pi/{client → webflo-url}/Url.js +27 -76
- package/src/runtime-pi/webflo-url/index.js +1 -0
- package/src/runtime-pi/webflo-url/urlpattern.js +38 -0
- package/src/runtime-pi/{util-url.js → webflo-url/util.js} +5 -43
- package/src/runtime-pi/webflo-url/xURL.js +94 -0
- package/src/runtime-pi/webflo-worker/WebfloWorker.js +234 -0
- package/src/runtime-pi/webflo-worker/WorkerSideCookies.js +19 -0
- package/src/runtime-pi/webflo-worker/WorkerSideWorkport.js +18 -0
- package/src/runtime-pi/webflo-worker/index.js +11 -0
- package/src/services-pi/index.js +2 -0
- package/src/services-pi/push/index.js +23 -0
- package/src/util.js +10 -0
- package/src/{webflo.js → webflo-cli.js} +4 -4
- package/src/runtime-pi/Application.js +0 -29
- package/src/runtime-pi/Cookies.js +0 -82
- package/src/runtime-pi/HttpEvent.js +0 -107
- package/src/runtime-pi/Router.js +0 -130
- package/src/runtime-pi/Runtime.js +0 -21
- package/src/runtime-pi/client/Application.js +0 -76
- package/src/runtime-pi/client/Context.js +0 -7
- package/src/runtime-pi/client/Router.js +0 -48
- package/src/runtime-pi/client/Runtime.js +0 -525
- package/src/runtime-pi/client/Workport.js +0 -190
- package/src/runtime-pi/client/createStorage.js +0 -58
- package/src/runtime-pi/client/generate.js +0 -481
- package/src/runtime-pi/client/index.js +0 -21
- package/src/runtime-pi/client/worker/Application.js +0 -44
- package/src/runtime-pi/client/worker/Context.js +0 -7
- package/src/runtime-pi/client/worker/Runtime.js +0 -275
- package/src/runtime-pi/client/worker/Workport.js +0 -78
- package/src/runtime-pi/client/worker/index.js +0 -21
- package/src/runtime-pi/server/Application.js +0 -101
- package/src/runtime-pi/server/Context.js +0 -16
- package/src/runtime-pi/server/Router.js +0 -159
- package/src/runtime-pi/server/Runtime.js +0 -558
- package/src/runtime-pi/server/index.js +0 -21
- package/src/runtime-pi/util-http.js +0 -86
- package/src/runtime-pi/xFormData.js +0 -24
- package/src/runtime-pi/xHeaders.js +0 -146
- package/src/runtime-pi/xRequest.js +0 -46
- package/src/runtime-pi/xRequestHeaders.js +0 -109
- package/src/runtime-pi/xResponse.js +0 -33
- package/src/runtime-pi/xResponseHeaders.js +0 -117
- package/src/runtime-pi/xURL.js +0 -105
- package/src/runtime-pi/xfetch.js +0 -23
- package/src/runtime-pi/xxHttpMessage.js +0 -102
- package/src/static-pi/index.js +0 -11
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# Webflo Realtime
|
|
2
|
+
|
|
3
|
+
Realtime experiences — collaborative documents, live dashboards, chat windows, streaming progress bars — are what make modern web apps feel _alive_.
|
|
4
|
+
They’re also the difference between form submissions that make you wait and form submissions that feel like a live conversation.
|
|
5
|
+
|
|
6
|
+
Webflo apps work this way by default.
|
|
7
|
+
|
|
8
|
+
They're powered by a rich set of APIs designed for every use case:
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
return new LiveResponse(data, { done: false });
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
event.respondWith({ progress: 10 }, { done: false });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```js
|
|
19
|
+
event.waitUntilNavigate();
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
event.client.addEventListener('message', handle);
|
|
24
|
+
event.client.postMessage({ message: 'You seem to be typing something.' });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const result = await event.user.confirm({ message: 'Should we proceed?' });
|
|
29
|
+
const result = await event.user.prompt({ message: 'What is your name?' });
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const { channel, leave } = event.client.enterChannel('test-channel');
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## The Traditional Setup
|
|
37
|
+
|
|
38
|
+
Historically, building realtime applications required a significant amount of setup.
|
|
39
|
+
It extended beyond the simple request–response model and into a world of parallel WebSocket connections and streaming protocols.
|
|
40
|
+
It presented two protocols, two lifecycles, and an awkward choreography between them — keeping them in sync, closing them both cleanly, and rebuilding them on every reconnect.
|
|
41
|
+
|
|
42
|
+
```mermaid
|
|
43
|
+
flowchart LR
|
|
44
|
+
subgraph Browser
|
|
45
|
+
B1[HTTP Client]
|
|
46
|
+
B2[WebSocket Client]
|
|
47
|
+
end
|
|
48
|
+
subgraph Server
|
|
49
|
+
S1[HTTP Handler]
|
|
50
|
+
S2[WebSocket Handler]
|
|
51
|
+
end
|
|
52
|
+
B1 <--> |HTTP request / response| S1
|
|
53
|
+
B2 <---> |Realtime messages| S2
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This works — but it’s hardly effortless.
|
|
57
|
+
That’s requests on one channel, updates on another.
|
|
58
|
+
|
|
59
|
+
## The Webflo Model
|
|
60
|
+
|
|
61
|
+
In Webflo, this behavior is built in.
|
|
62
|
+
The same request that starts over HTTP can seamlessly continue as a live, bidirectional exchange — with no moving parts or extra line of code at the application layer.
|
|
63
|
+
|
|
64
|
+
This is like the entire architecture above collapsed into one: an **auto-upgrading connection** that can be used for many different things.
|
|
65
|
+
|
|
66
|
+
```mermaid
|
|
67
|
+
flowchart LR
|
|
68
|
+
subgraph Browser
|
|
69
|
+
B1[Webflo Client]
|
|
70
|
+
end
|
|
71
|
+
subgraph Server
|
|
72
|
+
S1[Webflo Handler]
|
|
73
|
+
end
|
|
74
|
+
B1 <--> |Auto-upgrading
|
|
75
|
+
HTTP request / response| S1
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This is the same HTTP request / response lifecycle, only **with "live" mode built-in**.
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
In Webflo, you simply write route handlers that can talk for as long as they need to — without worrying about the transport layer or the lifecycle of the connection.
|
|
83
|
+
|
|
84
|
+
```js
|
|
85
|
+
export async function GET(event, next) {
|
|
86
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
87
|
+
|
|
88
|
+
event.respondWith({ step: 1 }, { done: false });
|
|
89
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
90
|
+
event.respondWith({ step: 2 });
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Here, the browser first renders `{ step: 1 }`, then `{ step: 2 }` half a second later — all within the same request.
|
|
95
|
+
|
|
96
|
+
When a handler continues sending responses or messages, Webflo transparently upgrades the underlying connection and handles the lifecycle behind the scenes.
|
|
97
|
+
|
|
98
|
+
### What Happens
|
|
99
|
+
|
|
100
|
+
1. The first response is sent normally over HTTP.
|
|
101
|
+
2. Webflo detects that the handler hasn’t returned yet.
|
|
102
|
+
3. It silently upgrades the connection, tagging the initial response with a header that tells the client to connect in the background.
|
|
103
|
+
4. The client renders the first data and immediately joins the background channel.
|
|
104
|
+
5. A two-way line now exists — and either side can send or receive messages.
|
|
105
|
+
6. The handler pushes its next response, this time, it's delivered via the upgraded connection.
|
|
106
|
+
7. The client renders it instantly — replacing the previous rendered state.
|
|
107
|
+
8. Webflo closes automatically when the handler finishes or the user navigates away.
|
|
108
|
+
|
|
109
|
+
::: info Further Details
|
|
110
|
+
|
|
111
|
+
- Full handshake details are covered in **[Appendix A – The Realtime Lifecycle](#appendix-a-–-the-realtime-lifecycle)**.
|
|
112
|
+
:::
|
|
113
|
+
|
|
114
|
+
## The Two Ends of the Line
|
|
115
|
+
|
|
116
|
+
A realtime conversation always has two sides — _Port A_ and _Port B_.
|
|
117
|
+
|
|
118
|
+
### _Port A_: Upstream Route Handlers
|
|
119
|
+
|
|
120
|
+
Realtime conversations are initiated by route handlers — as part of responses to HTTP requests.
|
|
121
|
+
These route handlers function as _Port A_.
|
|
122
|
+
|
|
123
|
+
The **primary** realtime API here is `event.client` — a Webflo `MessagePort` supporting things like: `.postMessage()`, `.addEventListener()`, etc.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
export default async function (event) {
|
|
127
|
+
event.client.postMessage({ hello: 'world' });
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
`event.client` is the API that other APIs like `event.respondWith()`, `event.user.prompt()`, etc. use under the hood.
|
|
132
|
+
|
|
133
|
+
### _Port B_: Downstream Clients
|
|
134
|
+
|
|
135
|
+
Realtime conversations are received downstream as part of responses to an initial HTTP request.
|
|
136
|
+
These HTTP clients or handlers are the _Port B_.
|
|
137
|
+
|
|
138
|
+
The primary realtime API here is `liveResponse.background` — the same Webflo `MessagePort` interface as `event.client`, supporting things like: `.postMessage()`, `.addEventListener()`, etc.
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
const response = await fetch('https://api.example.com/data');
|
|
142
|
+
const liveResponse = LiveResponse.from(response);
|
|
143
|
+
|
|
144
|
+
liveResponse.background.addEventListener('message', (message) => {
|
|
145
|
+
console.log(message);
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Typical Port B clients in Webflo are:
|
|
150
|
+
|
|
151
|
+
- The App UI
|
|
152
|
+
- Intermediate Route Handlers
|
|
153
|
+
|
|
154
|
+
::: info LiveResponse
|
|
155
|
+
`LiveResponse` is the "live" version of the standard `Response` API.
|
|
156
|
+
It extends the native `Response` object with a live body, headers, and status — all of which can be mutated in-place.
|
|
157
|
+
:::
|
|
158
|
+
|
|
159
|
+
#### The App UI
|
|
160
|
+
|
|
161
|
+
Every Webflo SPA has a client-side Webflo instance — `window.webqit.app`. It mounts via the _client_ script that Webflo injects into the page at build time.
|
|
162
|
+
|
|
163
|
+
```html
|
|
164
|
+
<script src="/app.js"></script>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This client manages in-app navigations, turning them into seamless request–response flows — becoming a typical _Port B_ in the realtime system. In each of those flows, the client occupies the top-most end of the line. It receives everything that handlers send as part of a response, applies live updates to the document, and manages any interactive exchanges.
|
|
168
|
+
|
|
169
|
+
The applicable realtime API here is `window.webqit.app.background` (`app.background`, for short) — an extension of Webflo `MessagePort` supporting things like: `.postMessage()`, `.addEventListener()`, etc.
|
|
170
|
+
|
|
171
|
+
The `app.background` port is also a _multi-port_ hub housing all _active_ background activities across page navigations.
|
|
172
|
+
Every interaction on this API is thus an interaction over all active realtime conversations.
|
|
173
|
+
|
|
174
|
+
```js
|
|
175
|
+
app.background.addEventListener('message', (message) => {
|
|
176
|
+
console.log(message);
|
|
177
|
+
});
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
::: info App-Level Background Activities
|
|
181
|
+
It is possible to listen to all background activities in the app via `app.background`. We'll use this API to drive a custom dialog UI just ahead.
|
|
182
|
+
:::
|
|
183
|
+
|
|
184
|
+
#### Intermediate Route Handlers
|
|
185
|
+
|
|
186
|
+
With Webflo's realtime capabilities being an **upgrade** to the existing HTTP request/response model,
|
|
187
|
+
realtime connections can be intercepted at _any point_ along that flow just as routing in Webflo can happen at various layers along that flow. A client-side handler or service-worker-side handler, for example, can intercept the conversation at their level.
|
|
188
|
+
|
|
189
|
+
The background connection comes along with the upstream response obtained via `next()` or `fetch()`.
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
export default async function (event, next) {
|
|
193
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
194
|
+
|
|
195
|
+
const response = await next();
|
|
196
|
+
const liveResponse = LiveResponse.from(response);
|
|
197
|
+
liveResponse.background.addEventListener('message', (message) => {
|
|
198
|
+
console.log(message);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
return liveResponse;
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
This intermediate handler essentially talks to the upstream via `liveResponse.background` (the _Port B_) while letting the same response flow downstream.
|
|
206
|
+
|
|
207
|
+
The handler’s own `event.client` port remains its own _Port A_ for communicating with the downstream.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
::: tip
|
|
212
|
+
While not explicitly mentioned, external Webflo servers are just as accessible and interactive as the local server. A server-to-server interaction, for example, is just a matter of `LiveResponse.from(await fetch('https://api.example.com/data'))`.
|
|
213
|
+
:::
|
|
214
|
+
|
|
215
|
+
## Entering Background Mode
|
|
216
|
+
|
|
217
|
+
A handler **enters background mode** when it responds to a request interactively as any of the below scenarios.
|
|
218
|
+
Webflo automatically upgrades the connection to a realtime connection — initiating the _handshake sequence_ with the client and keeping the communication open until _termination_ is triggered — as in [the Realtime Lifecycle](#appendix-a-–-the-realtime-lifecycle).
|
|
219
|
+
|
|
220
|
+
### _Handler sends a message at any point in its lifecycle_
|
|
221
|
+
|
|
222
|
+
A handler may send a message at any point in its lifecycle by either:
|
|
223
|
+
|
|
224
|
+
1. calling `event.client.postMessage()`:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
event.client.postMessage({ progress: 10 });
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
2. or achieving the same through higher-level APIs like `event.user.confirm()`:
|
|
231
|
+
|
|
232
|
+
```js
|
|
233
|
+
const result = await event.user.confirm({ message: 'Are you sure?' });
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### _Handler returns a response and explicitly marks it **not done**_
|
|
237
|
+
|
|
238
|
+
A handler may return a response and explicitly mark it **not done** by calling `event.respondWith()` (or returning `LiveResponse`) with `{ done: false }`:
|
|
239
|
+
|
|
240
|
+
```js
|
|
241
|
+
event.respondWith(data, { done: false });
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
return new LiveResponse(data, { done: false });
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### _Handler holds down the event lifecycle via promises_
|
|
249
|
+
|
|
250
|
+
A handler may hold down the event lifecycle via promises by either:
|
|
251
|
+
|
|
252
|
+
1. explicitly calling `event.waitUntil()` or `event.waitUntilNavigate()` before returning a response:
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
event.waitUntilNavigate();
|
|
256
|
+
event.respondWith(data);
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```js
|
|
260
|
+
event.waitUntil(new Promise(() => {}));
|
|
261
|
+
return data;
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
2. or calling `event.respondWith()` (or returning `LiveResponse`) with a promise that resolves to a response:
|
|
265
|
+
|
|
266
|
+
```js
|
|
267
|
+
event.respondWith(
|
|
268
|
+
new Promise((resolve) => {
|
|
269
|
+
setTimeout(() => {
|
|
270
|
+
resolve(data);
|
|
271
|
+
}, 1000);
|
|
272
|
+
})
|
|
273
|
+
);
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
3. or calling `event.respondWith()` (or returning `LiveResponse`) with an async callback as second argument:
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
event.respondWith(data, async ($data /* reactive copy of data */) => {
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
281
|
+
$data.someProp = 'someValue';
|
|
282
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
283
|
+
$data.someProp = 'someOtherValue';
|
|
284
|
+
});
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### _Handler returns a `Generator` object_
|
|
288
|
+
|
|
289
|
+
A handler may return a `Generator` object by either:
|
|
290
|
+
|
|
291
|
+
1. being itself a [`generator function declaration`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*):
|
|
292
|
+
|
|
293
|
+
```js
|
|
294
|
+
export default async function* (event) {
|
|
295
|
+
yield data1;
|
|
296
|
+
yield new Response(data2);
|
|
297
|
+
yield new LiveResponse(null, { headers: { Location: '/' } });
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
2. or returning a [`Generator` interface](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator) as value:
|
|
302
|
+
|
|
303
|
+
```js
|
|
304
|
+
export default async function (event) {
|
|
305
|
+
return {
|
|
306
|
+
next: async () => ({ done: true }),
|
|
307
|
+
return: async () => undefined,
|
|
308
|
+
throw: async () => undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### _Handler returns a `State` object_
|
|
314
|
+
|
|
315
|
+
A handler may return a `State` object by declaring a `live` function:
|
|
316
|
+
|
|
317
|
+
```js
|
|
318
|
+
export default async live function(event) {
|
|
319
|
+
const data = { progress: 0 };
|
|
320
|
+
|
|
321
|
+
setInterval(() => {
|
|
322
|
+
data.progress += 10;
|
|
323
|
+
}, 1000);
|
|
324
|
+
|
|
325
|
+
return data;
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
The `live` keyword is a shorthand for defining a quantum function that returns a `State` object.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
Webflo knows to switch the connection to background mode in all the above cases.
|
|
334
|
+
|
|
335
|
+
::: tip In Practice
|
|
336
|
+
|
|
337
|
+
- Entering or not entering background mode is often not something you need to be conscious of.
|
|
338
|
+
- A handler just happens to be interactive or not interactive depending on request types and how they lend themselves to be handled.
|
|
339
|
+
- **Webflo simply fulfills the intent of however a handler works**.
|
|
340
|
+
:::
|
|
341
|
+
|
|
342
|
+
## Real-world Examples
|
|
343
|
+
|
|
344
|
+
### Live Responses
|
|
345
|
+
|
|
346
|
+
#### Example — Replace state
|
|
347
|
+
|
|
348
|
+
This handler returns a `LiveResponse` whose body starts as `{ step: 1 }` and is replaced with `{ step: 2 }` after ~200ms; the UI updates automatically.
|
|
349
|
+
|
|
350
|
+
**_Result_:** The page first renders step: 1, then updates to step: 2 after ~200ms.
|
|
351
|
+
|
|
352
|
+
```js
|
|
353
|
+
import { LiveResponse } from '@webqit/webflo';
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
```js
|
|
357
|
+
export async function GET(event, next) {
|
|
358
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
359
|
+
|
|
360
|
+
const res = LiveResponse.from({ step: 1 }, { done: false });
|
|
361
|
+
setTimeout(() => res.replaceWith({ step: 2 }), 200); // [!code highlight]
|
|
362
|
+
|
|
363
|
+
return res;
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Example — Mutate state in place
|
|
368
|
+
|
|
369
|
+
This handler returns a skeleton object immediately for fast page load and then builds the object tree progressively as data arrives; the UI reflects every step of the object's construction.
|
|
370
|
+
|
|
371
|
+
**_Result_:** The UI renders a skeleton first, then progressively fills in as the object tree is built from the server.
|
|
372
|
+
|
|
373
|
+
```js
|
|
374
|
+
import { Observer } from '@webqit/observer';
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
export async function GET(event, next) {
|
|
379
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
380
|
+
|
|
381
|
+
const data = {
|
|
382
|
+
menu: [
|
|
383
|
+
{ name: 'Home', href: '/' },
|
|
384
|
+
{ name: 'About', href: '/about' },
|
|
385
|
+
{ name: 'Contact', href: '/contact' },
|
|
386
|
+
],
|
|
387
|
+
content: {
|
|
388
|
+
header: 'Loading...',
|
|
389
|
+
body: [],
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
event.waitUntil(
|
|
394
|
+
new Promise(async (resolve) => {
|
|
395
|
+
const someData = await fetch('https://api.example.com/data');
|
|
396
|
+
Observer.set(data.content, 'header', someData.header);
|
|
397
|
+
|
|
398
|
+
Observer.proxy(data.content.body).push(
|
|
399
|
+
{ text: 'Inventory 1:' + someData.items[0] },
|
|
400
|
+
{ text: 'Inventory 2:' + someData.items[1] }
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const someData2 = await fetch('https://api.example.com/data2');
|
|
404
|
+
Observer.proxy(data.content.body).push(
|
|
405
|
+
{ text: 'Inventory 3:' + someData2.items[0] },
|
|
406
|
+
{ text: 'Inventory 4:' + someData2.items[1] }
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
resolve();
|
|
410
|
+
})
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
return data;
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Example — Live database results (LinkedQL)
|
|
418
|
+
|
|
419
|
+
This handler responds with a live posts list from the database using [LinkedQL](https://github.com/linked-db/linked-ql); as upstream data changes, `rows` update continuously.
|
|
420
|
+
|
|
421
|
+
**_Result_:** On the UI, the list grows live as new posts are added upstream.
|
|
422
|
+
|
|
423
|
+
```js
|
|
424
|
+
export async function GET(event, next) {
|
|
425
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
426
|
+
|
|
427
|
+
const result = await client.query(
|
|
428
|
+
`SELECT * FROM posts WHERE date_created > '2023-01-01'`,
|
|
429
|
+
{ live: true, signal: event.signal }
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
event.waitUntilNavigate(); // Hold down the event lifecycle
|
|
433
|
+
|
|
434
|
+
return { posts: result.rows }; // [!code highlight]
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
#### Example — Redirect after work
|
|
439
|
+
|
|
440
|
+
This handler emits progress and then issues a redirect when the work completes.
|
|
441
|
+
|
|
442
|
+
**_Result_:** The page shows intermediate progress, then redirects to `/done` when work completes.
|
|
443
|
+
|
|
444
|
+
```js
|
|
445
|
+
export async function POST(event, next) {
|
|
446
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
447
|
+
|
|
448
|
+
// Initiate response
|
|
449
|
+
event.respondWith({ step: 1 }, { done: false });
|
|
450
|
+
// Make API call
|
|
451
|
+
await fetch('https://api.example.com/data');
|
|
452
|
+
// Update response
|
|
453
|
+
event.respondWith({ step: 2 }, { done: false });
|
|
454
|
+
|
|
455
|
+
// Redirect
|
|
456
|
+
event.respondWith(null, { status: 302, headers: { Location: '/done' } }); // [!code highlight]
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
### User Dialogs: Server ↔ User Interaction
|
|
461
|
+
|
|
462
|
+
#### Example — Ask for confirmation
|
|
463
|
+
|
|
464
|
+
This handler pauses request processing to interact with the user before proceeding, using realtime messaging.
|
|
465
|
+
|
|
466
|
+
**_Result_:** The user is prompted to confirm an action; on OK, the handler proceeds.
|
|
467
|
+
|
|
468
|
+
```js
|
|
469
|
+
export async function DELETE(event, next) {
|
|
470
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
471
|
+
|
|
472
|
+
const message = 'This item will be permanently deleted. Are you sure?';
|
|
473
|
+
const answer = await event.user.confirm({ message }); // [!code highlight]
|
|
474
|
+
if (answer?.data === true) {
|
|
475
|
+
// proceed
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return Response.json({ ok: true });
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
The Webflo UI client shows the dialog (native `confirm()/prompt()` or custom UI if configured) and returns the user's response.
|
|
483
|
+
|
|
484
|
+
Customization is as simple as intercepting these via `preventDefault()` to render your own modals.
|
|
485
|
+
|
|
486
|
+
```javascript
|
|
487
|
+
// Intercept at window.webqit.app.background
|
|
488
|
+
window.webqit.app.background.addEventListener('prompt', (e) => {
|
|
489
|
+
e.preventDefault(); // [!code highlight]
|
|
490
|
+
customDialog(e.data.message).then((result) => {
|
|
491
|
+
// Reply to the request on the provided port
|
|
492
|
+
e.ports[0].postMessage(result); // [!code highlight]
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### Message Channels
|
|
498
|
+
|
|
499
|
+
#### Example — Channel chat (server + client + UI)
|
|
500
|
+
|
|
501
|
+
These handlers establish a chat channel and broadcast messages to all participants.
|
|
502
|
+
|
|
503
|
+
**_Result_:** Messages appear in the chat trail every few seconds; incoming vs outgoing messages are styled differently.
|
|
504
|
+
|
|
505
|
+
```js
|
|
506
|
+
export async function GET(event, next) {
|
|
507
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
508
|
+
|
|
509
|
+
// Channel ID - must tally with the client-side ID
|
|
510
|
+
const channelID = 'test-channel';
|
|
511
|
+
|
|
512
|
+
const { channel, leave } = event.client.enterChannel(channelID, {
|
|
513
|
+
// Optionally annotate user's messages with user's name
|
|
514
|
+
resolveData: (message) => ({ ...message, user: 'sample name' }),
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
event.waitUntilNavigate(); // keep open
|
|
518
|
+
event.client.addEventListener('close', (e) => {
|
|
519
|
+
//e.preventDefault();
|
|
520
|
+
console.log('Chat closed');
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Then on the client:
|
|
526
|
+
|
|
527
|
+
```js
|
|
528
|
+
export default async function (event, next) {
|
|
529
|
+
if (next.stepname) return await next(); // The conventional delegation line
|
|
530
|
+
|
|
531
|
+
// Initialize states
|
|
532
|
+
const data = { messageTrail: [{ message: 'Beginning of chat...' }] };
|
|
533
|
+
await event.respondWith(data, { done: false }); // [!code highlight]
|
|
534
|
+
|
|
535
|
+
// The messaging part
|
|
536
|
+
const response = await next(); // Call server
|
|
537
|
+
const chatPort = LiveResponse.from(response).background;
|
|
538
|
+
|
|
539
|
+
// Channel ID - must tally with the server-side ID
|
|
540
|
+
const channelID = 'test-channel';
|
|
541
|
+
|
|
542
|
+
// Listen to upstream messages - from others in the same channel
|
|
543
|
+
chatPort.addEventListener(`${channelID}:message`, (e) => {
|
|
544
|
+
Observer.proxy(data.messageTrail).push(e.data); // [!code highlight]
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Send messages to the chat
|
|
548
|
+
setInterval(() => {
|
|
549
|
+
const message = { message: `Hello from ${navigator.userAgent}` };
|
|
550
|
+
|
|
551
|
+
// Send to others in the same channel
|
|
552
|
+
chatPort.postMessage(message, {
|
|
553
|
+
wqEventOptions: { type: `${channelID}:message` },
|
|
554
|
+
}); // [!code highlight]
|
|
555
|
+
|
|
556
|
+
// Add to the local trail
|
|
557
|
+
Observer.proxy(data.messageTrail).push(message); // [!code highlight]
|
|
558
|
+
}, 6000);
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
::: details Then on the UI
|
|
563
|
+
|
|
564
|
+
```html
|
|
565
|
+
<div>
|
|
566
|
+
Welcome to chat!
|
|
567
|
+
<ul render="#items: (m) of data?.messageTrail / 'message'">
|
|
568
|
+
<template def="message" scoped>
|
|
569
|
+
<li
|
|
570
|
+
class="chat-message"
|
|
571
|
+
render="#text: m.event || m.message; ~type: m.user && 'incoming' || 'outgoing';"
|
|
572
|
+
></li>
|
|
573
|
+
</template>
|
|
574
|
+
</ul>
|
|
575
|
+
<style scoped>
|
|
576
|
+
ul:has(.chat-message) {
|
|
577
|
+
list-style: none;
|
|
578
|
+
padding: 0;
|
|
579
|
+
}
|
|
580
|
+
.chat-message {
|
|
581
|
+
padding: 20px;
|
|
582
|
+
margin: 10px;
|
|
583
|
+
}
|
|
584
|
+
.chat-message[type="incoming"] {
|
|
585
|
+
margin-right: 200px;
|
|
586
|
+
background-color: blue;
|
|
587
|
+
}
|
|
588
|
+
.chat-message[type="outgoing"] {
|
|
589
|
+
margin-left: 200px;
|
|
590
|
+
text-align: right;
|
|
591
|
+
background-color: teal;
|
|
592
|
+
}
|
|
593
|
+
</style>
|
|
594
|
+
</div>
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
:::
|
|
598
|
+
|
|
599
|
+
::: warning
|
|
600
|
+
|
|
601
|
+
- Channel IDs must be unique and unguessable across the app.
|
|
602
|
+
- Channels are not persistent and are only active for the duration of the request. A database is required to store channel data.
|
|
603
|
+
:::
|
|
604
|
+
|
|
605
|
+
## Appendix A – The Realtime Lifecycle
|
|
606
|
+
|
|
607
|
+
```mermaid
|
|
608
|
+
sequenceDiagram
|
|
609
|
+
participant Browser
|
|
610
|
+
participant Handler
|
|
611
|
+
Browser->>Handler: HTTP request
|
|
612
|
+
Handler-->>Browser: Initial Response (or a Temporary 202 Accepted) <br>+ X-Background-Messaging-Port
|
|
613
|
+
Browser-->>Handler: Connect (WebSocket / Broadcast / MessageChannel)
|
|
614
|
+
Note over Browser,Handler: Background mode established
|
|
615
|
+
Handler-->>Browser: Messages, updates, dialogs, etc.
|
|
616
|
+
Browser-->>Handler: Replies, requests, etc.
|
|
617
|
+
Browser<<-->>Handler: Termination
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
### The Handshake
|
|
621
|
+
|
|
622
|
+
On entering background mode, Webflo initiates a handshake sequence as follows:
|
|
623
|
+
|
|
624
|
+
1. Webflo gives the initial response a unique `X-Background-Messaging-Port` header that tells the client to connect in the background after rendering the initial response.
|
|
625
|
+
> If the scenario is case `1.` above happening _before_ yielding a response, Webflo sends a temporary `202 Accepted` response to the client carrying this header.
|
|
626
|
+
2. The client reads that header and opens the background channel.
|
|
627
|
+
> If the client fails to connect within the handshake window, Webflo abandons the wait and concludes the request normally.
|
|
628
|
+
3. On connecting, both sides resume the same request context — now in live mode.
|
|
629
|
+
|
|
630
|
+
### Termination
|
|
631
|
+
|
|
632
|
+
Communication remains active until one of three events occurs and triggers a controlled closure:
|
|
633
|
+
|
|
634
|
+
1. The handler completes and no longer holds the event lifecycle.
|
|
635
|
+
2. The user navigates away, triggering a `navigate` event on the handler's `event.client` port.
|
|
636
|
+
> If the handler calls `event.preventDefault()` on the `navigate` event, Webflo knows to not close the connection. **You must explicitly call `event.client.close()` when finished.**
|
|
637
|
+
3. Either side explicitly calls `.close()` on its own port to end the conversation.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Rendering
|
|
2
|
+
|
|
3
|
+
Rendering in Webflo is about turning handler data into UI, both on the server (SSR) and in the browser (CSR).
|
|
4
|
+
|
|
5
|
+
## The Mental Model
|
|
6
|
+
|
|
7
|
+
- Your handler returns data (a plain object or Response).
|
|
8
|
+
- Webflo makes this data available as `document.bindings.data` in your HTML.
|
|
9
|
+
- Rendering can happen on the server, the client, or both—using the same mental model.
|
|
10
|
+
|
|
11
|
+
## Example: Returning Data
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
// app/handler.server.js
|
|
15
|
+
export default async function(event, next) {
|
|
16
|
+
if (next.stepname) return await next();
|
|
17
|
+
return { title: 'Home', greeting: 'Hello World!' };
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Accessing Data in HTML
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<!-- public/index.html -->
|
|
25
|
+
<!DOCTYPE html>
|
|
26
|
+
<html>
|
|
27
|
+
<head>
|
|
28
|
+
<title>My App</title>
|
|
29
|
+
<script>
|
|
30
|
+
setTimeout(() => {
|
|
31
|
+
document.title = document.bindings.data.title;
|
|
32
|
+
}, 0);
|
|
33
|
+
</script>
|
|
34
|
+
</head>
|
|
35
|
+
<body>
|
|
36
|
+
<h1 id="greeting"></h1>
|
|
37
|
+
<script>
|
|
38
|
+
setTimeout(() => {
|
|
39
|
+
document.getElementById('greeting').textContent = document.bindings.data.greeting;
|
|
40
|
+
}, 0);
|
|
41
|
+
</script>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## SSR & CSR
|
|
47
|
+
|
|
48
|
+
- **SSR:** Data is rendered into HTML on the server for fast first paint and SEO.
|
|
49
|
+
- **CSR:** Data is updated in the browser for dynamic, interactive UIs.
|
|
50
|
+
- **Hybrid:** You can use both together for best results.
|
|
51
|
+
|
|
52
|
+
## Reactivity & OOHTML
|
|
53
|
+
|
|
54
|
+
- Use [OOHTML](https://github.com/webqit/oohtml) for reactive UI logic and templates.
|
|
55
|
+
- OOHTML enables `<script quantum>` and `<template>` for declarative, reactive UIs.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
- [See Templates →](./templates.md)
|
|
60
|
+
- [See Lifecycle →](./lifecycle.md)
|