toiljs 0.0.70 → 0.0.72
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/CHANGELOG.md +10 -0
- package/examples/basic/client/routes/features/index.tsx +16 -2
- package/examples/basic/client/routes/features/realtime.tsx +23 -11
- package/examples/basic/client/routes/features/stream.tsx +81 -0
- package/examples/basic/server/main.stream.ts +20 -0
- package/examples/basic/server/streams/Echo.ts +44 -0
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -79,8 +79,22 @@ const groups: { heading: string; items: { href: Toil.Href; label: string; note:
|
|
|
79
79
|
{
|
|
80
80
|
heading: 'Components and runtime',
|
|
81
81
|
items: [
|
|
82
|
-
{ href: '/features/script', label: 'Script', note: 'Toil.Script with a load strategy' }
|
|
83
|
-
|
|
82
|
+
{ href: '/features/script', label: 'Script', note: 'Toil.Script with a load strategy' }
|
|
83
|
+
]
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
heading: 'Realtime and streams',
|
|
87
|
+
items: [
|
|
88
|
+
{
|
|
89
|
+
href: '/features/realtime',
|
|
90
|
+
label: 'Realtime socket',
|
|
91
|
+
note: 'Toil.useChannel -> a resident @stream box at /echo'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
href: '/features/stream',
|
|
95
|
+
label: 'Typed @stream',
|
|
96
|
+
note: 'Server.Stream.Echo.connect(), a resident per-connection box'
|
|
97
|
+
}
|
|
84
98
|
]
|
|
85
99
|
}
|
|
86
100
|
];
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
export const metadata: Toil.Metadata = {
|
|
2
|
-
title: 'Realtime',
|
|
3
|
-
description: 'A
|
|
2
|
+
title: 'Realtime socket',
|
|
3
|
+
description: 'A raw WebSocket channel to a resident @stream box: send a frame, watch the box reply.'
|
|
4
4
|
};
|
|
5
5
|
|
|
6
|
-
// Toil.useChannel opens a WebSocket to
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// Toil.useChannel opens a raw WebSocket to a @stream route (here /echo, served by the Echo @stream box in
|
|
7
|
+
// server/streams/Echo.ts). It tracks `connected`, collects every reply in `messages`, and exposes `send`;
|
|
8
|
+
// it reconnects automatically. The dev server bridges the socket to the resident box (each frame ->
|
|
9
|
+
// @message -> reply), and the Toil edge does the same over a real WebTransport session.
|
|
9
10
|
export default function RealtimeDemo() {
|
|
10
|
-
const chat = Toil.useChannel({ path: '/
|
|
11
|
+
const chat = Toil.useChannel({ path: '/echo' });
|
|
12
|
+
const text = (m: string | ArrayBuffer): string => (typeof m === 'string' ? m : new TextDecoder().decode(m));
|
|
11
13
|
return (
|
|
12
14
|
<main>
|
|
13
|
-
<h1>Realtime</h1>
|
|
15
|
+
<h1>Realtime socket</h1>
|
|
14
16
|
<p>
|
|
15
|
-
Connection: <strong>{chat.connected ? 'connected' : 'disconnected'}</strong>,
|
|
17
|
+
Connection: <strong>{chat.connected ? 'connected' : 'disconnected'}</strong>, replies:{' '}
|
|
16
18
|
<strong>{chat.messages.length}</strong>.
|
|
17
19
|
</p>
|
|
18
20
|
<p>
|
|
@@ -20,11 +22,21 @@ export default function RealtimeDemo() {
|
|
|
20
22
|
Send ping
|
|
21
23
|
</button>
|
|
22
24
|
</p>
|
|
25
|
+
{chat.messages.length > 0 && (
|
|
26
|
+
<ul>
|
|
27
|
+
{chat.messages.map((m, i) => (
|
|
28
|
+
<li key={i}>
|
|
29
|
+
<code>{text(m)}</code>
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ul>
|
|
33
|
+
)}
|
|
23
34
|
<p style={{ opacity: 0.6 }}>
|
|
24
35
|
<code>
|
|
25
|
-
|
|
26
|
-
</code>
|
|
27
|
-
|
|
36
|
+
Toil.useChannel({'{'} path: '/echo' {'}'})
|
|
37
|
+
</code>{' '}
|
|
38
|
+
- the resident box replies <code>pong #N</code>; the advancing counter proves its state survives every
|
|
39
|
+
frame.
|
|
28
40
|
</p>
|
|
29
41
|
<p>
|
|
30
42
|
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { StreamChannel } from 'toiljs/client';
|
|
4
|
+
|
|
5
|
+
export const metadata: Toil.Metadata = {
|
|
6
|
+
title: 'Typed @stream',
|
|
7
|
+
description: 'The generated Server.Stream.Echo client: a resident per-connection @stream box.'
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
// The typed @stream client (Server.Stream.Echo, generated into shared/server.ts from the @stream catalog)
|
|
11
|
+
// opens a resident box. In `npm run dev` it speaks the same-origin dev WebSocket; on the Toil edge it is a
|
|
12
|
+
// real WebTransport session. The box (server/streams/Echo.ts) replies `pong #N` - the counter proves the
|
|
13
|
+
// SAME box handled every message (residency).
|
|
14
|
+
export default function StreamDemo() {
|
|
15
|
+
const [log, setLog] = useState<string[]>([]);
|
|
16
|
+
const [msg, setMsg] = useState('hello from Server.Stream');
|
|
17
|
+
const [channel, setChannel] = useState<StreamChannel | null>(null);
|
|
18
|
+
const add = (line: string): void => setLog((l) => [...l, line]);
|
|
19
|
+
|
|
20
|
+
async function connect(): Promise<void> {
|
|
21
|
+
// shared/server.ts (generated by the server build) attaches globalThis.Server. Import it lazily,
|
|
22
|
+
// browser-only, so SSR never touches its globalThis.location access.
|
|
23
|
+
await import('../../../shared/server');
|
|
24
|
+
add('connecting -> /echo');
|
|
25
|
+
try {
|
|
26
|
+
const c = await Server.Stream.Echo.connect();
|
|
27
|
+
add('session READY');
|
|
28
|
+
c.onMessage((bytes) => add('<- ' + new TextDecoder().decode(bytes)));
|
|
29
|
+
c.onClose((code) => add('closed (0x' + code.toString(16) + ')'));
|
|
30
|
+
setChannel(c);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
add('connect failed: ' + String(e));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function send(): void {
|
|
37
|
+
if (channel === null) return;
|
|
38
|
+
channel.send(new TextEncoder().encode(msg));
|
|
39
|
+
add('-> ' + msg);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<main>
|
|
44
|
+
<h1>Typed @stream</h1>
|
|
45
|
+
<p>
|
|
46
|
+
<code>Server.Stream.Echo.connect()</code> opens a resident <code>@stream</code> box. In{' '}
|
|
47
|
+
<code>npm run dev</code> it is the same-origin dev WebSocket; on the edge it is a real WebTransport
|
|
48
|
+
session. The box replies <code>pong #N</code> - the counter proves its state survives every message.
|
|
49
|
+
</p>
|
|
50
|
+
<p>
|
|
51
|
+
<input
|
|
52
|
+
value={msg}
|
|
53
|
+
onChange={(e) => setMsg(e.target.value)}
|
|
54
|
+
style={{ font: 'inherit', padding: '4px 8px', minWidth: 260 }}
|
|
55
|
+
/>
|
|
56
|
+
</p>
|
|
57
|
+
<p>
|
|
58
|
+
<button type="button" onClick={() => void connect()} disabled={channel !== null}>
|
|
59
|
+
Connect
|
|
60
|
+
</button>{' '}
|
|
61
|
+
<button type="button" onClick={send} disabled={channel === null}>
|
|
62
|
+
Send
|
|
63
|
+
</button>
|
|
64
|
+
</p>
|
|
65
|
+
<pre
|
|
66
|
+
style={{
|
|
67
|
+
background: '#111',
|
|
68
|
+
color: '#ddd',
|
|
69
|
+
padding: 10,
|
|
70
|
+
borderRadius: 6,
|
|
71
|
+
minHeight: 90,
|
|
72
|
+
whiteSpace: 'pre-wrap'
|
|
73
|
+
}}>
|
|
74
|
+
{log.join('\n')}
|
|
75
|
+
</pre>
|
|
76
|
+
<p>
|
|
77
|
+
<Toil.Link href="/features">Back to features</Toil.Link>
|
|
78
|
+
</p>
|
|
79
|
+
</main>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The L2/L3 STREAM surface entry.
|
|
2
|
+
//
|
|
3
|
+
// This is a SECOND entry point of the project, distinct from `main.ts` (the L1 request surface). It
|
|
4
|
+
// compiles into its OWN artifact - `build/server/release-stream.wasm` - which the Toil edge loads as a
|
|
5
|
+
// RESIDENT per-connection box on the L2/L3 stream tier, and which the dev server serves over a WebSocket.
|
|
6
|
+
//
|
|
7
|
+
// Importing the `@stream` modules here pulls their compiler-generated `stream_dispatch` export (the
|
|
8
|
+
// connect/message/close/disconnect lifecycle entry) into this artifact. Add a stream as you grow:
|
|
9
|
+
// import './streams/Echo';
|
|
10
|
+
|
|
11
|
+
import { revertOnError } from 'toiljs/server/runtime/abort/abort';
|
|
12
|
+
|
|
13
|
+
import './streams/Echo';
|
|
14
|
+
|
|
15
|
+
// Required: re-export the WASM entry points (the linear memory + the runtime hooks the host binds) and
|
|
16
|
+
// the abort hook, exactly like main.ts.
|
|
17
|
+
export * from 'toiljs/server/runtime/exports';
|
|
18
|
+
export function abort(message: string, fileName: string, line: u32, column: u32): void {
|
|
19
|
+
revertOnError(message, fileName, line, column);
|
|
20
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `@stream` protocol handler mounted at `/echo` - the SERVER side of the Realtime feature.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a `@rest` route (a fresh handler per request), a `@stream` runs as a RESIDENT wasm box per
|
|
5
|
+
* connection: it is created on `@connect`, lives for the whole connection, and is torn down on `@close`.
|
|
6
|
+
* On the Toil edge that box is distributed across the L2/L3 stream nodes and pinned to ONE worker for the
|
|
7
|
+
* connection's life via QUIC connection-id steering, so its in-box state survives every event.
|
|
8
|
+
*
|
|
9
|
+
* The `count` field below is the proof: it persists across every `@message` because the box is never
|
|
10
|
+
* reset between events (a `@rest` handler's fields reset each request). Each reply carries the running
|
|
11
|
+
* count, so the client can watch the SAME resident box advance.
|
|
12
|
+
*
|
|
13
|
+
* Two browser clients drive this exact box (both land here in dev over a WebSocket, and on the edge over
|
|
14
|
+
* WebTransport):
|
|
15
|
+
* - the raw socket: Toil.useChannel({ path: '/echo' }) (client/routes/features/realtime.tsx)
|
|
16
|
+
* - the typed stream: await Server.Stream.Echo.connect() (client/routes/features/stream.tsx)
|
|
17
|
+
*/
|
|
18
|
+
@stream('echo')
|
|
19
|
+
class Echo {
|
|
20
|
+
// Resident per-connection state: survives across events (the box is never reset between them).
|
|
21
|
+
private count: i32 = 0;
|
|
22
|
+
|
|
23
|
+
@connect
|
|
24
|
+
onConnect(): void {
|
|
25
|
+
// A fresh connection: its dedicated box starts the counter at 0.
|
|
26
|
+
this.count = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
@message
|
|
30
|
+
onMessage(packet: StreamPacket): StreamOutbound {
|
|
31
|
+
// Count every inbound frame - and PERSIST across them, because this is the same resident box for
|
|
32
|
+
// the whole connection - then reply with the running count so the client sees it advance.
|
|
33
|
+
this.count = this.count + 1;
|
|
34
|
+
// The count proves residency (the same box handled every frame); the inbound length proves the
|
|
35
|
+
// frame arrived. We avoid decoding the inbound here to keep the handler bytes-safe.
|
|
36
|
+
const reply = 'pong #' + this.count.toString() + ' (' + packet.bytes().length.toString() + ' bytes in)';
|
|
37
|
+
return StreamOutbound.reply(Uint8Array.wrap(String.UTF8.encode(reply)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@close
|
|
41
|
+
onClose(): void {
|
|
42
|
+
// Graceful close: the per-connection box is torn down after this hook.
|
|
43
|
+
}
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toiljs",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.72",
|
|
5
5
|
"author": "Dacely",
|
|
6
6
|
"description": "The modern React framework: a file-based React frontend and a ToilScript-compiled WebAssembly backend.",
|
|
7
7
|
"repository": {
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"nodemailer": "^9.0.1",
|
|
135
135
|
"picocolors": "^1.1.1",
|
|
136
136
|
"sharp": "^0.35.2",
|
|
137
|
-
"toilscript": "^0.1.
|
|
137
|
+
"toilscript": "^0.1.47",
|
|
138
138
|
"typescript-eslint": "^8.62.0",
|
|
139
139
|
"vite": "^8.1.0",
|
|
140
140
|
"vite-imagetools": "^10.0.1",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"prettier": ">=3.0.0",
|
|
146
146
|
"react": ">=18.0.0",
|
|
147
147
|
"react-dom": ">=18.0.0",
|
|
148
|
-
"toilscript": ">=0.1.
|
|
148
|
+
"toilscript": ">=0.1.47",
|
|
149
149
|
"typescript": ">=6.0.0"
|
|
150
150
|
},
|
|
151
151
|
"overrides": {
|