relaxnative 0.1.1 → 0.1.2
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/README.md +130 -0
- package/dist/chunk-CRQZJC5B.js +397 -0
- package/dist/chunk-GVGWP3E2.js +786 -0
- package/dist/chunk-ZTBXEEMF.js +1042 -0
- package/dist/cli.js +3 -3
- package/dist/index.js +3 -3
- package/dist/memory/memory.selftest.js +2 -2
- package/dist/worker/processEntry.js +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -55,6 +55,65 @@ Requirements:
|
|
|
55
55
|
|
|
56
56
|
## Quickstart
|
|
57
57
|
|
|
58
|
+
### 0) Full end-to-end example (new folder → run)
|
|
59
|
+
|
|
60
|
+
This is the fastest way to try Relaxnative in a clean folder.
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
mkdir -p my-relaxnative-app/native
|
|
64
|
+
cd my-relaxnative-app
|
|
65
|
+
npm init -y
|
|
66
|
+
npm i relaxnative
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Optional `package.json` (ESM + a run script):
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"name": "my-relaxnative-app",
|
|
74
|
+
"private": true,
|
|
75
|
+
"type": "module",
|
|
76
|
+
"scripts": {
|
|
77
|
+
"start": "node index.js"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"relaxnative": "^0.1.0"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Create `native/add.c`:
|
|
86
|
+
|
|
87
|
+
```c
|
|
88
|
+
// @sync
|
|
89
|
+
int add(int a, int b) {
|
|
90
|
+
return a + b;
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Create `index.js`:
|
|
95
|
+
|
|
96
|
+
```js
|
|
97
|
+
import { loadNative } from 'relaxnative';
|
|
98
|
+
|
|
99
|
+
const mod = await loadNative('native/add.c', { isolation: 'worker' });
|
|
100
|
+
console.log('add(1,2)=', mod.add(1, 2));
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Run it:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm start
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
If something goes wrong, re-run with tracing:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
RELAXNATIVE_TRACE=1 npm start
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
58
117
|
### 1) Create a native file
|
|
59
118
|
|
|
60
119
|
`native/add.c`
|
|
@@ -77,6 +136,30 @@ console.log(mod.add(1, 2));
|
|
|
77
136
|
|
|
78
137
|
---
|
|
79
138
|
|
|
139
|
+
## Tracing (RELAXNATIVE_TRACE)
|
|
140
|
+
|
|
141
|
+
When native code crashes or a worker/process boundary hides the real error, enable tracing.
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
RELAXNATIVE_TRACE=1 node index.js
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
More control:
|
|
148
|
+
|
|
149
|
+
- `RELAXNATIVE_TRACE=1` enables trace events.
|
|
150
|
+
- `RELAXNATIVE_TRACE_LEVEL=info|debug` controls verbosity (default: `info`).
|
|
151
|
+
|
|
152
|
+
What you’ll see (examples):
|
|
153
|
+
|
|
154
|
+
- Load lifecycle: `loadNative.begin`, `loadNative.build.begin`, `loadNative.build.done`, `loadNative.done`
|
|
155
|
+
- Parsed signatures (debug): `loadNative.bindings`
|
|
156
|
+
- Dispatch decisions: `dispatch`, `isolation.worker.dispatch`, `isolation.process.call`
|
|
157
|
+
- Process helper lifecycle: `isolation.process.helper.start|exit|error`
|
|
158
|
+
|
|
159
|
+
Tip: `RELAXNATIVE_TRACE_LEVEL=debug` will print parsed function signatures.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
80
163
|
## API
|
|
81
164
|
|
|
82
165
|
### `loadNative(sourcePath, options?)`
|
|
@@ -119,16 +202,56 @@ console.log(buf.address); // numeric pointer
|
|
|
119
202
|
- fastest
|
|
120
203
|
- unsafe: native crashes take down your Node process
|
|
121
204
|
|
|
205
|
+
Example:
|
|
206
|
+
|
|
207
|
+
```js
|
|
208
|
+
import { loadNative } from 'relaxnative';
|
|
209
|
+
|
|
210
|
+
const mod = await loadNative('native/add.c', { isolation: 'in-process' });
|
|
211
|
+
console.log(mod.add(1, 2));
|
|
212
|
+
```
|
|
213
|
+
|
|
122
214
|
### `worker`
|
|
123
215
|
- worker-thread dispatch for async calls
|
|
124
216
|
- sync calls may execute directly for low overhead
|
|
125
217
|
|
|
218
|
+
Example (async heavy work):
|
|
219
|
+
|
|
220
|
+
```c
|
|
221
|
+
// native/heavy.c
|
|
222
|
+
// @async
|
|
223
|
+
int heavy(int n) {
|
|
224
|
+
long x = 0;
|
|
225
|
+
for (int i = 0; i < n * 10000000; i++) x += i;
|
|
226
|
+
return (int)x;
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
```js
|
|
231
|
+
import { loadNative } from 'relaxnative';
|
|
232
|
+
|
|
233
|
+
const mod = await loadNative('native/heavy.c', { isolation: 'worker' });
|
|
234
|
+
const result = await mod.heavy(5);
|
|
235
|
+
console.log(result);
|
|
236
|
+
```
|
|
237
|
+
|
|
126
238
|
### `process`
|
|
127
239
|
- forked helper process
|
|
128
240
|
- crash containment
|
|
129
241
|
- best-effort Node runtime guards (module import denial for fs/network/spawn)
|
|
130
242
|
- call timeout enforcement (kills helper)
|
|
131
243
|
|
|
244
|
+
Example:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { loadNative } from 'relaxnative';
|
|
248
|
+
|
|
249
|
+
// Process isolation always returns async wrappers.
|
|
250
|
+
const mod = await loadNative('native/heavy.c', { isolation: 'process' });
|
|
251
|
+
const result = await mod.heavy(5);
|
|
252
|
+
console.log(result);
|
|
253
|
+
```
|
|
254
|
+
|
|
132
255
|
---
|
|
133
256
|
|
|
134
257
|
## Annotations
|
|
@@ -140,6 +263,13 @@ Supported:
|
|
|
140
263
|
- `@async`
|
|
141
264
|
- `@cost low|medium|high`
|
|
142
265
|
|
|
266
|
+
### Annotation + isolation quick rules
|
|
267
|
+
|
|
268
|
+
- `@sync` + `in-process`: fastest, but a crash kills your app.
|
|
269
|
+
- `@sync` + `worker`: may execute directly (fast) unless the binding is marked async/high-cost.
|
|
270
|
+
- `@async` + `worker`: always goes through the worker thread and returns a Promise.
|
|
271
|
+
- `process` isolation: **always async**, regardless of annotations (IPC boundary).
|
|
272
|
+
|
|
143
273
|
### What annotations mean
|
|
144
274
|
|
|
145
275
|
- `@sync`
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
// src/worker/workerPool.ts
|
|
2
|
+
import { Worker } from "worker_threads";
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var WORKER_ENTRY_TS_URL = new URL(
|
|
6
|
+
"./workerEntry.vitest.ts",
|
|
7
|
+
import.meta.url
|
|
8
|
+
);
|
|
9
|
+
var WORKER_ENTRY_JS_URL = new URL("./workerEntry.js", import.meta.url);
|
|
10
|
+
var worker = null;
|
|
11
|
+
var seq = 0;
|
|
12
|
+
var pending = /* @__PURE__ */ new Map();
|
|
13
|
+
function getWorker() {
|
|
14
|
+
if (!worker) {
|
|
15
|
+
if (existsSync(fileURLToPath(WORKER_ENTRY_JS_URL))) {
|
|
16
|
+
worker = new Worker(WORKER_ENTRY_JS_URL, {
|
|
17
|
+
env: { ...process.env }
|
|
18
|
+
});
|
|
19
|
+
} else {
|
|
20
|
+
const entryHref = WORKER_ENTRY_TS_URL.href;
|
|
21
|
+
const bootstrap = `import(${JSON.stringify(entryHref)});`;
|
|
22
|
+
worker = new Worker(bootstrap, {
|
|
23
|
+
eval: true,
|
|
24
|
+
env: { ...process.env }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
worker.on("message", (msg) => {
|
|
28
|
+
const resolve = pending.get(msg.id);
|
|
29
|
+
if (resolve) {
|
|
30
|
+
resolve(msg);
|
|
31
|
+
pending.delete(msg.id);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return worker;
|
|
36
|
+
}
|
|
37
|
+
function runInWorker(req) {
|
|
38
|
+
const id = ++seq;
|
|
39
|
+
const w = getWorker();
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
pending.set(id, (res) => {
|
|
42
|
+
if (res.error) {
|
|
43
|
+
const err = new Error(res.error + (res.errorCallsite ? `
|
|
44
|
+
--- remote callsite ---
|
|
45
|
+
${res.errorCallsite}` : ""));
|
|
46
|
+
reject(err);
|
|
47
|
+
} else resolve(res.result);
|
|
48
|
+
});
|
|
49
|
+
w.postMessage({ ...req, id });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/dx/logger.ts
|
|
54
|
+
var enabled = false;
|
|
55
|
+
function isDebugEnabled() {
|
|
56
|
+
return enabled || process.env.RELAXNATIVE_DEBUG === "1";
|
|
57
|
+
}
|
|
58
|
+
function logDebug(...args) {
|
|
59
|
+
if (!isDebugEnabled()) return;
|
|
60
|
+
console.log("[relaxnative]", ...args);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/utils/callsite.ts
|
|
64
|
+
function captureCallsite() {
|
|
65
|
+
const err = new Error();
|
|
66
|
+
if (!err.stack) return void 0;
|
|
67
|
+
const parts = err.stack.split("\n");
|
|
68
|
+
if (parts.length <= 2) return err.stack;
|
|
69
|
+
return parts.slice(2).join("\n");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// src/dx/trace.ts
|
|
73
|
+
import { performance } from "perf_hooks";
|
|
74
|
+
function envTraceEnabled() {
|
|
75
|
+
const v = process.env.RELAXNATIVE_TRACE;
|
|
76
|
+
return v === "1" || v === "true" || v === "yes";
|
|
77
|
+
}
|
|
78
|
+
function envTraceLevel() {
|
|
79
|
+
const v = (process.env.RELAXNATIVE_TRACE_LEVEL ?? "").toLowerCase();
|
|
80
|
+
if (v === "error" || v === "warn" || v === "info" || v === "debug") return v;
|
|
81
|
+
return "info";
|
|
82
|
+
}
|
|
83
|
+
var order = {
|
|
84
|
+
error: 0,
|
|
85
|
+
warn: 1,
|
|
86
|
+
info: 2,
|
|
87
|
+
debug: 3
|
|
88
|
+
};
|
|
89
|
+
function shouldTrace(level) {
|
|
90
|
+
if (!envTraceEnabled()) return false;
|
|
91
|
+
return order[level] <= order[envTraceLevel()];
|
|
92
|
+
}
|
|
93
|
+
function trace(level, event, data) {
|
|
94
|
+
if (!shouldTrace(level)) return;
|
|
95
|
+
const payload = {
|
|
96
|
+
t: Number(performance.now().toFixed(3)),
|
|
97
|
+
pid: process.pid,
|
|
98
|
+
level,
|
|
99
|
+
event
|
|
100
|
+
};
|
|
101
|
+
if (data !== void 0) payload.data = data;
|
|
102
|
+
console.log("[relaxnative:trace]", JSON.stringify(payload));
|
|
103
|
+
}
|
|
104
|
+
function traceError(event, data) {
|
|
105
|
+
trace("error", event, data);
|
|
106
|
+
}
|
|
107
|
+
function traceInfo(event, data) {
|
|
108
|
+
trace("info", event, data);
|
|
109
|
+
}
|
|
110
|
+
function traceDebug(event, data) {
|
|
111
|
+
trace("debug", event, data);
|
|
112
|
+
}
|
|
113
|
+
function formatBindingSignature(binding) {
|
|
114
|
+
if (!binding) return "";
|
|
115
|
+
const name = binding?.name ?? "<anonymous>";
|
|
116
|
+
const returns = binding?.returns ?? "void";
|
|
117
|
+
const args = Array.isArray(binding?.args) ? binding.args : [];
|
|
118
|
+
return `${returns} ${name}(${args.join(", ")})`;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// src/worker/workerClient.ts
|
|
122
|
+
function callAsync(libPath, bindings, fn, args) {
|
|
123
|
+
const callsite = captureCallsite();
|
|
124
|
+
logDebug("worker dispatch", { fn, callsite: !!callsite });
|
|
125
|
+
traceDebug("isolation.worker.dispatch", {
|
|
126
|
+
fn,
|
|
127
|
+
libPath,
|
|
128
|
+
argc: args?.length ?? 0,
|
|
129
|
+
argTypes: Array.isArray(args) ? args.map((a) => {
|
|
130
|
+
if (a == null) return String(a);
|
|
131
|
+
if (ArrayBuffer.isView(a)) return a.constructor?.name ?? "TypedArray";
|
|
132
|
+
return typeof a;
|
|
133
|
+
}) : [],
|
|
134
|
+
hasCallsite: !!callsite
|
|
135
|
+
});
|
|
136
|
+
return runInWorker({ libPath, bindings, fn, args, callsite });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// src/worker/processClient.ts
|
|
140
|
+
import { fork } from "child_process";
|
|
141
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
142
|
+
import { dirname, join } from "path";
|
|
143
|
+
import { existsSync as existsSync2 } from "fs";
|
|
144
|
+
var ProcessIsolationError = class extends Error {
|
|
145
|
+
code;
|
|
146
|
+
details;
|
|
147
|
+
constructor(code, message, details) {
|
|
148
|
+
super(message);
|
|
149
|
+
this.code = code;
|
|
150
|
+
this.details = details;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var child = null;
|
|
154
|
+
var seq2 = 0;
|
|
155
|
+
var pending2 = /* @__PURE__ */ new Map();
|
|
156
|
+
function helperEntryPath() {
|
|
157
|
+
const here = dirname(fileURLToPath2(import.meta.url));
|
|
158
|
+
let cur = here;
|
|
159
|
+
for (let i = 0; i < 8; i++) {
|
|
160
|
+
const candidate = join(cur, "package.json");
|
|
161
|
+
if (existsSync2(candidate)) {
|
|
162
|
+
const pkgRoot2 = cur;
|
|
163
|
+
const distEntry2 = join(pkgRoot2, "dist", "worker", "processEntry.js");
|
|
164
|
+
return { distEntry: distEntry2, pkgRoot: pkgRoot2 };
|
|
165
|
+
}
|
|
166
|
+
const parent = join(cur, "..");
|
|
167
|
+
if (parent === cur) break;
|
|
168
|
+
cur = parent;
|
|
169
|
+
}
|
|
170
|
+
const pkgRoot = join(here, "..", "..");
|
|
171
|
+
const distEntry = join(pkgRoot, "dist", "worker", "processEntry.js");
|
|
172
|
+
return { distEntry, pkgRoot };
|
|
173
|
+
}
|
|
174
|
+
function startChild() {
|
|
175
|
+
if (child && child.connected) return child;
|
|
176
|
+
const { distEntry } = helperEntryPath();
|
|
177
|
+
if (!existsSync2(distEntry)) {
|
|
178
|
+
traceError("isolation.process.helper.missing", { distEntry });
|
|
179
|
+
throw new ProcessIsolationError(
|
|
180
|
+
"ISOLATED_PROCESS_START_FAILED",
|
|
181
|
+
`Process isolation helper was not found at: ${distEntry}. This usually means the package was published without dist/worker/processEntry.js`,
|
|
182
|
+
{ distEntry }
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const entry = distEntry;
|
|
186
|
+
traceInfo("isolation.process.helper.start", { entry });
|
|
187
|
+
const cp = fork(entry, {
|
|
188
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
189
|
+
env: {
|
|
190
|
+
...process.env,
|
|
191
|
+
RELAXNATIVE_PROCESS_ISOLATION: "1"
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
cp.on("message", (msg) => {
|
|
195
|
+
if (!msg || typeof msg.id !== "number") return;
|
|
196
|
+
const p = pending2.get(msg.id);
|
|
197
|
+
if (!p) return;
|
|
198
|
+
if (p.cp !== cp) return;
|
|
199
|
+
pending2.delete(msg.id);
|
|
200
|
+
const res = msg;
|
|
201
|
+
if (res.ok) p.resolve(res.result);
|
|
202
|
+
else {
|
|
203
|
+
const details = [res.error.message];
|
|
204
|
+
if (res.error.callsite) details.push("\n--- remote callsite ---\n" + res.error.callsite);
|
|
205
|
+
const err = new Error(details.join("\n"));
|
|
206
|
+
err.name = res.error.name;
|
|
207
|
+
err.stack = res.error.stack;
|
|
208
|
+
p.reject(err);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
const rejectAll = (reason) => {
|
|
212
|
+
for (const [id, p] of pending2) {
|
|
213
|
+
if (p.cp !== cp) continue;
|
|
214
|
+
pending2.delete(id);
|
|
215
|
+
p.reject(reason);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
cp.on("exit", (code, signal) => {
|
|
219
|
+
if (child === cp) child = null;
|
|
220
|
+
const reason = new ProcessIsolationError(
|
|
221
|
+
signal ? "ISOLATED_PROCESS_CRASH" : "ISOLATED_PROCESS_EXIT",
|
|
222
|
+
`Isolated runtime exited (code=${code}, signal=${signal ?? "none"})`,
|
|
223
|
+
{ code, signal }
|
|
224
|
+
);
|
|
225
|
+
traceError("isolation.process.helper.exit", { code, signal });
|
|
226
|
+
rejectAll(reason);
|
|
227
|
+
});
|
|
228
|
+
cp.on("error", (err) => {
|
|
229
|
+
const reason = new ProcessIsolationError(
|
|
230
|
+
"ISOLATED_PROCESS_START_FAILED",
|
|
231
|
+
`Failed to start isolated runtime: ${String(err)}`,
|
|
232
|
+
{ err }
|
|
233
|
+
);
|
|
234
|
+
traceError("isolation.process.helper.error", { message: String(err) });
|
|
235
|
+
child = null;
|
|
236
|
+
rejectAll(reason);
|
|
237
|
+
});
|
|
238
|
+
child = cp;
|
|
239
|
+
return cp;
|
|
240
|
+
}
|
|
241
|
+
async function ping(cp) {
|
|
242
|
+
const id = ++seq2;
|
|
243
|
+
const req = { id, type: "ping" };
|
|
244
|
+
traceDebug("isolation.process.ping", { id });
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
pending2.set(id, { resolve, reject, cp });
|
|
247
|
+
cp.send(req);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
async function callIsolated(libPath, bindings, fn, args, safety, callsite) {
|
|
251
|
+
const cp = startChild();
|
|
252
|
+
await ping(cp);
|
|
253
|
+
const id = ++seq2;
|
|
254
|
+
traceDebug("isolation.process.call", {
|
|
255
|
+
id,
|
|
256
|
+
fn,
|
|
257
|
+
argc: args?.length ?? 0,
|
|
258
|
+
hasSafety: !!safety,
|
|
259
|
+
hasCallsite: !!callsite
|
|
260
|
+
});
|
|
261
|
+
const req = {
|
|
262
|
+
id,
|
|
263
|
+
type: "call",
|
|
264
|
+
libPath,
|
|
265
|
+
bindings,
|
|
266
|
+
safety,
|
|
267
|
+
fn,
|
|
268
|
+
args,
|
|
269
|
+
// forward the captured JS callsite for richer crash diagnostics
|
|
270
|
+
...callsite ? { callsite } : {}
|
|
271
|
+
};
|
|
272
|
+
const timeoutMs = safety?.limits?.timeoutMs;
|
|
273
|
+
const baseCall = new Promise((resolve, reject) => {
|
|
274
|
+
let t = null;
|
|
275
|
+
let timedOut = false;
|
|
276
|
+
const wrappedResolve = (v) => {
|
|
277
|
+
if (timedOut) return;
|
|
278
|
+
if (t) clearTimeout(t);
|
|
279
|
+
resolve(v);
|
|
280
|
+
};
|
|
281
|
+
const wrappedReject = (e) => {
|
|
282
|
+
if (timedOut) return;
|
|
283
|
+
if (t) clearTimeout(t);
|
|
284
|
+
reject(e);
|
|
285
|
+
};
|
|
286
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
287
|
+
t = setTimeout(() => {
|
|
288
|
+
timedOut = true;
|
|
289
|
+
pending2.delete(id);
|
|
290
|
+
try {
|
|
291
|
+
cp.kill();
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
wrappedReject(
|
|
295
|
+
new ProcessIsolationError(
|
|
296
|
+
"ISOLATED_PROCESS_CRASH",
|
|
297
|
+
`Isolated runtime exceeded timeout (${timeoutMs}ms)`,
|
|
298
|
+
{ timeoutMs }
|
|
299
|
+
)
|
|
300
|
+
);
|
|
301
|
+
}, timeoutMs);
|
|
302
|
+
}
|
|
303
|
+
pending2.set(id, { resolve: wrappedResolve, reject: wrappedReject, cp });
|
|
304
|
+
cp.send(req);
|
|
305
|
+
});
|
|
306
|
+
if (typeof timeoutMs === "number" && timeoutMs > 0) {
|
|
307
|
+
return Promise.race([
|
|
308
|
+
baseCall,
|
|
309
|
+
new Promise((_, reject) => {
|
|
310
|
+
setTimeout(() => {
|
|
311
|
+
reject(
|
|
312
|
+
new ProcessIsolationError(
|
|
313
|
+
"ISOLATED_PROCESS_CRASH",
|
|
314
|
+
`Isolated runtime exceeded timeout (${timeoutMs}ms)`,
|
|
315
|
+
{ timeoutMs }
|
|
316
|
+
)
|
|
317
|
+
);
|
|
318
|
+
}, timeoutMs + 25);
|
|
319
|
+
})
|
|
320
|
+
]);
|
|
321
|
+
}
|
|
322
|
+
return baseCall;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/worker/wrapFunctions.ts
|
|
326
|
+
function wrapFunctions(api, libPath, bindings, opts) {
|
|
327
|
+
const wrapped = {};
|
|
328
|
+
const isolation = opts?.isolation ?? "worker";
|
|
329
|
+
const safety = bindings?.__safety ?? { trust: "local" };
|
|
330
|
+
const bindingNames = Object.keys(bindings?.functions ?? {});
|
|
331
|
+
const apiNames = Object.keys(api ?? {});
|
|
332
|
+
const names = Array.from(/* @__PURE__ */ new Set([...bindingNames, ...apiNames]));
|
|
333
|
+
for (const name of names) {
|
|
334
|
+
const fn = api?.[name];
|
|
335
|
+
const binding = bindings?.functions?.[name];
|
|
336
|
+
const isAsync = binding?.mode === "async" || binding?.async === true || binding?.thread === "worker" || binding?.cost === "high";
|
|
337
|
+
wrapped[name] = (...args) => {
|
|
338
|
+
traceDebug("dispatch", {
|
|
339
|
+
isolation,
|
|
340
|
+
fn: name,
|
|
341
|
+
argc: args?.length ?? 0,
|
|
342
|
+
mode: binding?.mode,
|
|
343
|
+
cost: binding?.cost
|
|
344
|
+
});
|
|
345
|
+
if (isolation === "process") {
|
|
346
|
+
return callIsolated(libPath, bindings, name, args, safety);
|
|
347
|
+
}
|
|
348
|
+
if (isolation === "worker") {
|
|
349
|
+
if (typeof fn === "function" && !isAsync) {
|
|
350
|
+
return fn(...args);
|
|
351
|
+
}
|
|
352
|
+
return callAsync(libPath, bindings, name, args);
|
|
353
|
+
}
|
|
354
|
+
if (typeof fn !== "function") {
|
|
355
|
+
const available = Object.keys(api ?? {});
|
|
356
|
+
const sig = binding ? formatBindingSignature(binding) : void 0;
|
|
357
|
+
const detail = [
|
|
358
|
+
`Function not found: ${name}`,
|
|
359
|
+
sig ? `Signature (from parser): ${sig}` : void 0,
|
|
360
|
+
available.length ? `Available exports: ${available.join(", ")}` : void 0
|
|
361
|
+
].filter(Boolean);
|
|
362
|
+
throw new Error(detail.join("\n"));
|
|
363
|
+
}
|
|
364
|
+
return fn(...args);
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
if (isolation === "process") {
|
|
368
|
+
wrapped.__call = (fn, ...args) => callIsolated(libPath, bindings, fn, args, safety, captureCallsite());
|
|
369
|
+
return new Proxy(wrapped, {
|
|
370
|
+
get(target, prop) {
|
|
371
|
+
if (typeof prop !== "string") return target[prop];
|
|
372
|
+
if (prop === "then") return void 0;
|
|
373
|
+
if (prop in target) return target[prop];
|
|
374
|
+
return (...args) => callIsolated(libPath, bindings, prop, args, safety, captureCallsite());
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (isolation === "worker") {
|
|
379
|
+
return new Proxy(wrapped, {
|
|
380
|
+
get(target, prop) {
|
|
381
|
+
if (typeof prop !== "string") return target[prop];
|
|
382
|
+
if (prop === "then") return void 0;
|
|
383
|
+
if (prop in target) return target[prop];
|
|
384
|
+
return (...args) => callAsync(libPath, bindings, prop, args);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
return wrapped;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export {
|
|
392
|
+
logDebug,
|
|
393
|
+
traceInfo,
|
|
394
|
+
traceDebug,
|
|
395
|
+
formatBindingSignature,
|
|
396
|
+
wrapFunctions
|
|
397
|
+
};
|