toiljs 0.0.16 → 0.0.19

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 (100) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +313 -128
  3. package/as-pect.config.js +1 -1
  4. package/build/backend/.tsbuildinfo +1 -1
  5. package/build/backend/index.d.ts +1 -0
  6. package/build/backend/index.js +20 -1
  7. package/build/cli/.tsbuildinfo +1 -1
  8. package/build/cli/index.js +1320 -696
  9. package/build/client/.tsbuildinfo +1 -1
  10. package/build/client/dev/devtools.js +42 -5
  11. package/build/client/errors.d.ts +1 -0
  12. package/build/client/errors.js +3 -0
  13. package/build/client/index.d.ts +2 -0
  14. package/build/client/index.js +2 -0
  15. package/build/client/rpc.d.ts +1 -0
  16. package/build/client/rpc.js +37 -0
  17. package/build/compiler/.tsbuildinfo +1 -1
  18. package/build/compiler/config.js +3 -1
  19. package/build/compiler/docs.js +62 -5
  20. package/build/compiler/generate.js +5 -4
  21. package/build/compiler/index.d.ts +1 -0
  22. package/build/compiler/index.js +1 -1
  23. package/build/compiler/plugin.js +80 -8
  24. package/build/compiler/seo.js +15 -1
  25. package/build/compiler/ssg.js +7 -1
  26. package/build/compiler/vite.js +25 -0
  27. package/build/io/.tsbuildinfo +1 -1
  28. package/build/io/codec.d.ts +54 -0
  29. package/build/io/codec.js +143 -0
  30. package/build/io/index.d.ts +1 -2
  31. package/build/io/index.js +1 -2
  32. package/eslint.config.js +1 -1
  33. package/examples/basic/client/routes/features/index.tsx +1 -1
  34. package/examples/basic/client/routes/io.tsx +6 -7
  35. package/examples/basic/client/routes/rest.tsx +74 -0
  36. package/examples/basic/client/routes/rpc.tsx +43 -0
  37. package/package.json +19 -7
  38. package/presets/prettier-plugin.js +51 -0
  39. package/presets/prettier.json +1 -0
  40. package/server/runtime/README.md +97 -0
  41. package/server/runtime/abort/abort.ts +27 -0
  42. package/server/runtime/env/Server.ts +61 -0
  43. package/server/runtime/envelope.ts +191 -0
  44. package/server/runtime/exports/index.ts +52 -0
  45. package/server/runtime/handlers/ToilHandler.ts +34 -0
  46. package/server/runtime/index.ts +26 -0
  47. package/server/runtime/lang/Potential.ts +5 -0
  48. package/server/runtime/memory.ts +81 -0
  49. package/server/runtime/request.ts +55 -0
  50. package/server/runtime/response.ts +86 -0
  51. package/server/runtime/rest/Rest.ts +39 -0
  52. package/server/runtime/rest/RestHandler.ts +20 -0
  53. package/server/runtime/rest/RouteContext.ts +82 -0
  54. package/server/runtime/rest/match.ts +48 -0
  55. package/server/runtime/tsconfig.json +7 -0
  56. package/src/backend/index.ts +45 -3
  57. package/src/cli/create.ts +15 -5
  58. package/src/cli/diagnostics.ts +81 -0
  59. package/src/cli/doctor.ts +384 -7
  60. package/src/cli/index.ts +11 -2
  61. package/src/client/dev/devtools.tsx +49 -4
  62. package/src/client/errors.ts +11 -0
  63. package/src/client/index.ts +2 -0
  64. package/src/client/rpc.ts +64 -0
  65. package/src/compiler/config.ts +3 -1
  66. package/src/compiler/docs.ts +62 -5
  67. package/src/compiler/generate.ts +6 -5
  68. package/src/compiler/index.ts +3 -1
  69. package/src/compiler/plugin.ts +99 -11
  70. package/src/compiler/seo.ts +23 -3
  71. package/src/compiler/ssg.ts +10 -1
  72. package/src/compiler/vite.ts +34 -0
  73. package/src/io/FastMap.ts +24 -0
  74. package/src/io/FastSet.ts +15 -1
  75. package/src/io/codec.ts +217 -0
  76. package/src/io/index.ts +1 -2
  77. package/src/io/types.ts +2 -1
  78. package/test/assembly/example.spec.ts +14 -4
  79. package/test/doctor.test.ts +65 -0
  80. package/test/errors.test.ts +21 -0
  81. package/test/io.test.ts +65 -41
  82. package/test/prettier-plugin.test.ts +46 -0
  83. package/test/rpc.test.ts +50 -0
  84. package/tests/data-parity/generated-parity.ts +99 -0
  85. package/tests/data-parity/parity.ts +80 -0
  86. package/tests/data-parity/spec.ts +46 -0
  87. package/tsconfig.json +1 -1
  88. package/tsconfig.server.json +1 -1
  89. package/build/io/BinaryReader.d.ts +0 -44
  90. package/build/io/BinaryReader.js +0 -244
  91. package/build/io/BinaryWriter.d.ts +0 -44
  92. package/build/io/BinaryWriter.js +0 -297
  93. package/build/server/release.wasm +0 -0
  94. package/build/server/release.wat +0 -9
  95. package/src/io/BinaryReader.ts +0 -340
  96. package/src/io/BinaryWriter.ts +0 -385
  97. package/src/server/index.ts +0 -10
  98. package/src/server/main.ts +0 -13
  99. package/src/server/tsconfig.json +0 -4
  100. package/toilconfig.json +0 -30
@@ -0,0 +1,74 @@
1
+ // Demo of the generated REST fetch client (see ../../shared/server.ts, emitted by the
2
+ // server build from the `@rest` controllers in server/api.ts). Unlike `Server.<service>`
3
+ // RPC, this is working code: `Server.REST.<controller>.<route>(args)` is a real, typed
4
+ // `fetch`. `args` is `{ params?, body?, query?, headers? }`; a `@data` return is parsed
5
+ // into its typed class, and a route that returns a `Response` hands you the raw fetch
6
+ // `Response` to inspect (status, `.json()`, ...). Needs the server running to respond.
7
+ import { useState } from 'react';
8
+
9
+ import { NewPlayer, type Player, ScoreDelta } from 'shared/server';
10
+
11
+ export default function RestDemo() {
12
+ const [log, setLog] = useState<string[]>([]);
13
+ const note = (line: string) => setLog((prev) => [line, ...prev].slice(0, 8));
14
+
15
+ // POST /players -> typed Promise<Player>, body is a @data class.
16
+ const onCreate = async () => {
17
+ try {
18
+ const names = ['Ada', 'Linus', 'Grace', 'Dennis'];
19
+ const name = names[Math.floor(Math.random() * names.length)];
20
+ const player = await Server.REST.players.create({ body: new NewPlayer(name) });
21
+ note(`created #${player.id} ${player.name}`);
22
+ } catch (err) {
23
+ note(parseError(err));
24
+ }
25
+ };
26
+
27
+ // POST /players/:id/score -> path param + body; route returns a Response, so we get
28
+ // the raw fetch Response and parse it ourselves.
29
+ const onScore = async () => {
30
+ try {
31
+ const points = BigInt(1 + Math.floor(Math.random() * 10));
32
+ const res = await Server.REST.players.addScore({
33
+ params: { id: 1 },
34
+ body: new ScoreDelta(points)
35
+ });
36
+ if (!res.ok) {
37
+ note(`addScore -> ${res.status} (create player #1 first)`);
38
+ return;
39
+ }
40
+ const p = (await res.json()) as Player;
41
+ note(`#${p.id} ${p.name} -> ${p.score}`);
42
+ } catch (err) {
43
+ note(parseError(err));
44
+ }
45
+ };
46
+
47
+ // GET /leaderboard -> typed Promise<Standings>, a @data wrapper of Player[].
48
+ const onBoard = async () => {
49
+ try {
50
+ const board = await Server.REST.leaderboard.top();
51
+ note('leaderboard: ' + board.players.map((p) => `${p.name}(${p.score})`).join(', '));
52
+ } catch (err) {
53
+ note(parseError(err));
54
+ }
55
+ };
56
+
57
+ return (
58
+ <main>
59
+ <h1>REST</h1>
60
+ <p>
61
+ <code>Server.REST.*</code> is a real, typed <code>fetch</code> client generated from the{' '}
62
+ <code>@rest</code> controllers. It needs the server running to respond.
63
+ </p>
64
+ <button onClick={onCreate}>create player</button> <button onClick={onScore}>award points to #1</button>{' '}
65
+ <button onClick={onBoard}>leaderboard</button>
66
+ <ul>
67
+ {log.map((line, i) => (
68
+ <li key={i}>{line}</li>
69
+ ))}
70
+ </ul>
71
+ <Toil.Link href="/">Back home</Toil.Link>
72
+ </main>
73
+ );
74
+ }
@@ -0,0 +1,43 @@
1
+ // Demo of the generated, typed `Server` RPC surface (see ../../shared/server.ts, emitted
2
+ // by the server build from `@service`/`@remote`). `Server` is global (no import) and typed
3
+ // from the server: `Server.ping(n)` (free `@remote`) and `Server.admin.reset()` (a
4
+ // `@service` method). Transport is not wired yet, so a real call throws; this page shows
5
+ // the typing and reports the stub error via the global `parseError`.
6
+ import { useState } from 'react';
7
+
8
+ export default function RpcDemo() {
9
+ const [result, setResult] = useState('not called');
10
+
11
+ // Scalar in / scalar out: Server.ping is typed (n: number) => Promise<number>.
12
+ const onPing = async () => {
13
+ try {
14
+ const next = await Server.ping(10);
15
+ setResult(`ping -> ${next}`);
16
+ } catch (err) {
17
+ setResult(parseError(err));
18
+ }
19
+ };
20
+
21
+ // A @service method: namespaced under its service key.
22
+ const onReset = async () => {
23
+ try {
24
+ await Server.admin.reset();
25
+ setResult('admin.reset -> store cleared');
26
+ } catch (err) {
27
+ setResult(parseError(err));
28
+ }
29
+ };
30
+
31
+ return (
32
+ <main>
33
+ <h1>RPC</h1>
34
+ <p>
35
+ <code>Server</code> (RPC) is typed from the server build, no import. Calling throws until the transport
36
+ lands. For working server calls today, use the REST client.
37
+ </p>
38
+ <button onClick={onPing}>Server.ping(10)</button> <button onClick={onReset}>Server.admin.reset()</button>
39
+ <p>{result}</p>
40
+ <Toil.Link href="/rest">See the REST demo</Toil.Link> · <Toil.Link href="/">Back home</Toil.Link>
41
+ </main>
42
+ );
43
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "toiljs",
3
3
  "type": "module",
4
- "version": "0.0.16",
4
+ "version": "0.0.19",
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": {
@@ -50,9 +50,22 @@
50
50
  "import": "./build/io/index.js",
51
51
  "default": "./build/io/index.js"
52
52
  },
53
+ "./server/runtime": {
54
+ "types": "./server/runtime/index.ts",
55
+ "default": "./server/runtime/index.ts"
56
+ },
57
+ "./server/runtime/exports": {
58
+ "types": "./server/runtime/exports/index.ts",
59
+ "default": "./server/runtime/exports/index.ts"
60
+ },
61
+ "./server/runtime/*": {
62
+ "types": "./server/runtime/*.ts",
63
+ "default": "./server/runtime/*.ts"
64
+ },
53
65
  "./tsconfig": "./presets/tsconfig.json",
54
66
  "./eslint": "./presets/eslint.js",
55
- "./prettier": "./presets/prettier.json"
67
+ "./prettier": "./presets/prettier.json",
68
+ "./prettier-plugin": "./presets/prettier-plugin.js"
56
69
  },
57
70
  "bin": {
58
71
  "toiljs": "./build/cli/index.js"
@@ -77,7 +90,7 @@
77
90
  },
78
91
  "scripts": {
79
92
  "watch": "tsc -p tsconfig.json --watch",
80
- "build": "npm run build:shared && npm run build:logger && npm run build:client && npm run build:io && npm run build:backend && npm run build:compiler && npm run build:cli && npm run build:server",
93
+ "build": "npm run build:shared && npm run build:logger && npm run build:client && npm run build:io && npm run build:backend && npm run build:compiler && npm run build:cli",
81
94
  "build:shared": "tsc -p tsconfig.shared.json",
82
95
  "build:logger": "tsc -p tsconfig.logger.json",
83
96
  "build:client": "tsc -p tsconfig.client.json",
@@ -85,7 +98,6 @@
85
98
  "build:backend": "tsc -p tsconfig.backend.json",
86
99
  "build:compiler": "tsc -p tsconfig.compiler.json",
87
100
  "build:cli": "tsc -p tsconfig.cli.json --noEmit && esbuild src/cli/index.ts --bundle --platform=node --format=esm --outfile=build/cli/index.js --external:toiljs/*",
88
- "build:server": "toilscript --target release",
89
101
  "test": "vitest run --coverage",
90
102
  "test:watch": "vitest",
91
103
  "test:ui": "vitest --ui",
@@ -94,7 +106,7 @@
94
106
  "test:server:ci": "asp --config as-pect.config.js --summary --no-logo",
95
107
  "test:all": "npm run test && npm run test:server",
96
108
  "lint": "eslint src",
97
- "docs": "typedoc --out docs --tsconfig tsconfig.json --readme README.md --name TOILJS --plugin typedoc-material-theme --themeColor '#cb9820' --exclude src/server src",
109
+ "docs": "typedoc --out docs --tsconfig tsconfig.json --readme README.md --name TOILJS --plugin typedoc-material-theme --themeColor '#cb9820' src",
98
110
  "setup": "npm i && npm run build"
99
111
  },
100
112
  "dependencies": {
@@ -109,7 +121,7 @@
109
121
  "eslint-plugin-react-refresh": "^0.5.2",
110
122
  "picocolors": "^1.1.1",
111
123
  "sharp": "^0.34.5",
112
- "toilscript": "^0.1.4",
124
+ "toilscript": "^0.1.11",
113
125
  "typescript-eslint": "^8.60.0",
114
126
  "vite": "^8.0.14",
115
127
  "vite-imagetools": "^10.0.0",
@@ -124,7 +136,6 @@
124
136
  },
125
137
  "devDependencies": {
126
138
  "@babel/core": "^7.29.7",
127
- "@clack/prompts": "^1.5.0",
128
139
  "@babel/preset-env": "^7.29.7",
129
140
  "@babel/preset-typescript": "^7.29.7",
130
141
  "@btc-vision/as-covers-assembly": "^0.4.5",
@@ -132,6 +143,7 @@
132
143
  "@btc-vision/as-pect-assembly": "^8.3.0",
133
144
  "@btc-vision/as-pect-cli": "^8.3.0",
134
145
  "@btc-vision/as-pect-transform": "^8.3.0",
146
+ "@clack/prompts": "^1.5.0",
135
147
  "@microsoft/api-extractor": "7.58.7",
136
148
  "@testing-library/dom": "^10.4.1",
137
149
  "@testing-library/react": "^16.3.2",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Prettier plugin that lets prettier format toilscript server code, which uses native
3
+ * decorators on free functions (`@main`, `@remote function ...`). Those are valid
4
+ * toilscript but not valid ECMAScript/TypeScript grammar, so prettier's parser rejects
5
+ * them outright.
6
+ *
7
+ * The fix is a parse-time/print-time round-trip: in `preprocess` each function decorator
8
+ * is rewritten to a marker block comment (which the TS parser accepts), and the estree
9
+ * printer's `printComment` renders that marker back as the original `@decorator`. Class
10
+ * and method decorators are already valid grammar and pass through untouched.
11
+ */
12
+ import * as tsPlugin from 'prettier/plugins/typescript';
13
+ import * as estreePlugin from 'prettier/plugins/estree';
14
+
15
+ const baseTs = tsPlugin.parsers.typescript;
16
+ const baseEstree = estreePlugin.printers.estree;
17
+
18
+ const MARKER = '::toil-decorator ';
19
+ // One-or-more bare decorators (`@name`, no args) immediately before a function declaration.
20
+ const FN_DECORATORS =
21
+ /((?:@[A-Za-z_$][\w$]*[ \t]*\r?\n[ \t]*)+)((?:export[ \t]+)?(?:default[ \t]+)?(?:async[ \t]+)?function\b)/g;
22
+ const ONE_DECORATOR = /@([A-Za-z_$][\w$]*)([ \t]*\r?\n[ \t]*)/g;
23
+
24
+ function preprocess(text, options) {
25
+ const pre = baseTs.preprocess ? baseTs.preprocess(text, options) : text;
26
+ return pre.replace(FN_DECORATORS, (_match, decorators, fn) => {
27
+ const masked = decorators.replace(
28
+ ONE_DECORATOR,
29
+ (_d, name, gap) => `/*${MARKER}${name}*/${gap}`,
30
+ );
31
+ return masked + fn;
32
+ });
33
+ }
34
+
35
+ export const parsers = {
36
+ typescript: { ...baseTs, preprocess },
37
+ };
38
+
39
+ export const printers = {
40
+ estree: {
41
+ ...baseEstree,
42
+ printComment(path, options) {
43
+ const comment = path.node ?? path.getValue();
44
+ const value = comment?.value;
45
+ if (typeof value === 'string' && value.startsWith(MARKER)) {
46
+ return '@' + value.slice(MARKER.length).trim();
47
+ }
48
+ return baseEstree.printComment(path, options);
49
+ },
50
+ },
51
+ };
@@ -1,4 +1,5 @@
1
1
  {
2
+ "plugins": ["toiljs/prettier-plugin"],
2
3
  "printWidth": 120,
3
4
  "tabWidth": 4,
4
5
  "useTabs": false,
@@ -0,0 +1,97 @@
1
+ # toiljs server runtime
2
+
3
+ In-tree SDK that bridges a toilscript handler to the toil-backend
4
+ edge's wasm ABI.
5
+
6
+ ## What it does
7
+
8
+ The edge calls `handle(req_ofs: i32, req_len: i32) -> i64` on every
9
+ request. This runtime gives you:
10
+
11
+ - `Request` / `Response` AssemblyScript types
12
+ - A byte-for-byte envelope codec matching
13
+ `toil-backend/src/http/envelope.rs`
14
+ - A `ToilHandler` base class you extend, plus a `Server` singleton you
15
+ assign `Server.handler = () => new MyHandler()`. The `handle` wasm
16
+ export (re-exported from `toiljs/server/runtime/exports`) decodes the
17
+ request, runs your handler, encodes the response, and returns the
18
+ packed i64 the host expects.
19
+
20
+ ## Wire contract
21
+
22
+ Source of truth: `toil-backend/src/http/envelope.rs`.
23
+
24
+ ```
25
+ request envelope (LE, no padding):
26
+ u8 method 0=GET 1=POST 2=PUT 3=DELETE 4=PATCH 5=HEAD 6=OPTIONS
27
+ u16 path_len
28
+ [u8] path
29
+ u16 n_headers
30
+ for each header: u16 name_len, u16 val_len, [u8] name, [u8] val
31
+ u32 body_len
32
+ [u8] body
33
+ ```
34
+
35
+ The response envelope is the same shape with the first `u8 method +
36
+ u16 path_len + path` replaced by `u16 status`.
37
+
38
+ The handler must return a packed `i64`:
39
+
40
+ ```
41
+ (resp_ofs << 32) | resp_len
42
+ ```
43
+
44
+ The host reads `resp_len` bytes starting at `resp_ofs` in linear
45
+ memory and decodes them as a response envelope.
46
+
47
+ ## Memory layout
48
+
49
+ - `[0, req_len)` — request envelope, written by the host before
50
+ `handle` is called.
51
+ - `[65536, 65536 + resp_len)` — response envelope, written by
52
+ `dispatch` (the response base is the second 64 KiB page so the
53
+ request and response never overlap).
54
+
55
+ The edge enforces a 1024-page (64 MiB) linear memory cap via
56
+ `LimitingTunables`, so leaving the first page for the request is
57
+ fine.
58
+
59
+ ## Example
60
+
61
+ A user app extends `ToilHandler` and wires it up in `server/main.ts`.
62
+ The runtime is consumed as the `toiljs/server/runtime` library export:
63
+
64
+ ```ts
65
+ // server/HelloHandler.ts
66
+ import { ToilHandler, Request, Response } from 'toiljs/server/runtime';
67
+
68
+ export class HelloHandler extends ToilHandler {
69
+ public handle(req: Request): Response {
70
+ if (req.path == '/') return Response.text('hello\n');
71
+ if (req.path == '/json') return Response.json('{"ok":true}');
72
+ return Response.notFound();
73
+ }
74
+ }
75
+ ```
76
+
77
+ ```ts
78
+ // server/main.ts
79
+ import { Server } from 'toiljs/server/runtime';
80
+ import { revertOnError } from 'toiljs/server/runtime/abort/abort';
81
+ import { HelloHandler } from './HelloHandler';
82
+
83
+ Server.handler = () => new HelloHandler();
84
+
85
+ // Surface the wasm `handle(i32, i32) -> i64` entry the edge calls.
86
+ export * from 'toiljs/server/runtime/exports';
87
+
88
+ // Forward AS runtime panics to the host's env::abort import.
89
+ export function abort(message: string, fileName: string, line: u32, column: u32): void {
90
+ revertOnError(message, fileName, line, column);
91
+ }
92
+ ```
93
+
94
+ Compile with `toilscript --target release`, drop the resulting
95
+ `build/server/release.wasm` at `<toil-backend>/hosts/<hostname>.wasm`,
96
+ and the edge will route requests with that `Host:` header to it. A
97
+ complete app lives in `examples/basic`.
@@ -0,0 +1,27 @@
1
+ /**
2
+ * AssemblyScript runtime panic hook.
3
+ *
4
+ * Any unhandled `assert()`, out-of-bounds array access, or other
5
+ * runtime failure in the compiled wasm reaches the host's `env::abort`
6
+ * import (see toil-backend's `AbortImport`). For that import to be
7
+ * satisfied at link time the compiled module needs an exported
8
+ * `abort` function with the AS-standard signature; the user's
9
+ * `main.ts` re-exports it as:
10
+ *
11
+ * ```ts
12
+ * export function abort(message: string, fileName: string, line: u32, column: u32): void {
13
+ * revertOnError(message, fileName, line, column);
14
+ * }
15
+ * ```
16
+ *
17
+ * We just call `unreachable()` after recording the location: wasmer
18
+ * traps the call, the edge's pump catches the trap, marks the
19
+ * instance poisoned, and returns 502 to the client. The location
20
+ * fields are deliberately left untouched in case a future host
21
+ * import wants to read them off the message/fileName strings; today
22
+ * the edge's `AbortImport::execute` only logs `line`/`column`.
23
+ */
24
+
25
+ export function revertOnError(_message: string, _fileName: string, _line: u32, _column: u32): void {
26
+ unreachable();
27
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Server — the runtime singleton, analog to btc-runtime's
3
+ * `Blockchain`.
4
+ *
5
+ * The user's `main.ts` assigns `Server.handler = () => new MyHandler()`.
6
+ * The `handle(req_ofs, req_len)` wasm export in `runtime/exports`
7
+ * calls that factory once per request.
8
+ */
9
+
10
+ import { Potential } from '../lang/Potential';
11
+ import { ToilHandler } from '../handlers/ToilHandler';
12
+
13
+ @final
14
+ export class ServerEnvironment {
15
+ /**
16
+ * The user-supplied handler factory. Assigned at module init by
17
+ * the contract's `main.ts`. We use a factory rather than a
18
+ * pre-built instance so the user gets fresh state per request
19
+ * (the alternative would be threading reset logic through every
20
+ * handler class).
21
+ */
22
+ public handler: () => ToilHandler = defaultHandler;
23
+
24
+ /**
25
+ * Cached handler instance for the current request. Cleared at the
26
+ * end of every dispatch so the next request runs the factory
27
+ * again. Exposed for tests; user code should not touch it.
28
+ */
29
+ public _current: Potential<ToilHandler> = null;
30
+
31
+ /**
32
+ * Build (or reuse) the handler for this request. Called once per
33
+ * dispatch from `runtime/exports::handle`.
34
+ */
35
+ public currentHandler(): ToilHandler {
36
+ if (this._current == null) {
37
+ this._current = this.handler();
38
+ }
39
+ return <ToilHandler>this._current;
40
+ }
41
+
42
+ /**
43
+ * Drop the cached handler so the next request gets a fresh one.
44
+ * Called at the tail of `runtime/exports::handle`.
45
+ */
46
+ public resetCurrentHandler(): void {
47
+ this._current = null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Default factory used until the user's `main.ts` assigns
53
+ * `Server.handler`. Returns a base handler whose `handle` produces a
54
+ * 404 — useful as a no-op state during early bring-up and as a
55
+ * fallback if the user forgot to wire one up.
56
+ */
57
+ function defaultHandler(): ToilHandler {
58
+ return new ToilHandler();
59
+ }
60
+
61
+ export const Server: ServerEnvironment = new ServerEnvironment();
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Wire envelope codec, byte-for-byte compatible with
3
+ * `toil-backend/src/http/envelope.rs`.
4
+ *
5
+ * Layout (LE, no padding):
6
+ *
7
+ * request:
8
+ * u8 method (0=GET, 1=POST, 2=PUT, 3=DELETE,
9
+ * 4=PATCH, 5=HEAD, 6=OPTIONS)
10
+ * u16 path_len
11
+ * [u8] path
12
+ * u16 n_headers
13
+ * for each header: u16 name_len, u16 val_len, [u8] name, [u8] val
14
+ * u32 body_len
15
+ * [u8] body
16
+ *
17
+ * response: same shape but the first u8+u16 (method + path_len)
18
+ * is replaced by `u16 status`.
19
+ */
20
+
21
+ import {
22
+ readBytes,
23
+ readU16,
24
+ readU32,
25
+ readU8,
26
+ readUtf8,
27
+ utf8Length,
28
+ writeBytes,
29
+ writeU16,
30
+ writeU32,
31
+ writeUtf8
32
+ } from './memory';
33
+ import { Header, Method, Request } from './request';
34
+ import { Response } from './response';
35
+
36
+ class DecodeCursor {
37
+ base: usize;
38
+ end: usize;
39
+ cur: usize;
40
+ ok: bool;
41
+
42
+ constructor(base: usize, len: usize) {
43
+ this.base = base;
44
+ this.end = base + len;
45
+ this.cur = base;
46
+ this.ok = true;
47
+ }
48
+
49
+ @inline canTake(n: usize): bool {
50
+ return this.cur + n <= this.end;
51
+ }
52
+
53
+ takeU8(): u8 {
54
+ if (!this.canTake(1)) { this.ok = false; return 0; }
55
+ const v = readU8(this.cur);
56
+ this.cur += 1;
57
+ return v;
58
+ }
59
+
60
+ takeU16(): u16 {
61
+ if (!this.canTake(2)) { this.ok = false; return 0; }
62
+ const v = readU16(this.cur);
63
+ this.cur += 2;
64
+ return v;
65
+ }
66
+
67
+ takeU32(): u32 {
68
+ if (!this.canTake(4)) { this.ok = false; return 0; }
69
+ const v = readU32(this.cur);
70
+ this.cur += 4;
71
+ return v;
72
+ }
73
+
74
+ takeBytes(n: u32): Uint8Array {
75
+ if (!this.canTake(<usize>n)) { this.ok = false; return new Uint8Array(0); }
76
+ const out = readBytes(this.cur, n);
77
+ this.cur += <usize>n;
78
+ return out;
79
+ }
80
+
81
+ takeUtf8(n: u32): string {
82
+ if (!this.canTake(<usize>n)) { this.ok = false; return ''; }
83
+ const s = readUtf8(this.cur, n);
84
+ this.cur += <usize>n;
85
+ return s;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Decode the request envelope the host wrote at `req_ofs`. Returns
91
+ * a populated `Request` on success or `null` on truncation /
92
+ * malformed bytes.
93
+ */
94
+ export function decodeRequest(req_ofs: usize, req_len: usize): Request | null {
95
+ const c = new DecodeCursor(req_ofs, req_len);
96
+ const methodByte = c.takeU8();
97
+ if (!c.ok) return null;
98
+ const method = methodFromByte(methodByte);
99
+
100
+ const pathLen = c.takeU16();
101
+ const path = c.takeUtf8(<u32>pathLen);
102
+ if (!c.ok) return null;
103
+
104
+ const nHeaders = c.takeU16();
105
+ const headers = new Array<Header>();
106
+ for (let i: u32 = 0; i < <u32>nHeaders; i++) {
107
+ const nameLen = c.takeU16();
108
+ const valLen = c.takeU16();
109
+ const name = c.takeUtf8(<u32>nameLen);
110
+ const val = c.takeUtf8(<u32>valLen);
111
+ if (!c.ok) return null;
112
+ headers.push(new Header(name, val));
113
+ }
114
+
115
+ const bodyLen = c.takeU32();
116
+ const body = c.takeBytes(bodyLen);
117
+ if (!c.ok) return null;
118
+
119
+ return new Request(method, path, headers, body);
120
+ }
121
+
122
+ @inline function methodFromByte(b: u8): Method {
123
+ if (b == 0) return Method.GET;
124
+ if (b == 1) return Method.POST;
125
+ if (b == 2) return Method.PUT;
126
+ if (b == 3) return Method.DELETE;
127
+ if (b == 4) return Method.PATCH;
128
+ if (b == 5) return Method.HEAD;
129
+ if (b == 6) return Method.OPTIONS;
130
+ return Method.UNKNOWN;
131
+ }
132
+
133
+ /**
134
+ * Serialise `resp` into linear memory starting at `dst_ofs`.
135
+ * Returns the total byte length written. Status, header count and
136
+ * body length are bounds-checked: header names and values must each
137
+ * fit in u16, the body must fit in u32, the header count must fit in
138
+ * u16. A handler returning an unrepresentable response gets a
139
+ * minimal 500 envelope written instead so the host never sees an
140
+ * encoder fault.
141
+ */
142
+ export function encodeResponse(resp: Response, dst_ofs: usize): u32 {
143
+ // Validate fits-in-u16/u32 limits up front so we never half-write.
144
+ if (resp.headers.length > 0xffff) {
145
+ return encodeFallback500(dst_ofs);
146
+ }
147
+ for (let i = 0; i < resp.headers.length; i++) {
148
+ const h = resp.headers[i];
149
+ if (utf8Length(h.name) > 0xffff || utf8Length(h.value) > 0xffff) {
150
+ return encodeFallback500(dst_ofs);
151
+ }
152
+ }
153
+ if (<u64>resp.body.length > <u64>0xffffffff) {
154
+ return encodeFallback500(dst_ofs);
155
+ }
156
+
157
+ let cur: usize = dst_ofs;
158
+
159
+ writeU16(cur, resp.status);
160
+ cur += 2;
161
+
162
+ writeU16(cur, <u16>resp.headers.length);
163
+ cur += 2;
164
+
165
+ for (let i = 0; i < resp.headers.length; i++) {
166
+ const h = resp.headers[i];
167
+ const nameLen = utf8Length(h.name);
168
+ const valLen = utf8Length(h.value);
169
+ writeU16(cur, <u16>nameLen);
170
+ cur += 2;
171
+ writeU16(cur, <u16>valLen);
172
+ cur += 2;
173
+ cur += writeUtf8(cur, h.name);
174
+ cur += writeUtf8(cur, h.value);
175
+ }
176
+
177
+ const bodyLen = <u32>resp.body.length;
178
+ writeU32(cur, bodyLen);
179
+ cur += 4;
180
+ cur += writeBytes(cur, resp.body);
181
+
182
+ return <u32>(cur - dst_ofs);
183
+ }
184
+
185
+ function encodeFallback500(dst_ofs: usize): u32 {
186
+ // Minimal valid 500 envelope: status + 0 headers + 0-length body.
187
+ writeU16(dst_ofs, 500);
188
+ writeU16(dst_ofs + 2, 0);
189
+ writeU32(dst_ofs + 4, 0);
190
+ return 8;
191
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Wasm exports the edge calls.
3
+ *
4
+ * The user's `main.ts` does
5
+ *
6
+ * ```ts
7
+ * export * from './runtime/exports';
8
+ * ```
9
+ *
10
+ * to surface `handle(i32, i32) -> i64` (and any future entry points)
11
+ * as wasm exports. The actual work — decode the envelope, run the
12
+ * user's handler via `Server.currentHandler()`, encode the response —
13
+ * lives here.
14
+ */
15
+
16
+ import { Server } from '../env/Server';
17
+ import { decodeRequest, encodeResponse } from '../envelope';
18
+ import { Response } from '../response';
19
+
20
+ /**
21
+ * Linear-memory offset where we lay out the response envelope.
22
+ *
23
+ * The host writes the request envelope starting at offset 0. We pick
24
+ * `65536` (one wasm page in) so the response never overlaps with the
25
+ * request, no matter how big the request grew. The edge's
26
+ * LimitingTunables caps the linear memory at 1024 pages (64 MiB), so
27
+ * we still have 63 MiB of room past `RESPONSE_BASE` for the response
28
+ * envelope.
29
+ */
30
+ const RESPONSE_BASE: usize = 65536;
31
+
32
+ @main
33
+ export function handle(req_ofs: i32, req_len: i32): i64 {
34
+ let resp: Response;
35
+
36
+ const req = decodeRequest(<usize>req_ofs, <usize>req_len);
37
+ if (req == null) {
38
+ // Truncated or malformed envelope — host shouldn't send these
39
+ // but produce a clean 400 so the dispatcher doesn't see a
40
+ // garbage return value.
41
+ resp = Response.badRequest('malformed request envelope');
42
+ } else {
43
+ const handler = Server.currentHandler();
44
+ handler.onRequestStarted(req);
45
+ resp = handler.handle(req);
46
+ handler.onRequestCompleted(req, resp);
47
+ }
48
+
49
+ const total = encodeResponse(resp, RESPONSE_BASE);
50
+ Server.resetCurrentHandler();
51
+ return ((<i64>RESPONSE_BASE) << 32) | (<i64>total);
52
+ }