toiljs 0.0.68 → 0.0.70
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/build/cli/.tsbuildinfo +1 -1
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/rpc.js +10 -4
- package/build/client/stream/client.js +108 -5
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/index.d.ts +2 -0
- package/build/compiler/index.js +282 -2
- package/build/compiler/toil-docs.generated.js +3 -2
- package/build/compiler/vite.js +8 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/host.d.ts +1 -7
- package/build/devserver/daemon/host.js +5 -59
- package/build/devserver/daemon/index.d.ts +1 -0
- package/build/devserver/daemon/index.js +17 -4
- package/build/devserver/db/database.js +1 -1
- package/build/devserver/db/routeKinds.d.ts +6 -0
- package/build/devserver/db/routeKinds.js +40 -0
- package/build/devserver/index.d.ts +0 -1
- package/build/devserver/index.js +0 -1
- package/build/devserver/runtime/module.d.ts +1 -0
- package/build/devserver/runtime/module.js +18 -2
- package/build/devserver/stream/index.js +4 -3
- package/build/devserver/wasm/surface.d.ts +2 -0
- package/build/devserver/wasm/surface.js +35 -4
- package/docs/derive.md +159 -0
- package/docs/index.md +1 -1
- package/docs/streams.md +49 -18
- package/examples/basic/server/services/Stats.ts +11 -3
- package/examples/basic/server/services/remotes.ts +8 -2
- package/package.json +3 -2
- package/server/runtime/exports/index.ts +8 -1
- package/server/runtime/index.ts +1 -0
- package/server/runtime/rpc/Rpc.ts +66 -0
- package/src/client/rpc.ts +21 -12
- package/src/client/stream/client.ts +138 -8
- package/src/compiler/index.ts +352 -2
- package/src/compiler/toil-docs.generated.ts +3 -2
- package/src/compiler/vite.ts +16 -0
- package/src/devserver/daemon/host.ts +10 -110
- package/src/devserver/daemon/index.ts +19 -6
- package/src/devserver/db/database.ts +1 -1
- package/src/devserver/db/routeKinds.ts +44 -0
- package/src/devserver/index.ts +0 -1
- package/src/devserver/runtime/host.ts +3 -7
- package/src/devserver/runtime/module.ts +30 -4
- package/src/devserver/stream/index.ts +8 -4
- package/src/devserver/wasm/surface.ts +33 -4
- package/test/daemon-build.test.ts +53 -0
- package/test/daemon-catalog.test.ts +78 -3
- package/test/daemon-emulation.test.ts +27 -29
- package/test/devserver-database.test.ts +93 -0
- package/test/fixtures/bignum-wire/spec.ts +3 -5
- package/test/fixtures/daemon-app.ts +25 -21
- package/test/fixtures/stream-typed.ts +41 -0
- package/test/rpc-dispatch.test.ts +132 -0
- package/test/rpc-kinds.test.ts +18 -0
- package/test/rpc.test.ts +20 -4
- package/test/stream-emulation.test.ts +39 -0
- package/build/devserver/mstore/store.d.ts +0 -18
- package/build/devserver/mstore/store.js +0 -82
- package/src/devserver/mstore/store.ts +0 -121
package/src/client/rpc.ts
CHANGED
|
@@ -3,14 +3,12 @@
|
|
|
3
3
|
* the toilscript server build into the project's `shared/server.ts`
|
|
4
4
|
* (`declare global { const Server }`); this module is the runtime behind it.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* - `Server.REST.<controller>.<route>(args)`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* network dispatch is a TODO. Build the server (`npm run build:server`) to
|
|
13
|
-
* (re)generate the typed surface.
|
|
6
|
+
* Three surfaces live under `Server`, all attached by the generated `shared/server.ts` on import:
|
|
7
|
+
* - `Server.REST.<controller>.<route>(args)` - a fetch client (`globalThis.__toilRest`).
|
|
8
|
+
* - `Server.Stream.<class>.connect()` - the stream client (`globalThis.__toilStream`).
|
|
9
|
+
* - `Server.<service>.<method>(args)` and a free `Server.<remote>(args)` - the RPC client
|
|
10
|
+
* (`globalThis.__toilRpc`): each POSTs to `/__toil_rpc` with a method id and @data-encoded args.
|
|
11
|
+
* Build the server (`npm run build:server`) to (re)generate the typed surface + attach the clients.
|
|
14
12
|
*/
|
|
15
13
|
|
|
16
14
|
/** A recursive proxy that throws on call, used when the REST client hasn't loaded. */
|
|
@@ -18,7 +16,7 @@ function restMissingStub(path: string): unknown {
|
|
|
18
16
|
const call = (): never => {
|
|
19
17
|
throw new Error(
|
|
20
18
|
`toiljs REST: ${path}() is unavailable. The generated REST client has not loaded - ` +
|
|
21
|
-
`
|
|
19
|
+
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
22
20
|
);
|
|
23
21
|
};
|
|
24
22
|
return new Proxy(call, {
|
|
@@ -37,7 +35,7 @@ function streamMissingStub(path: string): unknown {
|
|
|
37
35
|
const call = (): never => {
|
|
38
36
|
throw new Error(
|
|
39
37
|
`toiljs Stream: ${path}() is unavailable. The generated stream client has not loaded - ` +
|
|
40
|
-
`
|
|
38
|
+
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
41
39
|
);
|
|
42
40
|
};
|
|
43
41
|
return new Proxy(call, {
|
|
@@ -55,8 +53,8 @@ function streamMissingStub(path: string): unknown {
|
|
|
55
53
|
function rpcStub(path: string): unknown {
|
|
56
54
|
const call = (): never => {
|
|
57
55
|
throw new Error(
|
|
58
|
-
`toiljs RPC: ${path}() is
|
|
59
|
-
`
|
|
56
|
+
`toiljs RPC: ${path}() is unavailable. The generated RPC client has not loaded - ` +
|
|
57
|
+
`run 'npm run build:server' to generate shared/server.ts; the client then attaches automatically.`,
|
|
60
58
|
);
|
|
61
59
|
};
|
|
62
60
|
return new Proxy(call, {
|
|
@@ -73,6 +71,12 @@ function rpcStub(path: string): unknown {
|
|
|
73
71
|
const stream = (globalThis as { __toilStream?: unknown }).__toilStream;
|
|
74
72
|
return stream !== undefined ? stream : streamMissingStub('Server.Stream');
|
|
75
73
|
}
|
|
74
|
+
// `Server.<service>` / `Server.<remote>` surface the generated RPC client. Its top-level
|
|
75
|
+
// keys are the @service names (-> a `{ method }` object) and free @remote functions.
|
|
76
|
+
if (path === 'Server') {
|
|
77
|
+
const rpc = (globalThis as { __toilRpc?: Record<string, unknown> }).__toilRpc;
|
|
78
|
+
if (rpc !== undefined && prop in rpc) return rpc[prop];
|
|
79
|
+
}
|
|
76
80
|
return rpcStub(`${path}.${prop}`);
|
|
77
81
|
},
|
|
78
82
|
apply() {
|
|
@@ -86,3 +90,8 @@ function rpcStub(path: string): unknown {
|
|
|
86
90
|
* come from the generated `shared/server.ts`. toiljs assigns this to `globalThis`.
|
|
87
91
|
*/
|
|
88
92
|
export const Server: unknown = rpcStub('Server');
|
|
93
|
+
|
|
94
|
+
// Back the generated `declare global { const Server }` (shared/server.ts) with a runtime value, so an
|
|
95
|
+
// app reaches `Server.REST` / `Server.Stream.<Name>` via the global without an import (the design note
|
|
96
|
+
// above). Idempotent - the proxy is a singleton; importing `toiljs/client` evaluates this once.
|
|
97
|
+
(globalThis as Record<string, unknown>).Server = Server;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Client runtime for the typed `@stream` channel (doc 08 section 8.2). `Server.Stream.<Class>.connect(path?)`
|
|
3
|
-
* opens a browser `
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* opens a browser `WebTransport` session to the class's `route` on the production edge (an `https://`
|
|
4
|
+
* stream-tier origin), or a `WebSocket` against the dev server (a `ws(s)://` origin), and returns a channel:
|
|
5
|
+
* `onMessage` / `send` / `onClose` / `close`. A raw `@stream` channel sends `Uint8Array`; a typed
|
|
6
|
+
* `@stream({ message: T })` channel sends the `@data` class and encodes it on send (the per-class
|
|
7
|
+
* encoder the generated module passes). The inbound reply is always raw bytes. The generated
|
|
8
|
+
* `shared/server.ts` (toilscript hot pass) attaches `makeStreamClient(routes, undefined, encoders)` to
|
|
9
|
+
* `globalThis.__toilStream`, and `Server.Stream`
|
|
7
10
|
* (`rpc.ts`) surfaces it - the same wiring the REST client uses via `globalThis.__toilRest`.
|
|
8
11
|
*/
|
|
9
12
|
|
|
@@ -42,10 +45,13 @@ function connectStream<TSend = Uint8Array>(
|
|
|
42
45
|
let opened = false;
|
|
43
46
|
let messageCb: ((data: Uint8Array) => void) | undefined;
|
|
44
47
|
let closeCb: ((code: number) => void) | undefined;
|
|
48
|
+
const pending: Uint8Array[] = [];
|
|
45
49
|
|
|
46
50
|
const channel: StreamChannel<TSend> = {
|
|
47
51
|
onMessage: (cb): void => {
|
|
48
52
|
messageCb = cb;
|
|
53
|
+
for (const m of pending) cb(m);
|
|
54
|
+
pending.length = 0;
|
|
49
55
|
},
|
|
50
56
|
onClose: (cb): void => {
|
|
51
57
|
closeCb = cb;
|
|
@@ -54,7 +60,11 @@ function connectStream<TSend = Uint8Array>(
|
|
|
54
60
|
if (ws.readyState !== WebSocket.OPEN) return;
|
|
55
61
|
// A typed channel encodes the @data message; a raw channel sends the bytes as-is.
|
|
56
62
|
const bytes = encode ? encode(data as never) : (data as unknown as Uint8Array);
|
|
57
|
-
|
|
63
|
+
try {
|
|
64
|
+
ws.send(bytes as BufferSource);
|
|
65
|
+
} catch {
|
|
66
|
+
/* socket transitioned OPEN -> CLOSING between the readyState check and send */
|
|
67
|
+
}
|
|
58
68
|
},
|
|
59
69
|
close: (): void => {
|
|
60
70
|
ws.close();
|
|
@@ -66,7 +76,12 @@ function connectStream<TSend = Uint8Array>(
|
|
|
66
76
|
resolve(channel);
|
|
67
77
|
});
|
|
68
78
|
ws.addEventListener('message', (event: MessageEvent) => {
|
|
69
|
-
if (event.data instanceof ArrayBuffer)
|
|
79
|
+
if (!(event.data instanceof ArrayBuffer)) return;
|
|
80
|
+
const bytes = new Uint8Array(event.data);
|
|
81
|
+
// Buffer a reply that arrives before onMessage() registers (mirror the WebTransport path),
|
|
82
|
+
// so an eager server reply is not silently dropped.
|
|
83
|
+
if (messageCb) messageCb(bytes);
|
|
84
|
+
else pending.push(bytes);
|
|
70
85
|
});
|
|
71
86
|
ws.addEventListener('close', (event: CloseEvent) => {
|
|
72
87
|
if (!opened) reject(new Error(`stream connect failed (closed ${String(event.code)})`));
|
|
@@ -78,12 +93,125 @@ function connectStream<TSend = Uint8Array>(
|
|
|
78
93
|
});
|
|
79
94
|
}
|
|
80
95
|
|
|
96
|
+
/** Open a browser `WebTransport` session to `url` (an `https://` stream-tier origin) and resolve a
|
|
97
|
+
* channel once the session is ready. The browser drives the QUIC handshake, H3 Extended-CONNECT, and
|
|
98
|
+
* the RFC 9297 Quarter-Stream-ID datagram framing, so `send`/`onMessage` deal in raw bytes (no manual
|
|
99
|
+
* prefix). Rejects if the session fails to open (wrong node / unreachable / cert); a server close AFTER
|
|
100
|
+
* open surfaces via `onClose(code)`. This is the PRODUCTION transport - the L2/L3 edge is
|
|
101
|
+
* WebTransport-only; the dev server uses the `connectStream` WebSocket above. */
|
|
102
|
+
function connectStreamWT<TSend = Uint8Array>(
|
|
103
|
+
url: string,
|
|
104
|
+
encode?: (msg: never) => Uint8Array,
|
|
105
|
+
): Promise<StreamChannel<TSend>> {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
if (!('WebTransport' in globalThis)) {
|
|
108
|
+
reject(new Error('WebTransport is not available in this browser'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
let transport: WebTransport;
|
|
112
|
+
try {
|
|
113
|
+
transport = new WebTransport(url);
|
|
114
|
+
} catch (e) {
|
|
115
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let messageCb: ((data: Uint8Array) => void) | undefined;
|
|
119
|
+
let closeCb: ((code: number) => void) | undefined;
|
|
120
|
+
let writer: WritableStreamDefaultWriter<Uint8Array> | undefined;
|
|
121
|
+
let opened = false;
|
|
122
|
+
const pending: Uint8Array[] = [];
|
|
123
|
+
|
|
124
|
+
const channel: StreamChannel<TSend> = {
|
|
125
|
+
onMessage: (cb): void => {
|
|
126
|
+
messageCb = cb;
|
|
127
|
+
for (const m of pending) cb(m);
|
|
128
|
+
pending.length = 0;
|
|
129
|
+
},
|
|
130
|
+
onClose: (cb): void => {
|
|
131
|
+
closeCb = cb;
|
|
132
|
+
},
|
|
133
|
+
send: (data): void => {
|
|
134
|
+
if (!writer) return;
|
|
135
|
+
const bytes = encode ? encode(data as never) : (data as unknown as Uint8Array);
|
|
136
|
+
// Fire-and-forget; a write that rejects (transport closing / backpressure) is surfaced
|
|
137
|
+
// via onClose, so swallow it here to avoid an unhandled promise rejection.
|
|
138
|
+
void writer.write(bytes).catch(() => {});
|
|
139
|
+
},
|
|
140
|
+
close: (): void => {
|
|
141
|
+
try {
|
|
142
|
+
transport.close();
|
|
143
|
+
} catch {
|
|
144
|
+
/* already closing */
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// A close AFTER open (guest reject, idle sweep, server shutdown) reports through onClose; a
|
|
150
|
+
// failure BEFORE open rejects the connect() promise via the `ready` catch below.
|
|
151
|
+
transport.closed
|
|
152
|
+
.then((info) => {
|
|
153
|
+
if (opened) closeCb?.(info?.closeCode ?? 0);
|
|
154
|
+
})
|
|
155
|
+
.catch(() => {
|
|
156
|
+
if (opened) closeCb?.(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
transport.ready
|
|
160
|
+
.then(() => {
|
|
161
|
+
opened = true;
|
|
162
|
+
writer = transport.datagrams.writable.getWriter();
|
|
163
|
+
const reader = transport.datagrams.readable.getReader();
|
|
164
|
+
void (async (): Promise<void> => {
|
|
165
|
+
try {
|
|
166
|
+
for (;;) {
|
|
167
|
+
const { value, done } = await reader.read();
|
|
168
|
+
if (done) break;
|
|
169
|
+
const bytes =
|
|
170
|
+
value instanceof Uint8Array
|
|
171
|
+
? value
|
|
172
|
+
: new Uint8Array(value as ArrayBufferLike);
|
|
173
|
+
if (messageCb) messageCb(bytes);
|
|
174
|
+
else pending.push(bytes);
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
/* read loop ended on session close */
|
|
178
|
+
}
|
|
179
|
+
})();
|
|
180
|
+
resolve(channel);
|
|
181
|
+
})
|
|
182
|
+
.catch((e) => reject(e instanceof Error ? e : new Error(String(e))));
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Pick the transport by origin scheme: `https://` -> WebTransport (the production L2/L3 edge),
|
|
187
|
+
* `ws(s)://` -> WebSocket (the dev server). The generated `shared/server.ts` sets the scheme per build
|
|
188
|
+
* (edge: `https://wt.<tenant>`; dev: same-origin `wss://`). */
|
|
189
|
+
function openChannel<TSend = Uint8Array>(
|
|
190
|
+
url: string,
|
|
191
|
+
encode?: (msg: never) => Uint8Array,
|
|
192
|
+
): Promise<StreamChannel<TSend>> {
|
|
193
|
+
return url.startsWith('https://')
|
|
194
|
+
? connectStreamWT<TSend>(url, encode)
|
|
195
|
+
: connectStream<TSend>(url, encode);
|
|
196
|
+
}
|
|
197
|
+
|
|
81
198
|
/** The same-origin WebSocket base (`ws://` / `wss://` per the page protocol). */
|
|
82
199
|
function defaultOrigin(): string {
|
|
83
200
|
const loc = globalThis.location;
|
|
84
201
|
return `${loc.protocol === 'https:' ? 'wss:' : 'ws:'}//${loc.host}`;
|
|
85
202
|
}
|
|
86
203
|
|
|
204
|
+
/** Resolve the stream-tier origin when the generated client passes none. A deploy/runtime override -
|
|
205
|
+
* `globalThis.__TOIL_STREAM_ORIGIN__` (e.g. `"https://wt.dacely.com"`, the production L2/L3 edge) -
|
|
206
|
+
* wins; otherwise fall back to the same-origin WebSocket base (the dev server). The edge override is
|
|
207
|
+
* `https://` so `openChannel` selects WebTransport; the dev fallback is `ws(s)://` so it selects
|
|
208
|
+
* WebSocket. This is how a deployed app points at its `wt.<tenant>` tier without the build knowing it. */
|
|
209
|
+
function resolveStreamOrigin(): string {
|
|
210
|
+
const override = (globalThis as { __TOIL_STREAM_ORIGIN__?: unknown }).__TOIL_STREAM_ORIGIN__;
|
|
211
|
+
if (typeof override === 'string' && override.length > 0) return override;
|
|
212
|
+
return defaultOrigin();
|
|
213
|
+
}
|
|
214
|
+
|
|
87
215
|
/**
|
|
88
216
|
* Build the `Server.Stream` client from the generated route map (`{ ClassName: route }`). `origin`
|
|
89
217
|
* defaults to the page origin. `encoders` carries one `@data` encoder per typed `@stream({ message: T })`
|
|
@@ -95,12 +223,14 @@ export function makeStreamClient(
|
|
|
95
223
|
origin?: string,
|
|
96
224
|
encoders?: Record<string, (msg: never) => Uint8Array>,
|
|
97
225
|
): StreamClient {
|
|
98
|
-
const base = origin ?? defaultOrigin();
|
|
99
226
|
const client: StreamClient = {};
|
|
100
227
|
for (const [name, route] of Object.entries(routes)) {
|
|
101
228
|
const encode = encoders?.[name];
|
|
102
229
|
client[name] = {
|
|
103
|
-
|
|
230
|
+
// Resolve the origin LAZILY, per connect() - so a deploy/app that sets
|
|
231
|
+
// `__TOIL_STREAM_ORIGIN__` after this module loads is still honoured.
|
|
232
|
+
connect: (path = ''): Promise<StreamChannel> =>
|
|
233
|
+
openChannel(`${origin ?? resolveStreamOrigin()}${route}${path}`, encode),
|
|
104
234
|
};
|
|
105
235
|
}
|
|
106
236
|
return client;
|