@stacks/clarinet-sdk-wasm 3.20.0 → 3.21.1
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 {
|
|
2
|
+
const { syncHttpRequest, syncSleep } = require(String.raw`./snippets/clarity-repl-bf6e36e8a1aaa79a/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
|
-
|
|
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_5a69fed6aba4e49a: 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
|
-
|
|
1164
|
+
__wbg_mkdirSync_a3b1399461833e3a: function(arg0, arg1, arg2) {
|
|
1173
1165
|
mkdirSync(getStringFromWasm0(arg0, arg1), arg2);
|
|
1174
1166
|
},
|
|
1175
1167
|
__wbg_new_0837727332ac86ba: function() { return handleError(function () {
|
|
@@ -1219,7 +1211,7 @@ function __wbg_get_imports() {
|
|
|
1219
1211
|
const a = state0.a;
|
|
1220
1212
|
state0.a = 0;
|
|
1221
1213
|
try {
|
|
1222
|
-
return
|
|
1214
|
+
return wasm_bindgen__convert__closures_____invoke__h002a313088afd9d2(a, state0.b, arg0, arg1);
|
|
1223
1215
|
} finally {
|
|
1224
1216
|
state0.a = a;
|
|
1225
1217
|
}
|
|
@@ -1252,7 +1244,7 @@ function __wbg_get_imports() {
|
|
|
1252
1244
|
__wbg_queueMicrotask_a082d78ce798393e: function(arg0) {
|
|
1253
1245
|
queueMicrotask(arg0);
|
|
1254
1246
|
},
|
|
1255
|
-
|
|
1247
|
+
__wbg_readFileSync_96e21bc4d1bcaf3e: 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_4746ee2838da131c: 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_b0686bdceca103a4: 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,17 +1355,17 @@ function __wbg_get_imports() {
|
|
|
1360
1355
|
const ret = arg0.value;
|
|
1361
1356
|
return ret;
|
|
1362
1357
|
},
|
|
1363
|
-
|
|
1358
|
+
__wbg_writeFileSync_d14c5b4930433627: 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:
|
|
1368
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
1362
|
+
// Cast intrinsic for `Closure(Closure { dtor_idx: 370, function: Function { arguments: [], shim_idx: 371, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
1363
|
+
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h24f1008371b7a634, wasm_bindgen__convert__closures_____invoke__h6af13b07a06b3d89);
|
|
1369
1364
|
return ret;
|
|
1370
1365
|
},
|
|
1371
1366
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
|
1372
|
-
// Cast intrinsic for `Closure(Closure { dtor_idx:
|
|
1373
|
-
const ret = makeMutClosure(arg0, arg1, wasm.
|
|
1367
|
+
// Cast intrinsic for `Closure(Closure { dtor_idx: 572, function: Function { arguments: [Externref], shim_idx: 573, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
|
|
1368
|
+
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__hbd502242fe259504, wasm_bindgen__convert__closures_____invoke__h008b08bdfa17cd4b);
|
|
1374
1369
|
return ret;
|
|
1375
1370
|
},
|
|
1376
1371
|
__wbindgen_cast_0000000000000003: function(arg0) {
|
|
@@ -1414,19 +1409,19 @@ function __wbg_get_imports() {
|
|
|
1414
1409
|
};
|
|
1415
1410
|
}
|
|
1416
1411
|
|
|
1417
|
-
function
|
|
1418
|
-
wasm.
|
|
1412
|
+
function wasm_bindgen__convert__closures_____invoke__h6af13b07a06b3d89(arg0, arg1) {
|
|
1413
|
+
wasm.wasm_bindgen__convert__closures_____invoke__h6af13b07a06b3d89(arg0, arg1);
|
|
1419
1414
|
}
|
|
1420
1415
|
|
|
1421
|
-
function
|
|
1422
|
-
const ret = wasm.
|
|
1416
|
+
function wasm_bindgen__convert__closures_____invoke__h008b08bdfa17cd4b(arg0, arg1, arg2) {
|
|
1417
|
+
const ret = wasm.wasm_bindgen__convert__closures_____invoke__h008b08bdfa17cd4b(arg0, arg1, arg2);
|
|
1423
1418
|
if (ret[1]) {
|
|
1424
1419
|
throw takeFromExternrefTable0(ret[0]);
|
|
1425
1420
|
}
|
|
1426
1421
|
}
|
|
1427
1422
|
|
|
1428
|
-
function
|
|
1429
|
-
wasm.
|
|
1423
|
+
function wasm_bindgen__convert__closures_____invoke__h002a313088afd9d2(arg0, arg1, arg2, arg3) {
|
|
1424
|
+
wasm.wasm_bindgen__convert__closures_____invoke__h002a313088afd9d2(arg0, arg1, arg2, arg3);
|
|
1430
1425
|
}
|
|
1431
1426
|
|
|
1432
1427
|
|
package/clarinet_sdk_bg.wasm
CHANGED
|
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.
|
|
4
|
+
"version": "3.21.1",
|
|
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"
|
package/snippets/clarity-repl-bf6e36e8a1aaa79a/src/repl/remote_data/http_request/sync_http.cjs
ADDED
|
@@ -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 };
|