@stacks/clarinet-sdk-wasm 3.20.0 → 3.21.0

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/clarinet_sdk.js CHANGED
@@ -1,7 +1,6 @@
1
1
  /* @ts-self-types="./clarinet_sdk.d.ts" */
2
- const { execSync } = require(`child_process`);
2
+ const { syncHttpRequest, syncSleep } = require(String.raw`./snippets/clarity-repl-5a3dc0b2445b5d42/src/repl/remote_data/http_request/sync_http.cjs`);
3
3
  const { existsSync, mkdirSync, readFileSync, writeFileSync } = require(`fs`);
4
- const { env } = require(`process`);
5
4
 
6
5
  class CallFnArgs {
7
6
  __destroy_into_raw() {
@@ -1062,14 +1061,7 @@ function __wbg_get_imports() {
1062
1061
  wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
1063
1062
  }
1064
1063
  },
1065
- __wbg_execSync_e2c70c9dbfcff7e6: function(arg0, arg1, arg2) {
1066
- const ret = execSync(getStringFromWasm0(arg1, arg2));
1067
- const ptr1 = passArray8ToWasm0(ret, wasm.__wbindgen_malloc);
1068
- const len1 = WASM_VECTOR_LEN;
1069
- getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
1070
- getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
1071
- },
1072
- __wbg_existsSync_036cfc17615a0d6b: function(arg0, arg1) {
1064
+ __wbg_existsSync_8b294c716abe69ad: function(arg0, arg1) {
1073
1065
  const ret = existsSync(getStringFromWasm0(arg0, arg1));
1074
1066
  return ret;
1075
1067
  },
@@ -1169,7 +1161,7 @@ function __wbg_get_imports() {
1169
1161
  __wbg_log_524eedafa26daa59: function(arg0) {
1170
1162
  console.log(arg0);
1171
1163
  },
1172
- __wbg_mkdirSync_24eaf73f4b98bffc: function(arg0, arg1, arg2) {
1164
+ __wbg_mkdirSync_e4557cb3dda4e26c: function(arg0, arg1, arg2) {
1173
1165
  mkdirSync(getStringFromWasm0(arg0, arg1), arg2);
1174
1166
  },
1175
1167
  __wbg_new_0837727332ac86ba: function() { return handleError(function () {
@@ -1252,7 +1244,7 @@ function __wbg_get_imports() {
1252
1244
  __wbg_queueMicrotask_a082d78ce798393e: function(arg0) {
1253
1245
  queueMicrotask(arg0);
1254
1246
  },
1255
- __wbg_readFileSync_7e86a7d642ed7aae: function(arg0, arg1, arg2) {
1247
+ __wbg_readFileSync_1661436156cbc8a5: function(arg0, arg1, arg2) {
1256
1248
  const ret = readFileSync(getStringFromWasm0(arg1, arg2));
1257
1249
  const ptr1 = passArray8ToWasm0(ret, wasm.__wbindgen_malloc);
1258
1250
  const len1 = WASM_VECTOR_LEN;
@@ -1313,10 +1305,6 @@ function __wbg_get_imports() {
1313
1305
  getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
1314
1306
  getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
1315
1307
  },
1316
- __wbg_static_accessor_ENV_8e51995cf06886bc: function() {
1317
- const ret = env;
1318
- return ret;
1319
- },
1320
1308
  __wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() {
1321
1309
  const ret = typeof global === 'undefined' ? null : global;
1322
1310
  return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
@@ -1341,6 +1329,13 @@ function __wbg_get_imports() {
1341
1329
  const ret = JSON.stringify(arg0);
1342
1330
  return ret;
1343
1331
  }, arguments); },
1332
+ __wbg_syncHttpRequest_1103c04352de404b: function() { return handleError(function (arg0, arg1, arg2, arg3) {
1333
+ const ret = syncHttpRequest(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
1334
+ return ret;
1335
+ }, arguments); },
1336
+ __wbg_syncSleep_80597e2e6faa6ee9: function(arg0) {
1337
+ syncSleep(arg0 >>> 0);
1338
+ },
1344
1339
  __wbg_then_098abe61755d12f6: function(arg0, arg1) {
1345
1340
  const ret = arg0.then(arg1);
1346
1341
  return ret;
@@ -1360,11 +1355,11 @@ function __wbg_get_imports() {
1360
1355
  const ret = arg0.value;
1361
1356
  return ret;
1362
1357
  },
1363
- __wbg_writeFileSync_42cee1c221b2718d: function(arg0, arg1, arg2, arg3) {
1358
+ __wbg_writeFileSync_5550322cf3e7fced: function(arg0, arg1, arg2, arg3) {
1364
1359
  writeFileSync(getStringFromWasm0(arg0, arg1), getArrayU8FromWasm0(arg2, arg3));
1365
1360
  },
1366
1361
  __wbindgen_cast_0000000000000001: function(arg0, arg1) {
1367
- // Cast intrinsic for `Closure(Closure { dtor_idx: 357, function: Function { arguments: [], shim_idx: 358, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1362
+ // Cast intrinsic for `Closure(Closure { dtor_idx: 358, function: Function { arguments: [], shim_idx: 359, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
1368
1363
  const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h4f53b7943fceff9f, wasm_bindgen__convert__closures_____invoke__h7fb3d9536d4d27f0);
1369
1364
  return ret;
1370
1365
  },
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacks/clarinet-sdk-wasm",
3
3
  "description": "The core lib that powers @stacks/clarinet-sdk",
4
- "version": "3.20.0",
4
+ "version": "3.21.0",
5
5
  "license": "GPL-3.0",
6
6
  "repository": {
7
7
  "type": "git",
@@ -10,7 +10,8 @@
10
10
  "files": [
11
11
  "clarinet_sdk_bg.wasm",
12
12
  "clarinet_sdk.js",
13
- "clarinet_sdk.d.ts"
13
+ "clarinet_sdk.d.ts",
14
+ "snippets"
14
15
  ],
15
16
  "main": "clarinet_sdk.js",
16
17
  "types": "clarinet_sdk.d.ts"
@@ -0,0 +1,449 @@
1
+ "use strict";
2
+
3
+ // Synchronous HTTP for the Node-WASM build of clarity-repl.
4
+ //
5
+ // The WASM main thread can't await Promises, so we offload `fetch` to a
6
+ // long-lived Node worker thread (see workerMain below) and block the main
7
+ // thread on `Atomics.wait` against a SharedArrayBuffer until the worker flips
8
+ // the signal byte. All cross-thread state — request URL/headers, response
9
+ // status/headers/body or error — flows through that SAB; no `postMessage`,
10
+ // because the main thread is blocked and can't run JS callbacks.
11
+ //
12
+ // This file is deliberately self-contained: it is the only snippet
13
+ // wasm-bindgen knows about, so anything loaded from a sibling file would be
14
+ // missing wherever the snippet is copied without our build script
15
+ // (wasm-bindgen-test runners, bundlers). The worker is therefore spawned from
16
+ // an eval'd serialization of workerMain() and the layout constants travel via
17
+ // workerData instead of a shared module.
18
+
19
+ const { Worker } = require("node:worker_threads");
20
+
21
+ // SAB header at offset 0, request/response data area at offset HEADER_BYTES:
22
+ // 0 Int32 signal 0=response_done (worker idle), 1=request_pending
23
+ // 4 Int32 status_code HTTP status, or -1 for transport error
24
+ // 8 Int32 status_text_len response status text byte length
25
+ // 12 Int32 headers_len response headers JSON byte length
26
+ // 16 Int32 body_len response body byte length; negative means error
27
+ // 20 Int32 url_len request URL byte length
28
+ // 24 Int32 req_hdrs_len request headers JSON byte length
29
+ // 28 Int32 liveness counter the worker bumps on a timer; main reads
30
+ // it to tell a slow response from a dead worker
31
+ const LAYOUT = {
32
+ HEADER_BYTES: 1024,
33
+ OFFSET_STATUS: 4,
34
+ OFFSET_STATUS_TEXT_LEN: 8,
35
+ OFFSET_HEADERS_LEN: 12,
36
+ OFFSET_BODY_LEN: 16,
37
+ OFFSET_URL_LEN: 20,
38
+ OFFSET_REQ_HDRS_LEN: 24,
39
+ OFFSET_LIVENESS: 28,
40
+ // How often the worker bumps the liveness counter. Its event loop is free
41
+ // while a fetch is in flight (and while idle it parks in Atomics.wait, but
42
+ // main only checks liveness while a request is pending), so a live worker
43
+ // always bumps at least once per main-side heartbeat.
44
+ LIVENESS_INTERVAL_MS: 250,
45
+ // The signal alternates between RESPONSE_DONE (worker idle, main may issue
46
+ // next request) and REQUEST_PENDING (worker is processing). A three-state
47
+ // scheme races: by the time the worker loops back to wait, main may already
48
+ // have reset the signal and the worker would re-enter with stale data.
49
+ SIGNAL_RESPONSE_DONE: 0,
50
+ SIGNAL_REQUEST_PENDING: 1,
51
+ FETCH_TIMEOUT_MS: 30_000,
52
+ };
53
+
54
+ const {
55
+ HEADER_BYTES,
56
+ OFFSET_STATUS,
57
+ OFFSET_STATUS_TEXT_LEN,
58
+ OFFSET_HEADERS_LEN,
59
+ OFFSET_BODY_LEN,
60
+ OFFSET_URL_LEN,
61
+ OFFSET_REQ_HDRS_LEN,
62
+ OFFSET_LIVENESS,
63
+ SIGNAL_RESPONSE_DONE,
64
+ SIGNAL_REQUEST_PENDING,
65
+ } = LAYOUT;
66
+
67
+ // Entire worker thread, serialized via toString() and spawned with
68
+ // `new Worker(code, { eval: true })`. It must be fully self-contained: no
69
+ // references to this module's scope — everything arrives through workerData.
70
+ function workerMain() {
71
+ "use strict";
72
+
73
+ const { workerData } = require("node:worker_threads");
74
+ const {
75
+ HEADER_BYTES,
76
+ OFFSET_STATUS,
77
+ OFFSET_STATUS_TEXT_LEN,
78
+ OFFSET_HEADERS_LEN,
79
+ OFFSET_BODY_LEN,
80
+ OFFSET_URL_LEN,
81
+ OFFSET_REQ_HDRS_LEN,
82
+ OFFSET_LIVENESS,
83
+ SIGNAL_RESPONSE_DONE,
84
+ SIGNAL_REQUEST_PENDING,
85
+ FETCH_TIMEOUT_MS,
86
+ LIVENESS_INTERVAL_MS,
87
+ } = workerData.layout;
88
+
89
+ const sab = workerData.sab;
90
+ const dataCapacity = sab.byteLength - HEADER_BYTES;
91
+ const signal = new Int32Array(sab, 0, 1);
92
+ const header = new Int32Array(sab, 0, HEADER_BYTES / 4);
93
+ const data = new Uint8Array(sab, HEADER_BYTES, dataCapacity);
94
+ const decoder = new TextDecoder();
95
+ const encoder = new TextEncoder();
96
+
97
+ // Test-only hook: boot the thread but never serve or bump liveness, so the
98
+ // main-thread dead-worker detection can be exercised end-to-end.
99
+ if (process.env.CLARINET_SDK_TEST_SYNC_HTTP_STALL_WORKER) return;
100
+
101
+ // Liveness beacon for the blocked main thread (its event loop can't pump, so
102
+ // it can't observe worker 'error'/'exit' — see WORKER_HEARTBEAT_MS in the
103
+ // parent module). If this thread dies, the counter freezes and main bails out.
104
+ setInterval(() => {
105
+ Atomics.add(header, OFFSET_LIVENESS / 4, 1);
106
+ }, LIVENESS_INTERVAL_MS).unref();
107
+
108
+ function readUrl() {
109
+ const len = Atomics.load(header, OFFSET_URL_LEN / 4);
110
+ return decoder.decode(data.subarray(0, len));
111
+ }
112
+
113
+ function readRequestHeaders() {
114
+ const urlLen = Atomics.load(header, OFFSET_URL_LEN / 4);
115
+ const hdrLen = Atomics.load(header, OFFSET_REQ_HDRS_LEN / 4);
116
+ if (hdrLen === 0) return {};
117
+ return JSON.parse(decoder.decode(data.subarray(urlLen, urlLen + hdrLen)));
118
+ }
119
+
120
+ function writeError(message) {
121
+ const encoded = encoder.encode(message);
122
+ let len = Math.min(encoded.length, dataCapacity);
123
+ // Don't cut a multi-byte UTF-8 sequence in half: back off past any
124
+ // continuation bytes (0b10xxxxxx) so the truncated message decodes cleanly.
125
+ if (len < encoded.length) {
126
+ while (len > 0 && (encoded[len] & 0xc0) === 0x80) len -= 1;
127
+ }
128
+ data.set(encoded.subarray(0, len), 0);
129
+ Atomics.store(header, OFFSET_STATUS / 4, -1);
130
+ Atomics.store(header, OFFSET_STATUS_TEXT_LEN / 4, 0);
131
+ Atomics.store(header, OFFSET_HEADERS_LEN / 4, 0);
132
+ // Negative body_len signals an error; magnitude is the message length.
133
+ Atomics.store(header, OFFSET_BODY_LEN / 4, -len);
134
+ }
135
+
136
+ function writeResponse(status, statusText, headers, body) {
137
+ const encodedStatusText = encoder.encode(statusText || "");
138
+ const encodedHeaders = encoder.encode(JSON.stringify(headers));
139
+ const total = encodedStatusText.length + encodedHeaders.length + body.length;
140
+ if (total > dataCapacity) {
141
+ writeError("response too large: " + total + " bytes, capacity " + dataCapacity);
142
+ return;
143
+ }
144
+ // Layout in data area: [statusText][headersJson][body]
145
+ data.set(encodedStatusText, 0);
146
+ data.set(encodedHeaders, encodedStatusText.length);
147
+ data.set(body, encodedStatusText.length + encodedHeaders.length);
148
+ Atomics.store(header, OFFSET_STATUS / 4, status);
149
+ Atomics.store(header, OFFSET_STATUS_TEXT_LEN / 4, encodedStatusText.length);
150
+ Atomics.store(header, OFFSET_HEADERS_LEN / 4, encodedHeaders.length);
151
+ Atomics.store(header, OFFSET_BODY_LEN / 4, body.length);
152
+ }
153
+
154
+ function signalDone() {
155
+ Atomics.store(signal, 0, SIGNAL_RESPONSE_DONE);
156
+ Atomics.notify(signal, 0, 1);
157
+ }
158
+
159
+ function errMessage(e) {
160
+ return e && e.message ? e.message : String(e);
161
+ }
162
+
163
+ // `requestActive` gates the unhandled-error nets below: a late-arriving
164
+ // rejection (e.g. an orphan AbortController abort from a prior request) must
165
+ // NOT overwrite the SAB while main is blocked waiting on a different request
166
+ // or hasn't yet issued one.
167
+ //
168
+ // `generation` guards the other direction: when failRequest answers an
169
+ // in-flight request early, the still-pending fetch in handleOneRequest must
170
+ // not write its eventual result over the SAB — main may already have issued
171
+ // the NEXT request into the same buffer. failRequest bumps the generation;
172
+ // handleOneRequest only writes/signals if its generation is still current.
173
+ let requestActive = false;
174
+ let generation = 0;
175
+
176
+ function failRequest(prefix, err) {
177
+ if (!requestActive) return;
178
+ requestActive = false;
179
+ generation += 1;
180
+ try {
181
+ writeError(prefix + ": " + errMessage(err));
182
+ signalDone();
183
+ } catch (_) {}
184
+ }
185
+
186
+ // Belt-and-braces: any uncaught async error would terminate the worker by
187
+ // default (Node ≥ 20), silently deadlocking main on the next request. Catch
188
+ // and surface them, but only when a request is actually in flight.
189
+ process.on("unhandledRejection", (err) => failRequest("worker unhandledRejection", err));
190
+ process.on("uncaughtException", (err) => failRequest("worker uncaughtException", err));
191
+
192
+ async function handleOneRequest(gen) {
193
+ let url, headers;
194
+ try {
195
+ url = readUrl();
196
+ headers = readRequestHeaders();
197
+ } catch (e) {
198
+ writeError("failed to decode request: " + errMessage(e));
199
+ return;
200
+ }
201
+ // Explicit AbortController + clearTimeout so the timer doesn't keep ticking
202
+ // after a fast success — an orphan abort firing post-completion can produce
203
+ // rejections that fire on the *next* request's await checkpoint.
204
+ const controller = new AbortController();
205
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
206
+ try {
207
+ const res = await fetch(url, { headers, signal: controller.signal });
208
+ const buf = new Uint8Array(await res.arrayBuffer());
209
+ if (gen !== generation) return;
210
+ const hdrObj = {};
211
+ res.headers.forEach((v, k) => {
212
+ hdrObj[k] = v;
213
+ });
214
+ writeResponse(res.status, res.statusText || "", hdrObj, buf);
215
+ } catch (e) {
216
+ if (gen !== generation) return;
217
+ writeError(errMessage(e));
218
+ } finally {
219
+ clearTimeout(timer);
220
+ }
221
+ }
222
+
223
+ (async () => {
224
+ for (;;) {
225
+ try {
226
+ // Blocking Atomics.wait is reliable across Node versions; the Node 24 /
227
+ // macOS issue was specific to Atomics.waitAsync.
228
+ while (Atomics.load(signal, 0) !== SIGNAL_REQUEST_PENDING) {
229
+ Atomics.wait(signal, 0, SIGNAL_RESPONSE_DONE);
230
+ }
231
+ // Yield to the event loop before flipping requestActive=true. Atomics.wait
232
+ // parks the loop, so any unhandledRejection queued while we were idle
233
+ // (e.g. an orphan abort from a prior request) is still pending. Letting
234
+ // it fire here — while requestActive=false — keeps the gate effective
235
+ // and avoids attributing the orphan error to a fresh in-flight request.
236
+ await new Promise((r) => setImmediate(r));
237
+ requestActive = true;
238
+ const gen = generation;
239
+ await handleOneRequest(gen);
240
+ // failRequest may have answered (and invalidated) this request already;
241
+ // signalling again here would ack a request we never handled.
242
+ if (gen !== generation) continue;
243
+ signalDone();
244
+ requestActive = false;
245
+ } catch (loopErr) {
246
+ // Last-ditch recovery: never let the loop exit silently.
247
+ try {
248
+ writeError("worker loop error: " + errMessage(loopErr));
249
+ signalDone();
250
+ requestActive = false;
251
+ } catch (_) {}
252
+ }
253
+ }
254
+ })();
255
+ }
256
+
257
+ const WORKER_SOURCE = `(${workerMain})()`;
258
+
259
+ const MIN_SAB_SIZE = 64 * 1024;
260
+ const MAX_SAB_SIZE = 256 * 1024 * 1024;
261
+ const DEFAULT_SAB_SIZE = 8 * 1024 * 1024;
262
+
263
+ function parseSabSize() {
264
+ const raw = process.env.CLARINET_SDK_SAB_SIZE;
265
+ if (!raw) return DEFAULT_SAB_SIZE;
266
+ const n = Number.parseInt(raw, 10);
267
+ if (!Number.isFinite(n)) return DEFAULT_SAB_SIZE;
268
+ if (n < MIN_SAB_SIZE) return MIN_SAB_SIZE;
269
+ if (n > MAX_SAB_SIZE) return MAX_SAB_SIZE;
270
+ return n;
271
+ }
272
+
273
+ const SAB_SIZE = parseSabSize();
274
+ const DATA_CAPACITY = SAB_SIZE - HEADER_BYTES;
275
+
276
+ // Main-thread Atomics.wait heartbeat (ms). While main is blocked, its event
277
+ // loop never pumps, so worker.on('error')/'exit' CANNOT fire — JS callbacks
278
+ // only run when the stack empties. Death detection therefore can't rely on
279
+ // state.dead; instead the worker bumps a liveness counter in the SAB from a
280
+ // timer (see LIVENESS_INTERVAL_MS), and main samples it on each heartbeat
281
+ // timeout. A counter frozen for MAX_STALE_HEARTBEATS consecutive heartbeats
282
+ // means the worker thread is gone (failed to start, terminated, OOM, native
283
+ // crash) and main throws instead of blocking forever.
284
+ const WORKER_HEARTBEAT_MS = 1000;
285
+ const MAX_STALE_HEARTBEATS = 5;
286
+
287
+ let workerState = null;
288
+
289
+ // Registered once at module load — references whichever worker is current via
290
+ // the workerState closure, so respawns don't accumulate listeners.
291
+ process.on("beforeExit", () => {
292
+ const w = workerState?.worker;
293
+ if (!w) return;
294
+ try {
295
+ w.terminate();
296
+ } catch (_) {}
297
+ });
298
+
299
+ function ensureWorker() {
300
+ if (workerState && !workerState.dead) return workerState;
301
+
302
+ const sab = new SharedArrayBuffer(SAB_SIZE);
303
+ const header = new Int32Array(sab, 0, HEADER_BYTES / 4);
304
+ const data = new Uint8Array(sab, HEADER_BYTES, DATA_CAPACITY);
305
+ const signal = new Int32Array(sab, 0, 1);
306
+ const worker = new Worker(WORKER_SOURCE, {
307
+ eval: true,
308
+ workerData: { sab, layout: LAYOUT },
309
+ });
310
+ worker.unref();
311
+
312
+ const state = { worker, sab, header, data, signal, dead: false, cause: null };
313
+
314
+ worker.on("error", (err) => {
315
+ state.dead = true;
316
+ state.cause = err;
317
+ if (process.env.CLARINET_SDK_DEBUG_SYNC_HTTP) {
318
+ process.stderr.write(`[sync_http] worker error: ${err && err.stack ? err.stack : err}\n`);
319
+ }
320
+ });
321
+ worker.on("exit", (code) => {
322
+ state.dead = true;
323
+ if (state.cause == null && code !== 0) {
324
+ state.cause = new Error(`sync_http worker exited with code ${code}`);
325
+ }
326
+ if (process.env.CLARINET_SDK_DEBUG_SYNC_HTTP) {
327
+ process.stderr.write(`[sync_http] worker exit code=${code}\n`);
328
+ }
329
+ });
330
+
331
+ workerState = state;
332
+ return state;
333
+ }
334
+
335
+ const encoder = new TextEncoder();
336
+ const decoder = new TextDecoder();
337
+
338
+ function workerCause(state) {
339
+ if (!state.cause) return "unknown error";
340
+ return state.cause.message || String(state.cause);
341
+ }
342
+
343
+ function syncHttpRequest(url, headersJson) {
344
+ const state = ensureWorker();
345
+ if (state.dead) {
346
+ throw new Error("sync_http worker is not running: " + workerCause(state));
347
+ }
348
+
349
+ // Read HIRO_API_KEY per-request so callers can set it after module load
350
+ // (e.g. dotenv.config() / beforeEach()). JSON.parse is intentionally not
351
+ // wrapped — malformed input is a caller bug, not something to silently drop.
352
+ const baseHeaders = headersJson ? JSON.parse(headersJson) : {};
353
+ const apiKey = process.env.HIRO_API_KEY;
354
+ if (apiKey && baseHeaders["x-api-key"] === undefined) {
355
+ baseHeaders["x-api-key"] = apiKey;
356
+ }
357
+
358
+ const urlBytes = encoder.encode(url);
359
+ const hdrBytes = encoder.encode(JSON.stringify(baseHeaders));
360
+ if (urlBytes.length + hdrBytes.length > DATA_CAPACITY) {
361
+ throw new Error(
362
+ `request too large: ${urlBytes.length + hdrBytes.length} bytes, capacity ${DATA_CAPACITY}`,
363
+ );
364
+ }
365
+
366
+ state.data.set(urlBytes, 0);
367
+ state.data.set(hdrBytes, urlBytes.length);
368
+ Atomics.store(state.header, OFFSET_URL_LEN / 4, urlBytes.length);
369
+ Atomics.store(state.header, OFFSET_REQ_HDRS_LEN / 4, hdrBytes.length);
370
+ Atomics.store(state.header, OFFSET_STATUS / 4, 0);
371
+ Atomics.store(state.header, OFFSET_STATUS_TEXT_LEN / 4, 0);
372
+ Atomics.store(state.header, OFFSET_HEADERS_LEN / 4, 0);
373
+ Atomics.store(state.header, OFFSET_BODY_LEN / 4, 0);
374
+
375
+ Atomics.store(state.signal, 0, SIGNAL_REQUEST_PENDING);
376
+ Atomics.notify(state.signal, 0, 1);
377
+
378
+ // Block until the worker flips signal back to RESPONSE_DONE. The heartbeat
379
+ // bound lets us sample the worker's liveness counter so a dead worker
380
+ // doesn't deadlock main — see comment on WORKER_HEARTBEAT_MS.
381
+ let lastBeat = Atomics.load(state.header, OFFSET_LIVENESS / 4);
382
+ let staleHeartbeats = 0;
383
+ for (;;) {
384
+ const r = Atomics.wait(state.signal, 0, SIGNAL_REQUEST_PENDING, WORKER_HEARTBEAT_MS);
385
+ if (r === "not-equal" || r === "ok") break;
386
+ const beat = Atomics.load(state.header, OFFSET_LIVENESS / 4);
387
+ if (beat !== lastBeat) {
388
+ lastBeat = beat;
389
+ staleHeartbeats = 0;
390
+ continue;
391
+ }
392
+ staleHeartbeats += 1;
393
+ if (staleHeartbeats >= MAX_STALE_HEARTBEATS) {
394
+ state.dead = true;
395
+ state.cause ??= new Error(
396
+ `worker unresponsive for ${staleHeartbeats * WORKER_HEARTBEAT_MS}ms (thread died or failed to start)`,
397
+ );
398
+ break;
399
+ }
400
+ }
401
+
402
+ if (state.dead) {
403
+ throw new Error("sync_http worker died during request: " + workerCause(state));
404
+ }
405
+
406
+ const status = Atomics.load(state.header, OFFSET_STATUS / 4);
407
+ const statusTextLen = Atomics.load(state.header, OFFSET_STATUS_TEXT_LEN / 4);
408
+ const headersLen = Atomics.load(state.header, OFFSET_HEADERS_LEN / 4);
409
+ const bodyLen = Atomics.load(state.header, OFFSET_BODY_LEN / 4);
410
+
411
+ if (bodyLen < 0) {
412
+ throw new Error(decoder.decode(state.data.subarray(0, -bodyLen)));
413
+ }
414
+
415
+ // Layout: [statusText][headers][body]
416
+ const headersStart = statusTextLen;
417
+ const bodyStart = headersStart + headersLen;
418
+ const statusText =
419
+ statusTextLen > 0 ? decoder.decode(state.data.subarray(0, statusTextLen)) : "";
420
+ const headers =
421
+ headersLen > 0
422
+ ? JSON.parse(decoder.decode(state.data.subarray(headersStart, bodyStart)))
423
+ : {};
424
+ const body = decoder.decode(state.data.subarray(bodyStart, bodyStart + bodyLen));
425
+
426
+ return { status, statusText, headers, body };
427
+ }
428
+
429
+ // Block the current thread for `ms` ms via Atomics.wait on a never-notified
430
+ // SAB. Required because std::thread::sleep is a no-op on wasm32.
431
+ const sleepSab = new SharedArrayBuffer(4);
432
+ const sleepView = new Int32Array(sleepSab);
433
+
434
+ function syncSleep(ms) {
435
+ if (!Number.isFinite(ms) || ms <= 0) return;
436
+ Atomics.wait(sleepView, 0, 0, ms);
437
+ }
438
+
439
+ // For tests and graceful shutdown. Next syncHttpRequest lazily starts a fresh worker.
440
+ function closeWorker() {
441
+ if (!workerState) return;
442
+ const w = workerState.worker;
443
+ workerState = null;
444
+ try {
445
+ w.terminate();
446
+ } catch (_) {}
447
+ }
448
+
449
+ module.exports = { syncHttpRequest, syncSleep, closeWorker };