toilscript 0.1.59 → 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.
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "imports": {
3
- "toilscript": "https://cdn.jsdelivr.net/npm/toilscript@0.1.59/dist/toilscript.js",
4
- "toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.59/dist/cli.js",
5
- "binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0-nightly.20260609/index.js",
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.59";
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.59/dist/toilscript.js",
5
- "toilscript/cli": "https://cdn.jsdelivr.net/npm/toilscript@0.1.59/dist/cli.js",
6
- "binaryen": "https://cdn.jsdelivr.net/npm/binaryen@130.0.0-nightly.20260609/index.js",
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.59",
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-nightly.20260609",
29
- "long": "^5.2.4"
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": "^25.6.0",
33
+ "@types/node": "^26.1.1",
34
34
  "as-float": "^1.0.1",
35
35
  "diff": "^9.0.0",
36
- "esbuild": "^0.28.0",
37
- "eslint": "^10.2.1",
36
+ "esbuild": "^0.28.1",
37
+ "eslint": "^10.6.0",
38
38
  "glob": "^13.0.6",
39
- "globals": "^17.5.0",
39
+ "globals": "^17.7.0",
40
40
  "typescript": "^6.0.3",
41
- "typescript-eslint": "^8.59.0"
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 (v2 series frame) stashed for takeResult,
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
+ }