@aztec/bb.js 3.0.0-nightly.20251124 → 3.0.0-nightly.20251126

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.
Files changed (80) hide show
  1. package/build/amd64-linux/bb +0 -0
  2. package/build/amd64-linux/nodejs_module.node +0 -0
  3. package/build/amd64-macos/bb +0 -0
  4. package/build/amd64-macos/nodejs_module.node +0 -0
  5. package/build/arm64-linux/bb +0 -0
  6. package/build/arm64-linux/nodejs_module.node +0 -0
  7. package/build/arm64-macos/bb +0 -0
  8. package/build/arm64-macos/nodejs_module.node +0 -0
  9. package/dest/browser/barretenberg/index.js +7 -6
  10. package/dest/browser/barretenberg_wasm/fetch_code/browser/barretenberg-threads.js +1 -1
  11. package/dest/browser/barretenberg_wasm/fetch_code/browser/barretenberg.js +1 -1
  12. package/dest/browser/bb_backends/index.d.ts +1 -1
  13. package/dest/browser/bb_backends/index.d.ts.map +1 -1
  14. package/dest/browser/bb_backends/index.js +2 -2
  15. package/dest/browser/bin/index.d.ts +3 -0
  16. package/dest/browser/bin/index.d.ts.map +1 -0
  17. package/dest/browser/bin/index.js +11 -0
  18. package/dest/node/barretenberg/index.js +7 -6
  19. package/dest/node/barretenberg/poseidon.bench.test.js +55 -13
  20. package/dest/node/barretenberg_wasm/barretenberg-threads.wasm.gz +0 -0
  21. package/dest/node/bb_backends/index.d.ts +1 -1
  22. package/dest/node/bb_backends/index.d.ts.map +1 -1
  23. package/dest/node/bb_backends/index.js +2 -2
  24. package/dest/node/bb_backends/node/index.d.ts.map +1 -1
  25. package/dest/node/bb_backends/node/index.js +6 -8
  26. package/dest/node/bb_backends/node/native_shm.d.ts +1 -2
  27. package/dest/node/bb_backends/node/native_shm.d.ts.map +1 -1
  28. package/dest/node/bb_backends/node/native_shm.js +9 -20
  29. package/dest/node/bb_backends/node/native_shm_async.d.ts +51 -0
  30. package/dest/node/bb_backends/node/native_shm_async.d.ts.map +1 -0
  31. package/dest/node/bb_backends/node/native_shm_async.js +234 -0
  32. package/dest/node/bbapi/exception_handling.test.d.ts +2 -0
  33. package/dest/node/bbapi/exception_handling.test.d.ts.map +1 -0
  34. package/dest/node/bbapi/exception_handling.test.js +48 -0
  35. package/dest/node/bin/index.d.ts +3 -0
  36. package/dest/node/bin/index.d.ts.map +1 -0
  37. package/dest/node/bin/index.js +11 -0
  38. package/dest/node-cjs/barretenberg/index.js +7 -6
  39. package/dest/node-cjs/barretenberg/poseidon.bench.test.js +55 -13
  40. package/dest/node-cjs/barretenberg_wasm/barretenberg-threads.wasm.gz +0 -0
  41. package/dest/node-cjs/bb_backends/index.d.ts +1 -1
  42. package/dest/node-cjs/bb_backends/index.d.ts.map +1 -1
  43. package/dest/node-cjs/bb_backends/index.js +2 -2
  44. package/dest/node-cjs/bb_backends/node/index.d.ts.map +1 -1
  45. package/dest/node-cjs/bb_backends/node/index.js +6 -8
  46. package/dest/node-cjs/bb_backends/node/native_shm.d.ts +1 -2
  47. package/dest/node-cjs/bb_backends/node/native_shm.d.ts.map +1 -1
  48. package/dest/node-cjs/bb_backends/node/native_shm.js +9 -20
  49. package/dest/node-cjs/bb_backends/node/native_shm_async.d.ts +51 -0
  50. package/dest/node-cjs/bb_backends/node/native_shm_async.d.ts.map +1 -0
  51. package/dest/node-cjs/bb_backends/node/native_shm_async.js +238 -0
  52. package/dest/node-cjs/bbapi/exception_handling.test.d.ts +2 -0
  53. package/dest/node-cjs/bbapi/exception_handling.test.d.ts.map +1 -0
  54. package/dest/node-cjs/bbapi/exception_handling.test.js +50 -0
  55. package/dest/node-cjs/bin/index.d.ts +3 -0
  56. package/dest/node-cjs/bin/index.d.ts.map +1 -0
  57. package/dest/node-cjs/bin/index.js +13 -0
  58. package/package.json +5 -3
  59. package/src/barretenberg/index.ts +5 -5
  60. package/src/barretenberg/poseidon.bench.test.ts +62 -12
  61. package/src/bb_backends/index.ts +1 -1
  62. package/src/bb_backends/node/index.ts +5 -17
  63. package/src/bb_backends/node/native_shm.ts +8 -21
  64. package/src/bb_backends/node/native_shm_async.ts +266 -0
  65. package/src/bbapi/exception_handling.test.ts +54 -0
  66. package/src/bin/index.ts +14 -0
  67. package/src/cbind/generated/api_types.ts +4271 -0
  68. package/src/cbind/generated/async.ts +717 -0
  69. package/src/cbind/generated/curve_constants.ts +53 -0
  70. package/src/cbind/generated/sync.ts +663 -0
  71. package/dest/browser/bb_backends/sync_to_async_adapter.d.ts +0 -15
  72. package/dest/browser/bb_backends/sync_to_async_adapter.d.ts.map +0 -1
  73. package/dest/browser/bb_backends/sync_to_async_adapter.js +0 -20
  74. package/dest/node/bb_backends/sync_to_async_adapter.d.ts +0 -15
  75. package/dest/node/bb_backends/sync_to_async_adapter.d.ts.map +0 -1
  76. package/dest/node/bb_backends/sync_to_async_adapter.js +0 -20
  77. package/dest/node-cjs/bb_backends/sync_to_async_adapter.d.ts +0 -15
  78. package/dest/node-cjs/bb_backends/sync_to_async_adapter.d.ts.map +0 -1
  79. package/dest/node-cjs/bb_backends/sync_to_async_adapter.js +0 -24
  80. package/src/bb_backends/sync_to_async_adapter.ts +0 -21
@@ -94,7 +94,20 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => {
94
94
  nativeSocketTime = performance.now() - nativeSocketStart;
95
95
  }
96
96
 
97
- // Benchmark 3: Native Shared Memory (async)
97
+ // Benchmark 3: Native Socket (async, request pipelined)
98
+ let nativeSocketPipelinedTime = 0;
99
+ if (nativeSocketApi) {
100
+ const nativeSocketPipelinedStart = performance.now();
101
+ // Use promise.all to pipeline requests
102
+ const promises = [];
103
+ for (let i = 0; i < ITERATIONS; i++) {
104
+ promises.push(nativeSocketApi.poseidon2Hash({ inputs }));
105
+ }
106
+ await Promise.all(promises);
107
+ nativeSocketPipelinedTime = performance.now() - nativeSocketPipelinedStart;
108
+ }
109
+
110
+ // Benchmark 4: Native Shared Memory (async)
98
111
  let nativeShmTime = 0;
99
112
  if (nativeShmApi) {
100
113
  const nativeShmStart = performance.now();
@@ -104,7 +117,20 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => {
104
117
  nativeShmTime = performance.now() - nativeShmStart;
105
118
  }
106
119
 
107
- // Benchmark 4: Native Shared Memory (sync)
120
+ // Benchmark 5: Native Shared Memory (async, request pipelined)
121
+ let nativeShmPipelinedTime = 0;
122
+ if (nativeShmApi) {
123
+ const nativeShmPipelinedStart = performance.now();
124
+ // Use promise.all to pipeline requests
125
+ const promises = [];
126
+ for (let i = 0; i < ITERATIONS; i++) {
127
+ promises.push(nativeShmApi.poseidon2Hash({ inputs }));
128
+ }
129
+ await Promise.all(promises);
130
+ nativeShmPipelinedTime = performance.now() - nativeShmPipelinedStart;
131
+ }
132
+
133
+ // Benchmark 6: Native Shared Memory (sync)
108
134
  let nativeShmSyncTime = 0;
109
135
  if (nativeShmSyncApi) {
110
136
  const nativeShmSyncStart = performance.now();
@@ -116,16 +142,20 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => {
116
142
 
117
143
  // Calculate metrics (all relative to WASM baseline)
118
144
  const nativeSocketOverhead = ((nativeSocketTime - wasmTime) / wasmTime) * 100;
145
+ const nativeSocketPipelinedOverhead = ((nativeSocketPipelinedTime - wasmTime) / wasmTime) * 100;
119
146
  const nativeShmOverhead = ((nativeShmTime - wasmTime) / wasmTime) * 100;
147
+ const nativeShmPipelinedOverhead = ((nativeShmPipelinedTime - wasmTime) / wasmTime) * 100;
120
148
  const nativeShmSyncOverhead = ((nativeShmSyncTime - wasmTime) / wasmTime) * 100;
121
149
 
122
150
  const avgWasmTimeUs = (wasmTime / ITERATIONS) * 1000;
123
151
  const avgNativeSocketTimeUs = (nativeSocketTime / ITERATIONS) * 1000;
152
+ const avgNativeSocketPipelinedTimeUs = (nativeSocketPipelinedTime / ITERATIONS) * 1000;
124
153
  const avgNativeShmTimeUs = (nativeShmTime / ITERATIONS) * 1000;
154
+ const avgNativeShmPipelinedTimeUs = (nativeShmPipelinedTime / ITERATIONS) * 1000;
125
155
  const avgNativeShmSyncTimeUs = (nativeShmSyncTime / ITERATIONS) * 1000;
126
156
 
127
157
  process.stdout.write(
128
- `┌─ Size ${size.toString().padStart(3)} field elements ──────────────────────────────────┐\n`,
158
+ `┌─ Size ${size.toString().padStart(3)} field elements ───────────────────────────────────────┐\n`,
129
159
  );
130
160
  const formatOverhead = (overhead: number): string => {
131
161
  const sign = overhead >= 0 ? '+' : '-';
@@ -135,37 +165,57 @@ describe('poseidon2Hash benchmark (Async API): WASM vs Native', () => {
135
165
 
136
166
  if (wasmApi) {
137
167
  process.stdout.write(
138
- `│ WASM: ${wasmTime.toFixed(2).padStart(8)}ms (${avgWasmTimeUs.toFixed(2).padStart(7)}µs/call) [baseline] │\n`,
168
+ `│ WASM: ${wasmTime.toFixed(2).padStart(8)}ms (${avgWasmTimeUs.toFixed(2).padStart(7)}µs/call) [baseline] │\n`,
169
+ );
170
+ } else {
171
+ process.stdout.write(`│ WASM: unavailable │\n`);
172
+ }
173
+
174
+ if (nativeSocketApi) {
175
+ process.stdout.write(
176
+ `│ Native Socket: ${nativeSocketTime.toFixed(2).padStart(8)}ms (${avgNativeSocketTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeSocketOverhead)} │\n`,
139
177
  );
140
178
  } else {
141
- process.stdout.write(`│ WASM: unavailable │\n`);
179
+ process.stdout.write(`│ Native Socket: unavailable │\n`);
142
180
  }
143
181
 
144
182
  if (nativeSocketApi) {
145
183
  process.stdout.write(
146
- `│ Native Socket: ${nativeSocketTime.toFixed(2).padStart(8)}ms (${avgNativeSocketTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeSocketOverhead)} │\n`,
184
+ `│ Native Socket Pipelined: ${nativeSocketPipelinedTime
185
+ .toFixed(2)
186
+ .padStart(8)}ms (${avgNativeSocketPipelinedTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(
187
+ nativeSocketPipelinedOverhead,
188
+ )} │\n`,
189
+ );
190
+ } else {
191
+ process.stdout.write(`│ Native Socket Pipelined: unavailable │\n`);
192
+ }
193
+
194
+ if (nativeShmApi) {
195
+ process.stdout.write(
196
+ `│ Native Shared: ${nativeShmTime.toFixed(2).padStart(8)}ms (${avgNativeShmTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeShmOverhead)} │\n`,
147
197
  );
148
198
  } else {
149
- process.stdout.write(`│ Native Socket: unavailable │\n`);
199
+ process.stdout.write(`│ Native Shared: unavailable │\n`);
150
200
  }
151
201
 
152
202
  if (nativeShmApi) {
153
203
  process.stdout.write(
154
- `│ Native Shared: ${nativeShmTime.toFixed(2).padStart(8)}ms (${avgNativeShmTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeShmOverhead)} │\n`,
204
+ `│ Native Shared Pipelined: ${nativeShmPipelinedTime.toFixed(2).padStart(8)}ms (${avgNativeShmPipelinedTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeShmPipelinedOverhead)} │\n`,
155
205
  );
156
206
  } else {
157
- process.stdout.write(`│ Native Shared: unavailable │\n`);
207
+ process.stdout.write(`│ Native Shared Pipelined: unavailable │\n`);
158
208
  }
159
209
 
160
210
  if (nativeShmSyncApi) {
161
211
  process.stdout.write(
162
- `│ Native Shared Sync: ${nativeShmSyncTime.toFixed(2).padStart(8)}ms (${avgNativeShmSyncTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeShmSyncOverhead)} │\n`,
212
+ `│ Native Shared Sync: ${nativeShmSyncTime.toFixed(2).padStart(8)}ms (${avgNativeShmSyncTimeUs.toFixed(2).padStart(7)}µs/call) ${formatOverhead(nativeShmSyncOverhead)} │\n`,
163
213
  );
164
214
  } else {
165
- process.stdout.write(`│ Native Shared Sync: unavailable │\n`);
215
+ process.stdout.write(`│ Native Shared Sync: unavailable │\n`);
166
216
  }
167
217
 
168
- process.stdout.write(`└────────────────────────────────────────────────────────────┘\n`);
218
+ process.stdout.write(`└─────────────────────────────────────────────────────────────────┘\n`);
169
219
 
170
220
  const wasmResult = await wasmApi!.poseidon2Hash({ inputs });
171
221
 
@@ -8,7 +8,7 @@ export enum BackendType {
8
8
  WasmWorker = 'WasmWorker',
9
9
  /** Native via Unix domain socket (async only) */
10
10
  NativeUnixSocket = 'NativeUnixSocket',
11
- /** Native via shared memory (sync only currently) */
11
+ /** Native via shared memory */
12
12
  NativeSharedMemory = 'NativeSharedMemory',
13
13
  }
14
14
 
@@ -1,7 +1,7 @@
1
1
  import { BarretenbergNativeSocketAsyncBackend } from './native_socket.js';
2
2
  import { BarretenbergWasmSyncBackend, BarretenbergWasmAsyncBackend } from '../wasm.js';
3
3
  import { BarretenbergNativeShmSyncBackend } from './native_shm.js';
4
- import { SyncToAsyncAdapter } from '../sync_to_async_adapter.js';
4
+ import { BarretenbergNativeShmAsyncBackend } from './native_shm_async.js';
5
5
  import { findBbBinary, findNapiBinary } from './platform.js';
6
6
  import { Barretenberg, BarretenbergSync } from '../../barretenberg/index.js';
7
7
  import { BackendOptions, BackendType } from '../index.js';
@@ -37,17 +37,10 @@ export async function createAsyncBackend(
37
37
  }
38
38
  const napiPath = findNapiBinary();
39
39
  if (!napiPath) {
40
- throw new Error('Native sync backend requires napi client stub.');
40
+ throw new Error('Native async backend requires napi client stub.');
41
41
  }
42
- logger(`Using native shared memory backend (via sync adapter): ${bbPath}`);
43
- // Use sync backend with adapter to provide async interface
44
- const syncBackend = await BarretenbergNativeShmSyncBackend.new(
45
- bbPath,
46
- options.threads,
47
- options.maxClients,
48
- options.logger,
49
- );
50
- const asyncBackend = new SyncToAsyncAdapter(syncBackend);
42
+ logger(`Using native shared memory async backend: ${bbPath}`);
43
+ const asyncBackend = await BarretenbergNativeShmAsyncBackend.new(bbPath, options.threads, options.logger);
51
44
  return new Barretenberg(asyncBackend, options);
52
45
  }
53
46
 
@@ -94,12 +87,7 @@ export async function createSyncBackend(
94
87
  throw new Error('Native sync backend requires napi client stub.');
95
88
  }
96
89
  logger(`Using native shared memory backend: ${bbPath}`);
97
- const shm = await BarretenbergNativeShmSyncBackend.new(
98
- bbPath,
99
- options.threads,
100
- options.maxClients,
101
- options.logger,
102
- );
90
+ const shm = await BarretenbergNativeShmSyncBackend.new(bbPath, options.threads, options.logger);
103
91
  return new BarretenbergSync(shm);
104
92
  }
105
93
 
@@ -3,7 +3,6 @@ import { spawn, ChildProcess } from 'child_process';
3
3
  import { openSync, closeSync } from 'fs';
4
4
  import { IMsgpackBackendSync } from '../interface.js';
5
5
  import { findNapiBinary, findPackageRoot } from './platform.js';
6
- import readline from 'readline';
7
6
 
8
7
  // Import the NAPI module
9
8
  // The addon is built to the nodejs_module directory
@@ -20,6 +19,8 @@ try {
20
19
  addon = null;
21
20
  }
22
21
 
22
+ let instanceCounter = 0;
23
+
23
24
  /**
24
25
  * Synchronous shared memory backend that communicates with bb binary via shared memory.
25
26
  * Uses NAPI module to interface with shared memory IPC.
@@ -46,12 +47,10 @@ export class BarretenbergNativeShmSyncBackend implements IMsgpackBackendSync {
46
47
  * Create and initialize a shared memory backend.
47
48
  * @param bbBinaryPath Path to bb binary
48
49
  * @param threads Optional number of threads
49
- * @param maxClients Optional maximum concurrent clients (default: 1)
50
50
  */
51
51
  static async new(
52
52
  bbBinaryPath: string,
53
53
  threads?: number,
54
- maxClients?: number,
55
54
  logger?: (msg: string) => void,
56
55
  ): Promise<BarretenbergNativeShmSyncBackend> {
57
56
  if (!addon || !addon.MsgpackClient) {
@@ -59,14 +58,11 @@ export class BarretenbergNativeShmSyncBackend implements IMsgpackBackendSync {
59
58
  }
60
59
 
61
60
  // Create a unique shared memory name
62
- const shmName = `bb-${process.pid}`;
63
-
64
- // Default maxClients to 1 if not specified
65
- const clientCount = maxClients ?? 1;
61
+ const shmName = `bb-sync-${process.pid}-${instanceCounter++}`;
66
62
 
67
63
  // If threads not set use 1 thread. We're not expected to do long lived work on sync backends.
68
64
  const hwc = threads ? threads.toString() : '1';
69
- const env = { ...process.env, HARDWARE_CONCURRENCY: '1' };
65
+ const env = { ...process.env, HARDWARE_CONCURRENCY: hwc };
70
66
 
71
67
  // Set up file logging if logger is provided.
72
68
  // Direct file redirection bypasses Node event loop - logs are written even if process hangs.
@@ -78,17 +74,8 @@ export class BarretenbergNativeShmSyncBackend implements IMsgpackBackendSync {
78
74
  logger(`BB process logs redirected to: ${logPath}`);
79
75
  }
80
76
 
81
- // Spawn bb process with shared memory mode
82
- const args = [
83
- 'msgpack',
84
- 'run',
85
- '--input',
86
- `${shmName}.shm`,
87
- '--max-clients',
88
- clientCount.toString(),
89
- '--request-ring-size',
90
- `${1024 * 1024 * 2}`,
91
- ];
77
+ // Spawn bb process with shared memory mode (SPSC-only, no max-clients needed)
78
+ const args = ['msgpack', 'run', '--input', `${shmName}.shm`, '--request-ring-size', `${1024 * 1024 * 4}`];
92
79
  const bbProcess = spawn(bbBinaryPath, args, {
93
80
  stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'],
94
81
  env,
@@ -137,8 +124,8 @@ export class BarretenbergNativeShmSyncBackend implements IMsgpackBackendSync {
137
124
  }
138
125
 
139
126
  try {
140
- // Create NAPI client with matching max_clients value
141
- client = new addon.MsgpackClient(shmName, clientCount);
127
+ // Create NAPI client (SPSC-only, no max_clients needed)
128
+ client = new addon.MsgpackClient(shmName);
142
129
  break; // Success!
143
130
  } catch (err: any) {
144
131
  // Connection failed, will retry
@@ -0,0 +1,266 @@
1
+ import { createRequire } from 'module';
2
+ import { spawn, ChildProcess } from 'child_process';
3
+ import { openSync, closeSync } from 'fs';
4
+ import { IMsgpackBackendAsync } from '../interface.js';
5
+ import { findNapiBinary, findPackageRoot } from './platform.js';
6
+
7
+ // Import the NAPI module
8
+ // The addon is built to the nodejs_module directory
9
+ const addonPath = findNapiBinary();
10
+ // Try loading, but don't throw if it doesn't exist (will be caught in constructor)
11
+ let addon: any = null;
12
+ try {
13
+ if (addonPath) {
14
+ const require = createRequire(findPackageRoot()!);
15
+ addon = require(addonPath);
16
+ }
17
+ } catch (err) {
18
+ // Addon not built yet or not available
19
+ addon = null;
20
+ }
21
+
22
+ let instanceCounter = 0;
23
+
24
+ /**
25
+ * Asynchronous shared memory backend that communicates with bb binary via shared memory.
26
+ * Uses NAPI module with background thread polling for async operations.
27
+ * Supports request pipelining - multiple requests can be in flight simultaneously.
28
+ *
29
+ * Architecture (matches socket backend pattern):
30
+ * - bb acts as the SERVER, TypeScript is the CLIENT
31
+ * - bb creates the shared memory region
32
+ * - TypeScript connects via NAPI wrapper (MsgpackClientAsync)
33
+ * - TypeScript manages promise queue (single-threaded, no mutex needed)
34
+ * - C++ background thread polls for responses, calls JavaScript callback
35
+ * - JavaScript callback pops queue and resolves promises in FIFO order
36
+ */
37
+ export class BarretenbergNativeShmAsyncBackend implements IMsgpackBackendAsync {
38
+ private process: ChildProcess;
39
+ private client: any; // NAPI MsgpackClientAsync instance
40
+ private logFd?: number; // File descriptor for logs
41
+
42
+ // Queue of pending callbacks for pipelined requests
43
+ // Responses come back in FIFO order, so we match them with queued callbacks
44
+ private pendingCallbacks: Array<{
45
+ resolve: (data: Uint8Array) => void;
46
+ reject: (error: Error) => void;
47
+ }> = [];
48
+
49
+ private constructor(process: ChildProcess, client: any, logFd?: number) {
50
+ this.process = process;
51
+ this.client = client;
52
+ this.logFd = logFd;
53
+
54
+ // Register our response handler with the C++ client
55
+ // This callback will be invoked from the background thread via ThreadSafeFunction
56
+ this.client.setResponseCallback((responseBuffer: Buffer) => {
57
+ this.handleResponse(responseBuffer);
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Handle response from C++ background thread
63
+ * Dequeues the next pending callback and resolves it (FIFO order)
64
+ */
65
+ private handleResponse(responseBuffer: Buffer): void {
66
+ // Response is complete - dequeue the next pending callback (FIFO)
67
+ const callback = this.pendingCallbacks.shift();
68
+ if (callback) {
69
+ callback.resolve(new Uint8Array(responseBuffer));
70
+ } else {
71
+ // This shouldn't happen - response without a pending request
72
+ console.warn('Received response but no pending callback');
73
+ }
74
+
75
+ // If no more pending callbacks, release ref to allow process to exit
76
+ if (this.pendingCallbacks.length === 0) {
77
+ this.client.release();
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Create and initialize an async shared memory backend.
83
+ * @param bbBinaryPath Path to bb binary
84
+ * @param threads Optional number of threads (defaults to min(32, num_cpus))
85
+ * @param logger Optional logger function for bb output
86
+ */
87
+ static async new(
88
+ bbBinaryPath: string,
89
+ threads?: number,
90
+ logger?: (msg: string) => void,
91
+ ): Promise<BarretenbergNativeShmAsyncBackend> {
92
+ if (!addon || !addon.MsgpackClientAsync) {
93
+ throw new Error('Shared memory async NAPI not available.');
94
+ }
95
+
96
+ // Create a unique shared memory name
97
+ const shmName = `bb-async-${process.pid}-${instanceCounter++}`;
98
+
99
+ // If threads not set use num cpu cores, max 32 (same as socket backend)
100
+ const hwc = threads ? threads.toString() : '1';
101
+ const env = { ...process.env, HARDWARE_CONCURRENCY: hwc };
102
+
103
+ // Set up file logging if logger is provided
104
+ // Direct file redirection bypasses Node event loop - logs are written even if process hangs
105
+ let logFd: number | undefined;
106
+ let logPath: string | undefined;
107
+ if (logger) {
108
+ logPath = `/tmp/${shmName}.log`;
109
+ logFd = openSync(logPath, 'w');
110
+ logger(`BB process logs redirected to: ${logPath}`);
111
+ }
112
+
113
+ // Spawn bb process with shared memory mode
114
+ // Use larger ring buffers for async mode to support pipelining
115
+ const args = [
116
+ 'msgpack',
117
+ 'run',
118
+ '--input',
119
+ `${shmName}.shm`,
120
+ '--request-ring-size',
121
+ `${1024 * 1024 * 4}`,
122
+ '--response-ring-size',
123
+ `${1024 * 1024 * 4}`,
124
+ ];
125
+ const bbProcess = spawn(bbBinaryPath, args, {
126
+ stdio: ['ignore', logFd ?? 'ignore', logFd ?? 'ignore'],
127
+ env,
128
+ });
129
+
130
+ // Disconnect from event loop so process can exit without waiting for bb
131
+ // The bb process has parent death monitoring (prctl on Linux, kqueue on macOS)
132
+ // so it will automatically exit when Node.js exits
133
+ bbProcess.unref();
134
+
135
+ // Track if process has exited
136
+ let processExited = false;
137
+ let exitError: Error | null = null;
138
+
139
+ bbProcess.on('error', err => {
140
+ processExited = true;
141
+ exitError = new Error(`Native backend process error: ${err.message}`);
142
+ });
143
+
144
+ bbProcess.on('exit', (code, signal) => {
145
+ processExited = true;
146
+ if (code !== null && code !== 0) {
147
+ exitError = new Error(`Native backend process exited with code ${code}`);
148
+ } else if (signal && signal !== 'SIGTERM') {
149
+ exitError = new Error(`Native backend process killed with signal ${signal}`);
150
+ }
151
+ });
152
+
153
+ // Wait for bb to create shared memory
154
+ // Retry connection every 100ms for up to 5 seconds (longer than sync for thread startup)
155
+ const retryInterval = 100; // ms
156
+ const timeout = 5000; // ms
157
+ const maxAttempts = Math.floor(timeout / retryInterval);
158
+ let client: any = null;
159
+
160
+ try {
161
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
162
+ // Check if bb process has exited before attempting connection
163
+ if (processExited) {
164
+ throw exitError || new Error('Native backend process exited unexpectedly during startup');
165
+ }
166
+
167
+ // Wait before attempting connection (except first attempt)
168
+ if (attempt > 0) {
169
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
170
+ }
171
+
172
+ try {
173
+ // Create NAPI async client
174
+ client = new addon.MsgpackClientAsync(shmName);
175
+ break; // Success!
176
+ } catch (err: any) {
177
+ // Connection failed, will retry
178
+ if (attempt === maxAttempts - 1) {
179
+ // Last attempt failed - check one more time if process exited
180
+ if (processExited && exitError) {
181
+ throw exitError;
182
+ }
183
+ throw new Error(`Failed to connect to shared memory after ${timeout}ms: ${err.message}`);
184
+ }
185
+ }
186
+ }
187
+
188
+ if (!client) {
189
+ throw new Error('Failed to create client connection');
190
+ }
191
+
192
+ return new BarretenbergNativeShmAsyncBackend(bbProcess, client, logFd);
193
+ } finally {
194
+ // If we failed to connect, ensure the process is killed and log file closed
195
+ if (!client) {
196
+ bbProcess.kill('SIGKILL');
197
+ if (logFd !== undefined) {
198
+ try {
199
+ closeSync(logFd);
200
+ } catch (e) {
201
+ // Ignore errors during cleanup
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Send a msgpack request asynchronously.
210
+ * Supports pipelining - can be called multiple times before awaiting responses.
211
+ * Use Promise.all() to send multiple requests concurrently.
212
+ *
213
+ * Example:
214
+ * const results = await Promise.all([
215
+ * backend.call(buf1),
216
+ * backend.call(buf2),
217
+ * backend.call(buf3)
218
+ * ]);
219
+ *
220
+ * @param inputBuffer The msgpack-encoded request
221
+ * @returns Promise resolving to msgpack-encoded response
222
+ */
223
+ async call(inputBuffer: Uint8Array): Promise<Uint8Array> {
224
+ return new Promise((resolve, reject) => {
225
+ // If this is the first pending callback, acquire ref to keep event loop alive
226
+ if (this.pendingCallbacks.length === 0) {
227
+ this.client.acquire();
228
+ }
229
+
230
+ // Enqueue this promise's callbacks (FIFO order)
231
+ this.pendingCallbacks.push({ resolve, reject });
232
+
233
+ try {
234
+ // Send request to shared memory (synchronous write)
235
+ // C++ call() no longer returns a promise - we manage them here
236
+ this.client.call(Buffer.from(inputBuffer));
237
+ } catch (err: any) {
238
+ // Send failed - dequeue the callback we just added and reject
239
+ this.pendingCallbacks.pop();
240
+
241
+ // If queue is now empty, release ref to allow exit
242
+ if (this.pendingCallbacks.length === 0) {
243
+ this.client.release();
244
+ }
245
+
246
+ reject(new Error(`Shared memory async call failed: ${err.message}`));
247
+ }
248
+ });
249
+ }
250
+
251
+ async destroy(): Promise<void> {
252
+ // Kill the bb process
253
+ // Background thread and callbacks will be cleaned up by OS on process exit
254
+ this.process.kill('SIGTERM');
255
+ this.process.removeAllListeners();
256
+
257
+ // Close log file if open
258
+ if (this.logFd !== undefined) {
259
+ try {
260
+ closeSync(this.logFd);
261
+ } catch (e) {
262
+ // Ignore errors during cleanup
263
+ }
264
+ }
265
+ }
266
+ }
@@ -0,0 +1,54 @@
1
+ import { BarretenbergWasmSyncBackend } from '../bb_backends/wasm.js';
2
+ import { SyncApi } from '../cbind/generated/sync.js';
3
+ import { BBApiException } from '../bbapi_exception.js';
4
+
5
+ describe('BBApi Exception Handling from bb.js', () => {
6
+ let backend: BarretenbergWasmSyncBackend;
7
+ let api: SyncApi;
8
+
9
+ beforeAll(async () => {
10
+ backend = await BarretenbergWasmSyncBackend.new();
11
+ api = new SyncApi(backend);
12
+ }, 60000);
13
+
14
+ afterAll(() => {
15
+ backend.destroy();
16
+ });
17
+
18
+ it('should catch CRS initialization exceptions from WASM', () => {
19
+ // Create an SrsInitSrs command with invalid data that will cause an exception in C++
20
+ // We pass buffers that are too small, which will cause validation to fail
21
+ const invalidCommand = {
22
+ numPoints: 100, // Request 100 points (requires 6400 bytes)
23
+ pointsBuf: new Uint8Array(10), // Only 10 bytes - will cause exception
24
+ g2Point: new Uint8Array(10), // Only 10 bytes (needs 128) - will cause exception
25
+ };
26
+
27
+ // In WASM builds, throw_or_abort calls abort directly which throws a generic Error
28
+ // In native builds with exceptions, our try-catch in bbapi converts it to ErrorResponse
29
+ // This test verifies that errors are catchable from bb.js (even if as generic Error in WASM)
30
+ expect(() => {
31
+ api.srsInitSrs(invalidCommand);
32
+ }).toThrow();
33
+ });
34
+
35
+ it('should return error message from caught exception', () => {
36
+ const invalidCommand = {
37
+ numPoints: 100,
38
+ pointsBuf: new Uint8Array(10),
39
+ g2Point: new Uint8Array(10),
40
+ };
41
+
42
+ try {
43
+ api.srsInitSrs(invalidCommand);
44
+ fail('Expected exception to be thrown');
45
+ } catch (error) {
46
+ // Error is catchable and contains a useful message
47
+ expect(error).toBeInstanceOf(Error);
48
+ expect((error as Error).message).toBeTruthy();
49
+ expect((error as Error).message.length).toBeGreaterThan(0);
50
+ expect((error as Error).message).toContain('g1_identity');
51
+ console.log('Successfully caught exception from bb.js with message:', (error as Error).message);
52
+ }
53
+ });
54
+ });
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { findBbBinary } from '../bb_backends/node/platform.js';
3
+ import { spawnSync } from 'node:child_process';
4
+
5
+ const bin = findBbBinary();
6
+
7
+ if (!bin) {
8
+ console.error('Could not find bb binary. Please ensure it is built and accessible.');
9
+ process.exit(1);
10
+ }
11
+
12
+ const result = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' });
13
+
14
+ process.exit(result.status ?? 1);