wstp-node 0.3.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/LICENSE +21 -0
- package/README.md +1046 -0
- package/binding.gyp +52 -0
- package/build/Release/wstp.node +0 -0
- package/build.sh +134 -0
- package/examples/demo.js +340 -0
- package/examples/monitor_demo.js +206 -0
- package/index.d.ts +284 -0
- package/package.json +51 -0
- package/src/addon.cc +2116 -0
- package/test.js +660 -0
- package/test_interrupt_dialog.js +220 -0
package/README.md
ADDED
|
@@ -0,0 +1,1046 @@
|
|
|
1
|
+
# wstp-backend — API Reference
|
|
2
|
+
|
|
3
|
+
**Author:** Nikolay Gromov
|
|
4
|
+
|
|
5
|
+
Native Node.js addon for Wolfram kernel communication over WSTP — supports full
|
|
6
|
+
notebook-style evaluation with automatic `In`/`Out` history, real-time streaming,
|
|
7
|
+
and an internal evaluation queue. All blocking I/O runs on the libuv thread pool;
|
|
8
|
+
the JS event loop is never stalled.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
const { WstpSession, WstpReader, setDiagHandler } = require('./build/Release/wstp.node');
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Table of Contents
|
|
17
|
+
|
|
18
|
+
1. [Installation](#installation)
|
|
19
|
+
2. [Batch mode vs interactive mode](#batch-mode-vs-interactive-mode)
|
|
20
|
+
3. [Return types — `WExpr` and `EvalResult`](#return-types)
|
|
21
|
+
4. [`WstpSession` — main evaluation session](#wstpsession)
|
|
22
|
+
- [Constructor](#constructor) — launch a kernel and open a session
|
|
23
|
+
- [`evaluate(expr, opts?)`](#evaluateexpr-opts) — queue an expression for evaluation; supports streaming `Print` callbacks
|
|
24
|
+
- [`sub(expr)`](#subexpr) — priority evaluation that jumps ahead of the queue, for quick queries during a long computation
|
|
25
|
+
- [`abort()`](#abort) — interrupt the currently running evaluation
|
|
26
|
+
- [`dialogEval(expr)`](#dialogevalexpr) — evaluate inside an active `Dialog[]` subsession
|
|
27
|
+
- [`exitDialog(retVal?)`](#exitdialogretval) — exit the current dialog and resume the main evaluation
|
|
28
|
+
- [`interrupt()`](#interrupt) — send a low-level interrupt signal to the kernel
|
|
29
|
+
- [`createSubsession(kernelPath?)`](#createsubsessionkernelpath) — spawn an independent parallel kernel session
|
|
30
|
+
- [`close()`](#close) — gracefully shut down the kernel and free resources
|
|
31
|
+
- [`isOpen` / `isDialogOpen`](#isopen--isdialogopen) — read-only status flags
|
|
32
|
+
5. [`WstpReader` — kernel-pushed side channel](#wstpreader)
|
|
33
|
+
6. [`setDiagHandler(fn)`](#setdiaghandlerfn)
|
|
34
|
+
5. [Usage examples](#usage-examples)
|
|
35
|
+
- [Basic evaluation](#basic-evaluation)
|
|
36
|
+
- [Interactive mode — In/Out history](#interactive-mode--inout-history)
|
|
37
|
+
- [Streaming output](#streaming-output)
|
|
38
|
+
- [Concurrent evaluations](#concurrent-evaluations)
|
|
39
|
+
- [Priority `sub()` calls](#priority-sub-calls)
|
|
40
|
+
- [Abort a long computation](#abort-a-long-computation)
|
|
41
|
+
- [Dialog subsessions](#dialog-subsessions)
|
|
42
|
+
- [Variable monitor — peeking at a running loop](#variable-monitor--peeking-at-a-running-loop)
|
|
43
|
+
- [Real-time side channel (`WstpReader`)](#real-time-side-channel-wstpreader)
|
|
44
|
+
- [Parallel independent kernels](#parallel-independent-kernels)
|
|
45
|
+
6. [Error handling](#error-handling)
|
|
46
|
+
7. [Diagnostic logging](#diagnostic-logging)
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
### Prerequisites
|
|
53
|
+
|
|
54
|
+
| Requirement | Notes |
|
|
55
|
+
|-------------|-------|
|
|
56
|
+
| macOS | Tested on macOS 13+; Linux should work with minor path changes |
|
|
57
|
+
| Node.js ≥ 18 | Earlier versions may work but are untested |
|
|
58
|
+
| Clang / Xcode Command Line Tools | `xcode-select --install` |
|
|
59
|
+
| Wolfram Mathematica or Wolfram Engine | Provides `WolframKernel` and the WSTP SDK headers/libraries |
|
|
60
|
+
|
|
61
|
+
### 1. Clone
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
git clone https://github.com/vanbaalon/mathematica-wstp-node.git
|
|
65
|
+
cd mathematica-wstp-node
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### 2. Install Node dependencies
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This pulls in `node-addon-api` and `node-gyp` (used by the build script).
|
|
75
|
+
|
|
76
|
+
### 3. Compile the native addon
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
bash build.sh
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Output: `build/Release/wstp.node`
|
|
83
|
+
|
|
84
|
+
The script automatically locates the WSTP SDK inside the default Wolfram installation
|
|
85
|
+
(`/Applications/Wolfram 3.app/...`). If your Wolfram is installed elsewhere, edit the
|
|
86
|
+
`WSTP_INC` and `WSTP_LIB` variables at the top of `build.sh`.
|
|
87
|
+
|
|
88
|
+
### 4. Run the test suite
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
node test.js
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Expected last line: `All 28 tests passed.`
|
|
95
|
+
|
|
96
|
+
A more comprehensive suite (both modes + In/Out + comparison) lives in `tmp/tests_all.js`:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
node tmp/tests_all.js
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Expected last line: `All 56 tests passed.`
|
|
103
|
+
|
|
104
|
+
### 5. Quick smoke test
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
const { WstpSession } = require('./build/Release/wstp.node');
|
|
108
|
+
|
|
109
|
+
(async () => {
|
|
110
|
+
const session = new WstpSession();
|
|
111
|
+
const r = await session.evaluate('Prime[10]');
|
|
112
|
+
console.log(r.result.value); // 29
|
|
113
|
+
console.log(r.cellIndex); // 1
|
|
114
|
+
console.log(r.outputName); // "Out[1]=" (may vary by session)
|
|
115
|
+
session.close();
|
|
116
|
+
})();
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Default kernel path** (macOS): `/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel`
|
|
120
|
+
|
|
121
|
+
Pass an explicit path as the first argument to `new WstpSession(path)` if yours differs.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Batch mode vs interactive mode
|
|
126
|
+
|
|
127
|
+
The session constructor accepts an optional second argument:
|
|
128
|
+
|
|
129
|
+
```js
|
|
130
|
+
// Batch mode (default) — EvaluatePacket, bypasses kernel main loop
|
|
131
|
+
const session = new WstpSession(kernelPath);
|
|
132
|
+
// or explicitly:
|
|
133
|
+
const session = new WstpSession(kernelPath, { interactive: false });
|
|
134
|
+
|
|
135
|
+
// Interactive mode — EnterExpressionPacket, full notebook-style evaluation
|
|
136
|
+
const session = new WstpSession(kernelPath, { interactive: true });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Batch mode (`interactive: false`, default)
|
|
140
|
+
|
|
141
|
+
Uses `EvaluatePacket` which **bypasses** the kernel's main evaluation loop.
|
|
142
|
+
Fast and lightweight — the kernel evaluates the expression and returns the result
|
|
143
|
+
without touching `In[n]`/`Out[n]`/`$Line`.
|
|
144
|
+
|
|
145
|
+
- `In[n]`, `Out[n]`, `%`, `%%` are **not** populated by evaluations
|
|
146
|
+
- `$Line` stays at 1 regardless of how many evaluations are run
|
|
147
|
+
- `outputName` in `EvalResult` is always `""` in steady state
|
|
148
|
+
- Suitable for scripting and batch processing where history is not needed
|
|
149
|
+
|
|
150
|
+
### Interactive mode (`interactive: true`)
|
|
151
|
+
|
|
152
|
+
Uses `EnterExpressionPacket` which runs each evaluation through the kernel's full
|
|
153
|
+
main loop — exactly as a Mathematica notebook does.
|
|
154
|
+
|
|
155
|
+
- `In[n]`, `Out[n]`, `%`, `%%` all work and persist across evaluations
|
|
156
|
+
- `$Line` increments with every evaluation
|
|
157
|
+
- `cellIndex` in `EvalResult` reflects the actual kernel `$Line` (not a JS counter)
|
|
158
|
+
- `outputName` in `EvalResult` is `"Out[n]="` for non-Null results, `""` for suppressed
|
|
159
|
+
- `$HistoryLength` controls memory usage (default `100`)
|
|
160
|
+
- Suitable for notebook-like workflows and sessions that rely on running history
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Return Types
|
|
165
|
+
|
|
166
|
+
### `WExpr`
|
|
167
|
+
|
|
168
|
+
A Wolfram Language expression as a plain JS object. The `type` field determines the shape:
|
|
169
|
+
|
|
170
|
+
| `type` | Additional fields | Example |
|
|
171
|
+
|--------|------------------|---------|
|
|
172
|
+
| `"integer"` | `value: number` | `{ type: "integer", value: 42 }` |
|
|
173
|
+
| `"real"` | `value: number` | `{ type: "real", value: 3.14 }` |
|
|
174
|
+
| `"string"` | `value: string` | `{ type: "string", value: "hello" }` |
|
|
175
|
+
| `"symbol"` | `value: string` | `{ type: "symbol", value: "Pi" }` |
|
|
176
|
+
| `"function"` | `head: string`, `args: WExpr[]` | `{ type: "function", head: "Plus", args: [{type:"integer",value:1}, {type:"symbol",value:"x"}] }` |
|
|
177
|
+
| *(absent)* | `error: string` | internal error — normally never seen |
|
|
178
|
+
|
|
179
|
+
Symbols are returned with their context stripped: `System\`Pi` → `"Pi"`.
|
|
180
|
+
|
|
181
|
+
### `EvalResult`
|
|
182
|
+
|
|
183
|
+
The full result of one `evaluate()` call:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
{
|
|
187
|
+
cellIndex: number; // Batch: JS-tracked counter (1-based per session).
|
|
188
|
+
// Interactive: kernel $Line at time of evaluation.
|
|
189
|
+
outputName: string; // Interactive: "Out[n]=" for non-Null results; "" for suppressed.
|
|
190
|
+
// Batch: derived from kernel OUTPUTNAMEPKT if sent.
|
|
191
|
+
result: WExpr; // the expression returned by the kernel
|
|
192
|
+
print: string[]; // lines written by Print[], EchoFunction[], etc.
|
|
193
|
+
messages: string[]; // kernel messages, e.g. "Power::infy: Infinite expression..."
|
|
194
|
+
aborted: boolean; // true when the evaluation was stopped by abort()
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## `WstpSession`
|
|
201
|
+
|
|
202
|
+
### Constructor
|
|
203
|
+
|
|
204
|
+
```js
|
|
205
|
+
const session = new WstpSession(kernelPath?, options?);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Launches a `WolframKernel` process, connects over WSTP, and verifies that `$Output` routing
|
|
209
|
+
is working. Consecutive kernel launches occasionally start with broken output routing (a WSTP
|
|
210
|
+
quirk); the constructor detects this automatically and retries up to 3 times, so it is
|
|
211
|
+
transparent to callers.
|
|
212
|
+
|
|
213
|
+
| Parameter | Type | Default |
|
|
214
|
+
|-----------|------|---------|
|
|
215
|
+
| `kernelPath` | `string` | `/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel` |
|
|
216
|
+
| `options.interactive` | `boolean` | `false` — see [Batch mode vs interactive mode](#batch-mode-vs-interactive-mode) |
|
|
217
|
+
|
|
218
|
+
Throws if the kernel cannot be launched or the WSTP link fails to activate.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
### `evaluate(expr, opts?)`
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
session.evaluate(expr: string, opts?: EvalOptions): Promise<EvalResult>
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Evaluate `expr` in the kernel and return the full result.
|
|
229
|
+
|
|
230
|
+
`expr` is passed to `ToExpression[]` on the kernel side, so it must be valid Wolfram Language
|
|
231
|
+
syntax. Multiple concurrent calls are serialised through an internal queue — it is safe to
|
|
232
|
+
fire them without awaiting.
|
|
233
|
+
|
|
234
|
+
**One call = one cell.** Newlines and semicolons inside `expr` do not split it into multiple
|
|
235
|
+
evaluations; the kernel sees them as a single `CompoundExpression` and returns only the last
|
|
236
|
+
value. Use separate `evaluate()` calls to get separate `cellIndex` / `outputName` values.
|
|
237
|
+
A trailing semicolon suppresses the return value (the kernel returns `Null` and `outputName`
|
|
238
|
+
will be `""`).
|
|
239
|
+
|
|
240
|
+
**`opts` fields** (all optional):
|
|
241
|
+
|
|
242
|
+
| Option | Type | Description |
|
|
243
|
+
|--------|------|-------------|
|
|
244
|
+
| `onPrint(line: string)` | callback | Each `Print[]` or similar output line, as it arrives |
|
|
245
|
+
| `onMessage(msg: string)` | callback | Each kernel warning/error, as it arrives |
|
|
246
|
+
| `onDialogBegin(level: number)` | callback | When `Dialog[]` opens |
|
|
247
|
+
| `onDialogPrint(line: string)` | callback | `Print[]` output inside a dialog |
|
|
248
|
+
| `onDialogEnd(level: number)` | callback | When the dialog closes |
|
|
249
|
+
| `interactive` | `boolean` | **Per-call override** of the session's interactive mode. `true` forces `EnterExpressionPacket` (populates `In`/`Out`); `false` forces `EvaluatePacket` (batch, no history). Omit to use the session default set in the constructor. |
|
|
250
|
+
|
|
251
|
+
All callbacks fire on the JS main thread before the Promise resolves. The Promise is
|
|
252
|
+
guaranteed not to resolve until all queued callback deliveries have completed.
|
|
253
|
+
|
|
254
|
+
```js
|
|
255
|
+
const r = await session.evaluate('Do[Print[i]; Pause[0.3], {i,1,4}]', {
|
|
256
|
+
onPrint: (line) => console.log('live:', line), // fires 4 times during eval
|
|
257
|
+
});
|
|
258
|
+
// r.print is also ['1','2','3','4'] — same data, delivered after eval completes
|
|
259
|
+
|
|
260
|
+
// Session is interactive (default), but force this one call to be batch (no Out/In side-effects):
|
|
261
|
+
const internal = await session.evaluate('VsCodeRenderNth[1, "SVG", 1.0]', { interactive: false });
|
|
262
|
+
|
|
263
|
+
// Session is batch (default), but force this one call to go through the main loop:
|
|
264
|
+
const r2 = await session.evaluate('x = 42', { interactive: true });
|
|
265
|
+
console.log(r2.outputName); // "Out[1]="
|
|
266
|
+
console.log(r2.cellIndex); // 1
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### `sub(expr)`
|
|
272
|
+
|
|
273
|
+
```ts
|
|
274
|
+
session.sub(expr: string): Promise<WExpr>
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Lightweight evaluation that resolves with just the result `WExpr` (no cell index,
|
|
278
|
+
no print/messages arrays).
|
|
279
|
+
|
|
280
|
+
`sub()` has **higher priority** than `evaluate()`: it always runs before the next
|
|
281
|
+
queued `evaluate()`, regardless of arrival order. If the session is currently busy,
|
|
282
|
+
`sub()` waits for the in-flight evaluation to finish, then runs ahead of all other
|
|
283
|
+
queued `evaluate()` calls. Multiple `sub()` calls are ordered FIFO among themselves.
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
const pid = await session.sub('$ProcessID'); // { type: 'integer', value: 12345 }
|
|
287
|
+
const info = await session.sub('$Version'); // { type: 'string', value: '14.1 ...' }
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
### `abort()`
|
|
293
|
+
|
|
294
|
+
```ts
|
|
295
|
+
session.abort(): boolean
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Interrupt the currently-running `evaluate()` call. Thread-safe — safe to call from
|
|
299
|
+
any callback or timer.
|
|
300
|
+
|
|
301
|
+
The in-flight `evaluate()` Promise resolves (not rejects) with:
|
|
302
|
+
```js
|
|
303
|
+
{ aborted: true, result: { type: 'symbol', value: '$Aborted' }, ... }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
The kernel remains alive after abort. Subsequent `evaluate()` and `sub()` calls work
|
|
307
|
+
normally.
|
|
308
|
+
|
|
309
|
+
```js
|
|
310
|
+
const p = session.evaluate('Do[Pause[0.1], {1000}]'); // ~100 s computation
|
|
311
|
+
await sleep(500);
|
|
312
|
+
session.abort(); // cancels after ~500 ms
|
|
313
|
+
const r = await p;
|
|
314
|
+
// r.aborted === true
|
|
315
|
+
// r.result === { type: 'symbol', value: '$Aborted' }
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
---
|
|
319
|
+
|
|
320
|
+
### `dialogEval(expr)`
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
session.dialogEval(expr: string): Promise<WExpr>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Evaluate `expr` inside the currently-open `Dialog[]` subsession. Rejects immediately
|
|
327
|
+
if `isDialogOpen` is false.
|
|
328
|
+
|
|
329
|
+
Returns just the `WExpr` result, not a full `EvalResult`.
|
|
330
|
+
|
|
331
|
+
> **Important**: kernel global state persists into and out of a dialog.
|
|
332
|
+
> Variables set before `Dialog[]` are in scope inside; mutations made with `dialogEval()`
|
|
333
|
+
> persist after the dialog closes.
|
|
334
|
+
|
|
335
|
+
```js
|
|
336
|
+
const p = session.evaluate('x = 10; Dialog[]; x^2');
|
|
337
|
+
await pollUntil(() => session.isDialogOpen);
|
|
338
|
+
|
|
339
|
+
const xVal = await session.dialogEval('x'); // { value: 10 }
|
|
340
|
+
await session.dialogEval('x = 99'); // mutates kernel state
|
|
341
|
+
await session.exitDialog();
|
|
342
|
+
|
|
343
|
+
const r = await p; // r.result.value === 9801 (99^2)
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### `exitDialog(retVal?)`
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
session.exitDialog(retVal?: string): Promise<null>
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
Close the currently-open `Dialog[]` subsession.
|
|
355
|
+
|
|
356
|
+
Sends `EnterTextPacket["Return[retVal]"]` — the interactive-REPL packet that the kernel
|
|
357
|
+
recognises as "exit the dialog". This is **not** the same as `dialogEval('Return[]')`,
|
|
358
|
+
which uses `EvaluatePacket` and leaves `Return[]` unevaluated.
|
|
359
|
+
|
|
360
|
+
Resolves with `null` when `EndDialogPacket` is received.
|
|
361
|
+
Rejects immediately if `isDialogOpen` is false.
|
|
362
|
+
|
|
363
|
+
| Call | Effect |
|
|
364
|
+
|------|--------|
|
|
365
|
+
| `exitDialog()` | `Dialog[]` evaluates to `Null` |
|
|
366
|
+
| `exitDialog('42')` | `Dialog[]` evaluates to `42` |
|
|
367
|
+
| `exitDialog('myVar')` | `Dialog[]` evaluates to the current value of `myVar` |
|
|
368
|
+
|
|
369
|
+
```js
|
|
370
|
+
// Pattern: open dialog, interact, close with a return value
|
|
371
|
+
const p = session.evaluate('result = Dialog[]; result * 2');
|
|
372
|
+
await pollUntil(() => session.isDialogOpen);
|
|
373
|
+
await session.dialogEval('Print["inside the dialog"]');
|
|
374
|
+
await session.exitDialog('21');
|
|
375
|
+
const r = await p; // r.result.value === 42
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
### `interrupt()`
|
|
381
|
+
|
|
382
|
+
```ts
|
|
383
|
+
session.interrupt(): boolean
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Send `WSInterruptMessage` to the kernel (best-effort). The C++ backend handles the
|
|
387
|
+
kernel's `MENUPKT` interrupt-menu response automatically by reading the type+prompt
|
|
388
|
+
payload and replying with bare string `"i"` (inspect mode), causing the kernel to open a
|
|
389
|
+
`Dialog[]` subsession. `isDialogOpen` will flip to `true` when `BEGINDLGPKT` arrives.
|
|
390
|
+
|
|
391
|
+
> The evaluated expression **must** have been started with `onDialogBegin` / `onDialogEnd`
|
|
392
|
+
> callbacks for the dialog to be serviced correctly.
|
|
393
|
+
|
|
394
|
+
```js
|
|
395
|
+
// interrupt() — no Wolfram-side handler needed (C++ handles MENUPKT automatically)
|
|
396
|
+
// The evaluate() call must include dialog callbacks:
|
|
397
|
+
const mainEval = session.evaluate('Do[Pause[0.1], {1000}]', {
|
|
398
|
+
onDialogBegin: () => {},
|
|
399
|
+
onDialogEnd: () => {},
|
|
400
|
+
});
|
|
401
|
+
session.interrupt();
|
|
402
|
+
await pollUntil(() => session.isDialogOpen);
|
|
403
|
+
const val = await session.dialogEval('$Line');
|
|
404
|
+
await session.exitDialog();
|
|
405
|
+
await mainEval;
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
> Optionally, install a Wolfram-side handler to bypass `MENUPKT` entirely:
|
|
409
|
+
> ```js
|
|
410
|
+
> await session.evaluate('Internal`AddHandler["Interrupt", Function[Null, Dialog[]]]');
|
|
411
|
+
> ```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
### `createSubsession(kernelPath?)`
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
session.createSubsession(kernelPath?: string): WstpSession
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Launch a completely independent kernel as a new `WstpSession`. The child has isolated
|
|
422
|
+
state (variables, definitions, memory) and must be closed with `child.close()`.
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
### `close()`
|
|
427
|
+
|
|
428
|
+
```ts
|
|
429
|
+
session.close(): void
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Terminate the kernel process, close the WSTP link, and free all resources.
|
|
433
|
+
Idempotent — safe to call multiple times. After `close()`, calls to `evaluate()` reject
|
|
434
|
+
immediately with `"Session is closed"`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### `isOpen` / `isDialogOpen`
|
|
439
|
+
|
|
440
|
+
```ts
|
|
441
|
+
session.isOpen: boolean // true while the link is open and the kernel is running
|
|
442
|
+
session.isDialogOpen: boolean // true while inside a Dialog[] subsession
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
---
|
|
446
|
+
|
|
447
|
+
## `WstpReader`
|
|
448
|
+
|
|
449
|
+
A reader that **connects to** a named WSTP link created by the kernel (via `LinkCreate`)
|
|
450
|
+
and receives expressions pushed from the kernel side (via `LinkWrite`).
|
|
451
|
+
|
|
452
|
+
Use this when you need real-time data from the kernel while the main link is blocked on a
|
|
453
|
+
long evaluation.
|
|
454
|
+
|
|
455
|
+
### Constructor
|
|
456
|
+
|
|
457
|
+
```js
|
|
458
|
+
const reader = new WstpReader(linkName, protocol?);
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
| Parameter | Type | Default |
|
|
462
|
+
|-----------|------|---------|
|
|
463
|
+
| `linkName` | `string` | *(required)* — value of `linkObject[[1]]` or `LinkName[linkObject]` on the Wolfram side |
|
|
464
|
+
| `protocol` | `string` | `"TCPIP"` |
|
|
465
|
+
|
|
466
|
+
Throws if the connection fails. The WSTP handshake (`WSActivate`) is deferred to the
|
|
467
|
+
first `readNext()` call, so the constructor never blocks the JS main thread.
|
|
468
|
+
|
|
469
|
+
### `readNext()`
|
|
470
|
+
|
|
471
|
+
```ts
|
|
472
|
+
reader.readNext(): Promise<WExpr>
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
Block (on the thread pool) until the kernel writes the next expression with `LinkWrite`.
|
|
476
|
+
Resolves with the expression as a `WExpr`.
|
|
477
|
+
|
|
478
|
+
Rejects when the kernel closes the link (`LinkClose[link]`) or the link encounters an error.
|
|
479
|
+
|
|
480
|
+
### `close()` / `isOpen`
|
|
481
|
+
|
|
482
|
+
```ts
|
|
483
|
+
reader.close(): void
|
|
484
|
+
reader.isOpen: boolean
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
### Full pattern
|
|
488
|
+
|
|
489
|
+
```wolfram
|
|
490
|
+
(* Wolfram side — create a push link *)
|
|
491
|
+
$mon = LinkCreate[LinkProtocol -> "TCPIP"];
|
|
492
|
+
linkName = $mon[[1]]; (* share this string with the JS side somehow *)
|
|
493
|
+
|
|
494
|
+
(* Write immediately, then pause (not: pause then write) *)
|
|
495
|
+
Do[
|
|
496
|
+
LinkWrite[$mon, {i, randomVal}];
|
|
497
|
+
Pause[0.5],
|
|
498
|
+
{i, 1, 20}
|
|
499
|
+
];
|
|
500
|
+
Pause[1]; (* give reader time to drain final value *)
|
|
501
|
+
LinkClose[$mon];
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
```js
|
|
505
|
+
// JS side — connect and read
|
|
506
|
+
const reader = new WstpReader(linkName, 'TCPIP');
|
|
507
|
+
try {
|
|
508
|
+
while (reader.isOpen) {
|
|
509
|
+
const v = await reader.readNext();
|
|
510
|
+
console.log('received:', JSON.stringify(v));
|
|
511
|
+
}
|
|
512
|
+
} catch (e) {
|
|
513
|
+
if (!e.message.includes('closed')) throw e; // normal link-close rejection
|
|
514
|
+
} finally {
|
|
515
|
+
reader.close();
|
|
516
|
+
}
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
> **Timing rules for reliable delivery**:
|
|
520
|
+
> - Call `LinkWrite[link, expr]` *before* any `Pause[]` after each value.
|
|
521
|
+
> A `Pause` before `LinkWrite` can cause the reader to block inside `WSGetType`, which
|
|
522
|
+
> then races with the simultaneous `LinkClose` on the last value.
|
|
523
|
+
> - Add `Pause[1]` before `LinkClose` so the reader receives the final expression before
|
|
524
|
+
> the link-close signal arrives.
|
|
525
|
+
|
|
526
|
+
---
|
|
527
|
+
|
|
528
|
+
## `setDiagHandler(fn)`
|
|
529
|
+
|
|
530
|
+
```ts
|
|
531
|
+
setDiagHandler(fn: ((msg: string) => void) | null | undefined): void
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Register a JS callback that receives internal diagnostic messages from the C++ layer.
|
|
535
|
+
The callback fires on the JS main thread. Pass `null` to clear.
|
|
536
|
+
|
|
537
|
+
Messages cover:
|
|
538
|
+
- `[Session]` — kernel launch, restart attempts, WarmUp results
|
|
539
|
+
- `[WarmUp]` — per-attempt `$WARMUP$` probe
|
|
540
|
+
- `[Eval] pkt=N` — every WSTP packet in the evaluation drain loop
|
|
541
|
+
- `[TSFN][onPrint] dispatch +Nms "..."` — TSFN call timestamp (compare with your
|
|
542
|
+
JS callback timestamp to measure delivery latency)
|
|
543
|
+
- `[WstpReader]` — WSActivate, spin-wait trace, ReadExprRaw result
|
|
544
|
+
|
|
545
|
+
```js
|
|
546
|
+
setDiagHandler((msg) => {
|
|
547
|
+
const ts = new Date().toISOString().slice(11, 23);
|
|
548
|
+
process.stderr.write(`[diag ${ts}] ${msg}\n`);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
// Disable:
|
|
552
|
+
setDiagHandler(null);
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
**Alternative** — set `DEBUG_WSTP=1` in the environment to write the same messages
|
|
556
|
+
directly to `stderr` (no JS handler needed, useful in scripts):
|
|
557
|
+
|
|
558
|
+
```bash
|
|
559
|
+
DEBUG_WSTP=1 node compute.js 2>diag.txt
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Usage Examples
|
|
565
|
+
|
|
566
|
+
### Basic evaluation
|
|
567
|
+
|
|
568
|
+
```js
|
|
569
|
+
const { WstpSession } = require('./build/Release/wstp.node');
|
|
570
|
+
|
|
571
|
+
const KERNEL = '/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel';
|
|
572
|
+
const session = new WstpSession(KERNEL);
|
|
573
|
+
|
|
574
|
+
// Simple expression
|
|
575
|
+
const r = await session.evaluate('Expand[(a + b)^4]');
|
|
576
|
+
console.log(r.result);
|
|
577
|
+
// { type: 'function', head: 'Plus', args: [ ... ] }
|
|
578
|
+
|
|
579
|
+
// Integer result
|
|
580
|
+
const n = await session.evaluate('Prime[100]');
|
|
581
|
+
console.log(n.result.value); // 541
|
|
582
|
+
|
|
583
|
+
// String result
|
|
584
|
+
const v = await session.evaluate('"Hello, " <> "World"');
|
|
585
|
+
console.log(v.result.value); // "Hello, World"
|
|
586
|
+
|
|
587
|
+
session.close();
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
---
|
|
591
|
+
|
|
592
|
+
### Interactive mode — In/Out history
|
|
593
|
+
|
|
594
|
+
With `{ interactive: true }` the kernel runs each evaluation through its full main loop,
|
|
595
|
+
populating `In[n]`, `Out[n]`, and `%`/`%%` exactly as in a Mathematica notebook.
|
|
596
|
+
|
|
597
|
+
```js
|
|
598
|
+
const session = new WstpSession(KERNEL, { interactive: true });
|
|
599
|
+
|
|
600
|
+
// Out[n] is populated automatically; cellIndex reflects the kernel's actual $Line
|
|
601
|
+
const r1 = await session.evaluate('Prime[10]');
|
|
602
|
+
console.log(r1.cellIndex); // e.g. 1
|
|
603
|
+
console.log(r1.outputName); // "Out[1]="
|
|
604
|
+
console.log(r1.result.value); // 29
|
|
605
|
+
|
|
606
|
+
// % and %% return last / second-to-last outputs
|
|
607
|
+
const r2 = await session.evaluate('42');
|
|
608
|
+
const pct = await session.evaluate('%');
|
|
609
|
+
console.log(pct.result.value); // 42
|
|
610
|
+
|
|
611
|
+
// Out[n] is accessible from later evaluations
|
|
612
|
+
const r3 = await session.evaluate('6 * 7');
|
|
613
|
+
const arith = await session.evaluate(`Out[${r3.cellIndex}] + 1`);
|
|
614
|
+
console.log(arith.result.value); // 43
|
|
615
|
+
|
|
616
|
+
// In[n] stores the input; it evaluates to the result of the expression
|
|
617
|
+
const r4 = await session.evaluate('2 + 2');
|
|
618
|
+
const inVal = await session.sub(`In[${r4.cellIndex}]`);
|
|
619
|
+
console.log(inVal.value); // 4
|
|
620
|
+
|
|
621
|
+
// Suppressed evaluations (trailing ;) have empty outputName
|
|
622
|
+
// but Out[n] is still stored internally by the kernel
|
|
623
|
+
const r5 = await session.evaluate('77;');
|
|
624
|
+
console.log(r5.outputName); // ""
|
|
625
|
+
console.log(r5.result.value); // "System`Null"
|
|
626
|
+
const out5 = await session.sub(`Out[${r5.cellIndex}]`);
|
|
627
|
+
console.log(out5.value); // 77 (stored internally)
|
|
628
|
+
|
|
629
|
+
// $Line tracks the kernel counter
|
|
630
|
+
const line = await session.sub('$Line');
|
|
631
|
+
console.log(line.value); // r5.cellIndex + 1
|
|
632
|
+
|
|
633
|
+
session.close();
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
### Streaming output
|
|
639
|
+
|
|
640
|
+
Callbacks fire in real time as the kernel produces output, before the Promise resolves.
|
|
641
|
+
|
|
642
|
+
```js
|
|
643
|
+
const lines = [];
|
|
644
|
+
const r = await session.evaluate(
|
|
645
|
+
'Do[Print["step " <> ToString[k]]; Pause[0.5], {k, 1, 5}]',
|
|
646
|
+
{
|
|
647
|
+
onPrint: (line) => { lines.push(line); console.log('[live]', line); },
|
|
648
|
+
onMessage: (msg) => console.warn('[msg]', msg),
|
|
649
|
+
}
|
|
650
|
+
);
|
|
651
|
+
// lines === ['step 1', 'step 2', 'step 3', 'step 4', 'step 5']
|
|
652
|
+
// r.print === ['step 1', 'step 2', 'step 3', 'step 4', 'step 5'] (same data)
|
|
653
|
+
// r.result.value === 'Null'
|
|
654
|
+
|
|
655
|
+
// Use a promise latch if you need to confirm delivery before acting:
|
|
656
|
+
let resolveAll;
|
|
657
|
+
const allFired = new Promise(r => resolveAll = r);
|
|
658
|
+
let count = 0;
|
|
659
|
+
await session.evaluate('Do[Print[i]; Pause[0.2], {i, 4}]', {
|
|
660
|
+
onPrint: () => { if (++count === 4) resolveAll(); }
|
|
661
|
+
});
|
|
662
|
+
await Promise.race([allFired, timeout(5000)]);
|
|
663
|
+
console.assert(count === 4);
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
---
|
|
667
|
+
|
|
668
|
+
### Concurrent evaluations
|
|
669
|
+
|
|
670
|
+
All queued evaluations run in strict FIFO order — the link is never corrupted.
|
|
671
|
+
|
|
672
|
+
```js
|
|
673
|
+
// Fire all three at once; results arrive in the same order they were queued.
|
|
674
|
+
const [r1, r2, r3] = await Promise.all([
|
|
675
|
+
session.evaluate('Pause[1]; "first"'),
|
|
676
|
+
session.evaluate('Pause[1]; "second"'),
|
|
677
|
+
session.evaluate('Pause[1]; "third"'),
|
|
678
|
+
]);
|
|
679
|
+
// Total time: ~3 s (serialised, not parallel)
|
|
680
|
+
// r1.result.value === 'first', r2.result.value === 'second', etc.
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
---
|
|
684
|
+
|
|
685
|
+
### Priority `sub()` calls
|
|
686
|
+
|
|
687
|
+
`sub()` always jumps ahead of queued `evaluate()` calls — ideal for UI queries like
|
|
688
|
+
"what is the current value of this variable?" while a long computation is running.
|
|
689
|
+
|
|
690
|
+
```js
|
|
691
|
+
// Start a slow batch job
|
|
692
|
+
const batch = session.evaluate('Pause[5]; result = 42');
|
|
693
|
+
|
|
694
|
+
// While it runs, query progress via sub() — fires after the in-flight eval finishes
|
|
695
|
+
// but before any other queued evaluate():
|
|
696
|
+
const val = await session.sub('$Version'); // runs next
|
|
697
|
+
const pid = await session.sub('$ProcessID'); // runs after val
|
|
698
|
+
|
|
699
|
+
await batch;
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
---
|
|
703
|
+
|
|
704
|
+
### Abort a long computation
|
|
705
|
+
|
|
706
|
+
```js
|
|
707
|
+
// Use Do[Pause[...]] so the kernel checks for abort signals regularly
|
|
708
|
+
const p = session.evaluate('Do[Pause[0.1], {1000}]');
|
|
709
|
+
|
|
710
|
+
await new Promise(r => setTimeout(r, 500));
|
|
711
|
+
session.abort();
|
|
712
|
+
|
|
713
|
+
const r = await p;
|
|
714
|
+
console.log(r.aborted); // true
|
|
715
|
+
console.log(r.result.value); // '$Aborted'
|
|
716
|
+
|
|
717
|
+
// Session is still alive — keep evaluating
|
|
718
|
+
const r2 = await session.evaluate('2 + 2');
|
|
719
|
+
console.log(r2.result.value); // 4
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
---
|
|
723
|
+
|
|
724
|
+
### Dialog subsessions
|
|
725
|
+
|
|
726
|
+
`Dialog[]` opens an interactive subsession inside the kernel. Use `dialogEval()` to
|
|
727
|
+
send expressions to it and `exitDialog()` to close it.
|
|
728
|
+
|
|
729
|
+
```js
|
|
730
|
+
// Basic dialog round-trip
|
|
731
|
+
const evalDone = session.evaluate('Dialog[]; "finished"', {
|
|
732
|
+
onDialogBegin: (level) => console.log('dialog opened at level', level),
|
|
733
|
+
onDialogEnd: (level) => console.log('dialog closed at level', level),
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
// Wait for the dialog to open (isDialogOpen flips to true when BEGINDLGPKT arrives)
|
|
737
|
+
await pollUntil(() => session.isDialogOpen);
|
|
738
|
+
|
|
739
|
+
const two = await session.dialogEval('1 + 1'); // { type: 'integer', value: 2 }
|
|
740
|
+
const pi = await session.dialogEval('N[Pi]'); // { type: 'real', value: 3.14159... }
|
|
741
|
+
|
|
742
|
+
await session.exitDialog(); // sends EnterTextPacket["Return[]"]
|
|
743
|
+
const r = await evalDone; // r.result.value === 'finished'
|
|
744
|
+
|
|
745
|
+
// exitDialog with a return value
|
|
746
|
+
const p2 = session.evaluate('x = Dialog[]; x^2');
|
|
747
|
+
await pollUntil(() => session.isDialogOpen);
|
|
748
|
+
await session.exitDialog('7'); // Dialog[] returns 7
|
|
749
|
+
const r2 = await p2; // r2.result.value === 49
|
|
750
|
+
|
|
751
|
+
// dialogEval with Print[] inside
|
|
752
|
+
const prints = [];
|
|
753
|
+
const p3 = session.evaluate('Dialog[]', {
|
|
754
|
+
onDialogPrint: (line) => prints.push(line),
|
|
755
|
+
});
|
|
756
|
+
await pollUntil(() => session.isDialogOpen);
|
|
757
|
+
await session.dialogEval('Print["hello from the dialog"]');
|
|
758
|
+
// Use a promise latch if you need delivery confirmation before exitDialog:
|
|
759
|
+
await session.exitDialog();
|
|
760
|
+
await p3;
|
|
761
|
+
// prints === ['hello from the dialog']
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
> **`dialogEval('Return[]')` does NOT close the dialog.**
|
|
765
|
+
> `Return[]` via `EvaluatePacket` is unevaluated at top level — there is no enclosing
|
|
766
|
+
> structure to return from. Only `exitDialog()` (which uses `EnterTextPacket`) truly
|
|
767
|
+
> exits the dialog.
|
|
768
|
+
|
|
769
|
+
---
|
|
770
|
+
|
|
771
|
+
### Variable monitor — peeking at a running loop
|
|
772
|
+
|
|
773
|
+
You can inspect the current value of any variable while a long computation runs, without
|
|
774
|
+
aborting it. The trick is to use the `Dialog[]`/interrupt mechanism to pause the kernel
|
|
775
|
+
briefly, peek, then resume — all transparent to the running evaluation.
|
|
776
|
+
|
|
777
|
+
#### How it works
|
|
778
|
+
|
|
779
|
+
1. `session.interrupt()` posts `WSInterruptMessage` to the kernel.
|
|
780
|
+
2. The kernel suspends the running evaluation and sends `MENUPKT` (the interrupt menu).
|
|
781
|
+
3. The C++ backend automatically handles `MENUPKT` by reading its type+prompt payload and
|
|
782
|
+
responding with the bare string `"i"` (inspect mode) — no Wolfram-side handler needed.
|
|
783
|
+
4. The kernel opens `Dialog[]` inline: sends `TEXTPKT` (option list) → `BEGINDLGPKT` →
|
|
784
|
+
`INPUTNAMEPKT`. `isDialogOpen` flips to `true` when `INPUTNAMEPKT` arrives.
|
|
785
|
+
5. The JS monitor calls `session.dialogEval('i')` to read the current loop variable, then
|
|
786
|
+
`session.exitDialog()` to resume.
|
|
787
|
+
6. When the main eval resolves the monitor stops.
|
|
788
|
+
|
|
789
|
+
> **Wolfram-side interrupt handler (optional):**
|
|
790
|
+
> If `Internal\`AddHandler["Interrupt", Function[{}, Dialog[]]]` is installed in
|
|
791
|
+
> `init.wl`, the Wolfram handler opens `Dialog[]` directly and the kernel sends
|
|
792
|
+
> `BEGINDLGPKT` without going through `MENUPKT`. Either path works — the C++ backend
|
|
793
|
+
> handles both the `MENUPKT` (interrupt-menu) path and the direct `BEGINDLGPKT` path.
|
|
794
|
+
|
|
795
|
+
#### Prerequisites
|
|
796
|
+
|
|
797
|
+
No special Wolfram-side configuration is required. The C++ backend handles `MENUPKT`
|
|
798
|
+
automatically. The evaluated expression **must** be started with `onDialogBegin` /
|
|
799
|
+
`onDialogEnd` callbacks so the drain loop is in dialog-aware mode:
|
|
800
|
+
|
|
801
|
+
```js
|
|
802
|
+
// The evaluate() call must include dialog callbacks so the drain loop
|
|
803
|
+
// handles BEGINDLGPKT / ENDDLGPKT correctly.
|
|
804
|
+
const mainEval = session.evaluate(expr, {
|
|
805
|
+
onDialogBegin: (_level) => {},
|
|
806
|
+
onDialogEnd: (_level) => {},
|
|
807
|
+
});
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
#### Full example
|
|
811
|
+
|
|
812
|
+
```js
|
|
813
|
+
const { WstpSession } = require('./build/Release/wstp.node');
|
|
814
|
+
const KERNEL = '/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel';
|
|
815
|
+
const session = new WstpSession(KERNEL);
|
|
816
|
+
|
|
817
|
+
// Helper: poll until predicate returns true or deadline expires
|
|
818
|
+
const pollUntil = (pred, intervalMs = 50, timeoutMs = 3000) =>
|
|
819
|
+
new Promise((resolve, reject) => {
|
|
820
|
+
const deadline = Date.now() + timeoutMs;
|
|
821
|
+
const tick = () => {
|
|
822
|
+
if (pred()) return resolve();
|
|
823
|
+
if (Date.now() > deadline) return reject(new Error('pollUntil: timeout'));
|
|
824
|
+
setTimeout(tick, intervalMs);
|
|
825
|
+
};
|
|
826
|
+
tick();
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Step 1: install the interrupt handler
|
|
830
|
+
await session.evaluate(
|
|
831
|
+
'Internal`AddHandler["Interrupt", Function[{}, Dialog[]]]'
|
|
832
|
+
);
|
|
833
|
+
|
|
834
|
+
// Step 2: start the long computation — NOT awaited, runs in background
|
|
835
|
+
// evaluate() accepts onDialogBegin/End so the drain loop services dialogs
|
|
836
|
+
const mainEval = session.evaluate(
|
|
837
|
+
'Do[i = k; Pause[0.2], {k, 1, 50}]; "done"',
|
|
838
|
+
{
|
|
839
|
+
onDialogBegin: (_level) => { /* optional: log */ },
|
|
840
|
+
onDialogEnd: (_level) => { /* optional: log */ },
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
|
|
844
|
+
// Step 3: variable monitor — peek at `i` every second until mainEval resolves
|
|
845
|
+
let running = true;
|
|
846
|
+
mainEval.finally(() => { running = false; });
|
|
847
|
+
|
|
848
|
+
async function monitor() {
|
|
849
|
+
while (running) {
|
|
850
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
851
|
+
if (!running) break;
|
|
852
|
+
|
|
853
|
+
// Send WSInterruptMessage → Wolfram handler opens Dialog[]
|
|
854
|
+
const sent = session.interrupt();
|
|
855
|
+
if (!sent) break; // session closed or idle
|
|
856
|
+
|
|
857
|
+
// Wait for BEGINDLGPKT to arrive (C++ sets isDialogOpen = true)
|
|
858
|
+
try {
|
|
859
|
+
await pollUntil(() => session.isDialogOpen, 50, 3000);
|
|
860
|
+
} catch (_) {
|
|
861
|
+
// Dialog didn't open — computation may have already finished
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Read current value of the loop variable
|
|
866
|
+
let val;
|
|
867
|
+
try {
|
|
868
|
+
val = await session.dialogEval('i');
|
|
869
|
+
} catch (e) {
|
|
870
|
+
await session.exitDialog().catch(() => {});
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
console.log(`[monitor] i = ${val.value}`);
|
|
874
|
+
|
|
875
|
+
// Resume the main evaluation
|
|
876
|
+
await session.exitDialog();
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
await Promise.all([
|
|
881
|
+
mainEval,
|
|
882
|
+
monitor(),
|
|
883
|
+
]);
|
|
884
|
+
|
|
885
|
+
const r = await mainEval;
|
|
886
|
+
console.log('final:', r.result.value); // "done"
|
|
887
|
+
session.close();
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
Expected output (approximate — timing depends on CPU load):
|
|
891
|
+
|
|
892
|
+
```
|
|
893
|
+
[monitor] i = 5
|
|
894
|
+
[monitor] i = 10
|
|
895
|
+
[monitor] i = 15
|
|
896
|
+
...
|
|
897
|
+
final: done
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
#### Extension implementation note
|
|
901
|
+
|
|
902
|
+
For a VS Code notebook extension using this backend:
|
|
903
|
+
|
|
904
|
+
- **No Wolfram-side interrupt handler is required** — the C++ backend handles `MENUPKT`
|
|
905
|
+
automatically by responding with `"i"` (inspect mode). Optionally, installing
|
|
906
|
+
`Internal\`AddHandler["Interrupt", Function[{}, Dialog[]]]` in `init.wl` bypasses
|
|
907
|
+
`MENUPKT` entirely and sends `BEGINDLGPKT` directly; both paths are supported.
|
|
908
|
+
- The ⌥⇧↵ command flow is: `session.interrupt()` → poll `isDialogOpen` → `session
|
|
909
|
+
.dialogEval(cellCode)` → render result as `"Dialog: Out"` in the cell → `session
|
|
910
|
+
.exitDialog()`.
|
|
911
|
+
- The `evaluate()` call for the main long computation **must** pass `onDialogBegin`,
|
|
912
|
+
`onDialogPrint`, and `onDialogEnd` callbacks; these wire the C++ `BEGINDLGPKT` /
|
|
913
|
+
`ENDDLGPKT` handlers that drive `isDialogOpen` and the dialog inner loop.
|
|
914
|
+
- `dialogEval()` and `exitDialog()` push to `dialogQueue_` in C++, which the drain loop
|
|
915
|
+
on the thread-pool thread services between kernel packets — no second link/thread needed.
|
|
916
|
+
- Dialog results from inspect mode are returned as `RETURNTEXTPKT` (OutputForm text) rather
|
|
917
|
+
than `RETURNPKT` (full WL expression); the C++ SDR layer parses these transparently.
|
|
918
|
+
|
|
919
|
+
---
|
|
920
|
+
|
|
921
|
+
### Real-time side channel (`WstpReader`)
|
|
922
|
+
|
|
923
|
+
Use `WstpReader` to receive kernel-pushed data while a long evaluation is running on the
|
|
924
|
+
main link.
|
|
925
|
+
|
|
926
|
+
```js
|
|
927
|
+
// Step 1: create the push link inside the kernel and get its name
|
|
928
|
+
await session.evaluate('$pushLink = LinkCreate[LinkProtocol -> "TCPIP"]');
|
|
929
|
+
const { result: nameExpr } = await session.evaluate('$pushLink[[1]]');
|
|
930
|
+
const linkName = nameExpr.value; // e.g. "60423@127.0.0.1,0@127.0.0.1"
|
|
931
|
+
|
|
932
|
+
// Step 2: connect the JS reader
|
|
933
|
+
const reader = new WstpReader(linkName, 'TCPIP');
|
|
934
|
+
|
|
935
|
+
// Step 3: start the kernel writer (NOT awaited — runs concurrently)
|
|
936
|
+
const bgWriter = session.evaluate(
|
|
937
|
+
'Do[LinkWrite[$pushLink, {i, RandomReal[]}]; Pause[0.5], {i, 1, 10}];' +
|
|
938
|
+
'Pause[1]; LinkClose[$pushLink]; "writer done"'
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
// Step 4: read 10 values in real time
|
|
942
|
+
const received = [];
|
|
943
|
+
try {
|
|
944
|
+
for (let i = 0; i < 10; i++) {
|
|
945
|
+
const v = await reader.readNext();
|
|
946
|
+
// v = { type: 'function', head: 'List', args: [{value:i}, {value:rand}] }
|
|
947
|
+
received.push(v);
|
|
948
|
+
console.log(`item ${i + 1}:`, v.args[0].value, v.args[1].value);
|
|
949
|
+
}
|
|
950
|
+
} finally {
|
|
951
|
+
reader.close();
|
|
952
|
+
try { await bgWriter; } catch (_) {}
|
|
953
|
+
}
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
---
|
|
957
|
+
|
|
958
|
+
### Parallel independent kernels
|
|
959
|
+
|
|
960
|
+
Each `WstpSession` is an entirely separate process with its own state.
|
|
961
|
+
|
|
962
|
+
```js
|
|
963
|
+
// Launch two kernels in parallel
|
|
964
|
+
const [ka, kb] = await Promise.all([
|
|
965
|
+
Promise.resolve(new WstpSession(KERNEL)),
|
|
966
|
+
Promise.resolve(new WstpSession(KERNEL)),
|
|
967
|
+
]);
|
|
968
|
+
|
|
969
|
+
// Run independent computations simultaneously
|
|
970
|
+
const [ra, rb] = await Promise.all([
|
|
971
|
+
ka.evaluate('Sum[1/k^2, {k, 1, 10000}]'),
|
|
972
|
+
kb.evaluate('Sum[1/k^3, {k, 1, 10000}]'),
|
|
973
|
+
]);
|
|
974
|
+
|
|
975
|
+
console.log(ra.result); // Pi^2/6 approximation
|
|
976
|
+
console.log(rb.result); // Apéry's constant approximation
|
|
977
|
+
|
|
978
|
+
ka.close();
|
|
979
|
+
kb.close();
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
---
|
|
983
|
+
|
|
984
|
+
## Error Handling
|
|
985
|
+
|
|
986
|
+
| Situation | Behaviour |
|
|
987
|
+
|-----------|-----------|
|
|
988
|
+
| Syntax error in `expr` | Kernel sends a message; `evaluate()` resolves with `messages: ['...']` and `result: { type: 'symbol', value: 'Null' }` or `'$Failed'` |
|
|
989
|
+
| Expression too deep (> 512 nesting levels) | `evaluate()` **rejects** with `"expression too deep"` — the session stays alive |
|
|
990
|
+
| Abort | `evaluate()` **resolves** with `aborted: true`, `result.value === '$Aborted'` |
|
|
991
|
+
| Kernel crashes | `evaluate()` rejects with a link error message — create a new `WstpSession` |
|
|
992
|
+
| `dialogEval()` / `exitDialog()` when no dialog open | Rejects with `"no dialog subsession is open"` |
|
|
993
|
+
| `evaluate()` after `close()` | Rejects with `"Session is closed"` |
|
|
994
|
+
| `WstpReader.readNext()` after link closes | Rejects with a link-closed error |
|
|
995
|
+
|
|
996
|
+
```js
|
|
997
|
+
// Robust evaluate wrapper
|
|
998
|
+
async function safeEval(session, expr) {
|
|
999
|
+
try {
|
|
1000
|
+
const r = await session.evaluate(expr);
|
|
1001
|
+
if (r.aborted) return { ok: false, reason: 'aborted' };
|
|
1002
|
+
if (r.messages.length) console.warn('kernel messages:', r.messages);
|
|
1003
|
+
return { ok: true, result: r.result };
|
|
1004
|
+
} catch (e) {
|
|
1005
|
+
return { ok: false, reason: e.message };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
---
|
|
1011
|
+
|
|
1012
|
+
## Diagnostic Logging
|
|
1013
|
+
|
|
1014
|
+
Two mechanisms — both disabled by default, zero overhead when off:
|
|
1015
|
+
|
|
1016
|
+
### `setDiagHandler(fn)` — JS callback
|
|
1017
|
+
|
|
1018
|
+
```js
|
|
1019
|
+
const { setDiagHandler } = require('./build/Release/wstp.node');
|
|
1020
|
+
|
|
1021
|
+
setDiagHandler((msg) => {
|
|
1022
|
+
process.stderr.write(`[${new Date().toISOString().slice(11, 23)}] ${msg}\n`);
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
// Measure TSFN delivery latency:
|
|
1026
|
+
// C++ logs: "[TSFN][onPrint] dispatch +142ms ..."
|
|
1027
|
+
// Your handler timestamp - 142ms = module load time offset
|
|
1028
|
+
// Comparing both gives end-to-end callback delivery time
|
|
1029
|
+
|
|
1030
|
+
setDiagHandler(null); // disable
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
### `DEBUG_WSTP=1` — direct stderr from C++
|
|
1034
|
+
|
|
1035
|
+
```bash
|
|
1036
|
+
DEBUG_WSTP=1 node compute.js 2>diag.txt
|
|
1037
|
+
cat diag.txt
|
|
1038
|
+
# [wstp +23ms] [Session] restart attempt 1 — $Output routing broken on previous kernel
|
|
1039
|
+
# [wstp +1240ms] [WarmUp] $Output routing verified on attempt 1
|
|
1040
|
+
# [wstp +1503ms] [Eval] pkt=8
|
|
1041
|
+
# [wstp +1503ms] [Eval] pkt=2
|
|
1042
|
+
# [wstp +1503ms] [TSFN][onPrint] dispatch +1503ms "step 1"
|
|
1043
|
+
# ...
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
Timestamps are module-relative milliseconds (since the addon was loaded).
|