toiljs 0.0.34 → 0.0.37
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 +15 -0
- package/README.md +1 -0
- package/as-pect.config.js +8 -2
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +97 -0
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/auth.d.ts +42 -0
- package/build/client/auth.js +182 -0
- package/build/client/index.d.ts +5 -1
- package/build/client/index.js +3 -1
- package/build/client/routing/loader.d.ts +1 -0
- package/build/client/routing/loader.js +37 -0
- package/build/client/routing/mount.js +32 -1
- package/build/client/ssr/markers.d.ts +34 -0
- package/build/client/ssr/markers.js +49 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/docs.js +88 -1
- package/build/compiler/generate.d.ts +2 -0
- package/build/compiler/generate.js +2 -2
- package/build/compiler/index.js +2 -0
- package/build/compiler/ssr-codegen.d.ts +2 -0
- package/build/compiler/ssr-codegen.js +36 -0
- package/build/compiler/template-build.d.ts +29 -0
- package/build/compiler/template-build.js +150 -0
- package/build/compiler/template.d.ts +22 -0
- package/build/compiler/template.js +169 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/crypto.js +15 -0
- package/build/devserver/host.js +1 -0
- package/build/devserver/module.d.ts +1 -0
- package/build/devserver/module.js +23 -1
- package/docs/README.md +56 -0
- package/docs/auth.md +261 -0
- package/docs/caching.md +115 -0
- package/docs/cookies.md +457 -0
- package/docs/crypto.md +130 -0
- package/docs/data.md +131 -0
- package/docs/getting-started.md +128 -0
- package/docs/routing.md +259 -0
- package/docs/rpc.md +149 -0
- package/docs/ssr.md +184 -0
- package/docs/time.md +43 -0
- package/examples/basic/client/routes/auth.tsx +198 -0
- package/examples/basic/client/routes/cookies.tsx +199 -0
- package/examples/basic/client/routes/features/index.tsx +34 -10
- package/examples/basic/client/routes/hello.tsx +43 -0
- package/examples/basic/client/routes/pq.tsx +260 -0
- package/examples/basic/server/AuthTestHandler.ts +15 -0
- package/examples/basic/server/AuthVerifyHandler.ts +23 -0
- package/examples/basic/server/CacheHandler.ts +25 -0
- package/examples/basic/server/DecoCache.ts +18 -0
- package/examples/basic/server/FastTrapHandler.ts +8 -0
- package/examples/basic/server/SpinHandler.ts +18 -0
- package/examples/basic/server/SsrGreetingRender.ts +27 -0
- package/examples/basic/server/authexample-main.ts +8 -0
- package/examples/basic/server/authtest-main.ts +8 -0
- package/examples/basic/server/authverify-main.ts +8 -0
- package/examples/basic/server/cache-main.ts +8 -0
- package/examples/basic/server/core/AppHandler.ts +243 -0
- package/examples/basic/server/deco-main.ts +18 -0
- package/examples/basic/server/main.ts +2 -0
- package/examples/basic/server/routes/Auth.ts +184 -0
- package/examples/basic/server/routes/PqDemo.ts +130 -0
- package/examples/basic/server/routes/Session.ts +74 -0
- package/examples/basic/server/spin-main.ts +13 -0
- package/examples/basic/server/ssr/greeting.slots.ts +19 -0
- package/examples/basic/server/ssr-main.ts +18 -0
- package/examples/basic/server/toil-server-env.d.ts +94 -0
- package/examples/basic/server/trap-main.ts +8 -0
- package/package.json +5 -3
- package/server/globals/auth.ts +281 -0
- package/server/runtime/README.md +61 -0
- package/server/runtime/env/Server.ts +12 -0
- package/server/runtime/exports/index.ts +17 -0
- package/server/runtime/exports/render.ts +51 -0
- package/server/runtime/http/base64.ts +104 -0
- package/server/runtime/http/cookie.ts +416 -0
- package/server/runtime/http/cookies.ts +197 -0
- package/server/runtime/http/date.ts +72 -0
- package/server/runtime/http/percent.ts +76 -0
- package/server/runtime/http/securecookies.ts +224 -0
- package/server/runtime/index.ts +17 -0
- package/server/runtime/request.ts +24 -0
- package/server/runtime/response.ts +29 -0
- package/server/runtime/ssr/Ssr.ts +43 -0
- package/server/runtime/ssr/encode.ts +110 -0
- package/server/runtime/ssr/escape.ts +83 -0
- package/server/runtime/ssr/slots.ts +144 -0
- package/server/runtime/time.ts +29 -0
- package/src/cli/create.ts +105 -0
- package/src/client/auth.ts +327 -0
- package/src/client/index.ts +5 -1
- package/src/client/routing/loader.ts +56 -0
- package/src/client/routing/mount.tsx +37 -1
- package/src/client/ssr/markers.tsx +140 -0
- package/src/compiler/docs.ts +88 -1
- package/src/compiler/generate.ts +2 -2
- package/src/compiler/index.ts +5 -0
- package/src/compiler/ssr-codegen.ts +85 -0
- package/src/compiler/template-build.ts +275 -0
- package/src/compiler/template.ts +265 -0
- package/src/devserver/crypto.ts +23 -0
- package/src/devserver/host.ts +4 -0
- package/src/devserver/module.ts +39 -1
- package/test/assembly/cookie.spec.ts +302 -0
- package/test/assembly/example.spec.ts +5 -1
- package/test/assembly/ssr.spec.ts +94 -0
- package/test/devserver.test.ts +42 -0
- package/test/ssr-render.test.ts +128 -0
- package/test/ssr-template.test.tsx +348 -0
package/docs/data.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Data codec (`@data`)
|
|
2
|
+
|
|
3
|
+
`@data` turns a plain class into a typed, versionable value with a deterministic
|
|
4
|
+
binary codec and a JSON codec. It is the backbone of request/response bodies,
|
|
5
|
+
RPC arguments, sessions, and anything you persist. The same class becomes a
|
|
6
|
+
fully typed client type in the generated `shared/server.ts` (see
|
|
7
|
+
[RPC](./rpc.md)).
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
@data
|
|
11
|
+
class Player {
|
|
12
|
+
username: string = '';
|
|
13
|
+
admin: bool = false;
|
|
14
|
+
score: u64 = 0;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
From that the compiler synthesizes, on the class:
|
|
19
|
+
|
|
20
|
+
- `encode(): Uint8Array` / `static decode(buf): T` — the binary codec (with a
|
|
21
|
+
4-byte type id prefix).
|
|
22
|
+
- `encodeInto(w: DataWriter)` / `static decodeFrom(r: DataReader)` — the codec
|
|
23
|
+
without the type-id frame, for nesting.
|
|
24
|
+
- `toJSON()` / `static fromJSON(v)` — the JSON codec (64-bit-and-larger integers
|
|
25
|
+
as decimal strings, so they survive `JSON.parse` exactly).
|
|
26
|
+
- `static dataId(): u32` — a stable FNV-1a hash of the class name, written as the
|
|
27
|
+
type-id prefix by `encode()`.
|
|
28
|
+
|
|
29
|
+
Fields may be scalars (`u8`..`u256`, `i8`..`i256`, `f32`, `f64`, `bool`),
|
|
30
|
+
`string`, a nested `@data` class, or an array `T[]` of any of these. Give every
|
|
31
|
+
field a default; the generated decoder and the client constructor use them.
|
|
32
|
+
|
|
33
|
+
## Using `@data` in routes
|
|
34
|
+
|
|
35
|
+
In a **JSON** route, a `@data` parameter is revived from the parsed body and a
|
|
36
|
+
`@data` return value is serialized with `toJSON()`. In a **Binary** route, the
|
|
37
|
+
parameter is `decode`d from the raw body and the return value is `encode`d. The
|
|
38
|
+
route's stream mode (see [Routing](./routing.md#data-streams)) picks which.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
@post('/') // JSON route
|
|
42
|
+
public create(input: NewPlayer): Player { /* input from JSON, Player to JSON */ }
|
|
43
|
+
|
|
44
|
+
@route({ method: Methods.POST, path: '/blob', stream: DataStream.Binary })
|
|
45
|
+
public blob(input: FileData): FileResult { /* input.decode, result.encode */ }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The binary codec: `DataWriter` / `DataReader`
|
|
49
|
+
|
|
50
|
+
When you need to lay out bytes yourself — custom bodies, session payloads,
|
|
51
|
+
challenge messages — use the codec directly. It lives in the `data` module:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { DataWriter, DataReader } from 'data';
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The codec has a byte-for-byte identical TypeScript implementation in
|
|
58
|
+
`toiljs/io` (`src/io/codec.ts`), so the client can read and write the exact same
|
|
59
|
+
wire format the wasm guest does.
|
|
60
|
+
|
|
61
|
+
### `DataWriter`
|
|
62
|
+
|
|
63
|
+
Every writer method returns the writer for chaining.
|
|
64
|
+
|
|
65
|
+
| Method | Signature | Wire format |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| `writeU8` / `writeI8` | `(v): DataWriter` | 1 byte |
|
|
68
|
+
| `writeU16` / `writeI16` | `(v): DataWriter` | 2 bytes, little-endian |
|
|
69
|
+
| `writeU32` / `writeI32` | `(v): DataWriter` | 4 bytes, LE |
|
|
70
|
+
| `writeU64` / `writeI64` | `(v): DataWriter` | 8 bytes, LE |
|
|
71
|
+
| `writeF32` / `writeF64` | `(v): DataWriter` | 4 / 8 bytes, IEEE-754 LE |
|
|
72
|
+
| `writeBool` | `(v): DataWriter` | 1 byte (`1`/`0`) |
|
|
73
|
+
| `writeBytes` | `(b: Uint8Array): DataWriter` | `u32` length (LE) + raw bytes |
|
|
74
|
+
| `writeString` | `(s: string): DataWriter` | `u32` length (LE) + UTF-8 bytes |
|
|
75
|
+
| `writeU128` / `writeI128` | `(v): DataWriter` | two `u64` limbs (lo, hi) |
|
|
76
|
+
| `writeU256` / `writeI256` | `(v): DataWriter` | four `u64` limbs (lo1, lo2, hi1, hi2) |
|
|
77
|
+
| `length` | `(): i32` | bytes written so far |
|
|
78
|
+
| `toBytes` | `(): Uint8Array` | an exact-length copy of the buffer |
|
|
79
|
+
|
|
80
|
+
### `DataReader`
|
|
81
|
+
|
|
82
|
+
Reads are bounds-safe: an over-read never traps. It returns a zero/empty default
|
|
83
|
+
and sets the public `ok` flag to `false`. Check `ok` after a sequence of reads
|
|
84
|
+
to detect a truncated or malformed buffer.
|
|
85
|
+
|
|
86
|
+
| Method | Signature | On over-read |
|
|
87
|
+
| --- | --- | --- |
|
|
88
|
+
| `readU8` / `readI8` | `(): u8 / i8` | `0` |
|
|
89
|
+
| `readU16`..`readU64`, `readI16`..`readI64` | `(): integer` | `0` |
|
|
90
|
+
| `readF32` / `readF64` | `(): f32 / f64` | `0` |
|
|
91
|
+
| `readBool` | `(): bool` | `false` |
|
|
92
|
+
| `readBytes` | `(): Uint8Array` | empty array |
|
|
93
|
+
| `readString` | `(): string` | `""` |
|
|
94
|
+
| `readU128`/`readI128`/`readU256`/`readI256` | `(): bignum` | `0` |
|
|
95
|
+
| `remaining` | `(): i32` | bytes left unread |
|
|
96
|
+
| `ok` | `bool` (field) | `false` once any read over-ran |
|
|
97
|
+
|
|
98
|
+
### Example
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { DataWriter, DataReader } from 'data';
|
|
102
|
+
|
|
103
|
+
// Write: u8 version, str name, u64 score, bytes blob
|
|
104
|
+
const out = new DataWriter()
|
|
105
|
+
.writeU8(1)
|
|
106
|
+
.writeString('alice')
|
|
107
|
+
.writeU64(1234)
|
|
108
|
+
.writeBytes(payload)
|
|
109
|
+
.toBytes();
|
|
110
|
+
|
|
111
|
+
// Read it back
|
|
112
|
+
const r = new DataReader(out);
|
|
113
|
+
const version = r.readU8();
|
|
114
|
+
const name = r.readString();
|
|
115
|
+
const score = r.readU64();
|
|
116
|
+
const blob = r.readBytes();
|
|
117
|
+
if (!r.ok) return Response.badRequest('truncated');
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Notes
|
|
121
|
+
|
|
122
|
+
- **Endianness.** The AS guest codec is little-endian. The TypeScript `toiljs/io`
|
|
123
|
+
codec defaults to little-endian and also accepts a per-call `be` flag for
|
|
124
|
+
big-endian network formats; keep both ends on the same setting.
|
|
125
|
+
- **Field order is the format.** The binary layout is exactly the field
|
|
126
|
+
declaration order. Reordering fields, or changing a type, is a breaking format
|
|
127
|
+
change. Add new fields at the end and bump a leading version byte if you need
|
|
128
|
+
to evolve a hand-rolled payload.
|
|
129
|
+
- **`encode()` carries a type id.** The 4-byte `dataId()` prefix lets a decoder
|
|
130
|
+
confirm it is reading the type it expects. `encodeInto`/`decodeFrom` skip the
|
|
131
|
+
frame for nesting one `@data` value inside another.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
A toiljs app has two halves that build and ship together:
|
|
4
|
+
|
|
5
|
+
- a **server** written in ToilScript, compiled to a single WebAssembly module
|
|
6
|
+
(`build/server/release.wasm`), and
|
|
7
|
+
- a **client** (Vite + React) that talks to the server through a generated,
|
|
8
|
+
fully typed `Server` proxy.
|
|
9
|
+
|
|
10
|
+
The server runs one fresh wasm instance per request, identically on the dev
|
|
11
|
+
server and on the edge. There is no Node.js in the request path: your handler is
|
|
12
|
+
wasm.
|
|
13
|
+
|
|
14
|
+
## Project layout
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
project/
|
|
18
|
+
toilconfig.json server (wasm) build config: entries, target, AS options
|
|
19
|
+
toil.config.ts client config (defineConfig: dev/build/SEO options)
|
|
20
|
+
|
|
21
|
+
server/
|
|
22
|
+
main.ts wires Server.handler, re-exports the wasm exports + abort
|
|
23
|
+
routes/*.ts @rest controllers (auto-discovered)
|
|
24
|
+
services/*.ts @service / @remote (auto-discovered)
|
|
25
|
+
core/AppHandler.ts your top-level ToilHandler
|
|
26
|
+
models/*.ts @data / @user classes
|
|
27
|
+
|
|
28
|
+
shared/
|
|
29
|
+
server.ts GENERATED by the server build (--rpcModule): the typed
|
|
30
|
+
client surface (Server proxy, @data codecs, getUser)
|
|
31
|
+
|
|
32
|
+
client/
|
|
33
|
+
routes/*.tsx file-based pages
|
|
34
|
+
layout.tsx, 404.tsx root layout / not-found
|
|
35
|
+
styles/*.css
|
|
36
|
+
|
|
37
|
+
build/
|
|
38
|
+
server/release.wasm compiled server (+ release.wat text form)
|
|
39
|
+
client/ Vite output
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The compiler discovers every `.ts` under `server/` that declares a decorated
|
|
43
|
+
surface (`@rest`, `@service`, `@remote`, `@data`, `@user`) on its own. Importing
|
|
44
|
+
those modules from `main.ts` is still good practice: it keeps a direct
|
|
45
|
+
`toilscript` run (which only sees the `toilconfig.json` entries) building the
|
|
46
|
+
exact same server.
|
|
47
|
+
|
|
48
|
+
## `main.ts`
|
|
49
|
+
|
|
50
|
+
Three things are required, and the comments in the scaffold say so:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { Server } from 'toiljs/server/runtime';
|
|
54
|
+
import { revertOnError } from 'toiljs/server/runtime/abort/abort';
|
|
55
|
+
|
|
56
|
+
import { AppHandler } from './core/AppHandler';
|
|
57
|
+
|
|
58
|
+
// Pull every decorated surface into a direct `toilscript` build.
|
|
59
|
+
import './routes/Players';
|
|
60
|
+
import './services/Stats';
|
|
61
|
+
|
|
62
|
+
// 1. The handler factory: one fresh handler instance per request.
|
|
63
|
+
Server.handler = () => new AppHandler();
|
|
64
|
+
|
|
65
|
+
// 2. Re-export the wasm entrypoints (`handle`, `render`).
|
|
66
|
+
export * from 'toiljs/server/runtime/exports';
|
|
67
|
+
|
|
68
|
+
// 3. The AssemblyScript trap hook.
|
|
69
|
+
export function abort(message: string, fileName: string, line: u32, column: u32): void {
|
|
70
|
+
revertOnError(message, fileName, line, column);
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
If all you need is `@rest` routing, your handler can be `RestHandler` (see
|
|
75
|
+
[Routing](./routing.md)) and you do not have to write an `AppHandler` at all.
|
|
76
|
+
|
|
77
|
+
## The request lifecycle
|
|
78
|
+
|
|
79
|
+
For each request the runtime (`server/runtime/exports`):
|
|
80
|
+
|
|
81
|
+
1. decodes the request envelope into a [`Request`](./routing.md#request),
|
|
82
|
+
2. publishes it ambiently as `Server.currentRequest` (so `AuthService.getUser()`
|
|
83
|
+
and friends can read its cookies with no argument),
|
|
84
|
+
3. builds the handler via `Server.handler()` and calls
|
|
85
|
+
`onRequestStarted` → `handle(req)` → `onRequestCompleted`,
|
|
86
|
+
4. encodes the returned [`Response`](./routing.md#response) and clears the
|
|
87
|
+
ambient request.
|
|
88
|
+
|
|
89
|
+
Because the instance is fresh and memory is wiped between requests, **nothing in
|
|
90
|
+
module globals survives across requests.** Anything that must persist (accounts,
|
|
91
|
+
sessions you do not put in a cookie, rate-limit counters) belongs in an external
|
|
92
|
+
store reached through a host binding.
|
|
93
|
+
|
|
94
|
+
## CLI
|
|
95
|
+
|
|
96
|
+
The `toiljs` CLI drives both halves:
|
|
97
|
+
|
|
98
|
+
| Command | What it does |
|
|
99
|
+
| --- | --- |
|
|
100
|
+
| `toiljs create [name]` | Scaffold a new app (templates, styling, options). |
|
|
101
|
+
| `toiljs dev` | Dev server with hot reload: watches `server/`, rebuilds the wasm via toilscript, regenerates `shared/server.ts`, and runs Vite for the client. Flags: `--root <dir>`, `--port <n>`, `--host`. |
|
|
102
|
+
| `toiljs build` | Production build: server wasm first (so `shared/server.ts` is fresh), then the Vite client + static prerender. Flags: `--root <dir>`, `--server` (server only). |
|
|
103
|
+
| `toiljs start` | Self-host a built app. Flags: `--root`, `--port`, `--host`. |
|
|
104
|
+
| `toiljs doctor` | Diagnose setup/deps (`--json`, `--fix`). |
|
|
105
|
+
|
|
106
|
+
In dev, requests whose method matches a dispatchable verb go into the wasm
|
|
107
|
+
first; if the guest reports "no route matched" (the `x-toil-unhandled` marker)
|
|
108
|
+
the request falls through to Vite, so client routes and assets just work
|
|
109
|
+
alongside your API.
|
|
110
|
+
|
|
111
|
+
## Building the server by hand
|
|
112
|
+
|
|
113
|
+
`toiljs build` runs toilscript for you, but you can invoke it directly (this is
|
|
114
|
+
what the examples do):
|
|
115
|
+
|
|
116
|
+
```sh
|
|
117
|
+
toilscript --target release --rpcModule shared/server.ts
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`--target release` reads `toilconfig.json` and emits the wasm at
|
|
121
|
+
`targets.release.outFile`; `--rpcModule shared/server.ts` writes the generated
|
|
122
|
+
typed client (see [RPC](./rpc.md)).
|
|
123
|
+
|
|
124
|
+
## Next
|
|
125
|
+
|
|
126
|
+
- [Routing](./routing.md) to expose HTTP endpoints.
|
|
127
|
+
- [Data codec](./data.md) for request/response bodies.
|
|
128
|
+
- [Auth](./auth.md) for login and sessions.
|
package/docs/routing.md
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Routing
|
|
2
|
+
|
|
3
|
+
toiljs routing is decorator-driven. You write a controller class, annotate it
|
|
4
|
+
with `@rest` and its methods with verb decorators, and the ToilScript compiler
|
|
5
|
+
generates the dispatcher. Routes can take a typed body, read path params and the
|
|
6
|
+
raw request through a `RouteContext`, and return either a `Response` or a typed
|
|
7
|
+
value that is auto-encoded.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { Response, RouteContext } from 'toiljs/server/runtime';
|
|
11
|
+
|
|
12
|
+
@rest('players')
|
|
13
|
+
class Players {
|
|
14
|
+
@get('/:id')
|
|
15
|
+
public get(ctx: RouteContext): Response {
|
|
16
|
+
const id = ctx.param('id');
|
|
17
|
+
return Response.json(`{"id":"${id}"}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@post('/')
|
|
21
|
+
public create(input: NewPlayer): Player {
|
|
22
|
+
// `input` is the decoded request body; returning a @data value JSON-encodes it
|
|
23
|
+
return Player.from(input);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## `@rest` controllers
|
|
29
|
+
|
|
30
|
+
`@rest` marks a class as a route controller and mounts it at a prefix.
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
@rest('api') // mounted at /api
|
|
34
|
+
@rest('/') // or @rest('') // mounted at the root
|
|
35
|
+
@rest({ stream: DataStream.Binary }) // root mount, binary codec by default
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- The string argument is the mount prefix. `"api"`, `"/api"`, and `"api/"` all
|
|
39
|
+
normalize to `/api`; `""` and `"/"` mean the root.
|
|
40
|
+
- The object form sets class-wide defaults. `stream: DataStream.Binary` makes
|
|
41
|
+
every route in the class use the binary `@data` codec; the default is
|
|
42
|
+
`DataStream.JSON`. Individual routes override this with `@route`.
|
|
43
|
+
|
|
44
|
+
The compiler injects, at module init, a registration that adds the controller to
|
|
45
|
+
the global `Rest` registry. Controllers dispatch in the order their modules are
|
|
46
|
+
loaded; routes within a controller try in declaration order, first match wins.
|
|
47
|
+
|
|
48
|
+
## Verb decorators
|
|
49
|
+
|
|
50
|
+
Each HTTP method has a decorator taking a single path string:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
@get('/path') @post('/path') @put('/path') @delete('/path')
|
|
54
|
+
@patch('/path') @head('/path') @options('/path')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The full path is the controller prefix joined with the route path
|
|
58
|
+
(`prefix="/api"`, `@get("/todos/:id")` → `/api/todos/:id`).
|
|
59
|
+
|
|
60
|
+
### `@route` (explicit form)
|
|
61
|
+
|
|
62
|
+
`@route` is the general form; use it when you need to set the stream mode per
|
|
63
|
+
route or prefer an object:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
@route({ method: Methods.POST, path: '/upload', stream: DataStream.Binary })
|
|
67
|
+
public upload(body: FileData): FileResult { /* ... */ }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`method` (from the `Methods` enum) and `path` are required; `stream` is
|
|
71
|
+
optional and overrides the controller default.
|
|
72
|
+
|
|
73
|
+
## Path parameters
|
|
74
|
+
|
|
75
|
+
A `:name` segment captures that URL segment. Read it with `ctx.param("name")`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
@get('/todos/:id/items/:itemId')
|
|
79
|
+
public getItem(ctx: RouteContext): Response {
|
|
80
|
+
const id = ctx.param('id');
|
|
81
|
+
const itemId = ctx.param('itemId');
|
|
82
|
+
return Response.json(`{"todo":"${id}","item":"${itemId}"}`);
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Matching is segment-exact: the request path must have the same number of
|
|
87
|
+
segments, static segments must match literally, and `:param` segments capture
|
|
88
|
+
the value. The query string is stripped before matching.
|
|
89
|
+
|
|
90
|
+
## Method parameters
|
|
91
|
+
|
|
92
|
+
A route method takes zero, one, or two parameters, classified by type:
|
|
93
|
+
|
|
94
|
+
- a `RouteContext` parameter receives the match context (path params, query,
|
|
95
|
+
headers, raw body);
|
|
96
|
+
- any other type is treated as the **request body**, decoded as a `@data` value.
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
@get('/status')
|
|
100
|
+
public status(): StatusResponse { /* no body, no context */ }
|
|
101
|
+
|
|
102
|
+
@get('/user/:id')
|
|
103
|
+
public getUser(ctx: RouteContext): User { /* context only */ }
|
|
104
|
+
|
|
105
|
+
@post('/create')
|
|
106
|
+
public create(input: NewTodo): Todo { /* body only */ }
|
|
107
|
+
|
|
108
|
+
@post('/user/:id/score')
|
|
109
|
+
public addScore(input: ScoreDelta, ctx: RouteContext): Player {
|
|
110
|
+
const id = ctx.param('id'); /* body AND context */
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The body is decoded per the route's stream mode: in JSON mode from
|
|
115
|
+
`JSON.parse(ctx.text())`, in Binary mode from `Body.decode(req.body)`. See
|
|
116
|
+
[Data codec](./data.md).
|
|
117
|
+
|
|
118
|
+
## Return types
|
|
119
|
+
|
|
120
|
+
The compiler encodes the return value by its type:
|
|
121
|
+
|
|
122
|
+
| Return type | Result |
|
|
123
|
+
| --- | --- |
|
|
124
|
+
| `Response` | Returned as-is. Full control over status, headers, body. |
|
|
125
|
+
| `void` | `204 No Content`. |
|
|
126
|
+
| a `@data` type, JSON stream | `Response.json(value.toJSON().toString())`. |
|
|
127
|
+
| a `@data` type, Binary stream | `Response.bytes(value.encode())`. |
|
|
128
|
+
|
|
129
|
+
Returning a `Response` lets you set status, headers, cookies, and caching
|
|
130
|
+
directly; returning a typed value is the terse path when you just want the data
|
|
131
|
+
serialized.
|
|
132
|
+
|
|
133
|
+
## Data streams
|
|
134
|
+
|
|
135
|
+
Each route is either **JSON** (default) or **Binary**:
|
|
136
|
+
|
|
137
|
+
- **JSON** — the body is `JSON.parse`d and revived via the `@data` type's
|
|
138
|
+
`fromJSON`; the response is the type's `toJSON()`. 64-bit-and-larger integers
|
|
139
|
+
cross the wire as decimal strings (exact at any size). Best for endpoints a
|
|
140
|
+
browser or third party calls directly.
|
|
141
|
+
- **Binary** — the body is `Body.decode(bytes)` and the response is
|
|
142
|
+
`value.encode()`, using the deterministic `DataWriter`/`DataReader` codec. No
|
|
143
|
+
precision loss, smaller, faster. Best for app-to-app and anything
|
|
144
|
+
security-sensitive.
|
|
145
|
+
|
|
146
|
+
Set the mode on the controller (`@rest({ stream: DataStream.Binary })`) or per
|
|
147
|
+
route (`@route({ ..., stream: DataStream.Binary })`).
|
|
148
|
+
|
|
149
|
+
## Dispatch and the 404 fallback
|
|
150
|
+
|
|
151
|
+
At runtime the global `Rest` registry tries each controller in order:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const hit = Rest.dispatch(req); // Response | null
|
|
155
|
+
if (hit != null) return hit; // first matching route's Response
|
|
156
|
+
return Response.unhandled(); // no route matched
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`RestHandler` is a ready-made handler that does exactly this, so a REST-only app
|
|
160
|
+
needs no custom handler:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { RestHandler } from 'toiljs/server/runtime';
|
|
164
|
+
Server.handler = () => new RestHandler();
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
`Response.unhandled()` is a `404` carrying the `x-toil-unhandled` marker header.
|
|
168
|
+
On the dev server and edge that marker means "no route matched here" and lets
|
|
169
|
+
the request fall through to the next layer (Vite in dev, static/SSR on the
|
|
170
|
+
edge). A deliberate `Response.notFound()` does **not** carry the marker and is
|
|
171
|
+
sent to the client verbatim.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## `Request`
|
|
176
|
+
|
|
177
|
+
The decoded incoming request (`server/runtime/request.ts`).
|
|
178
|
+
|
|
179
|
+
### Fields
|
|
180
|
+
|
|
181
|
+
| Field | Type | Notes |
|
|
182
|
+
| --- | --- | --- |
|
|
183
|
+
| `method` | `Method` | `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `UNKNOWN`. |
|
|
184
|
+
| `path` | `string` | Path including the query string. |
|
|
185
|
+
| `headers` | `Array<Header>` | Ordered; a `Header` is `{ name, value }`. |
|
|
186
|
+
| `body` | `Uint8Array` | Raw request body bytes. |
|
|
187
|
+
|
|
188
|
+
### Methods
|
|
189
|
+
|
|
190
|
+
| Method | Signature | Notes |
|
|
191
|
+
| --- | --- | --- |
|
|
192
|
+
| `header` | `header(name: string): string \| null` | Case-insensitive lookup, `null` if absent. |
|
|
193
|
+
| `cookies` | `cookies(): CookieMap` | Parses the `Cookie` header (percent-decoded values); cached for the request. |
|
|
194
|
+
| `cookie` | `cookie(name: string): string \| null` | A single cookie value, or `null`. |
|
|
195
|
+
|
|
196
|
+
The `Method` enum and `Header` class are exported from
|
|
197
|
+
`toiljs/server/runtime`.
|
|
198
|
+
|
|
199
|
+
## `RouteContext`
|
|
200
|
+
|
|
201
|
+
Passed to any route method that declares a `RouteContext` parameter
|
|
202
|
+
(`server/runtime/rest/RouteContext.ts`).
|
|
203
|
+
|
|
204
|
+
| Member | Signature | Notes |
|
|
205
|
+
| --- | --- | --- |
|
|
206
|
+
| `request` | `Request` | The raw incoming request. |
|
|
207
|
+
| `param` | `param(name: string): string` | Captured path param; `""` if absent. |
|
|
208
|
+
| `query` | `query(name: string): string` | Query-string value; `""` if absent. Not URL-decoded in v1. |
|
|
209
|
+
| `header` | `header(name: string): string \| null` | Case-insensitive request header. |
|
|
210
|
+
| `text` | `text(): string` | The request body decoded as UTF-8. |
|
|
211
|
+
|
|
212
|
+
## `Response`
|
|
213
|
+
|
|
214
|
+
The outgoing response builder (`server/runtime/response.ts`). Construct one with
|
|
215
|
+
a static factory, then chain instance methods (each returns the same `Response`).
|
|
216
|
+
|
|
217
|
+
### Constructor
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
new Response(status: u16, body: Uint8Array, headers: Array<Header> | null = null)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Static factories
|
|
224
|
+
|
|
225
|
+
| Factory | Signature | Status | Content-Type |
|
|
226
|
+
| --- | --- | --- | --- |
|
|
227
|
+
| `Response.text` | `text(body: string, status: u16 = 200)` | 200 | `text/plain; charset=utf-8` |
|
|
228
|
+
| `Response.html` | `html(body: string, status: u16 = 200)` | 200 | `text/html; charset=utf-8` |
|
|
229
|
+
| `Response.json` | `json(body: string, status: u16 = 200)` | 200 | `application/json; charset=utf-8` |
|
|
230
|
+
| `Response.bytes` | `bytes(body: Uint8Array, status: u16 = 200)` | 200 | `application/octet-stream` |
|
|
231
|
+
| `Response.empty` | `empty(status: u16)` | custom | (none) |
|
|
232
|
+
| `Response.notFound` | `notFound()` | 404 | text |
|
|
233
|
+
| `Response.badRequest` | `badRequest(msg = 'bad request')` | 400 | text |
|
|
234
|
+
| `Response.internalError` | `internalError(msg = 'internal error')` | 500 | text |
|
|
235
|
+
| `Response.unhandled` | `unhandled()` | 404 | text + `x-toil-unhandled` marker |
|
|
236
|
+
|
|
237
|
+
`json` takes an already-serialized string; build it with `DataWriter`-free JSON
|
|
238
|
+
or a `@data` type's `toJSON().toString()`. For binary, prefer `bytes`.
|
|
239
|
+
|
|
240
|
+
### Instance methods
|
|
241
|
+
|
|
242
|
+
| Method | Signature | Notes |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| `setHeader` | `setHeader(name: string, value: string): Response` | Appends a header (repeatable). |
|
|
245
|
+
| `setCookie` | `setCookie(cookie: Cookie): Response` | Appends a `Set-Cookie`. Call again for more. |
|
|
246
|
+
| `setCookieKV` | `setCookieKV(name: string, value: string): Response` | Shorthand for `setCookie(new Cookie(name, value))`. |
|
|
247
|
+
| `clearCookie` | `clearCookie(name: string, path = '/', domain = ''): Response` | Emits a deletion `Set-Cookie` (empty value, `Max-Age=0`). |
|
|
248
|
+
| `cache` | `cache(edgeTtlMinutes: u16, browserTtlSeconds: u32 = 0, privateScope: bool = false, allowAuth: bool = false): Response` | Marks the response cacheable. See [Caching](./caching.md). |
|
|
249
|
+
| `cacheFor` | `cacheFor(minutes: u16): Response` | Shorthand for `cache(minutes)` (edge only). |
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
return Response.json('{"id":42}')
|
|
253
|
+
.setHeader('x-trace', traceId)
|
|
254
|
+
.setCookie(Cookie.create('sid', token).httpOnly().secure())
|
|
255
|
+
.cacheFor(5);
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
See [Cookies](./cookies.md) for the cookie builder, and [Caching](./caching.md)
|
|
259
|
+
for the cache directives.
|
package/docs/rpc.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# RPC and the generated client
|
|
2
|
+
|
|
3
|
+
The server build with `--rpcModule shared/server.ts` scans your decorated
|
|
4
|
+
surface (`@data`, `@user`, `@service`/`@remote`, `@rest`) and emits one
|
|
5
|
+
TypeScript module: a typed `Server` proxy, the `@data` codec classes, the REST
|
|
6
|
+
fetch client, and the `getUser()` accessor. The client imports that file and
|
|
7
|
+
calls the server with full type-safety and editor autocomplete. The file is
|
|
8
|
+
regenerated on every server build, so it never drifts from the server.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
toilscript --target release --rpcModule shared/server.ts
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## `@service` and `@remote`
|
|
15
|
+
|
|
16
|
+
A `@service` class exposes its `@remote` methods as callable RPC. A top-level
|
|
17
|
+
`@remote` function is exposed directly.
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
@service
|
|
21
|
+
class Stats {
|
|
22
|
+
@remote
|
|
23
|
+
public playerCount(): i32 { return store.size; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@remote
|
|
27
|
+
function ping(n: i32): i32 { return n + 1; }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The generated client surfaces these on the global `Server` proxy. A service is
|
|
31
|
+
keyed by its class name with the first letter lowercased (`Stats` → `stats`):
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
await Server.stats.playerCount(); // Promise<number>
|
|
35
|
+
await Server.ping(42); // Promise<number>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Arguments and return values are `@data`-typed; scalars map to TS as below.
|
|
39
|
+
|
|
40
|
+
## The generated `Server` surface
|
|
41
|
+
|
|
42
|
+
`shared/server.ts` declares a global `Server` whose shape is, schematically:
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
declare global {
|
|
46
|
+
const Server: {
|
|
47
|
+
// top-level @remote functions
|
|
48
|
+
ping(n: number): Promise<number>;
|
|
49
|
+
|
|
50
|
+
// @service classes (keyed by lowercased name)
|
|
51
|
+
readonly stats: {
|
|
52
|
+
playerCount(): Promise<number>;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// @rest controllers, under REST
|
|
56
|
+
readonly REST: {
|
|
57
|
+
readonly players: {
|
|
58
|
+
get(args: { params: { id: string | number | bigint }; query?: …; headers?: … }): Promise<Response>;
|
|
59
|
+
create(args: { body: NewPlayer; query?: …; headers?: … }): Promise<Player>;
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### Type mapping (ToilScript → TypeScript)
|
|
67
|
+
|
|
68
|
+
| ToilScript | TypeScript |
|
|
69
|
+
| --- | --- |
|
|
70
|
+
| `u8`,`u16`,`u32`,`i8`,`i16`,`i32`,`f32`,`f64` | `number` |
|
|
71
|
+
| `u64`,`i64`,`u128`,`i128`,`u256`,`i256` | `bigint` |
|
|
72
|
+
| `bool` | `boolean` |
|
|
73
|
+
| `string` | `string` |
|
|
74
|
+
| a `@data` class `T` | `T` (the emitted class) |
|
|
75
|
+
| `T[]` | `T[]` |
|
|
76
|
+
|
|
77
|
+
64-bit-and-larger integers are `bigint` on the client and travel as decimal
|
|
78
|
+
strings on the JSON wire, so they are exact at any magnitude.
|
|
79
|
+
|
|
80
|
+
### Emitted `@data` classes
|
|
81
|
+
|
|
82
|
+
Each `@data` (and `@user`) class becomes an exported TS class with the fields, a
|
|
83
|
+
defaulted constructor, and the matching codec:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
export class Player {
|
|
87
|
+
constructor(public username = '', public admin = false, public score = 0n) {}
|
|
88
|
+
encodeInto(w: DataWriter): void { /* … */ }
|
|
89
|
+
encode(): Uint8Array { /* dataId prefix + fields */ }
|
|
90
|
+
static decodeFrom(r: DataReader): Player { /* … */ }
|
|
91
|
+
static decode(buf: Uint8Array): Player { /* … */ }
|
|
92
|
+
static dataId(): number { /* FNV-1a of "Player" */ }
|
|
93
|
+
static fromJSONValue(v: any): Player { /* revive, 64-bit from strings */ }
|
|
94
|
+
toJSONValue(): any { /* 64-bit as decimal strings */ }
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The codec is byte-compatible with the server's `@data` codec, so binary bodies
|
|
99
|
+
round-trip exactly between client and wasm.
|
|
100
|
+
|
|
101
|
+
## The REST fetch client
|
|
102
|
+
|
|
103
|
+
Every `@rest` route also gets a typed fetch wrapper under `Server.REST.<key>`,
|
|
104
|
+
keyed by the controller name lowercased. The call argument is an object:
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
Server.REST.players.create({
|
|
108
|
+
body: new NewPlayer('alice'), // present iff the route takes a body
|
|
109
|
+
// params: { id: 7 }, // present iff the path has :params
|
|
110
|
+
query: { ref: 'home' }, // optional
|
|
111
|
+
headers: { 'x-trace': traceId }, // optional
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- If the route has no params and no body, the whole argument is optional
|
|
116
|
+
(`args?`).
|
|
117
|
+
- The wrapper builds the URL (substituting `:params`, appending `query`),
|
|
118
|
+
`fetch`es with `credentials` as configured, throws on a non-2xx status, and
|
|
119
|
+
decodes the response into the route's return type.
|
|
120
|
+
- A route declared to return `Response` resolves to the raw `fetch` `Response`,
|
|
121
|
+
so you can stream or inspect headers yourself.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const player = await Server.REST.players.create({ body: new NewPlayer('alice') });
|
|
125
|
+
// ^? Player
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## `getUser()`
|
|
129
|
+
|
|
130
|
+
When the server declares a `@user` class, the generated module also exports a
|
|
131
|
+
typed, no-argument `getUser()` that reads the readable companion cookie and
|
|
132
|
+
decodes it with the generated codec:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import { getUser } from './shared/server';
|
|
136
|
+
|
|
137
|
+
const user = getUser(); // Account | null, fully typed
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
This is **display-only**: the server re-verifies the signed session on every
|
|
141
|
+
`@auth` request. See [Auth](./auth.md) for the full picture.
|
|
142
|
+
|
|
143
|
+
## Notes
|
|
144
|
+
|
|
145
|
+
- `shared/server.ts` is generated; never edit it by hand. Re-run the server
|
|
146
|
+
build (or `toiljs dev`, which does it on save) to refresh it.
|
|
147
|
+
- The `Server` proxy is declared as an ambient global on the client; the runtime
|
|
148
|
+
implementation is provided by toiljs. The REST client and `getUser` are real
|
|
149
|
+
exported values in the generated module.
|