toilscript 0.1.58 → 0.1.60
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/dist/cli.js +615 -20
- package/dist/cli.js.map +2 -2
- package/dist/importmap.json +3 -3
- package/dist/web.js +4 -4
- package/package.json +11 -9
- package/std/assembly/bindings/daemon.ts +69 -0
- package/std/assembly/bindings/toildb.ts +1 -1
- package/std/assembly/bindings/webcrypto.ts +48 -0
- package/std/assembly/daemon.ts +315 -0
- package/std/assembly/toildb.ts +178 -15
- package/std/assembly/toilscript.d.ts +130 -6
package/dist/importmap.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"imports": {
|
|
3
|
-
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
4
|
-
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
5
|
-
"binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0
|
|
3
|
+
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.60/dist/toilscript.js",
|
|
4
|
+
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.60/dist/cli.js",
|
|
5
|
+
"binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0/index.js",
|
|
6
6
|
"long": "https://cdn.jsdelivr.net/npm/long@5.3.2/index.js"
|
|
7
7
|
}
|
|
8
8
|
}
|
package/dist/web.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
var ASSEMBLYSCRIPT_VERSION = "0.1.
|
|
1
|
+
var ASSEMBLYSCRIPT_VERSION = "0.1.60";
|
|
2
2
|
var ASSEMBLYSCRIPT_IMPORTMAP = {
|
|
3
3
|
"imports": {
|
|
4
|
-
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
5
|
-
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.
|
|
6
|
-
"binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0
|
|
4
|
+
"toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.60/dist/toilscript.js",
|
|
5
|
+
"toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.60/dist/cli.js",
|
|
6
|
+
"binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0/index.js",
|
|
7
7
|
"long": "https://cdn.jsdelivr.net/npm/long@5.3.2/index.js"
|
|
8
8
|
}
|
|
9
9
|
};
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"toilscript",
|
|
9
9
|
"wasm"
|
|
10
10
|
],
|
|
11
|
-
"version": "0.1.
|
|
11
|
+
"version": "0.1.60",
|
|
12
12
|
"author": "Daniel Wirtz <dcode+assemblyscript@dcode.io>",
|
|
13
13
|
"license": "Apache-2.0",
|
|
14
14
|
"homepage": "https://github.com/dacely-cloud/toilscript",
|
|
@@ -25,20 +25,20 @@
|
|
|
25
25
|
},
|
|
26
26
|
"engineStrict": true,
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"binaryen": "130.0.0
|
|
29
|
-
"long": "^5.2
|
|
28
|
+
"binaryen": "130.0.0",
|
|
29
|
+
"long": "^5.3.2"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@eslint/js": "^10.0.1",
|
|
33
|
-
"@types/node": "^
|
|
33
|
+
"@types/node": "^26.1.1",
|
|
34
34
|
"as-float": "^1.0.1",
|
|
35
35
|
"diff": "^9.0.0",
|
|
36
|
-
"esbuild": "^0.28.
|
|
37
|
-
"eslint": "^10.
|
|
36
|
+
"esbuild": "^0.28.1",
|
|
37
|
+
"eslint": "^10.6.0",
|
|
38
38
|
"glob": "^13.0.6",
|
|
39
|
-
"globals": "^17.
|
|
39
|
+
"globals": "^17.7.0",
|
|
40
40
|
"typescript": "^6.0.3",
|
|
41
|
-
"typescript-eslint": "^8.
|
|
41
|
+
"typescript-eslint": "^8.63.0"
|
|
42
42
|
},
|
|
43
43
|
"type": "module",
|
|
44
44
|
"exports": {
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"prepublishOnly": "node scripts/build",
|
|
81
81
|
"watch": "node scripts/build --watch",
|
|
82
82
|
"coverage": "npx c8 -- npm test",
|
|
83
|
-
"test": "npm run test:parser && npm run test:compiler -- --parallel && npm run test:browser && npm run test:toilconfig && npm run test:transform && npm run test:cli && npm run test:json && npm run test:data && npm run test:dbresolve && npm run test:dbstatic && npm run test:routekinds && npm run test:derive && npm run test:streams",
|
|
83
|
+
"test": "npm run test:parser && npm run test:compiler -- --parallel && npm run test:browser && npm run test:toilconfig && npm run test:transform && npm run test:cli && npm run test:json && npm run test:data && npm run test:dbresolve && npm run test:dbstatic && npm run test:routekinds && npm run test:derive && npm run test:streams && npm run test:daemon && npm run test:dberror",
|
|
84
84
|
"test:parser": "node --enable-source-maps tests/parser",
|
|
85
85
|
"test:compiler": "node --enable-source-maps --no-warnings tests/compiler",
|
|
86
86
|
"test:browser": "node --enable-source-maps tests/browser",
|
|
@@ -96,6 +96,8 @@
|
|
|
96
96
|
"test:routekinds": "node tests/routekinds/run.mjs",
|
|
97
97
|
"test:derive": "node tests/derive/run.mjs",
|
|
98
98
|
"test:streams": "node tests/streams/run.mjs && node tests/streams/catalog.mjs && node tests/streams/codegen.mjs",
|
|
99
|
+
"test:daemon": "node tests/daemon/run.mjs",
|
|
100
|
+
"test:dberror": "node tests/dberror/run.mjs",
|
|
99
101
|
"asbuild": "npm run asbuild:debug && npm run asbuild:release",
|
|
100
102
|
"asbuild:debug": "node bin/toilscript --config src/toilconfig.json --target debug",
|
|
101
103
|
"asbuild:release": "node bin/toilscript --config src/toilconfig.json --target release",
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Host-import declarations for the daemon (L4 cold box) scheduler surface.
|
|
2
|
+
//
|
|
3
|
+
// Unlike every other host import, these do NOT live under `env`. Production
|
|
4
|
+
// registers them in their OWN wasm module namespace, `("daemon", "<name>")`
|
|
5
|
+
// (toil-backend `src/wasm/cold/imports.rs`, `NS_DAEMON`), and only for a cold
|
|
6
|
+
// box: the request module's deploy-time allowlist carries no daemon entries.
|
|
7
|
+
// A cold box that declares them as dotted names under `env` resolves against a
|
|
8
|
+
// dev emulator but falls through to the trap-on-use stub at the edge, so the
|
|
9
|
+
// namespace here is load-bearing rather than cosmetic.
|
|
10
|
+
//
|
|
11
|
+
// The host registers only the functions the module actually declares, so a
|
|
12
|
+
// daemon that never calls one never pays for it.
|
|
13
|
+
//
|
|
14
|
+
// Return convention:
|
|
15
|
+
// >= 0 success: a count, an epoch, or a written byte length.
|
|
16
|
+
// -1 `httpCall`: the response did not fit `outCap` (retry bigger).
|
|
17
|
+
// `nextFireMs`: unknown id, or a cron that never fires again.
|
|
18
|
+
// Neither is an error code.
|
|
19
|
+
// -(0x10000 + c) a typed failure carrying the u16 subsystem code `c`; see
|
|
20
|
+
// `DaemonError` in `~lib/daemon`, which decodes this band.
|
|
21
|
+
//
|
|
22
|
+
// The `httpCall` request/response envelopes are a bespoke little-endian framing
|
|
23
|
+
// (NOT the `@data` codec); `DaemonHttpRequest` / `DaemonHttpResponse` own it.
|
|
24
|
+
|
|
25
|
+
export namespace daemonHost {
|
|
26
|
+
// daemon.is_leader() -> 1 when this node currently holds the lease, else 0.
|
|
27
|
+
// @ts-ignore: decorator
|
|
28
|
+
@external("daemon", "is_leader")
|
|
29
|
+
export declare function isLeader(): i32;
|
|
30
|
+
|
|
31
|
+
// daemon.current_epoch() -> the monotonic fencing token, or -1 when not leader.
|
|
32
|
+
// @ts-ignore: decorator
|
|
33
|
+
@external("daemon", "current_epoch")
|
|
34
|
+
export declare function currentEpoch(): i64;
|
|
35
|
+
|
|
36
|
+
// daemon.yield() -> 0 while still leader, else the bridged `LeaseLost`. Named
|
|
37
|
+
// `yieldNow` because `yield` is a reserved word.
|
|
38
|
+
// @ts-ignore: decorator
|
|
39
|
+
@external("daemon", "yield")
|
|
40
|
+
export declare function yieldNow(): i32;
|
|
41
|
+
|
|
42
|
+
// daemon.sleep_ms(ms) -> 0 when the sleep completed, else the bridged
|
|
43
|
+
// `LeaseLost` if the lease was lost while parked. The host CLAMPS `ms` to its
|
|
44
|
+
// own ceiling (3s) and treats a negative `ms` as 0, so neither can park a
|
|
45
|
+
// daemon thread for an unbounded interval.
|
|
46
|
+
// @ts-ignore: decorator
|
|
47
|
+
@external("daemon", "sleep_ms")
|
|
48
|
+
export declare function sleepMs(ms: i64): i32;
|
|
49
|
+
|
|
50
|
+
// daemon.task_count() -> the number of registered `@scheduled` tasks.
|
|
51
|
+
// @ts-ignore: decorator
|
|
52
|
+
@external("daemon", "task_count")
|
|
53
|
+
export declare function taskCount(): i32;
|
|
54
|
+
|
|
55
|
+
// daemon.next_fire_ms(taskId) -> the next wall-clock fire time in epoch ms, or
|
|
56
|
+
// -1 for an out-of-range id / a cron that never fires again.
|
|
57
|
+
// @ts-ignore: decorator
|
|
58
|
+
@external("daemon", "next_fire_ms")
|
|
59
|
+
export declare function nextFireMs(taskId: i32): i64;
|
|
60
|
+
|
|
61
|
+
// daemon.http_call(reqPtr, reqLen, outPtr, outCap) -> the response envelope
|
|
62
|
+
// LENGTH written to `outPtr` (>= 0, not packed with a pointer: the guest
|
|
63
|
+
// supplied the buffer), -1 when it did not fit `outCap`, or a bridged
|
|
64
|
+
// `DaemonError`. Soft-fenced: a non-leader gets `NotLeader` without hitting
|
|
65
|
+
// the wire.
|
|
66
|
+
// @ts-ignore: decorator
|
|
67
|
+
@external("daemon", "http_call")
|
|
68
|
+
export declare function httpCall(reqPtr: usize, reqLen: i32, outPtr: usize, outCap: i32): i64;
|
|
69
|
+
}
|
|
@@ -301,7 +301,7 @@ export namespace analyticsHost {
|
|
|
301
301
|
@external("env", "analytics_read")
|
|
302
302
|
export declare function read(domainPtr: usize, domainLen: i32): i32;
|
|
303
303
|
|
|
304
|
-
// analytics_series(domain, metricId, range): frame length (
|
|
304
|
+
// analytics_series(domain, metricId, range): frame length (v3 series frame) stashed for takeResult,
|
|
305
305
|
// or a negative status (-2 absent / -3 forbidden / bad metric or range).
|
|
306
306
|
// @ts-ignore: decorator
|
|
307
307
|
@external("env", "analytics_series")
|
|
@@ -103,4 +103,52 @@ export namespace webcrypto {
|
|
|
103
103
|
paramsLen: i32,
|
|
104
104
|
lengthBits: i32
|
|
105
105
|
): i32;
|
|
106
|
+
|
|
107
|
+
// --- Post-quantum -------------------------------------------------------
|
|
108
|
+
//
|
|
109
|
+
// These are NOT WebCrypto: they are the three post-quantum primitives the edge
|
|
110
|
+
// exposes, under the same `crypto.*` namespace. All are VERIFY / DECAPSULATE
|
|
111
|
+
// only. The host never generates or holds a long-lived secret on the guest's
|
|
112
|
+
// behalf, and there is no key handle: every input is passed by (ptr, len).
|
|
113
|
+
|
|
114
|
+
// mldsaVerify -> 1 valid, 0 invalid, negative error. `ctx` is the ML-DSA
|
|
115
|
+
// context string (may be empty).
|
|
116
|
+
// @ts-ignore: decorator
|
|
117
|
+
@external("env", "crypto.mldsa_verify")
|
|
118
|
+
export declare function mldsaVerify(
|
|
119
|
+
pkPtr: usize,
|
|
120
|
+
pkLen: i32,
|
|
121
|
+
msgPtr: usize,
|
|
122
|
+
msgLen: i32,
|
|
123
|
+
sigPtr: usize,
|
|
124
|
+
sigLen: i32,
|
|
125
|
+
ctxPtr: usize,
|
|
126
|
+
ctxLen: i32
|
|
127
|
+
): i32;
|
|
128
|
+
|
|
129
|
+
// mlkemDecapsulate -> 0 on success (the shared secret is written to `outPtr`)
|
|
130
|
+
// or a negative error. The caller owns `sk`; the host holds no KEM key.
|
|
131
|
+
// @ts-ignore: decorator
|
|
132
|
+
@external("env", "crypto.mlkem_decapsulate")
|
|
133
|
+
export declare function mlkemDecapsulate(
|
|
134
|
+
ctPtr: usize,
|
|
135
|
+
ctLen: i32,
|
|
136
|
+
skPtr: usize,
|
|
137
|
+
skLen: i32,
|
|
138
|
+
outPtr: usize
|
|
139
|
+
): i32;
|
|
140
|
+
|
|
141
|
+
// voprfEvaluate -> 0 on success (the evaluated element is written to `outPtr`)
|
|
142
|
+
// or a negative error. `seed` is the guest's own OPRF seed.
|
|
143
|
+
// @ts-ignore: decorator
|
|
144
|
+
@external("env", "crypto.voprf_evaluate")
|
|
145
|
+
export declare function voprfEvaluate(
|
|
146
|
+
seedPtr: usize,
|
|
147
|
+
seedLen: i32,
|
|
148
|
+
infoPtr: usize,
|
|
149
|
+
infoLen: i32,
|
|
150
|
+
blindedPtr: usize,
|
|
151
|
+
blindedLen: i32,
|
|
152
|
+
outPtr: usize
|
|
153
|
+
): i32;
|
|
106
154
|
}
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// Daemon: the developer-facing L4 (cold box) scheduler API.
|
|
2
|
+
//
|
|
3
|
+
// `@daemon class Jobs { onStart(): void {} @scheduled("1s") tick(): void {} }`
|
|
4
|
+
// declares the daemon; the compiler emits the `daemon_start` / `daemon_dispatch`
|
|
5
|
+
// entrypoints and the `toildaemon.catalog` section. THIS file is what a task body
|
|
6
|
+
// calls to read its own leadership, park, introspect its schedule, and make an
|
|
7
|
+
// outbound HTTP request.
|
|
8
|
+
//
|
|
9
|
+
// These wrap `bindings/daemon`, whose imports live in the `daemon` wasm module
|
|
10
|
+
// (not `env`). They resolve only inside a cold box: calling them from a request
|
|
11
|
+
// handler produces a module the edge will not run.
|
|
12
|
+
//
|
|
13
|
+
// Leadership is cooperative. Exactly one node holds the lease; `isLeader()` is a
|
|
14
|
+
// snapshot, and a long task should re-check it (or call `yieldNow()`) rather than
|
|
15
|
+
// assume the lease it started with is still held. Every call that can observe a
|
|
16
|
+
// lost lease reports it as `DaemonError.LeaseLost`.
|
|
17
|
+
|
|
18
|
+
import { daemonHost } from "bindings/daemon";
|
|
19
|
+
|
|
20
|
+
/// The host bridges a typed failure as `-(0x10000 + code)`; this is that base.
|
|
21
|
+
const DAEMON_ERR_BASE: i64 = 0x10000;
|
|
22
|
+
|
|
23
|
+
/// `http_call` request caps, mirrored from the host so an over-cap envelope fails
|
|
24
|
+
/// in the guest instead of costing a rejected round trip (toil-backend
|
|
25
|
+
/// `config::daemon_http`).
|
|
26
|
+
const DAEMON_MAX_METHOD_BYTES: i32 = 16;
|
|
27
|
+
const DAEMON_MAX_URL_BYTES: i32 = 8 * 1024;
|
|
28
|
+
const DAEMON_MAX_HEADERS: i32 = 64;
|
|
29
|
+
const DAEMON_MAX_HEADER_BYTES: i32 = 8 * 1024;
|
|
30
|
+
const DAEMON_MAX_REQUEST_BYTES: i32 = 256 * 1024;
|
|
31
|
+
|
|
32
|
+
/// The host truncates a response body to 1 MiB. The default `httpCall` buffer is
|
|
33
|
+
/// smaller; callers expecting large bodies pass their own capacity.
|
|
34
|
+
const DAEMON_DEFAULT_RESPONSE_CAP: i32 = 64 * 1024;
|
|
35
|
+
|
|
36
|
+
/// A typed daemon failure. The three positive values are the host's u16 subsystem
|
|
37
|
+
/// codes (toil-backend `ERR_DAEMON_*`), bridged as `-(0x10000 + code)`. The
|
|
38
|
+
/// negative values are guest-side outcomes that never travel the wire.
|
|
39
|
+
@global
|
|
40
|
+
export enum DaemonError {
|
|
41
|
+
/// No failure.
|
|
42
|
+
None = 0,
|
|
43
|
+
/// This node does not hold the lease; the host refused without acting.
|
|
44
|
+
NotLeader = 0x0401,
|
|
45
|
+
/// The lease was lost while the call was in flight (mid-park, mid-request).
|
|
46
|
+
LeaseLost = 0x0402,
|
|
47
|
+
/// The outbound request did not complete (SSRF guard, timeout, transport).
|
|
48
|
+
CallFailed = 0x0405,
|
|
49
|
+
/// The response did not fit the buffer. The call ALREADY HAPPENED: retrying it
|
|
50
|
+
/// re-issues the request. Pass a larger `responseCap` next time instead.
|
|
51
|
+
ResponseTooLarge = -1,
|
|
52
|
+
/// The request violates a host cap, or the response envelope was malformed.
|
|
53
|
+
/// Nothing was sent (encode) or the bytes could not be trusted (decode).
|
|
54
|
+
BadEnvelope = -2,
|
|
55
|
+
/// A negative status outside every band above.
|
|
56
|
+
Unknown = -3,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// The last failure observed by a `Daemon` call, in this instance. Read it right
|
|
60
|
+
/// after a call that returned `null`; any later call overwrites it.
|
|
61
|
+
// @ts-ignore: decorator
|
|
62
|
+
@lazy
|
|
63
|
+
var __daemonLastError: DaemonError = DaemonError.None;
|
|
64
|
+
|
|
65
|
+
/// Decode a negative host return into a typed failure. Non-negative is `None`.
|
|
66
|
+
function __daemonDecode(status: i64): DaemonError {
|
|
67
|
+
if (status >= 0) return DaemonError.None;
|
|
68
|
+
if (status == -1) return DaemonError.ResponseTooLarge;
|
|
69
|
+
const code: i64 = -status - DAEMON_ERR_BASE;
|
|
70
|
+
if (code < 0 || code > 0xFFFF) return DaemonError.Unknown;
|
|
71
|
+
const c = <i32>code;
|
|
72
|
+
if (c == <i32>DaemonError.NotLeader) return DaemonError.NotLeader;
|
|
73
|
+
if (c == <i32>DaemonError.LeaseLost) return DaemonError.LeaseLost;
|
|
74
|
+
if (c == <i32>DaemonError.CallFailed) return DaemonError.CallFailed;
|
|
75
|
+
return DaemonError.Unknown;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Record and return the failure for `status` (`None` when it is not a failure).
|
|
79
|
+
function __daemonNote(status: i64): DaemonError {
|
|
80
|
+
const e = __daemonDecode(status);
|
|
81
|
+
__daemonLastError = e;
|
|
82
|
+
return e;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// One `name: value` header of an outbound request or an inbound response.
|
|
86
|
+
@global
|
|
87
|
+
export class DaemonHttpHeader {
|
|
88
|
+
name: string;
|
|
89
|
+
value: string;
|
|
90
|
+
constructor(name: string, value: string) {
|
|
91
|
+
this.name = name;
|
|
92
|
+
this.value = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// An outbound HTTP request. `url` must be an absolute `http(s)` URL; the host
|
|
97
|
+
/// applies an SSRF guard to the resolved address, so a private/loopback target
|
|
98
|
+
/// fails with `DaemonError.CallFailed` rather than connecting.
|
|
99
|
+
@global
|
|
100
|
+
export class DaemonHttpRequest {
|
|
101
|
+
method: string = "GET";
|
|
102
|
+
url: string = "";
|
|
103
|
+
headers: DaemonHttpHeader[] = [];
|
|
104
|
+
body: Uint8Array = new Uint8Array(0);
|
|
105
|
+
|
|
106
|
+
constructor(method: string = "GET", url: string = "") {
|
|
107
|
+
this.method = method;
|
|
108
|
+
this.url = url;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Append a header. Returns `this` so calls chain.
|
|
112
|
+
header(name: string, value: string): DaemonHttpRequest {
|
|
113
|
+
this.headers.push(new DaemonHttpHeader(name, value));
|
|
114
|
+
return this;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Encode the little-endian request envelope the host decodes, or `null` when a
|
|
118
|
+
/// field breaks a host cap (nothing is sent). Layout:
|
|
119
|
+
/// u8 methodLen | method | u16 urlLen | url | u16 nHeaders
|
|
120
|
+
/// | nHeaders * (u16 nameLen | name | u16 valLen | val)
|
|
121
|
+
/// | u32 bodyLen | body
|
|
122
|
+
encode(): Uint8Array | null {
|
|
123
|
+
const method = Uint8Array.wrap(String.UTF8.encode(this.method));
|
|
124
|
+
const url = Uint8Array.wrap(String.UTF8.encode(this.url));
|
|
125
|
+
const nHeaders = this.headers.length;
|
|
126
|
+
if (method.byteLength < 1 || method.byteLength > DAEMON_MAX_METHOD_BYTES) return null;
|
|
127
|
+
if (url.byteLength > DAEMON_MAX_URL_BYTES) return null;
|
|
128
|
+
if (nHeaders > DAEMON_MAX_HEADERS) return null;
|
|
129
|
+
if (this.body.byteLength > DAEMON_MAX_REQUEST_BYTES) return null;
|
|
130
|
+
|
|
131
|
+
// Encode every header once: sizing and writing must agree on the byte counts.
|
|
132
|
+
const names = new Array<Uint8Array>();
|
|
133
|
+
const values = new Array<Uint8Array>();
|
|
134
|
+
let headerBytes = 0;
|
|
135
|
+
for (let i = 0; i < nHeaders; i++) {
|
|
136
|
+
const h = this.headers[i];
|
|
137
|
+
const n = Uint8Array.wrap(String.UTF8.encode(h.name));
|
|
138
|
+
const v = Uint8Array.wrap(String.UTF8.encode(h.value));
|
|
139
|
+
if (n.byteLength > DAEMON_MAX_HEADER_BYTES || v.byteLength > DAEMON_MAX_HEADER_BYTES) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
names.push(n);
|
|
143
|
+
values.push(v);
|
|
144
|
+
headerBytes += 4 + n.byteLength + v.byteLength;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// The host caps `req_len`, the WHOLE envelope, not just the body, and going over
|
|
148
|
+
// is a trap rather than a typed error (`daemon.http_call: req_len over cap`). So
|
|
149
|
+
// the total is what must be checked: a body just under the cap plus headers and a
|
|
150
|
+
// long URL would otherwise kill the daemon.
|
|
151
|
+
const size = 1 + method.byteLength + 2 + url.byteLength + 2 + headerBytes + 4 + this.body.byteLength;
|
|
152
|
+
if (size > DAEMON_MAX_REQUEST_BYTES) return null;
|
|
153
|
+
|
|
154
|
+
const buf = new Uint8Array(size);
|
|
155
|
+
const base = buf.dataStart;
|
|
156
|
+
let off: usize = 0;
|
|
157
|
+
|
|
158
|
+
store<u8>(base + off, <u8>method.byteLength); off += 1;
|
|
159
|
+
memory.copy(base + off, method.dataStart, <usize>method.byteLength); off += <usize>method.byteLength;
|
|
160
|
+
store<u16>(base + off, <u16>url.byteLength); off += 2;
|
|
161
|
+
memory.copy(base + off, url.dataStart, <usize>url.byteLength); off += <usize>url.byteLength;
|
|
162
|
+
store<u16>(base + off, <u16>nHeaders); off += 2;
|
|
163
|
+
for (let i = 0; i < nHeaders; i++) {
|
|
164
|
+
const n = names[i];
|
|
165
|
+
const v = values[i];
|
|
166
|
+
store<u16>(base + off, <u16>n.byteLength); off += 2;
|
|
167
|
+
memory.copy(base + off, n.dataStart, <usize>n.byteLength); off += <usize>n.byteLength;
|
|
168
|
+
store<u16>(base + off, <u16>v.byteLength); off += 2;
|
|
169
|
+
memory.copy(base + off, v.dataStart, <usize>v.byteLength); off += <usize>v.byteLength;
|
|
170
|
+
}
|
|
171
|
+
store<u32>(base + off, <u32>this.body.byteLength); off += 4;
|
|
172
|
+
memory.copy(base + off, this.body.dataStart, <usize>this.body.byteLength);
|
|
173
|
+
return buf;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/// An inbound HTTP response. `status` is 0 when the host itself failed before a
|
|
178
|
+
/// reply existed. The body is truncated by the host at 1 MiB.
|
|
179
|
+
@global
|
|
180
|
+
export class DaemonHttpResponse {
|
|
181
|
+
status: u16 = 0;
|
|
182
|
+
headers: DaemonHttpHeader[] = [];
|
|
183
|
+
body: Uint8Array = new Uint8Array(0);
|
|
184
|
+
|
|
185
|
+
/// Read a header's value (case-sensitive, first match), or `null`.
|
|
186
|
+
header(name: string): string | null {
|
|
187
|
+
for (let i = 0, n = this.headers.length; i < n; i++) {
|
|
188
|
+
if (this.headers[i].name == name) return this.headers[i].value;
|
|
189
|
+
}
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/// The body decoded as UTF-8.
|
|
194
|
+
text(): string {
|
|
195
|
+
return String.UTF8.decodeUnsafe(this.body.dataStart, <usize>this.body.byteLength);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Decode the little-endian response envelope, or `null` when it is truncated /
|
|
199
|
+
/// self-inconsistent. Layout:
|
|
200
|
+
/// u16 status | u16 nHeaders
|
|
201
|
+
/// | nHeaders * (u16 nameLen | name | u16 valLen | val)
|
|
202
|
+
/// | u32 bodyLen | body
|
|
203
|
+
static decode(buf: Uint8Array, len: i32): DaemonHttpResponse | null {
|
|
204
|
+
if (len < 0 || len > buf.byteLength) return null;
|
|
205
|
+
const base = buf.dataStart;
|
|
206
|
+
let off = 0;
|
|
207
|
+
|
|
208
|
+
if (off + 2 > len) return null;
|
|
209
|
+
const out = new DaemonHttpResponse();
|
|
210
|
+
out.status = load<u16>(base + <usize>off); off += 2;
|
|
211
|
+
|
|
212
|
+
if (off + 2 > len) return null;
|
|
213
|
+
const nHeaders = <i32>load<u16>(base + <usize>off); off += 2;
|
|
214
|
+
for (let i = 0; i < nHeaders; i++) {
|
|
215
|
+
if (off + 2 > len) return null;
|
|
216
|
+
const nameLen = <i32>load<u16>(base + <usize>off); off += 2;
|
|
217
|
+
if (off + nameLen > len) return null;
|
|
218
|
+
const name = String.UTF8.decodeUnsafe(base + <usize>off, <usize>nameLen); off += nameLen;
|
|
219
|
+
|
|
220
|
+
if (off + 2 > len) return null;
|
|
221
|
+
const valLen = <i32>load<u16>(base + <usize>off); off += 2;
|
|
222
|
+
if (off + valLen > len) return null;
|
|
223
|
+
const value = String.UTF8.decodeUnsafe(base + <usize>off, <usize>valLen); off += valLen;
|
|
224
|
+
|
|
225
|
+
out.headers.push(new DaemonHttpHeader(name, value));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (off + 4 > len) return null;
|
|
229
|
+
const bodyLen = <i32>load<u32>(base + <usize>off); off += 4;
|
|
230
|
+
if (bodyLen < 0 || off + bodyLen > len) return null;
|
|
231
|
+
const body = new Uint8Array(bodyLen);
|
|
232
|
+
memory.copy(body.dataStart, base + <usize>off, <usize>bodyLen); off += bodyLen;
|
|
233
|
+
|
|
234
|
+
// A trailing tail means the guest and the host disagree on the framing.
|
|
235
|
+
if (off != len) return null;
|
|
236
|
+
out.body = body;
|
|
237
|
+
return out;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// The daemon scheduler surface. Every member is static: a cold box has exactly
|
|
242
|
+
/// one daemon, and these read the resident host state.
|
|
243
|
+
@global
|
|
244
|
+
export class Daemon {
|
|
245
|
+
/// Whether THIS node currently holds the lease. A snapshot: re-check it inside a
|
|
246
|
+
/// long task rather than trusting the value it started with.
|
|
247
|
+
static isLeader(): bool {
|
|
248
|
+
return daemonHost.isLeader() != 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// The monotonic fencing token, bumped on every (re)acquire, or -1 when this
|
|
252
|
+
/// node does not hold the lease. Stamp it into work that must not outlive the
|
|
253
|
+
/// lease that authorized it.
|
|
254
|
+
static epoch(): i64 {
|
|
255
|
+
return daemonHost.currentEpoch();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/// The number of registered `@scheduled` tasks.
|
|
259
|
+
static taskCount(): i32 {
|
|
260
|
+
return daemonHost.taskCount();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/// The next wall-clock fire time for `taskId` in epoch ms, or -1 for an
|
|
264
|
+
/// out-of-range id or a cron that never fires again.
|
|
265
|
+
static nextFireMs(taskId: i32): i64 {
|
|
266
|
+
return daemonHost.nextFireMs(taskId);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/// Give the host a chance to observe a lost lease. `None` means still leader.
|
|
270
|
+
/// Named `yieldNow` because `yield` is reserved.
|
|
271
|
+
static yieldNow(): DaemonError {
|
|
272
|
+
return __daemonNote(<i64>daemonHost.yieldNow());
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Park for `ms`, clamped by the host to its 3s ceiling (a negative `ms` does
|
|
276
|
+
/// not park at all). `None` means the sleep completed and the lease still holds;
|
|
277
|
+
/// `LeaseLost` means it was lost while parked, and the task should stop.
|
|
278
|
+
static sleep(ms: i64): DaemonError {
|
|
279
|
+
return __daemonNote(<i64>daemonHost.sleepMs(ms));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Make an outbound HTTP request. Returns the response, or `null` on failure
|
|
283
|
+
/// (read `Daemon.lastError()`).
|
|
284
|
+
///
|
|
285
|
+
/// `responseCap` sizes the guest buffer the host writes into. When the envelope
|
|
286
|
+
/// does not fit, this returns `null` with `ResponseTooLarge` and DOES NOT retry:
|
|
287
|
+
/// the request already went out, so a blind retry would issue it twice. Pass a
|
|
288
|
+
/// bigger `responseCap` if you expect a large body.
|
|
289
|
+
static httpCall(
|
|
290
|
+
request: DaemonHttpRequest,
|
|
291
|
+
responseCap: i32 = DAEMON_DEFAULT_RESPONSE_CAP,
|
|
292
|
+
): DaemonHttpResponse | null {
|
|
293
|
+
const req = request.encode();
|
|
294
|
+
if (req == null) {
|
|
295
|
+
__daemonLastError = DaemonError.BadEnvelope;
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
const reqBuf = <Uint8Array>req;
|
|
299
|
+
const cap = responseCap > 0 ? responseCap : DAEMON_DEFAULT_RESPONSE_CAP;
|
|
300
|
+
const out = new Uint8Array(cap);
|
|
301
|
+
const n = daemonHost.httpCall(reqBuf.dataStart, reqBuf.byteLength, out.dataStart, cap);
|
|
302
|
+
if (n < 0) {
|
|
303
|
+
__daemonNote(n);
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
const res = DaemonHttpResponse.decode(out, <i32>n);
|
|
307
|
+
__daemonLastError = res == null ? DaemonError.BadEnvelope : DaemonError.None;
|
|
308
|
+
return res;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// The failure recorded by the most recent `Daemon` call.
|
|
312
|
+
static lastError(): DaemonError {
|
|
313
|
+
return __daemonLastError;
|
|
314
|
+
}
|
|
315
|
+
}
|