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.
Files changed (110) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +1 -0
  3. package/as-pect.config.js +8 -2
  4. package/build/cli/.tsbuildinfo +1 -1
  5. package/build/cli/index.js +97 -0
  6. package/build/client/.tsbuildinfo +1 -1
  7. package/build/client/auth.d.ts +42 -0
  8. package/build/client/auth.js +182 -0
  9. package/build/client/index.d.ts +5 -1
  10. package/build/client/index.js +3 -1
  11. package/build/client/routing/loader.d.ts +1 -0
  12. package/build/client/routing/loader.js +37 -0
  13. package/build/client/routing/mount.js +32 -1
  14. package/build/client/ssr/markers.d.ts +34 -0
  15. package/build/client/ssr/markers.js +49 -0
  16. package/build/compiler/.tsbuildinfo +1 -1
  17. package/build/compiler/docs.js +88 -1
  18. package/build/compiler/generate.d.ts +2 -0
  19. package/build/compiler/generate.js +2 -2
  20. package/build/compiler/index.js +2 -0
  21. package/build/compiler/ssr-codegen.d.ts +2 -0
  22. package/build/compiler/ssr-codegen.js +36 -0
  23. package/build/compiler/template-build.d.ts +29 -0
  24. package/build/compiler/template-build.js +150 -0
  25. package/build/compiler/template.d.ts +22 -0
  26. package/build/compiler/template.js +169 -0
  27. package/build/devserver/.tsbuildinfo +1 -1
  28. package/build/devserver/crypto.js +15 -0
  29. package/build/devserver/host.js +1 -0
  30. package/build/devserver/module.d.ts +1 -0
  31. package/build/devserver/module.js +23 -1
  32. package/docs/README.md +56 -0
  33. package/docs/auth.md +261 -0
  34. package/docs/caching.md +115 -0
  35. package/docs/cookies.md +457 -0
  36. package/docs/crypto.md +130 -0
  37. package/docs/data.md +131 -0
  38. package/docs/getting-started.md +128 -0
  39. package/docs/routing.md +259 -0
  40. package/docs/rpc.md +149 -0
  41. package/docs/ssr.md +184 -0
  42. package/docs/time.md +43 -0
  43. package/examples/basic/client/routes/auth.tsx +198 -0
  44. package/examples/basic/client/routes/cookies.tsx +199 -0
  45. package/examples/basic/client/routes/features/index.tsx +34 -10
  46. package/examples/basic/client/routes/hello.tsx +43 -0
  47. package/examples/basic/client/routes/pq.tsx +260 -0
  48. package/examples/basic/server/AuthTestHandler.ts +15 -0
  49. package/examples/basic/server/AuthVerifyHandler.ts +23 -0
  50. package/examples/basic/server/CacheHandler.ts +25 -0
  51. package/examples/basic/server/DecoCache.ts +18 -0
  52. package/examples/basic/server/FastTrapHandler.ts +8 -0
  53. package/examples/basic/server/SpinHandler.ts +18 -0
  54. package/examples/basic/server/SsrGreetingRender.ts +27 -0
  55. package/examples/basic/server/authexample-main.ts +8 -0
  56. package/examples/basic/server/authtest-main.ts +8 -0
  57. package/examples/basic/server/authverify-main.ts +8 -0
  58. package/examples/basic/server/cache-main.ts +8 -0
  59. package/examples/basic/server/core/AppHandler.ts +243 -0
  60. package/examples/basic/server/deco-main.ts +18 -0
  61. package/examples/basic/server/main.ts +2 -0
  62. package/examples/basic/server/routes/Auth.ts +184 -0
  63. package/examples/basic/server/routes/PqDemo.ts +130 -0
  64. package/examples/basic/server/routes/Session.ts +74 -0
  65. package/examples/basic/server/spin-main.ts +13 -0
  66. package/examples/basic/server/ssr/greeting.slots.ts +19 -0
  67. package/examples/basic/server/ssr-main.ts +18 -0
  68. package/examples/basic/server/toil-server-env.d.ts +94 -0
  69. package/examples/basic/server/trap-main.ts +8 -0
  70. package/package.json +5 -3
  71. package/server/globals/auth.ts +281 -0
  72. package/server/runtime/README.md +61 -0
  73. package/server/runtime/env/Server.ts +12 -0
  74. package/server/runtime/exports/index.ts +17 -0
  75. package/server/runtime/exports/render.ts +51 -0
  76. package/server/runtime/http/base64.ts +104 -0
  77. package/server/runtime/http/cookie.ts +416 -0
  78. package/server/runtime/http/cookies.ts +197 -0
  79. package/server/runtime/http/date.ts +72 -0
  80. package/server/runtime/http/percent.ts +76 -0
  81. package/server/runtime/http/securecookies.ts +224 -0
  82. package/server/runtime/index.ts +17 -0
  83. package/server/runtime/request.ts +24 -0
  84. package/server/runtime/response.ts +29 -0
  85. package/server/runtime/ssr/Ssr.ts +43 -0
  86. package/server/runtime/ssr/encode.ts +110 -0
  87. package/server/runtime/ssr/escape.ts +83 -0
  88. package/server/runtime/ssr/slots.ts +144 -0
  89. package/server/runtime/time.ts +29 -0
  90. package/src/cli/create.ts +105 -0
  91. package/src/client/auth.ts +327 -0
  92. package/src/client/index.ts +5 -1
  93. package/src/client/routing/loader.ts +56 -0
  94. package/src/client/routing/mount.tsx +37 -1
  95. package/src/client/ssr/markers.tsx +140 -0
  96. package/src/compiler/docs.ts +88 -1
  97. package/src/compiler/generate.ts +2 -2
  98. package/src/compiler/index.ts +5 -0
  99. package/src/compiler/ssr-codegen.ts +85 -0
  100. package/src/compiler/template-build.ts +275 -0
  101. package/src/compiler/template.ts +265 -0
  102. package/src/devserver/crypto.ts +23 -0
  103. package/src/devserver/host.ts +4 -0
  104. package/src/devserver/module.ts +39 -1
  105. package/test/assembly/cookie.spec.ts +302 -0
  106. package/test/assembly/example.spec.ts +5 -1
  107. package/test/assembly/ssr.spec.ts +94 -0
  108. package/test/devserver.test.ts +42 -0
  109. package/test/ssr-render.test.ts +128 -0
  110. 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.
@@ -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.