toiljs 0.0.15 → 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.
- package/.babelrc +13 -13
- package/.gitattributes +2 -2
- package/.github/ISSUE_TEMPLATE/bug_report.md +38 -38
- package/.github/ISSUE_TEMPLATE/bug_report.yml +90 -90
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
- package/.github/PULL_REQUEST_TEMPLATE.md +43 -43
- package/.github/changelog-config.json +45 -45
- package/.github/dependabot.yml +27 -27
- package/.github/workflows/ci.yml +191 -191
- package/.prettierrc.json +11 -11
- package/.vscode/settings.json +9 -9
- package/CHANGELOG.md +116 -5
- package/LICENSE +187 -187
- package/README.md +524 -315
- package/as-pect.asconfig.json +34 -34
- package/as-pect.config.js +65 -65
- package/assets/logo.svg +36 -36
- package/build/backend/.tsbuildinfo +1 -1
- package/build/backend/index.d.ts +1 -0
- package/build/backend/index.js +20 -1
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +1320 -696
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/dev/devtools.d.ts +6 -0
- package/build/client/dev/devtools.js +479 -0
- package/build/client/dev/error-overlay.d.ts +9 -0
- package/build/client/dev/error-overlay.js +19 -4
- package/build/client/errors.d.ts +1 -0
- package/build/client/errors.js +3 -0
- package/build/client/index.d.ts +2 -0
- package/build/client/index.js +2 -0
- package/build/client/navigation/prefetch.d.ts +1 -0
- package/build/client/navigation/prefetch.js +35 -0
- package/build/client/routing/Router.js +1 -1
- package/build/client/routing/hooks.js +6 -2
- package/build/client/routing/loader.d.ts +23 -0
- package/build/client/routing/loader.js +53 -7
- package/build/client/routing/mount.js +4 -3
- package/build/client/rpc.d.ts +1 -0
- package/build/client/rpc.js +37 -0
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +16 -0
- package/build/compiler/config.js +9 -0
- package/build/compiler/docs.js +78 -21
- package/build/compiler/generate.js +5 -4
- package/build/compiler/index.d.ts +3 -2
- package/build/compiler/index.js +2 -2
- package/build/compiler/plugin.js +228 -0
- package/build/compiler/prerender.d.ts +1 -0
- package/build/compiler/prerender.js +1 -1
- package/build/compiler/seo.d.ts +1 -1
- package/build/compiler/seo.js +20 -5
- package/build/compiler/ssg.js +39 -2
- package/build/compiler/vite.js +25 -0
- package/build/io/.tsbuildinfo +1 -1
- package/build/io/codec.d.ts +54 -0
- package/build/io/codec.js +143 -0
- package/build/io/index.d.ts +1 -2
- package/build/io/index.js +1 -2
- package/build/logger/.tsbuildinfo +1 -1
- package/build/shared/.tsbuildinfo +1 -1
- package/eslint.config.js +48 -48
- package/examples/basic/client/404.tsx +11 -11
- package/examples/basic/client/components/.gitkeep +1 -1
- package/examples/basic/client/global-error.tsx +13 -13
- package/examples/basic/client/layout.tsx +25 -25
- package/examples/basic/client/public/images/.gitkeep +1 -1
- package/examples/basic/client/public/images/logo.svg +36 -36
- package/examples/basic/client/public/robots.txt +2 -2
- package/examples/basic/client/routes/docs/[...slug].tsx +12 -12
- package/examples/basic/client/routes/features/error/error.tsx +16 -16
- package/examples/basic/client/routes/features/index.tsx +1 -1
- package/examples/basic/client/routes/features/template/b.tsx +14 -14
- package/examples/basic/client/routes/files/[[...slug]].tsx +21 -21
- package/examples/basic/client/routes/gallery/layout.tsx +13 -13
- package/examples/basic/client/routes/io.tsx +23 -24
- package/examples/basic/client/routes/loader-demo/loading.tsx +13 -13
- package/examples/basic/client/routes/rest.tsx +74 -0
- package/examples/basic/client/routes/rpc.tsx +43 -0
- package/examples/basic/client/routes/search.tsx +61 -61
- package/examples/basic/client/toil.tsx +5 -5
- package/package.json +167 -148
- package/presets/eslint.js +88 -88
- package/presets/no-uint8array-tostring.js +200 -200
- package/presets/prettier-plugin.js +51 -0
- package/presets/prettier.json +19 -18
- package/presets/tsconfig.json +37 -37
- package/server/runtime/README.md +97 -0
- package/server/runtime/abort/abort.ts +27 -0
- package/server/runtime/env/Server.ts +61 -0
- package/server/runtime/envelope.ts +191 -0
- package/server/runtime/exports/index.ts +52 -0
- package/server/runtime/handlers/ToilHandler.ts +34 -0
- package/server/runtime/index.ts +26 -0
- package/server/runtime/lang/Potential.ts +5 -0
- package/server/runtime/memory.ts +81 -0
- package/server/runtime/request.ts +55 -0
- package/server/runtime/response.ts +86 -0
- package/server/runtime/rest/Rest.ts +39 -0
- package/server/runtime/rest/RestHandler.ts +20 -0
- package/server/runtime/rest/RouteContext.ts +82 -0
- package/server/runtime/rest/match.ts +48 -0
- package/server/runtime/tsconfig.json +7 -0
- package/src/backend/index.ts +202 -160
- package/src/cli/create.ts +15 -5
- package/src/cli/diagnostics.ts +81 -0
- package/src/cli/doctor.ts +384 -7
- package/src/cli/index.ts +11 -2
- package/src/cli/proc.ts +50 -50
- package/src/cli/updates.ts +69 -69
- package/src/cli/validate.ts +31 -31
- package/src/client/channel/channel.ts +146 -146
- package/src/client/components/Form.tsx +65 -65
- package/src/client/components/Script.tsx +113 -113
- package/src/client/components/Slot.tsx +21 -21
- package/src/client/dev/devtools.tsx +1018 -0
- package/src/client/dev/error-overlay.tsx +30 -4
- package/src/client/errors.ts +11 -0
- package/src/client/head/head.ts +167 -167
- package/src/client/head/metadata.ts +112 -112
- package/src/client/index.ts +91 -89
- package/src/client/navigation/NavLink.tsx +86 -86
- package/src/client/navigation/navigation.ts +235 -235
- package/src/client/navigation/prefetch.ts +169 -130
- package/src/client/navigation/scroll.ts +53 -53
- package/src/client/routing/Router.tsx +8 -2
- package/src/client/routing/action.ts +122 -122
- package/src/client/routing/error-boundary.tsx +43 -43
- package/src/client/routing/hooks.ts +21 -6
- package/src/client/routing/loader.ts +325 -235
- package/src/client/routing/match.ts +47 -47
- package/src/client/routing/mount.tsx +54 -52
- package/src/client/routing/params-context.ts +10 -10
- package/src/client/routing/slot-context.ts +7 -7
- package/src/client/rpc.ts +64 -0
- package/src/client/search/search.ts +189 -189
- package/src/client/search/use-page-search.ts +73 -73
- package/src/client/types.ts +73 -73
- package/src/compiler/config.ts +221 -182
- package/src/compiler/docs.ts +285 -228
- package/src/compiler/generate.ts +395 -394
- package/src/compiler/index.ts +66 -57
- package/src/compiler/pages.ts +70 -70
- package/src/compiler/plugin.ts +258 -2
- package/src/compiler/prerender.ts +156 -156
- package/src/compiler/seo.ts +417 -390
- package/src/compiler/ssg.ts +171 -126
- package/src/compiler/vite.ts +34 -0
- package/src/io/FastMap.ts +151 -127
- package/src/io/FastSet.ts +15 -1
- package/src/io/codec.ts +217 -0
- package/src/io/index.ts +10 -11
- package/src/io/lengths.ts +14 -14
- package/src/io/types.ts +19 -18
- package/src/logger/index.ts +22 -22
- package/src/shared/index.ts +10 -10
- package/std/client/index.d.ts +15 -15
- package/std/client/package.json +3 -3
- package/test/assembly/example.spec.ts +17 -7
- package/test/channel.test.ts +21 -21
- package/test/doctor.test.ts +65 -0
- package/test/dom/Link.test.tsx +47 -47
- package/test/dom/NavLink.test.tsx +37 -37
- package/test/dom/error-overlay.test.tsx +44 -44
- package/test/dom/loader.test.tsx +121 -121
- package/test/dom/navigation.test.ts +59 -59
- package/test/dom/revalidate.test.tsx +38 -38
- package/test/dom/route-head.test.tsx +78 -78
- package/test/dom/router-loading.test.tsx +44 -44
- package/test/dom/scroll.test.ts +56 -56
- package/test/dom/use-metadata.test.tsx +58 -58
- package/test/errors.test.ts +21 -0
- package/test/io.test.ts +117 -93
- package/test/navlink.test.ts +28 -28
- package/test/placeholder.test.ts +9 -9
- package/test/prettier-plugin.test.ts +46 -0
- package/test/routes.test.ts +76 -76
- package/test/rpc.test.ts +50 -0
- package/test/seo.test.ts +175 -164
- package/test/slot-layouts.test.ts +69 -69
- package/test/ssg.test.ts +36 -36
- package/test/update.test.ts +44 -44
- package/test/validate.test.ts +42 -42
- package/tests/data-parity/generated-parity.ts +99 -0
- package/tests/data-parity/parity.ts +80 -0
- package/tests/data-parity/spec.ts +46 -0
- package/toil-routes.d.ts +7 -0
- package/tsconfig.backend.json +13 -13
- package/tsconfig.base.json +35 -35
- package/tsconfig.cli.json +13 -13
- package/tsconfig.client.json +14 -14
- package/tsconfig.compiler.json +13 -13
- package/tsconfig.io.json +12 -12
- package/tsconfig.json +22 -22
- package/tsconfig.logger.json +12 -12
- package/tsconfig.server.json +10 -10
- package/tsconfig.shared.json +12 -12
- package/vitest.config.ts +26 -26
- package/.idea/codeStyles/Project.xml +0 -54
- package/.idea/codeStyles/codeStyleConfig.xml +0 -5
- package/.idea/inspectionProfiles/Project_Default.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/prettier.xml +0 -7
- package/.idea/toiljs.iml +0 -8
- package/.idea/vcs.xml +0 -6
- package/.toil/entry.tsx +0 -9
- package/.toil/index.html +0 -12
- package/.toil/routes.ts +0 -9
- package/build/cli/configure.d.ts +0 -16
- package/build/cli/configure.js +0 -272
- package/build/cli/create.d.ts +0 -16
- package/build/cli/create.js +0 -420
- package/build/cli/diagnostics.d.ts +0 -55
- package/build/cli/diagnostics.js +0 -333
- package/build/cli/doctor.d.ts +0 -6
- package/build/cli/doctor.js +0 -249
- package/build/cli/features.d.ts +0 -25
- package/build/cli/features.js +0 -107
- package/build/cli/index.d.ts +0 -2
- package/build/cli/proc.d.ts +0 -6
- package/build/cli/proc.js +0 -31
- package/build/cli/ui.d.ts +0 -9
- package/build/cli/ui.js +0 -75
- package/build/cli/update.d.ts +0 -7
- package/build/cli/update.js +0 -117
- package/build/cli/updates.d.ts +0 -10
- package/build/cli/updates.js +0 -45
- package/build/cli/validate.d.ts +0 -4
- package/build/cli/validate.js +0 -19
- package/build/client/Link.d.ts +0 -8
- package/build/client/Link.js +0 -44
- package/build/client/NavLink.d.ts +0 -14
- package/build/client/NavLink.js +0 -37
- package/build/client/Router.d.ts +0 -7
- package/build/client/Router.js +0 -55
- package/build/client/channel.d.ts +0 -23
- package/build/client/channel.js +0 -94
- package/build/client/error-boundary.d.ts +0 -16
- package/build/client/error-boundary.js +0 -19
- package/build/client/head.d.ts +0 -26
- package/build/client/head.js +0 -87
- package/build/client/hooks.d.ts +0 -17
- package/build/client/hooks.js +0 -48
- package/build/client/lazy.d.ts +0 -16
- package/build/client/lazy.js +0 -53
- package/build/client/match.d.ts +0 -2
- package/build/client/match.js +0 -32
- package/build/client/mount.d.ts +0 -2
- package/build/client/mount.js +0 -13
- package/build/client/navigation.d.ts +0 -13
- package/build/client/navigation.js +0 -97
- package/build/client/params-context.d.ts +0 -2
- package/build/client/params-context.js +0 -2
- package/build/client/prefetch.d.ts +0 -11
- package/build/client/prefetch.js +0 -100
- package/build/client/runtime.d.ts +0 -31
- package/build/client/runtime.js +0 -112
- package/build/client/scroll.d.ts +0 -8
- package/build/client/scroll.js +0 -36
- package/build/io/BinaryReader.d.ts +0 -44
- package/build/io/BinaryReader.js +0 -244
- package/build/io/BinaryWriter.d.ts +0 -44
- package/build/io/BinaryWriter.js +0 -297
- package/build/server/release.wasm +0 -0
- package/build/server/release.wat +0 -9
- package/src/io/BinaryReader.ts +0 -340
- package/src/io/BinaryWriter.ts +0 -385
- package/src/server/index.ts +0 -10
- package/src/server/main.ts +0 -13
- package/src/server/tsconfig.json +0 -4
- package/toil-env.d.ts +0 -16
- package/toilconfig.json +0 -30
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class every toiljs server-side handler extends, analog to
|
|
3
|
+
* btc-runtime's `OP_NET`. Override `handle(req)` to produce the
|
|
4
|
+
* response. The framework calls `onRequestStarted` / `onRequestCompleted`
|
|
5
|
+
* around every call so the user can hook for logging or metrics
|
|
6
|
+
* without re-implementing `handle`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Request } from '../request';
|
|
10
|
+
import { Response } from '../response';
|
|
11
|
+
|
|
12
|
+
export class ToilHandler {
|
|
13
|
+
/**
|
|
14
|
+
* Override to declare your routes. The default implementation
|
|
15
|
+
* returns a generic 404 so a handler that hasn't been wired up
|
|
16
|
+
* still produces a valid envelope.
|
|
17
|
+
*/
|
|
18
|
+
public handle(_req: Request): Response {
|
|
19
|
+
return Response.notFound();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Called before each `handle` call. Empty by default; override
|
|
24
|
+
* for per-request setup (logging, header reads, etc.).
|
|
25
|
+
*/
|
|
26
|
+
public onRequestStarted(_req: Request): void {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Called after each `handle` call returns (also when it throws,
|
|
30
|
+
* after the runtime has converted the throw into a 500). Empty by
|
|
31
|
+
* default.
|
|
32
|
+
*/
|
|
33
|
+
public onRequestCompleted(_req: Request, _resp: Response): void {}
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the toiljs server runtime, analog to
|
|
3
|
+
* `@btc-vision/btc-runtime/runtime`. The user does
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* import { Server, ToilHandler, Response } from './runtime';
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* and then assigns `Server.handler = () => new MyHandler()` in their
|
|
10
|
+
* `main.ts`. The wasm `handle(i32, i32) -> i64` export comes from
|
|
11
|
+
* `./exports`, which the user re-exports with `export * from
|
|
12
|
+
* './runtime/exports'`. The `abort` runtime hook comes from
|
|
13
|
+
* `./abort/abort`, which the user re-exports as a top-level `abort`
|
|
14
|
+
* function in their `main.ts`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export { Header, Method, Request } from './request';
|
|
18
|
+
export { Response } from './response';
|
|
19
|
+
export { ToilHandler } from './handlers/ToilHandler';
|
|
20
|
+
export { Server, ServerEnvironment } from './env/Server';
|
|
21
|
+
|
|
22
|
+
// HTTP layer (`@rest` / `@route`).
|
|
23
|
+
export { Rest, RestRegistry, RouteFn } from './rest/Rest';
|
|
24
|
+
export { RouteContext } from './rest/RouteContext';
|
|
25
|
+
export { matchRoute } from './rest/match';
|
|
26
|
+
export { RestHandler } from './rest/RestHandler';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny load/store wrappers around AssemblyScript's linear-memory
|
|
3
|
+
* intrinsics. The envelope codec stays readable when it calls
|
|
4
|
+
* `readU16(p)` instead of `load<u16>(p)` everywhere.
|
|
5
|
+
*
|
|
6
|
+
* All reads are little-endian (matches the wire format produced by
|
|
7
|
+
* `toil-backend/src/http/envelope.rs`).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
@inline export function readU8(ofs: usize): u8 {
|
|
11
|
+
return load<u8>(ofs);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@inline export function readU16(ofs: usize): u16 {
|
|
15
|
+
return load<u16>(ofs);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@inline export function readU32(ofs: usize): u32 {
|
|
19
|
+
return load<u32>(ofs);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@inline export function writeU8(ofs: usize, v: u8): void {
|
|
23
|
+
store<u8>(ofs, v);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@inline export function writeU16(ofs: usize, v: u16): void {
|
|
27
|
+
store<u16>(ofs, v);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@inline export function writeU32(ofs: usize, v: u32): void {
|
|
31
|
+
store<u32>(ofs, v);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Read `len` bytes starting at `ofs` and return them as a fresh
|
|
36
|
+
* `Uint8Array` (copying out of linear memory). Used for the request
|
|
37
|
+
* body so the handler can hold onto it past the call to `dispatch`.
|
|
38
|
+
*/
|
|
39
|
+
export function readBytes(ofs: usize, len: u32): Uint8Array {
|
|
40
|
+
const out = new Uint8Array(<i32>len);
|
|
41
|
+
memory.copy(changetype<usize>(out.dataStart), ofs, <usize>len);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write `bytes` into linear memory starting at `ofs`. Returns the
|
|
47
|
+
* number of bytes written.
|
|
48
|
+
*/
|
|
49
|
+
export function writeBytes(ofs: usize, bytes: Uint8Array): u32 {
|
|
50
|
+
const n = <u32>bytes.length;
|
|
51
|
+
memory.copy(ofs, changetype<usize>(bytes.dataStart), <usize>n);
|
|
52
|
+
return n;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Decode `len` bytes of UTF-8 at `ofs` into a string. The bytes
|
|
57
|
+
* remain in linear memory; the returned string is a fresh AS string.
|
|
58
|
+
*/
|
|
59
|
+
export function readUtf8(ofs: usize, len: u32): string {
|
|
60
|
+
return String.UTF8.decodeUnsafe(ofs, <usize>len);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Encode `s` as UTF-8 into linear memory starting at `ofs`. Returns
|
|
65
|
+
* the number of bytes written.
|
|
66
|
+
*/
|
|
67
|
+
export function writeUtf8(ofs: usize, s: string): u32 {
|
|
68
|
+
const buf = String.UTF8.encode(s);
|
|
69
|
+
const n = <u32>buf.byteLength;
|
|
70
|
+
memory.copy(ofs, changetype<usize>(buf), <usize>n);
|
|
71
|
+
return n;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* UTF-8 byte length of `s` without actually writing it anywhere.
|
|
76
|
+
* Used by the response encoder to pre-compute total envelope size
|
|
77
|
+
* before laying out the bytes.
|
|
78
|
+
*/
|
|
79
|
+
@inline export function utf8Length(s: string): u32 {
|
|
80
|
+
return <u32>String.UTF8.byteLength(s, false);
|
|
81
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The incoming HTTP request handed to the user's handler. Decoded
|
|
3
|
+
* from the wire envelope the host wrote at offset 0 of linear
|
|
4
|
+
* memory. See `envelope.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export enum Method {
|
|
8
|
+
GET = 0,
|
|
9
|
+
POST = 1,
|
|
10
|
+
PUT = 2,
|
|
11
|
+
DELETE = 3,
|
|
12
|
+
PATCH = 4,
|
|
13
|
+
HEAD = 5,
|
|
14
|
+
OPTIONS = 6,
|
|
15
|
+
UNKNOWN = 255,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class Header {
|
|
19
|
+
name: string;
|
|
20
|
+
value: string;
|
|
21
|
+
|
|
22
|
+
constructor(name: string, value: string) {
|
|
23
|
+
this.name = name;
|
|
24
|
+
this.value = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Request {
|
|
29
|
+
method: Method;
|
|
30
|
+
path: string;
|
|
31
|
+
headers: Array<Header>;
|
|
32
|
+
body: Uint8Array;
|
|
33
|
+
|
|
34
|
+
constructor(method: Method, path: string, headers: Array<Header>, body: Uint8Array) {
|
|
35
|
+
this.method = method;
|
|
36
|
+
this.path = path;
|
|
37
|
+
this.headers = headers;
|
|
38
|
+
this.body = body;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Case-insensitive header lookup. Returns `null` if not present.
|
|
43
|
+
* O(n) over the header list; the request typically carries fewer
|
|
44
|
+
* than a dozen, so the linear scan is the right call.
|
|
45
|
+
*/
|
|
46
|
+
header(name: string): string | null {
|
|
47
|
+
const lower = name.toLowerCase();
|
|
48
|
+
for (let i = 0; i < this.headers.length; i++) {
|
|
49
|
+
if (this.headers[i].name.toLowerCase() == lower) {
|
|
50
|
+
return this.headers[i].value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The response the user's handler builds. Serialised into a wire
|
|
3
|
+
* envelope at a fixed offset before `handle()` returns. See
|
|
4
|
+
* `envelope.ts` and `dispatch.ts`.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Header } from './request';
|
|
8
|
+
|
|
9
|
+
export class Response {
|
|
10
|
+
status: u16;
|
|
11
|
+
headers: Array<Header>;
|
|
12
|
+
body: Uint8Array;
|
|
13
|
+
|
|
14
|
+
constructor(status: u16, body: Uint8Array, headers: Array<Header> | null = null) {
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.body = body;
|
|
17
|
+
this.headers = headers != null ? headers : new Array<Header>();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public static text(body: string, status: u16 = 200): Response {
|
|
21
|
+
const buf = String.UTF8.encode(body);
|
|
22
|
+
const bytes = Uint8Array.wrap(buf);
|
|
23
|
+
const r = new Response(status, bytes);
|
|
24
|
+
|
|
25
|
+
r.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
26
|
+
|
|
27
|
+
return r;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public static html(body: string, status: u16 = 200): Response {
|
|
31
|
+
const buf = String.UTF8.encode(body);
|
|
32
|
+
const bytes = Uint8Array.wrap(buf);
|
|
33
|
+
const r = new Response(status, bytes);
|
|
34
|
+
|
|
35
|
+
r.setHeader('content-type', 'text/html; charset=utf-8');
|
|
36
|
+
|
|
37
|
+
return r;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static json(body: string, status: u16 = 200): Response {
|
|
41
|
+
const buf = String.UTF8.encode(body);
|
|
42
|
+
const bytes = Uint8Array.wrap(buf);
|
|
43
|
+
const r = new Response(status, bytes);
|
|
44
|
+
|
|
45
|
+
r.setHeader('content-type', 'application/json; charset=utf-8');
|
|
46
|
+
|
|
47
|
+
return r;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A raw binary body, tagged `application/octet-stream`. Used by `@route`
|
|
52
|
+
* methods with `stream: DataStream.Binary` to ship a `@data` `encode()`.
|
|
53
|
+
*/
|
|
54
|
+
public static bytes(body: Uint8Array, status: u16 = 200): Response {
|
|
55
|
+
const r = new Response(status, body);
|
|
56
|
+
|
|
57
|
+
r.setHeader('content-type', 'application/octet-stream');
|
|
58
|
+
|
|
59
|
+
return r;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public static notFound(): Response {
|
|
63
|
+
return Response.text('not found\n', 404);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public static badRequest(msg: string = 'bad request'): Response {
|
|
67
|
+
return Response.text(msg + '\n', 400);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public static internalError(msg: string = 'internal error'): Response {
|
|
71
|
+
return Response.text(msg + '\n', 500);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public static empty(status: u16): Response {
|
|
75
|
+
return new Response(status, new Uint8Array(0));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Builder-style: returns `this` so calls can chain.
|
|
80
|
+
*/
|
|
81
|
+
public setHeader(name: string, value: string): Response {
|
|
82
|
+
this.headers.push(new Header(name, value));
|
|
83
|
+
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The auto-populated REST router. Every `@rest` controller self-registers a
|
|
3
|
+
* dispatcher here at module init (compiler-injected); your handler calls
|
|
4
|
+
* `Rest.dispatch(req)` to try them all. The first controller that matches the
|
|
5
|
+
* method + path wins; `null` means no route matched (fall through to your own
|
|
6
|
+
* logic / static files / 404).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Request } from '../request';
|
|
10
|
+
import { Response } from '../response';
|
|
11
|
+
|
|
12
|
+
/** A controller dispatcher: returns a Response on a route hit, null on a miss. */
|
|
13
|
+
export type RouteFn = (req: Request) => Response | null;
|
|
14
|
+
|
|
15
|
+
export class RestRegistry {
|
|
16
|
+
private fns: Array<RouteFn> = new Array<RouteFn>();
|
|
17
|
+
|
|
18
|
+
/** Compiler-injected: registers a controller's dispatcher. Not for direct use. */
|
|
19
|
+
register(fn: RouteFn): void {
|
|
20
|
+
this.fns.push(fn);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Try every registered controller in registration order; first match wins. */
|
|
24
|
+
dispatch(req: Request): Response | null {
|
|
25
|
+
for (let i = 0; i < this.fns.length; i++) {
|
|
26
|
+
const hit = this.fns[i](req);
|
|
27
|
+
if (hit != null) return hit;
|
|
28
|
+
}
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Number of registered controllers (diagnostics / tests). */
|
|
33
|
+
get size(): i32 {
|
|
34
|
+
return this.fns.length;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The process-wide REST router singleton. */
|
|
39
|
+
export const Rest: RestRegistry = new RestRegistry();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A drop-in handler for REST-only projects: dispatches to every `@rest`
|
|
3
|
+
* controller and 404s on a miss. Wire it with
|
|
4
|
+
* `Server.handler = () => new RestHandler()`. If you need custom logic, skip
|
|
5
|
+
* this and call `Rest.dispatch(req)` from your own `ToilHandler` instead.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { Request } from '../request';
|
|
9
|
+
import { Response } from '../response';
|
|
10
|
+
import { ToilHandler } from '../handlers/ToilHandler';
|
|
11
|
+
import { Rest } from './Rest';
|
|
12
|
+
|
|
13
|
+
export class RestHandler extends ToilHandler {
|
|
14
|
+
/** Dispatches to the registered `@rest` controllers; returns 404 when none match. */
|
|
15
|
+
handle(req: Request): Response {
|
|
16
|
+
const hit = Rest.dispatch(req);
|
|
17
|
+
if (hit != null) return hit;
|
|
18
|
+
return Response.notFound();
|
|
19
|
+
}
|
|
20
|
+
}
|