@stacks/clarinet-sdk-wasm 3.19.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.d.ts
CHANGED
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-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() {
|
|
@@ -320,6 +319,9 @@ class SDK {
|
|
|
320
319
|
const ptr0 = passStringToWasm0(contract, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
321
320
|
const len0 = WASM_VECTOR_LEN;
|
|
322
321
|
const ret = wasm.sdk_getContractSource(this.__wbg_ptr, ptr0, len0);
|
|
322
|
+
if (ret[3]) {
|
|
323
|
+
throw takeFromExternrefTable0(ret[2]);
|
|
324
|
+
}
|
|
323
325
|
let v2;
|
|
324
326
|
if (ret[0] !== 0) {
|
|
325
327
|
v2 = getStringFromWasm0(ret[0], ret[1]).slice();
|
|
@@ -1059,14 +1061,7 @@ function __wbg_get_imports() {
|
|
|
1059
1061
|
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
|
|
1060
1062
|
}
|
|
1061
1063
|
},
|
|
1062
|
-
|
|
1063
|
-
const ret = execSync(getStringFromWasm0(arg1, arg2));
|
|
1064
|
-
const ptr1 = passArray8ToWasm0(ret, wasm.__wbindgen_malloc);
|
|
1065
|
-
const len1 = WASM_VECTOR_LEN;
|
|
1066
|
-
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
1067
|
-
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
1068
|
-
},
|
|
1069
|
-
__wbg_existsSync_1d441dd1bfae15ff: function(arg0, arg1) {
|
|
1064
|
+
__wbg_existsSync_8b294c716abe69ad: function(arg0, arg1) {
|
|
1070
1065
|
const ret = existsSync(getStringFromWasm0(arg0, arg1));
|
|
1071
1066
|
return ret;
|
|
1072
1067
|
},
|
|
@@ -1166,7 +1161,7 @@ function __wbg_get_imports() {
|
|
|
1166
1161
|
__wbg_log_524eedafa26daa59: function(arg0) {
|
|
1167
1162
|
console.log(arg0);
|
|
1168
1163
|
},
|
|
1169
|
-
|
|
1164
|
+
__wbg_mkdirSync_e4557cb3dda4e26c: function(arg0, arg1, arg2) {
|
|
1170
1165
|
mkdirSync(getStringFromWasm0(arg0, arg1), arg2);
|
|
1171
1166
|
},
|
|
1172
1167
|
__wbg_new_0837727332ac86ba: function() { return handleError(function () {
|
|
@@ -1249,7 +1244,7 @@ function __wbg_get_imports() {
|
|
|
1249
1244
|
__wbg_queueMicrotask_a082d78ce798393e: function(arg0) {
|
|
1250
1245
|
queueMicrotask(arg0);
|
|
1251
1246
|
},
|
|
1252
|
-
|
|
1247
|
+
__wbg_readFileSync_1661436156cbc8a5: function(arg0, arg1, arg2) {
|
|
1253
1248
|
const ret = readFileSync(getStringFromWasm0(arg1, arg2));
|
|
1254
1249
|
const ptr1 = passArray8ToWasm0(ret, wasm.__wbindgen_malloc);
|
|
1255
1250
|
const len1 = WASM_VECTOR_LEN;
|
|
@@ -1310,10 +1305,6 @@ function __wbg_get_imports() {
|
|
|
1310
1305
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
|
1311
1306
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
|
1312
1307
|
},
|
|
1313
|
-
__wbg_static_accessor_ENV_d1de898b0d61375f: function() {
|
|
1314
|
-
const ret = env;
|
|
1315
|
-
return ret;
|
|
1316
|
-
},
|
|
1317
1308
|
__wbg_static_accessor_GLOBAL_8adb955bd33fac2f: function() {
|
|
1318
1309
|
const ret = typeof global === 'undefined' ? null : global;
|
|
1319
1310
|
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
|
|
@@ -1338,6 +1329,13 @@ function __wbg_get_imports() {
|
|
|
1338
1329
|
const ret = JSON.stringify(arg0);
|
|
1339
1330
|
return ret;
|
|
1340
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
|
+
},
|
|
1341
1339
|
__wbg_then_098abe61755d12f6: function(arg0, arg1) {
|
|
1342
1340
|
const ret = arg0.then(arg1);
|
|
1343
1341
|
return ret;
|
|
@@ -1357,11 +1355,11 @@ function __wbg_get_imports() {
|
|
|
1357
1355
|
const ret = arg0.value;
|
|
1358
1356
|
return ret;
|
|
1359
1357
|
},
|
|
1360
|
-
|
|
1358
|
+
__wbg_writeFileSync_5550322cf3e7fced: function(arg0, arg1, arg2, arg3) {
|
|
1361
1359
|
writeFileSync(getStringFromWasm0(arg0, arg1), getArrayU8FromWasm0(arg2, arg3));
|
|
1362
1360
|
},
|
|
1363
1361
|
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
|
|
1364
|
-
// Cast intrinsic for `Closure(Closure { dtor_idx:
|
|
1362
|
+
// Cast intrinsic for `Closure(Closure { dtor_idx: 358, function: Function { arguments: [], shim_idx: 359, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
|
|
1365
1363
|
const ret = makeMutClosure(arg0, arg1, wasm.wasm_bindgen__closure__destroy__h4f53b7943fceff9f, wasm_bindgen__convert__closures_____invoke__h7fb3d9536d4d27f0);
|
|
1366
1364
|
return ret;
|
|
1367
1365
|
},
|
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.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"
|
package/snippets/clarity-repl-5a3dc0b2445b5d42/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 };
|