concurrent-c-node 0.7.0__tar.gz → 0.10.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.7.0
3
+ Version: 0.10.0
4
4
  Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/sreekotay/concurrent-c
@@ -40,7 +40,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
40
40
  child (~28ms to first call), so you get real Node: full stdlib, native
41
41
  addons, whatever npm installs. Promise-based APIs look synchronous
42
42
  from Python, and bulk data crosses through **shared memory** — an 8MB
43
- array in **9ms** where the same values as a JSON list take 583ms.
43
+ array in **9.5ms** where the same values as a JSON list take 499ms.
44
44
 
45
45
  ```
46
46
  pip install concurrent-c-node # needs node on PATH (or point at one)
@@ -60,11 +60,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
60
60
  - **The domain rules hold**: handles never cross bridges; `stats()` is
61
61
  the handle ledger and `release()` drops one early; `destroy()` is
62
62
  idempotent, every door answers `bridge is closed` after, and the
63
- child dies with the bridge (and on host exit, via stdin EOF).
63
+ child dies with the bridge (and on host exit, via wire-fd EOF).
64
64
  Teardown is **cooperative** (farewell `close` + drain, then wait /
65
65
  kill-fallback): in-flight calls may still return a correct value.
66
- Hard child death (`SIGKILL`, `process.abort`) is a different contract
67
- in-flight ops must reject. See
66
+ There is no clean cancel of CPU-bound JS work wait, or kill the
67
+ child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
68
+ must reject in-flight ops. See
68
69
  [`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
69
70
 
70
71
  ## Async is free
@@ -146,21 +147,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
146
147
  From `python -m cc_node.examples.bench_wire` (sources under
147
148
  [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
148
149
  on a 4-vCPU x86-64 box, node 22 / python 3.11
149
- ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
150
+ ([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
150
151
  catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
151
152
 
152
153
  | what | result |
153
154
  |---|---|
154
155
  | spawn a domain (node child, first eval) | 28ms |
155
- | wire round trip (smallest call) | 116µs |
156
- | Python-callback round trip (JS → Python → JS) | 238µs |
157
- | 8MB `array('d')` argument, shm spill | **9.2ms** |
158
- | the same 8MB as a JSON list | 583ms — the spill is **63x** |
159
-
160
- The wire is strict request/response JSON over stdio with the
161
- shared-memory spill for bulk data the same discipline concurrent-c-python's
162
- isolated domains speak, mirrored. True pinned zero-copy leases remain
163
- future work.
156
+ | wire round trip (smallest call) | 105µs |
157
+ | Python-callback round trip (JS → Python → JS) | 153µs |
158
+ | 8MB `array('d')` argument, shm spill | **9.5ms** |
159
+ | the same 8MB as a JSON list | 499ms — the spill is **52x** |
160
+
161
+ The wire is strict request/response JSON on dedicated fds — replies
162
+ pair by request id, and stdio stays yours, so `console.log` in
163
+ evaluated JS reaches the real stdout and can never collide with a
164
+ protocol reply — with the shared-memory spill for bulk data (private
165
+ 0700 per-bridge directory, 0600 exclusive-create files, removed with
166
+ the bridge). The same discipline concurrent-c-python's isolated
167
+ domains speak, mirrored. True pinned zero-copy leases remain future
168
+ work.
169
+
170
+ One boundary, stated plainly: the domain is **crash isolation, not a
171
+ security sandbox** — the node child inherits your environment and runs
172
+ with your OS privileges, so do not run untrusted JavaScript through
173
+ it.
164
174
 
165
175
  A worked tour (builtin Node modules, chains, callbacks, thenables,
166
176
  buffers — no npm install needed):
@@ -29,7 +29,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
29
29
  child (~28ms to first call), so you get real Node: full stdlib, native
30
30
  addons, whatever npm installs. Promise-based APIs look synchronous
31
31
  from Python, and bulk data crosses through **shared memory** — an 8MB
32
- array in **9ms** where the same values as a JSON list take 583ms.
32
+ array in **9.5ms** where the same values as a JSON list take 499ms.
33
33
 
34
34
  ```
35
35
  pip install concurrent-c-node # needs node on PATH (or point at one)
@@ -49,11 +49,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
49
49
  - **The domain rules hold**: handles never cross bridges; `stats()` is
50
50
  the handle ledger and `release()` drops one early; `destroy()` is
51
51
  idempotent, every door answers `bridge is closed` after, and the
52
- child dies with the bridge (and on host exit, via stdin EOF).
52
+ child dies with the bridge (and on host exit, via wire-fd EOF).
53
53
  Teardown is **cooperative** (farewell `close` + drain, then wait /
54
54
  kill-fallback): in-flight calls may still return a correct value.
55
- Hard child death (`SIGKILL`, `process.abort`) is a different contract
56
- in-flight ops must reject. See
55
+ There is no clean cancel of CPU-bound JS work wait, or kill the
56
+ child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
57
+ must reject in-flight ops. See
57
58
  [`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
58
59
 
59
60
  ## Async is free
@@ -135,21 +136,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
135
136
  From `python -m cc_node.examples.bench_wire` (sources under
136
137
  [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
137
138
  on a 4-vCPU x86-64 box, node 22 / python 3.11
138
- ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
139
+ ([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
139
140
  catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
140
141
 
141
142
  | what | result |
142
143
  |---|---|
143
144
  | spawn a domain (node child, first eval) | 28ms |
144
- | wire round trip (smallest call) | 116µs |
145
- | Python-callback round trip (JS → Python → JS) | 238µs |
146
- | 8MB `array('d')` argument, shm spill | **9.2ms** |
147
- | the same 8MB as a JSON list | 583ms — the spill is **63x** |
148
-
149
- The wire is strict request/response JSON over stdio with the
150
- shared-memory spill for bulk data the same discipline concurrent-c-python's
151
- isolated domains speak, mirrored. True pinned zero-copy leases remain
152
- future work.
145
+ | wire round trip (smallest call) | 105µs |
146
+ | Python-callback round trip (JS → Python → JS) | 153µs |
147
+ | 8MB `array('d')` argument, shm spill | **9.5ms** |
148
+ | the same 8MB as a JSON list | 499ms — the spill is **52x** |
149
+
150
+ The wire is strict request/response JSON on dedicated fds — replies
151
+ pair by request id, and stdio stays yours, so `console.log` in
152
+ evaluated JS reaches the real stdout and can never collide with a
153
+ protocol reply — with the shared-memory spill for bulk data (private
154
+ 0700 per-bridge directory, 0600 exclusive-create files, removed with
155
+ the bridge). The same discipline concurrent-c-python's isolated
156
+ domains speak, mirrored. True pinned zero-copy leases remain future
157
+ work.
158
+
159
+ One boundary, stated plainly: the domain is **crash isolation, not a
160
+ security sandbox** — the node child inherits your environment and runs
161
+ with your OS privileges, so do not run untrusted JavaScript through
162
+ it.
153
163
 
154
164
  A worked tour (builtin Node modules, chains, callbacks, thenables,
155
165
  buffers — no npm install needed):
@@ -24,7 +24,9 @@ import base64
24
24
  import json
25
25
  import math
26
26
  import os
27
+ import shutil
27
28
  import subprocess
29
+ import tempfile
28
30
 
29
31
  __all__ = ["create", "JsError", "JsHandle", "__version__"]
30
32
  __version__ = "0.4.0"
@@ -49,20 +51,11 @@ def _numpy():
49
51
  return None
50
52
 
51
53
 
52
- def _shm_dir():
54
+ def _shm_base():
53
55
  d = os.environ.get("CC_NODE_SHM_DIR")
54
56
  if d:
55
57
  return d
56
- return "/dev/shm" if os.path.isdir("/dev/shm") else None
57
-
58
-
59
- def _shm_write(raw):
60
- _shm_seq[0] += 1
61
- path = os.path.join(_shm_dir(), "ccnode-%d-%d" % (os.getpid(),
62
- _shm_seq[0]))
63
- with open(path, "wb") as f:
64
- f.write(raw)
65
- return path
58
+ return "/dev/shm" if os.path.isdir("/dev/shm") else tempfile.gettempdir()
66
59
 
67
60
 
68
61
  class JsError(RuntimeError):
@@ -123,17 +116,39 @@ class Bridge:
123
116
  broker = os.path.join(os.path.dirname(os.path.abspath(__file__)),
124
117
  "broker.cjs")
125
118
  node = node or os.environ.get("CC_NODE_BIN", "node")
119
+ # Spills live in a private 0700 per-bridge directory (predictable
120
+ # names in a shared /dev/shm invite pre-creation races and
121
+ # umask-dependent exposure); the child writes its spills there
122
+ # too, and the directory goes with the bridge.
123
+ self._shm_dir = tempfile.mkdtemp(
124
+ prefix="ccnode-%d-" % os.getpid(), dir=_shm_base())
125
+ # The wire lives on dedicated fds; stdio is inherited, so
126
+ # console.log in evaluated JS reaches the real stdout and can
127
+ # never collide with a protocol reply. pass_fds keeps our fd
128
+ # numbers in the child, so the broker learns them from the env.
129
+ req_r, req_w = os.pipe()
130
+ resp_r, resp_w = os.pipe()
131
+ env = dict(os.environ,
132
+ CC_WIRE_IN=str(req_r), CC_WIRE_OUT=str(resp_w),
133
+ CC_NODE_SHM_DIR=self._shm_dir)
126
134
  try:
127
135
  self._p = subprocess.Popen(
128
136
  [node, broker],
129
- stdin=subprocess.PIPE,
130
- stdout=subprocess.PIPE,
137
+ pass_fds=(req_r, resp_w),
138
+ env=env,
131
139
  cwd=os.getcwd(),
132
140
  )
133
141
  except FileNotFoundError:
142
+ for fd in (req_r, req_w, resp_r, resp_w):
143
+ os.close(fd)
144
+ shutil.rmtree(self._shm_dir, ignore_errors=True)
134
145
  raise JsError(
135
146
  "cc-node: no node executable (install Node, or set "
136
147
  "CC_NODE_BIN)") from None
148
+ os.close(req_r)
149
+ os.close(resp_w)
150
+ self._wire_w = os.fdopen(req_w, "wb")
151
+ self._wire_r = os.fdopen(resp_r, "rb")
137
152
  self.closed = False
138
153
  self._nid = 1
139
154
  self._cbs = {}
@@ -150,8 +165,14 @@ class Bridge:
150
165
 
151
166
  def _send(self, obj):
152
167
  line = json.dumps(obj, allow_nan=False) + "\n"
153
- self._p.stdin.write(line.encode("utf-8"))
154
- self._p.stdin.flush()
168
+ try:
169
+ self._wire_w.write(line.encode("utf-8"))
170
+ self._wire_w.flush()
171
+ except (BrokenPipeError, ValueError, OSError):
172
+ # The child died under this write (e.g. killed from inside a
173
+ # callback). Every door answers the same way after death.
174
+ self.closed = True
175
+ raise JsError("cc-node: the node child exited") from None
155
176
 
156
177
  def _queue_release(self, hid):
157
178
  self._pending_release.append(hid)
@@ -166,6 +187,14 @@ class Bridge:
166
187
  except Exception:
167
188
  pass
168
189
 
190
+ def _shm_write(self, raw):
191
+ _shm_seq[0] += 1
192
+ path = os.path.join(self._shm_dir, "s%d" % _shm_seq[0])
193
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
194
+ with os.fdopen(fd, "wb") as f:
195
+ f.write(raw)
196
+ return path
197
+
169
198
  def _flush_shm(self):
170
199
  # The child unlinks spill files as it decodes; this sweep only
171
200
  # matters when it died first (ENOENT is the normal case).
@@ -186,7 +215,7 @@ class Bridge:
186
215
  if parked is not None:
187
216
  return self._take_reply(parked)
188
217
  while True:
189
- line = self._p.stdout.readline()
218
+ line = self._wire_r.readline()
190
219
  if not line:
191
220
  self.closed = True
192
221
  raise JsError("cc-node: the node child exited")
@@ -235,7 +264,10 @@ class Bridge:
235
264
  args = [self._decode_result(a) for a in msg["args"]]
236
265
  self._send({"cbr": msg["cbid"], "v": self._encode(fn(*args))})
237
266
  except Exception as e: # crosses back as a JS error, message intact
238
- self._send({"cbr": msg["cbid"], "e": str(e)})
267
+ try:
268
+ self._send({"cbr": msg["cbid"], "e": str(e)})
269
+ except Exception:
270
+ pass # child gone; the in-flight op surfaces EOF next read
239
271
 
240
272
  # ---- values ----
241
273
 
@@ -294,8 +326,8 @@ class Bridge:
294
326
  raw = np.ascontiguousarray(a).tobytes()
295
327
  if raw is None or kind is None:
296
328
  return None
297
- if len(raw) > _SHM_SPILL and _shm_dir():
298
- path = _shm_write(raw)
329
+ if len(raw) > _SHM_SPILL:
330
+ path = self._shm_write(raw)
299
331
  self._shm_out.append(path)
300
332
  return {"$shm": path, "t": kind}
301
333
  return {"$ta": kind, "b64": base64.b64encode(raw).decode("ascii")}
@@ -372,15 +404,22 @@ class Bridge:
372
404
  self._close_pending = False
373
405
  self.closed = True
374
406
  try:
375
- if self._p.poll() is None and self._p.stdin \
376
- and not self._p.stdin.closed:
407
+ if self._p.poll() is None and not self._wire_w.closed:
377
408
  rid = self._nid
378
409
  self._nid += 1
379
410
  self._send({"id": rid, "op": "close"})
380
411
  except Exception:
381
412
  pass
382
413
  try:
383
- self._p.stdin.close()
414
+ self._wire_w.close()
415
+ except Exception:
416
+ pass
417
+ # Drain to EOF so the broker's farewell reply has somewhere to
418
+ # land — closing the reply fd under its write is an EPIPE crash
419
+ # in the child.
420
+ try:
421
+ while self._wire_r.readline():
422
+ pass
384
423
  except Exception:
385
424
  pass
386
425
  try:
@@ -390,6 +429,11 @@ class Bridge:
390
429
  self._p.kill()
391
430
  except Exception:
392
431
  pass
432
+ try:
433
+ self._wire_r.close()
434
+ except Exception:
435
+ pass
436
+ shutil.rmtree(self._shm_dir, ignore_errors=True)
393
437
  if self in _live:
394
438
  _live.remove(self)
395
439
 
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  /* cc-node broker: the Node end of Python's JS bridge.
3
3
  *
4
- * Line-delimited JSON over stdio, strict request/response. Handles are
4
+ * Line-delimited JSON on DEDICATED wire fds, strict request/response
5
+ * stdin/stdout/stderr stay the user's, so console.log in evaluated
6
+ * code reaches the real stdout and can never collide with a protocol
7
+ * reply. Requests arrive on fd 3, replies leave on fd 4; a parent
8
+ * that cannot pin fd numbers (python's pass_fds keeps the parent's
9
+ * numbering) says which via CC_WIRE_IN / CC_WIRE_OUT. Handles are
5
10
  * integers into one table; results follow the bridge materialization
6
11
  * rule — plain data (finite numbers, strings, booleans, null, arrays
7
12
  * and plain objects of the same) crosses as a value, everything else
@@ -10,7 +15,7 @@
10
15
  * Python callable crosses as {$f: id}; invoking it sends a nested `cb`
11
16
  * request and BLOCKS on a synchronous read for the answer — legal
12
17
  * because the protocol is strictly alternating, so nothing else can be
13
- * in flight. stdin EOF is the host vanishing: exit.
18
+ * in flight. EOF on the request fd is the host vanishing: exit.
14
19
  *
15
20
  * cc/include/ccc/script/js.cch embeds this file verbatim (the CC
16
21
  * isolated tier speaks the same wire); js_iso_smoke pins the two
@@ -20,11 +25,14 @@
20
25
  const fs = require('fs');
21
26
  const { createRequire } = require('module');
22
27
 
28
+ const IN_FD = Number(process.env.CC_WIRE_IN || 3);
29
+ const OUT_FD = Number(process.env.CC_WIRE_OUT || 4);
30
+
23
31
  /* Resolve packages from the HOST's cwd — `npm install lodash` next to
24
32
  * your Python program is the point. */
25
33
  const requireCwd = createRequire(process.cwd() + '/');
26
34
 
27
- /* ---- one buffered reader over fd 0, sync and async ---- */
35
+ /* ---- one buffered reader over the request fd, sync and async ---- */
28
36
  const rbuf = { data: Buffer.alloc(0) };
29
37
 
30
38
  function takeLine() {
@@ -42,7 +50,7 @@ function readLineSync() {
42
50
  const chunk = Buffer.alloc(65536);
43
51
  let n = 0;
44
52
  try {
45
- n = fs.readSync(0, chunk, 0, chunk.length, null);
53
+ n = fs.readSync(IN_FD, chunk, 0, chunk.length, null);
46
54
  } catch (e) {
47
55
  if (e.code === 'EAGAIN') continue;
48
56
  if (e.code === 'EOF') return null;
@@ -58,7 +66,7 @@ function readLineAsync() {
58
66
  if (l !== null) return Promise.resolve(l);
59
67
  return new Promise((resolve, reject) => {
60
68
  const chunk = Buffer.alloc(65536);
61
- fs.read(0, chunk, 0, chunk.length, null, (err, n) => {
69
+ fs.read(IN_FD, chunk, 0, chunk.length, null, (err, n) => {
62
70
  if (err) return err.code === 'EOF' ? resolve(null) : reject(err);
63
71
  if (n === 0) return resolve(null);
64
72
  rbuf.data = Buffer.concat([rbuf.data, chunk.subarray(0, n)]);
@@ -68,7 +76,12 @@ function readLineAsync() {
68
76
  }
69
77
 
70
78
  function send(obj) {
71
- fs.writeSync(1, JSON.stringify(obj) + '\n');
79
+ try {
80
+ fs.writeSync(OUT_FD, JSON.stringify(obj) + '\n');
81
+ } catch (e) {
82
+ if (e.code === 'EPIPE') process.exit(0); /* host went away */
83
+ throw e;
84
+ }
72
85
  }
73
86
 
74
87
  /* ---- handles + materialization ---- */
@@ -102,7 +115,8 @@ function isPlain(v, depth) {
102
115
 
103
116
  /* Typed buffers cross as tagged bytes: small inline as base64, big
104
117
  * through the shared-memory spill (one memcpy per side; the receiver
105
- * consumes-and-unlinks). Same discipline as cc-python's wire. */
118
+ * consumes-and-unlinks; files are 0600, exclusive-create, inside the
119
+ * host's private bridge dir). Same discipline as cc-python's wire. */
106
120
  const TA_KIND = new Map([
107
121
  [Float64Array, 'f64'], [Float32Array, 'f32'],
108
122
  [Int32Array, 'i32'], [BigInt64Array, 'i64'], [Uint8Array, 'u8'],
@@ -120,8 +134,8 @@ let shmSeq = 0;
120
134
  function encodeBuffer(kind, buf) {
121
135
  if (buf.byteLength > SHM_SPILL) {
122
136
  const p = require('path').join(
123
- SHM_DIR, 'ccnode-' + process.pid + '-' + (++shmSeq));
124
- fs.writeFileSync(p, buf);
137
+ SHM_DIR, 'ccnode-c' + process.pid + '-' + (++shmSeq));
138
+ fs.writeFileSync(p, buf, { flag: 'wx', mode: 0o600 });
125
139
  return { shm: p, t: kind };
126
140
  }
127
141
  return { ta: kind, b64: buf.toString('base64') };
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.7.0
3
+ Version: 0.10.0
4
4
  Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
5
  License: MIT
6
6
  Project-URL: Repository, https://github.com/sreekotay/concurrent-c
@@ -40,7 +40,7 @@ dependencies, nothing to build. The domain **is** a spawned `node`
40
40
  child (~28ms to first call), so you get real Node: full stdlib, native
41
41
  addons, whatever npm installs. Promise-based APIs look synchronous
42
42
  from Python, and bulk data crosses through **shared memory** — an 8MB
43
- array in **9ms** where the same values as a JSON list take 583ms.
43
+ array in **9.5ms** where the same values as a JSON list take 499ms.
44
44
 
45
45
  ```
46
46
  pip install concurrent-c-node # needs node on PATH (or point at one)
@@ -60,11 +60,12 @@ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
60
60
  - **The domain rules hold**: handles never cross bridges; `stats()` is
61
61
  the handle ledger and `release()` drops one early; `destroy()` is
62
62
  idempotent, every door answers `bridge is closed` after, and the
63
- child dies with the bridge (and on host exit, via stdin EOF).
63
+ child dies with the bridge (and on host exit, via wire-fd EOF).
64
64
  Teardown is **cooperative** (farewell `close` + drain, then wait /
65
65
  kill-fallback): in-flight calls may still return a correct value.
66
- Hard child death (`SIGKILL`, `process.abort`) is a different contract
67
- in-flight ops must reject. See
66
+ There is no clean cancel of CPU-bound JS work wait, or kill the
67
+ child (`SIGKILL` / `process.abort`) and create a new domain. Hard death
68
+ must reject in-flight ops. See
68
69
  [`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md).
69
70
 
70
71
  ## Async is free
@@ -146,21 +147,30 @@ From the Concurrent-C repo root (packs this wheel and the npm sibling):
146
147
  From `python -m cc_node.examples.bench_wire` (sources under
147
148
  [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
148
149
  on a 4-vCPU x86-64 box, node 22 / python 3.11
149
- ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
150
+ ([`perf/baselines/cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt);
150
151
  catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
151
152
 
152
153
  | what | result |
153
154
  |---|---|
154
155
  | spawn a domain (node child, first eval) | 28ms |
155
- | wire round trip (smallest call) | 116µs |
156
- | Python-callback round trip (JS → Python → JS) | 238µs |
157
- | 8MB `array('d')` argument, shm spill | **9.2ms** |
158
- | the same 8MB as a JSON list | 583ms — the spill is **63x** |
159
-
160
- The wire is strict request/response JSON over stdio with the
161
- shared-memory spill for bulk data the same discipline concurrent-c-python's
162
- isolated domains speak, mirrored. True pinned zero-copy leases remain
163
- future work.
156
+ | wire round trip (smallest call) | 105µs |
157
+ | Python-callback round trip (JS → Python → JS) | 153µs |
158
+ | 8MB `array('d')` argument, shm spill | **9.5ms** |
159
+ | the same 8MB as a JSON list | 499ms — the spill is **52x** |
160
+
161
+ The wire is strict request/response JSON on dedicated fds — replies
162
+ pair by request id, and stdio stays yours, so `console.log` in
163
+ evaluated JS reaches the real stdout and can never collide with a
164
+ protocol reply — with the shared-memory spill for bulk data (private
165
+ 0700 per-bridge directory, 0600 exclusive-create files, removed with
166
+ the bridge). The same discipline concurrent-c-python's isolated
167
+ domains speak, mirrored. True pinned zero-copy leases remain future
168
+ work.
169
+
170
+ One boundary, stated plainly: the domain is **crash isolation, not a
171
+ security sandbox** — the node child inherits your environment and runs
172
+ with your OS privileges, so do not run untrusted JavaScript through
173
+ it.
164
174
 
165
175
  A worked tour (builtin Node modules, chains, callbacks, thenables,
166
176
  buffers — no npm install needed):
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.7.0"
7
+ version = "0.10.0"
8
8
  description = "JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"