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/binding.gyp ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "variables": {
3
+ # Detect arch: arm64 → MacOSX-ARM64, x86_64 → MacOSX-x86-64
4
+ # Override the whole path by setting WSTP_DIR in the environment.
5
+ "wstp_dir%": "<!(echo \"${WSTP_DIR:-/Applications/Wolfram 3.app/Contents/SystemFiles/Links/WSTP/DeveloperKit/$(uname -m | sed 's/x86_64/MacOSX-x86-64/;s/arm64/MacOSX-ARM64/')/CompilerAdditions}\")"
6
+ },
7
+ "targets": [
8
+ {
9
+ "target_name": "wstp",
10
+ "sources": ["src/addon.cc"],
11
+
12
+ "include_dirs": [
13
+ "<!@(node -p \"require('node-addon-api').include\")",
14
+ "<(wstp_dir)"
15
+ ],
16
+
17
+ "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"],
18
+
19
+ "libraries": [
20
+ # The static library ships as libWSTPi4.a in the macOS DeveloperKit.
21
+ "<(wstp_dir)/libWSTPi4.a"
22
+ ],
23
+
24
+ "conditions": [
25
+ ["OS=='mac'", {
26
+ "xcode_settings": {
27
+ "GCC_ENABLE_CPP_EXCEPTIONS": "YES",
28
+ "MACOSX_DEPLOYMENT_TARGET": "11.0",
29
+ "OTHER_CPLUSPLUSFLAGS": ["-std=c++17", "-Wall", "-Wextra"]
30
+ },
31
+ "link_settings": {
32
+ "libraries": [
33
+ "-framework Foundation",
34
+ "-framework SystemConfiguration",
35
+ "-framework CoreFoundation"
36
+ ]
37
+ }
38
+ }],
39
+ ["OS=='linux'", {
40
+ "cflags_cc": ["-std=c++17", "-Wall", "-Wextra"],
41
+ "libraries": ["-lrt", "-lpthread", "-ldl", "-lm"]
42
+ }],
43
+ ["OS=='win'", {
44
+ "msvs_settings": {
45
+ "VCCLCompilerTool": { "ExceptionHandling": 1 }
46
+ },
47
+ "libraries": ["<(wstp_dir)/wstp64i4s.lib"]
48
+ }]
49
+ ]
50
+ }
51
+ ]
52
+ }
Binary file
package/build.sh ADDED
@@ -0,0 +1,134 @@
1
+ #!/usr/bin/env bash
2
+ # =============================================================================
3
+ # build.sh — direct clang++ build of the WSTP NAPI addon
4
+ #
5
+ # Uses shell-level quoting everywhere, so paths containing spaces (like
6
+ # "WSTP Backend" and "Wolfram 3.app") are handled correctly.
7
+ #
8
+ # Usage:
9
+ # bash build.sh # Release build → build/Release/wstp.node
10
+ # bash build.sh debug # Debug build → build/Debug/wstp.node
11
+ # bash build.sh clean # Remove build/
12
+ # =============================================================================
13
+ set -euo pipefail
14
+
15
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
16
+ cd "$SCRIPT_DIR"
17
+
18
+ # ── clean ─────────────────────────────────────────────────────────────────────
19
+ if [[ "${1:-}" == "clean" ]]; then
20
+ rm -rf build
21
+ echo "Cleaned."
22
+ exit 0
23
+ fi
24
+
25
+ # ── mode ──────────────────────────────────────────────────────────────────────
26
+ BUILD_TYPE="${1:-release}"
27
+ if [[ "$BUILD_TYPE" == "debug" ]]; then
28
+ OUT_DIR="$SCRIPT_DIR/build/Debug"
29
+ OPT_FLAGS=(-O0 -g)
30
+ else
31
+ OUT_DIR="$SCRIPT_DIR/build/Release"
32
+ OPT_FLAGS=(-O2 -DNDEBUG)
33
+ fi
34
+ mkdir -p "$OUT_DIR"
35
+
36
+ OUTPUT="$OUT_DIR/wstp.node"
37
+
38
+ # ── locate node headers ───────────────────────────────────────────────────────
39
+ NODE_BIN="$(which node)"
40
+ NODE_PREFIX="$(node -p "'$NODE_BIN'.replace(/\\/(bin|MacOS)\\/node$/,'')" 2>/dev/null)"
41
+ NODE_HEADERS="${NODE_PREFIX}/include/node"
42
+
43
+ # Fallback: node-gyp cached headers
44
+ if [[ ! -f "$NODE_HEADERS/node_api.h" ]]; then
45
+ NODE_VER="$(node -p "process.version.replace('v','')")"
46
+ for candidate in \
47
+ "$HOME/Library/Caches/node-gyp/$NODE_VER/include/node" \
48
+ "$HOME/.node-gyp/$NODE_VER/include/node" \
49
+ "$HOME/.cache/node-gyp/$NODE_VER/include/node"; do
50
+ if [[ -f "$candidate/node_api.h" ]]; then
51
+ NODE_HEADERS="$candidate"
52
+ break
53
+ fi
54
+ done
55
+ fi
56
+ if [[ ! -f "$NODE_HEADERS/node_api.h" ]]; then
57
+ echo "ERROR: Could not find node_api.h. Run: node-gyp install" >&2
58
+ exit 1
59
+ fi
60
+ echo "Node headers : $NODE_HEADERS"
61
+
62
+ # ── locate node-addon-api headers ─────────────────────────────────────────────
63
+ NAPI_INCLUDE="$(node -p "require('node-addon-api').include.replace(/[\"']/g,'')" 2>/dev/null)"
64
+ if [[ -z "$NAPI_INCLUDE" || ! -f "$NAPI_INCLUDE/napi.h" ]]; then
65
+ echo "ERROR: node-addon-api not found — run: npm install" >&2
66
+ exit 1
67
+ fi
68
+ echo "NAPI headers : $NAPI_INCLUDE"
69
+
70
+ # ── locate WSTP SDK ───────────────────────────────────────────────────────────
71
+ ARCH="$(uname -m)"
72
+ case "$ARCH" in
73
+ arm64) WSTP_SUBDIR="MacOSX-ARM64" ;;
74
+ x86_64) WSTP_SUBDIR="MacOSX-x86-64" ;;
75
+ *) WSTP_SUBDIR="$ARCH" ;;
76
+ esac
77
+
78
+ # WSTP_DIR overrides everything; otherwise derive from WOLFRAM_APP (default: Wolfram 3.app)
79
+ WOLFRAM_APP_DEFAULT="/Applications/Wolfram 3.app"
80
+ if [[ -z "${WSTP_DIR:-}" ]]; then
81
+ WOLFRAM_APP="${WOLFRAM_APP:-$WOLFRAM_APP_DEFAULT}"
82
+ WSTP_SDK="$WOLFRAM_APP/Contents/SystemFiles/Links/WSTP/DeveloperKit/$WSTP_SUBDIR/CompilerAdditions"
83
+ else
84
+ WSTP_SDK="$WSTP_DIR"
85
+ fi
86
+
87
+ if [[ ! -f "$WSTP_SDK/wstp.h" ]]; then
88
+ echo "ERROR: WSTP SDK not found at: $WSTP_SDK" >&2
89
+ echo " Set WOLFRAM_APP to your Wolfram/Mathematica app bundle, e.g.:" >&2
90
+ echo " WOLFRAM_APP=/Applications/Mathematica.app bash build.sh" >&2
91
+ echo " Or set WSTP_DIR directly to the CompilerAdditions directory." >&2
92
+ exit 1
93
+ fi
94
+ echo "WSTP SDK : $WSTP_SDK"
95
+
96
+ # ── find the node shared library for linking ──────────────────────────────────
97
+ # On macOS the node binary itself acts as the import library.
98
+ NODE_LIB="$(node -p "process.execPath")"
99
+ echo "Node lib : $NODE_LIB"
100
+
101
+ # ── compile source ────────────────────────────────────────────────────────────
102
+ SOURCE="$SCRIPT_DIR/src/addon.cc"
103
+
104
+ echo ""
105
+ echo "Compiling $SOURCE …"
106
+
107
+ clang++ \
108
+ -std=c++17 \
109
+ "${OPT_FLAGS[@]}" \
110
+ -Wall -Wextra \
111
+ -fPIC \
112
+ -fvisibility=hidden \
113
+ -DBUILDING_NODE_EXTENSION \
114
+ -DNAPI_DISABLE_CPP_EXCEPTIONS \
115
+ -I "$NODE_HEADERS" \
116
+ -I "$NAPI_INCLUDE" \
117
+ -I "$WSTP_SDK" \
118
+ -c "$SOURCE" \
119
+ -o "$OUT_DIR/addon.o"
120
+
121
+ echo "Linking $OUTPUT …"
122
+
123
+ clang++ \
124
+ -dynamiclib \
125
+ -undefined dynamic_lookup \
126
+ -o "$OUTPUT" \
127
+ "$OUT_DIR/addon.o" \
128
+ "$WSTP_SDK/libWSTPi4.a" \
129
+ -framework Foundation \
130
+ -framework SystemConfiguration \
131
+ -framework CoreFoundation
132
+
133
+ echo ""
134
+ echo "Build succeeded: $OUTPUT"
@@ -0,0 +1,340 @@
1
+ // =============================================================================
2
+ // wstp-backend/demo.js
3
+ //
4
+ // Demonstrates the main features of the WSTP Node.js addon:
5
+ // 1. Basic evaluation and typed-tree results
6
+ // 2. Symbolic / algebraic computation
7
+ // 3. Abort a long-running computation
8
+ // 4. Isolated subsessions (independent kernel processes)
9
+ // =============================================================================
10
+
11
+ 'use strict';
12
+
13
+ const path = require('path');
14
+
15
+ // Load the compiled native addon.
16
+ const { WstpSession } = require(path.join(__dirname, '..', 'build', 'Release', 'wstp'));
17
+
18
+ // ─── pretty-printer for the typed ExprTree ───────────────────────────────────
19
+ function prettyExpr(node, indent = 0) {
20
+ const pad = ' '.repeat(indent);
21
+ if (!node || typeof node !== 'object') return String(node);
22
+
23
+ switch (node.type) {
24
+ case 'integer':
25
+ case 'real':
26
+ return `${node.value}`;
27
+ case 'string':
28
+ return `"${node.value}"`;
29
+ case 'symbol':
30
+ return node.value;
31
+ case 'function': {
32
+ if (!node.args || node.args.length === 0)
33
+ return `${node.head}[]`;
34
+ if (node.args.length <= 3 && node.args.every(a => a.type !== 'function'))
35
+ return `${node.head}[${node.args.map(a => prettyExpr(a)).join(', ')}]`;
36
+ const inner = node.args
37
+ .map(a => `${pad} ${prettyExpr(a, indent + 1)}`)
38
+ .join(',\n');
39
+ return `${node.head}[\n${inner}\n${pad}]`;
40
+ }
41
+ default:
42
+ return JSON.stringify(node);
43
+ }
44
+ }
45
+
46
+ // ─── utility: run one evaluation and print result ────────────────────────────
47
+ async function evalAndPrint(session, expr, label) {
48
+ process.stdout.write(`\n[${label}]\n In: ${expr}\n`);
49
+ try {
50
+ const r = await session.evaluate(expr);
51
+ r.print.forEach(line => process.stdout.write(` Print: ${line}\n`));
52
+ r.messages.forEach(msg => process.stdout.write(` Msg: ${msg}\n`));
53
+ const tag = r.outputName ? r.outputName : `(cell ${r.cellIndex})`;
54
+ process.stdout.write(` ${tag} ${prettyExpr(r.result)}\n`);
55
+ return r;
56
+ } catch (err) {
57
+ process.stdout.write(` ERR: ${err.message}\n`);
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // ─── separator ───────────────────────────────────────────────────────────────
63
+ function section(title) {
64
+ console.log('\n' + '═'.repeat(60));
65
+ console.log(' ' + title);
66
+ console.log('═'.repeat(60));
67
+ }
68
+
69
+ // =============================================================================
70
+ async function main() {
71
+
72
+ // ── 1. Launch the main kernel session ────────────────────────────────────
73
+ section('1. Launch kernel');
74
+ console.log(' Starting WolframKernel via WSTP…');
75
+ const session = new WstpSession(); // uses default kernel path
76
+ console.log(` isOpen: ${session.isOpen}`);
77
+
78
+ // ── 2. EvalResult assertions ──────────────────────────────────────────────
79
+ section('2. EvalResult assertions — verify all fields');
80
+
81
+ // Test 1: basic integer, cellIndex populated, no side-effects
82
+ let r = await session.evaluate('1 + 1');
83
+ console.assert(r.cellIndex > 0, 'cellIndex > 0');
84
+ console.assert(r.result.type === 'integer', 'result.type === integer');
85
+ console.assert(r.result.value === 2, 'result.value === 2');
86
+ console.assert(r.print.length === 0, 'no print output');
87
+ console.assert(r.messages.length === 0, 'no messages');
88
+ console.assert(r.aborted === false, 'not aborted');
89
+ console.log(` [PASS] basic integer cellIndex=${r.cellIndex} outputName="${r.outputName}"`);
90
+
91
+ // Test 2: Print[] captured in r.print
92
+ r = await session.evaluate('Print["hello"]; 42');
93
+ console.assert(r.print[0] === 'hello', 'print[0] === "hello"');
94
+ console.assert(r.result.type === 'integer', 'result.type === integer');
95
+ console.assert(r.result.value === 42, 'result.value === 42');
96
+ console.log(` [PASS] Print[] captured: "${r.print[0]}", return=${r.result.value}`);
97
+
98
+ // Test 3: kernel message captured in r.messages
99
+ r = await session.evaluate('1/0');
100
+ console.assert(r.messages.length > 0, 'message captured');
101
+ console.log(` [PASS] message: ${r.messages[0]}`);
102
+
103
+ // Test 4: abort → aborted flag + $Aborted symbol, kernel survives
104
+ console.log(' Starting Pause[30]; aborting after 300 ms…');
105
+ const abortPromise4 = session.evaluate('Pause[30]');
106
+ await new Promise(res => setTimeout(res, 300));
107
+ session.abort();
108
+ r = await abortPromise4;
109
+ console.assert(r.aborted === true, 'aborted === true');
110
+ console.assert(r.result.value === '$Aborted', 'result is $Aborted symbol');
111
+ console.log(' [PASS] abort → aborted:true, result.$Aborted');
112
+ await evalAndPrint(session, '1+1', 'kernel alive after abort');
113
+
114
+ // ── 3. Arithmetic & typed integers / reals ────────────────────────────────
115
+ section('3. Basic arithmetic — typed result tree');
116
+
117
+ await evalAndPrint(session, '2 + 2', 'integer addition');
118
+ await evalAndPrint(session, 'N[Pi, 20]', 'high-precision real');
119
+ await evalAndPrint(session, '"Hello WSTP"', 'string literal');
120
+ await evalAndPrint(session, 'True', 'symbol');
121
+
122
+ // A nested function (List of List):
123
+ await evalAndPrint(session, '{{1,2},{3,4}}', 'matrix (nested List)');
124
+
125
+ // ── 4. Symbolic / algebraic ───────────────────────────────────────────────
126
+ section('4. Symbolic computation');
127
+
128
+ await evalAndPrint(session, 'Expand[(x + y)^4]', 'polynomial expansion');
129
+ await evalAndPrint(session, 'D[Sin[x]^2, x]', 'derivative');
130
+ await evalAndPrint(session, 'Integrate[x^2, {x,0,1}]', 'definite integral');
131
+ await evalAndPrint(session, 'Factor[x^6 - 1]', 'factoring');
132
+
133
+ // ── 5. Persistent state across evaluations ──────────────────────────────────
134
+ section('5. Persistent state');
135
+
136
+ await evalAndPrint(session, 'myVar = 42', 'set variable');
137
+ await evalAndPrint(session, 'myVar * 2', 'use variable');
138
+ await evalAndPrint(session, 'myList = Range[5]', 'build list');
139
+ await evalAndPrint(session, 'Total[myList]', 'sum of list');
140
+
141
+ // ── 6. Abort a long-running computation ───────────────────────────────────
142
+ section('6. Abort — interrupt a slow computation');
143
+
144
+ console.log(' Starting Pause[10] (10-second sleep) then aborting after 300 ms…');
145
+ const evalPromise = session.evaluate('Pause[10]; "should never reach here"');
146
+
147
+ await new Promise(r => setTimeout(r, 300)); // let the kernel start
148
+ const abortOk = session.abort();
149
+ console.log(` abort() sent: ${abortOk}`);
150
+
151
+ const abortResult = await evalPromise;
152
+ console.log(` aborted: ${abortResult.aborted}`);
153
+ console.log(` result: ${prettyExpr(abortResult.result)}`);
154
+
155
+ // Give the kernel a moment to reset.
156
+ await evalAndPrint(session, '1 + 1', 'kernel still alive after abort');
157
+
158
+ // ── 7. Subsession — isolated independent kernel ───────────────────────────
159
+ section('7. Subsession — isolated child kernel');
160
+
161
+ console.log(' Launching child kernel…');
162
+ const sub = session.createSubsession();
163
+ console.log(` subsession isOpen: ${sub.isOpen}`);
164
+
165
+ // Set a variable in the subsession that shadows the parent's myVar.
166
+ await evalAndPrint(sub, 'myVar = 999', 'sub: set myVar = 999');
167
+ await evalAndPrint(session, 'myVar', 'main: myVar still 42');
168
+ await evalAndPrint(sub, 'myVar', 'sub: myVar is 999');
169
+
170
+ // Run an independent heavy computation in the subsession.
171
+ await evalAndPrint(sub, 'PrimeQ[2^127 - 1]', 'sub: Mersenne prime test');
172
+
173
+ // Close child — does NOT affect parent.
174
+ sub.close();
175
+ console.log(` subsession isOpen after close: ${sub.isOpen}`);
176
+
177
+ // Parent still works.
178
+ await evalAndPrint(session, 'myVar', 'main: myVar unaffected after sub.close()');
179
+
180
+ // ── 8. Parallel subsessions ───────────────────────────────────────────────
181
+ section('8. Parallel subsessions — two isolated kernels at once');
182
+
183
+ const subA = session.createSubsession();
184
+ const subB = session.createSubsession();
185
+
186
+ // Kick off two evaluations simultaneously.
187
+ const [resA, resB] = await Promise.all([
188
+ subA.evaluate('Total[Table[k^2, {k, 1, 1000}]]'),
189
+ subB.evaluate('Total[Table[k^3, {k, 1, 1000}]]'),
190
+ ]);
191
+ console.log(` subA result (Σ k², k=1..1000): ${prettyExpr(resA.result)}`);
192
+ console.log(` subB result (Σ k³, k=1..1000): ${prettyExpr(resB.result)}`);
193
+
194
+ subA.close();
195
+ subB.close();
196
+
197
+ // ── 9. Evaluation queue ──────────────────────────────────────────────────
198
+ section('9. Evaluation queue — fire 3 evals without awaiting');
199
+
200
+ // Fire three evaluate() calls without awaiting — the addon queues them and
201
+ // runs them sequentially so the kernel link is never corrupted.
202
+ const qp1 = session.evaluate('1');
203
+ const qp2 = session.evaluate('2');
204
+ const qp3 = session.evaluate('3');
205
+ const [qr1, qr2, qr3] = await Promise.all([qp1, qp2, qp3]);
206
+ console.assert(qr1.result.value === 1, 'queue result 1 === 1');
207
+ console.assert(qr2.result.value === 2, 'queue result 2 === 2');
208
+ console.assert(qr3.result.value === 3, 'queue result 3 === 3');
209
+ console.log(` [PASS] queue results: ${qr1.result.value}, ${qr2.result.value}, ${qr3.result.value}`);
210
+
211
+ // ── 10. Streaming Print callbacks ─────────────────────────────────────────
212
+ section('10. Streaming onPrint — receive Print[] lines in real time');
213
+
214
+ const streamedLines = [];
215
+ const streamPrintResult = await session.evaluate(
216
+ 'Do[Print["line " <> ToString[i]], {i, 1, 4}]',
217
+ {
218
+ onPrint: (line) => {
219
+ streamedLines.push(line);
220
+ process.stdout.write(` [stream] Print: ${line}\n`);
221
+ }
222
+ }
223
+ );
224
+ console.assert(streamedLines.length === 4, 'streamed 4 lines');
225
+ console.assert(streamedLines[0] === 'line 1', 'first line is "line 1"');
226
+ console.assert(streamedLines[3] === 'line 4', 'last line is "line 4"');
227
+ // Callbacks fire before the promise resolves, so all lines already in r.print.
228
+ console.assert(streamPrintResult.print.length === 4, 'r.print also has 4 lines');
229
+ console.log(` [PASS] onPrint fired ${streamedLines.length}x, r.print has ${streamPrintResult.print.length} entries`);
230
+
231
+ // ── 11. Streaming message callbacks ───────────────────────────────────────
232
+ section('11. Streaming onMessage — receive kernel messages in real time');
233
+
234
+ const streamedMsgs = [];
235
+ const streamMsgResult = await session.evaluate(
236
+ '1/0',
237
+ {
238
+ onMessage: (msg) => {
239
+ streamedMsgs.push(msg);
240
+ process.stdout.write(` [stream] Msg: ${msg}\n`);
241
+ }
242
+ }
243
+ );
244
+ console.assert(streamedMsgs.length > 0, 'at least one message streamed');
245
+ console.assert(streamMsgResult.messages.length > 0, 'r.messages also populated');
246
+ console.log(` [PASS] onMessage fired ${streamedMsgs.length}x: ${streamedMsgs[0]}`);
247
+
248
+ // ── 12. Abort + queue drain ────────────────────────────────────────────────
249
+ section('12. Abort + queue drain — in-flight aborts, queued evals still run');
250
+
251
+ // First eval will be aborted in-flight.
252
+ const abortQueueP1 = session.evaluate('Pause[30]');
253
+ // Second eval is queued — should run normally after the abort resolves.
254
+ const abortQueueP2 = session.evaluate('"after-abort"');
255
+
256
+ await new Promise(res => setTimeout(res, 300));
257
+ session.abort();
258
+
259
+ const aqr1 = await abortQueueP1;
260
+ console.assert(aqr1.aborted === true, 'first eval was aborted');
261
+ console.log(` [PASS] first eval aborted: ${aqr1.aborted}`);
262
+
263
+ const aqr2 = await abortQueueP2;
264
+ console.assert(aqr2.result.type === 'string', 'second eval ran normally');
265
+ console.assert(aqr2.result.value === 'after-abort', 'second eval returned correct value');
266
+ console.log(` [PASS] queued eval ran after abort: result = "${aqr2.result.value}"`);
267
+
268
+ // ── 13. sub() when idle ───────────────────────────────────────────────────
269
+ section('13. sub() when idle — lightweight eval, resolves with just WExpr');
270
+
271
+ const subIdleResult = await session.sub('2 + 2');
272
+ console.assert(subIdleResult.type === 'integer', 'sub idle: type is integer');
273
+ console.assert(subIdleResult.value === 4, 'sub idle: value is 4');
274
+ console.log(` [PASS] sub() idle → ${JSON.stringify(subIdleResult)}`);
275
+
276
+ // ── 14. sub() queued before pending evaluate() ────────────────────────────
277
+ section('14. sub() priority — sub() runs before queued evaluate() calls');
278
+
279
+ // Fire a quick eval, then immediately queue: sub + another evaluate.
280
+ // sub() should resolve before the queued evaluate().
281
+ const orderEvalP = session.evaluate('"main"');
282
+ const subP14 = session.sub('"sub-first"');
283
+ const afterP14 = session.evaluate('"after"');
284
+
285
+ const subR14 = await subP14;
286
+ const mainR14 = await orderEvalP;
287
+ const aftR14 = await afterP14;
288
+
289
+ console.assert(subR14.type === 'string', 'sub result type');
290
+ console.assert(subR14.value === 'sub-first', 'sub resolved with correct value');
291
+ console.assert(mainR14.result.value === 'main', 'main eval resolved');
292
+ console.assert(aftR14.result.value === 'after','after eval resolved');
293
+ console.log(` [PASS] sub() ran before queued evaluate(): "${subR14.value}"`);
294
+
295
+ // ── 15. Multiple sub()s queued — all resolve in order ─────────────────────
296
+ section('15. Multiple sub()s — three subs resolve with correct values');
297
+
298
+ const [s1, s2, s3] = await Promise.all([
299
+ session.sub('1 + 1'),
300
+ session.sub('2 + 2'),
301
+ session.sub('3 + 3'),
302
+ ]);
303
+ console.assert(s1.value === 2, 'sub1 = 2');
304
+ console.assert(s2.value === 4, 'sub2 = 4');
305
+ console.assert(s3.value === 6, 'sub3 = 6');
306
+ console.log(` [PASS] three subs: ${s1.value}, ${s2.value}, ${s3.value}`);
307
+
308
+ // ── 16. sub() sets a variable, next eval sees it ───────────────────────────
309
+ section('16. sub() side-effect — sub() sets a variable read by next eval');
310
+
311
+ await session.sub('subVar$ = 99');
312
+ const sideR = await session.sub('subVar$');
313
+ console.assert(sideR.value === 99, 'sub-set variable readable by next sub');
314
+ console.log(` [PASS] side-effecting sub: subVar$ = ${sideR.value}`);
315
+
316
+ // ── 17. Session survives abort, sub() still works ─────────────────────────
317
+ section('17. Post-abort sub() — session stays healthy after abort');
318
+
319
+ const abortP = session.evaluate('Pause[30]');
320
+ await new Promise(res => setTimeout(res, 200));
321
+ session.abort();
322
+ const abortR = await abortP;
323
+ console.assert(abortR.aborted === true, 'aborted');
324
+
325
+ const subAfterAbort = await session.sub('"hello"');
326
+ console.assert(subAfterAbort.type === 'string', 'post-abort sub type');
327
+ console.assert(subAfterAbort.value === 'hello', 'post-abort sub value');
328
+ console.log(` [PASS] sub() after abort → ${JSON.stringify(subAfterAbort)}`);
329
+
330
+ // ── 18. Clean up ──────────────────────────────────────────────────────────
331
+ section('18. Shutdown');
332
+ session.close();
333
+ console.log(` main session isOpen: ${session.isOpen}`);
334
+ console.log('\n Demo complete.\n');
335
+ }
336
+
337
+ main().catch(err => {
338
+ console.error('\nFatal error:', err);
339
+ process.exitCode = 1;
340
+ });