concurrent-c-node 0.22.1__tar.gz → 0.23.1__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.
Files changed (19) hide show
  1. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/PKG-INFO +67 -41
  2. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/README.md +66 -40
  3. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/__init__.py +80 -23
  4. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/vs_alts.py +75 -2
  5. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/magics.py +7 -7
  6. concurrent_c_node-0.23.1/cc_node/stdio_line.cjs +13 -0
  7. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/PKG-INFO +67 -41
  8. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/SOURCES.txt +1 -0
  9. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/pyproject.toml +2 -2
  10. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/__init__.py +0 -0
  11. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/benchmarks/multi_domain.py +0 -0
  12. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/broker.cjs +0 -0
  13. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/__init__.py +0 -0
  14. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/bench_wire.py +0 -0
  15. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/cc_node/examples/use_node.py +0 -0
  16. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
  17. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/requires.txt +0 -0
  18. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/top_level.txt +0 -0
  19. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.22.1
3
+ Version: 0.23.1
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
@@ -28,15 +28,19 @@ bridge):
28
28
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
29
29
 
30
30
  ```python
31
- import cc_node
32
-
33
- js = cc_node.create() # always a child `node` process
34
- _ = js.require('lodash') # cwd node_modules
31
+ from cc_node import require # one session child (lazy)
32
+ _ = require('lodash') # cwd node_modules
35
33
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
34
+ ```
36
35
 
36
+ A private child (`create()`) is still there when you want N Nodes or
37
+ an explicit lifetime — not required for the first call.
38
+
39
+ ```python
40
+ import cc_node
41
+ js = cc_node.create() # a private Node, not the session
37
42
  semver = js.require('semver')
38
43
  semver.satisfies('1.2.3', '^1.0.0') # True
39
-
40
44
  js.destroy() # or: with cc_node.create() as js:
41
45
  ```
42
46
 
@@ -44,12 +48,13 @@ The other direction (Python from Node):
44
48
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
45
49
  (in-process by default; vs pymport / ncp / pythonia in that README).
46
50
 
47
- Every `create()` here is a separate Node — real addons, crash isolation,
48
- measurable wire. N domains = N processes.
51
+ Every `create()` is a separate Node — real addons, crash isolation,
52
+ measurable wire. N domains = N processes. `require()` / `get()` share
53
+ one session for the process (the Jupyter kernel).
49
54
 
50
55
  | | this package | CC hosted (`cc_js_new(false, …)`) |
51
56
  |---|---|---|
52
- | API | `cc_node.create()` | `.ccs` program |
57
+ | API | `require()` / `create()` | `.ccs` program |
53
58
  | Where | child `node` | libnode in-process |
54
59
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
55
60
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -60,10 +65,12 @@ measurable wire. N domains = N processes.
60
65
 
61
66
  - Always a child `node`. A call blocks until JS answers; thenables wait
62
67
  in the child. No `{ async: true }`.
68
+ - `from cc_node import require` is the session. `create()` is a private
69
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
63
70
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
71
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `%load_ext cc_node` then `%%js` — one kernel domain
66
- (`cc_node.kernel()`), not a child per cell.
72
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
73
+ session as `require()`.
67
74
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
68
75
  cwd `require` once.
69
76
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -73,47 +80,58 @@ measurable wire. N domains = N processes.
73
80
 
74
81
  ```
75
82
  pip install concurrent-c-node # needs node on PATH
76
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
83
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
77
84
  python -m cc_node.examples.use_node
78
85
  python -m cc_node.examples.bench_wire
79
86
  python -m cc_node.benchmarks.multi_domain
80
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
87
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
81
88
  ```
82
89
 
83
90
  ## Jupyter / Colab
84
91
 
85
- Colab and the usual Jupyter kernel are **Python** — this package. Same
86
- calling convention as a script: a cell blocks until Node answers;
87
- thenables wait in the child. Magics share **one** domain for the
88
- kernel, not a spawn per cell (~28ms).
92
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
93
+ `destroy()` for the happy path. One session child for the kernel;
94
+ `console.log` lands in the cell.
89
95
 
90
96
  ```python
91
97
  %pip install concurrent-c-node
92
98
  # if `node` is missing (typical Colab):
93
99
  !apt-get install -y nodejs
94
- %load_ext cc_node
95
100
 
101
+ from cc_node import require
102
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
103
+ ```
104
+
105
+ `import cc_node` also registers `%%js` (IPython already running; the
106
+ `[jupyter]` extra is only if you want magics without IPython already
107
+ installed).
108
+
109
+ ```python
96
110
  %%js
97
- const path = require('path')
98
- path.join('a', 'b') // last expression comes back as Python
111
+ console.log('hi') # shows in the cell
112
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
99
113
  ```
100
114
 
101
115
  ```python
102
116
  xs = [1, 2, 3, 4]
103
117
 
104
118
  %%js -b xs -t chunks
105
- xs.map(x => x * 2) // wire types only; no pickle fallback
119
+ xs.map(x => x * 2) # wire types only; no pickle fallback
106
120
  ```
107
121
 
108
122
  | | |
109
123
  |---|---|
110
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
111
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
124
+ | `from cc_node import require` | session `require`; spawns on first call |
125
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
126
+ | `%load_ext cc_node` | same, idempotent; not required |
127
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
112
128
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
113
129
  | `-t chunks` / `--to` | store the result in the notebook namespace |
114
130
  | `%js_stats` | handle-table size (spawns if needed) |
115
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
116
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
131
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
132
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
133
+ | `cc_node.kernel()` | alias of `get()` |
134
+ | `cc_node.require('fs')` | `get().require('fs')` |
117
135
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
118
136
 
119
137
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -151,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
151
169
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
152
170
  0700 dir, 0600 files, removed with the bridge.
153
171
 
154
- ### Vs pythonmonkey / mini-racer / DIY node
172
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
155
173
 
156
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
157
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
174
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
175
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
176
+ packaged peer that is: `require()` on import, one child. Bulk is a
177
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
178
+ lies). Snapshot:
158
179
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
159
180
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
160
181
 
161
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
162
- |---|---|---|---|---|---|
163
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
164
- | callback | **36µs** | — | — | 1µs | — |
165
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
166
- | `require('fs')` | yes | yes | yes | no | no |
167
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
182
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
183
+ |---|---|---|---|---|---|---|
184
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
185
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
186
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
187
+ | `require('fs')` | yes | yes | yes | yes | no | no |
188
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
168
189
 
169
190
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
170
191
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
171
- package. Isolated `node -e` per call is ~1000× a persistent child.
172
- Optional engines SKIP if not importable — not package deps.
192
+ package — ~2× pythonia on identity, shm for bulk, Python callables are
193
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
194
+ ~1000× a persistent child. Optional engines SKIP if not importable —
195
+ not package deps.
173
196
 
174
197
  The other direction (Python from Node):
175
198
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -184,6 +207,7 @@ vs pymport / ncp / pythonia.
184
207
  `destroy()`; afterwards: `bridge is closed`.
185
208
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
186
209
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
210
+ `get()` / `require()` / `eval()` at module level share that session.
187
211
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
188
212
  in-flight CPU-bound call finishes or you kill the child
189
213
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -222,9 +246,11 @@ total(array.array('d', range(1_000_000)))
222
246
  ## Common issues
223
247
 
224
248
  **`Cannot find module`.** `require` / `import` resolve from the Python
225
- process cwd (`node_modules` next to your program), not from this wheel’s
226
- site-packages. `npm install lodash` in the project directory is the fix;
227
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
249
+ process cwd (`node_modules` next to your notebook or program), not from
250
+ this wheel’s site-packages, and this package does **not** `npm install`
251
+ on a miss (pythonia does). `npm install lodash` in that directory is
252
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
253
+ `PATH`.
228
254
 
229
255
  ### Empty `{}` stays a handle
230
256
 
@@ -282,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
282
308
 
283
309
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
284
310
  `benchmarks.vs_alts`.
285
- Jupyter: `%load_ext cc_node`.
311
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
286
312
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
287
313
  Own hot path in C/CC → native module (40–90ns) —
288
314
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -13,15 +13,19 @@ bridge):
13
13
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
14
14
 
15
15
  ```python
16
- import cc_node
17
-
18
- js = cc_node.create() # always a child `node` process
19
- _ = js.require('lodash') # cwd node_modules
16
+ from cc_node import require # one session child (lazy)
17
+ _ = require('lodash') # cwd node_modules
20
18
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
19
+ ```
21
20
 
21
+ A private child (`create()`) is still there when you want N Nodes or
22
+ an explicit lifetime — not required for the first call.
23
+
24
+ ```python
25
+ import cc_node
26
+ js = cc_node.create() # a private Node, not the session
22
27
  semver = js.require('semver')
23
28
  semver.satisfies('1.2.3', '^1.0.0') # True
24
-
25
29
  js.destroy() # or: with cc_node.create() as js:
26
30
  ```
27
31
 
@@ -29,12 +33,13 @@ The other direction (Python from Node):
29
33
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
30
34
  (in-process by default; vs pymport / ncp / pythonia in that README).
31
35
 
32
- Every `create()` here is a separate Node — real addons, crash isolation,
33
- measurable wire. N domains = N processes.
36
+ Every `create()` is a separate Node — real addons, crash isolation,
37
+ measurable wire. N domains = N processes. `require()` / `get()` share
38
+ one session for the process (the Jupyter kernel).
34
39
 
35
40
  | | this package | CC hosted (`cc_js_new(false, …)`) |
36
41
  |---|---|---|
37
- | API | `cc_node.create()` | `.ccs` program |
42
+ | API | `require()` / `create()` | `.ccs` program |
38
43
  | Where | child `node` | libnode in-process |
39
44
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
40
45
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -45,10 +50,12 @@ measurable wire. N domains = N processes.
45
50
 
46
51
  - Always a child `node`. A call blocks until JS answers; thenables wait
47
52
  in the child. No `{ async: true }`.
53
+ - `from cc_node import require` is the session. `create()` is a private
54
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
48
55
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
49
56
  else is a `JsHandle` until `str()` / attrs / a call.
50
- - `%load_ext cc_node` then `%%js` — one kernel domain
51
- (`cc_node.kernel()`), not a child per cell.
57
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
58
+ session as `require()`.
52
59
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
53
60
  cwd `require` once.
54
61
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -58,47 +65,58 @@ measurable wire. N domains = N processes.
58
65
 
59
66
  ```
60
67
  pip install concurrent-c-node # needs node on PATH
61
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
68
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
62
69
  python -m cc_node.examples.use_node
63
70
  python -m cc_node.examples.bench_wire
64
71
  python -m cc_node.benchmarks.multi_domain
65
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
72
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
66
73
  ```
67
74
 
68
75
  ## Jupyter / Colab
69
76
 
70
- Colab and the usual Jupyter kernel are **Python** — this package. Same
71
- calling convention as a script: a cell blocks until Node answers;
72
- thenables wait in the child. Magics share **one** domain for the
73
- kernel, not a spawn per cell (~28ms).
77
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
78
+ `destroy()` for the happy path. One session child for the kernel;
79
+ `console.log` lands in the cell.
74
80
 
75
81
  ```python
76
82
  %pip install concurrent-c-node
77
83
  # if `node` is missing (typical Colab):
78
84
  !apt-get install -y nodejs
79
- %load_ext cc_node
80
85
 
86
+ from cc_node import require
87
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
88
+ ```
89
+
90
+ `import cc_node` also registers `%%js` (IPython already running; the
91
+ `[jupyter]` extra is only if you want magics without IPython already
92
+ installed).
93
+
94
+ ```python
81
95
  %%js
82
- const path = require('path')
83
- path.join('a', 'b') // last expression comes back as Python
96
+ console.log('hi') # shows in the cell
97
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
84
98
  ```
85
99
 
86
100
  ```python
87
101
  xs = [1, 2, 3, 4]
88
102
 
89
103
  %%js -b xs -t chunks
90
- xs.map(x => x * 2) // wire types only; no pickle fallback
104
+ xs.map(x => x * 2) # wire types only; no pickle fallback
91
105
  ```
92
106
 
93
107
  | | |
94
108
  |---|---|
95
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
96
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
109
+ | `from cc_node import require` | session `require`; spawns on first call |
110
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
111
+ | `%load_ext cc_node` | same, idempotent; not required |
112
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
97
113
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
98
114
  | `-t chunks` / `--to` | store the result in the notebook namespace |
99
115
  | `%js_stats` | handle-table size (spawns if needed) |
100
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
101
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
116
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
117
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
118
+ | `cc_node.kernel()` | alias of `get()` |
119
+ | `cc_node.require('fs')` | `get().require('fs')` |
102
120
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
103
121
 
104
122
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -136,25 +154,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
136
154
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
137
155
  0700 dir, 0600 files, removed with the bridge.
138
156
 
139
- ### Vs pythonmonkey / mini-racer / DIY node
157
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
140
158
 
141
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
142
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
159
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
160
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
161
+ packaged peer that is: `require()` on import, one child. Bulk is a
162
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
163
+ lies). Snapshot:
143
164
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
144
165
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
145
166
 
146
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
147
- |---|---|---|---|---|---|
148
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
149
- | callback | **36µs** | — | — | 1µs | — |
150
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
151
- | `require('fs')` | yes | yes | yes | no | no |
152
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
167
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
168
+ |---|---|---|---|---|---|---|
169
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
170
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
171
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
172
+ | `require('fs')` | yes | yes | yes | yes | no | no |
173
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
153
174
 
154
175
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
155
176
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
156
- package. Isolated `node -e` per call is ~1000× a persistent child.
157
- Optional engines SKIP if not importable — not package deps.
177
+ package — ~2× pythonia on identity, shm for bulk, Python callables are
178
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
179
+ ~1000× a persistent child. Optional engines SKIP if not importable —
180
+ not package deps.
158
181
 
159
182
  The other direction (Python from Node):
160
183
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -169,6 +192,7 @@ vs pymport / ncp / pythonia.
169
192
  `destroy()`; afterwards: `bridge is closed`.
170
193
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
171
194
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
195
+ `get()` / `require()` / `eval()` at module level share that session.
172
196
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
173
197
  in-flight CPU-bound call finishes or you kill the child
174
198
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -207,9 +231,11 @@ total(array.array('d', range(1_000_000)))
207
231
  ## Common issues
208
232
 
209
233
  **`Cannot find module`.** `require` / `import` resolve from the Python
210
- process cwd (`node_modules` next to your program), not from this wheel’s
211
- site-packages. `npm install lodash` in the project directory is the fix;
212
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
234
+ process cwd (`node_modules` next to your notebook or program), not from
235
+ this wheel’s site-packages, and this package does **not** `npm install`
236
+ on a miss (pythonia does). `npm install lodash` in that directory is
237
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
238
+ `PATH`.
213
239
 
214
240
  ### Empty `{}` stays a handle
215
241
 
@@ -267,7 +293,7 @@ Both bridges (npm OIDC + PyPI OIDC):
267
293
 
268
294
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
269
295
  `benchmarks.vs_alts`.
270
- Jupyter: `%load_ext cc_node`.
296
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
271
297
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
272
298
  Own hot path in C/CC → native module (40–90ns) —
273
299
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -1,9 +1,11 @@
1
1
  """cc-node: JavaScript (and every npm package) from Python.
2
2
 
3
- import cc_node
4
- js = cc_node.create() # an Isolation Domain: one node child
5
- _ = js.require('lodash') # resolved from YOUR cwd's node_modules
3
+ from cc_node import require
4
+ _ = require('lodash') # session child; cwd node_modules
6
5
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
6
+
7
+ import cc_node
8
+ js = cc_node.create() # a private child, not the session
7
9
  js.destroy()
8
10
 
9
11
  The mirror of the cc-python bridge, same rules pointed the other way:
@@ -19,8 +21,8 @@ Handles never cross domains; every door after destroy() answers
19
21
  articulately; destroy is idempotent and `with cc_node.create() as js:`
20
22
  scopes it.
21
23
 
22
- Notebooks (Jupyter / Colab): `%load_ext cc_node` then `%%js` — one
23
- kernel-scoped domain (`cc_node.kernel()`), same calling convention.
24
+ Notebooks (Jupyter / Colab): `from cc_node import require` or
25
+ `import cc_node` then `%%js` — one session, same calling convention.
24
26
  """
25
27
  import array
26
28
  import atexit
@@ -36,10 +38,12 @@ import sys
36
38
  import tempfile
37
39
 
38
40
  __all__ = [
39
- "create", "kernel", "reset_kernel", "JsError", "JsHandle",
41
+ "create", "get", "reset", "kernel", "reset_kernel",
42
+ "require", "eval", "eval_cell", "import_module", "stats",
43
+ "JsError", "JsHandle",
40
44
  "load_ipython_extension", "unload_ipython_extension", "__version__",
41
45
  ]
42
- __version__ = "0.22.1"
46
+ __version__ = "0.23.1"
43
47
 
44
48
  _NO_NODE = (
45
49
  "cc-node: no node executable (install Node, or set CC_NODE_BIN). "
@@ -161,12 +165,14 @@ class Bridge:
161
165
  # numbers in the child, so the broker learns them from the env.
162
166
  req_r, req_w = os.pipe()
163
167
  resp_r, resp_w = os.pipe()
168
+ preload = os.path.join(os.path.dirname(os.path.abspath(__file__)),
169
+ "stdio_line.cjs")
164
170
  env = dict(os.environ,
165
171
  CC_WIRE_IN=str(req_r), CC_WIRE_OUT=str(resp_w),
166
172
  CC_NODE_SHM_DIR=self._shm_dir)
167
173
  try:
168
174
  self._p = subprocess.Popen(
169
- [node, broker],
175
+ [node, "-r", preload, broker],
170
176
  pass_fds=(req_r, resp_w),
171
177
  env=env,
172
178
  cwd=os.getcwd(),
@@ -645,19 +651,19 @@ def create(node=None):
645
651
  return Bridge(node=node)
646
652
 
647
653
 
648
- _kernel = None
654
+ _session = None
649
655
 
650
656
 
651
- def kernel(node=None):
652
- """Process-wide domain for notebooks. Lazy; magics share it.
653
- Scripts that want a private child still call `create()`."""
654
- global _kernel
655
- b = _kernel
657
+ def get(node=None):
658
+ """Session domain: one child for this process (the Jupyter kernel).
659
+ Lazy. Magics share it. `create()` is still a private child."""
660
+ global _session
661
+ b = _session
656
662
  if b is not None and not b.closed:
657
663
  if node is not None and node != b._node_bin:
658
664
  raise JsError(
659
- "cc-node: kernel() already live with a different node; "
660
- "reset_kernel() / %js_reset first")
665
+ "cc-node: get() already live with a different node; "
666
+ "reset() / %js_reset first")
661
667
  return b
662
668
  b = create(node=node)
663
669
  try:
@@ -665,15 +671,15 @@ def kernel(node=None):
665
671
  except Exception:
666
672
  b.destroy()
667
673
  raise
668
- _kernel = b
674
+ _session = b
669
675
  return b
670
676
 
671
677
 
672
- def reset_kernel():
673
- """Destroy the kernel-scoped domain. Next `kernel()` / `%%js` spawns."""
674
- global _kernel
675
- b = _kernel
676
- _kernel = None
678
+ def reset():
679
+ """Destroy the session domain. Next get() / %%js spawns."""
680
+ global _session
681
+ b = _session
682
+ _session = None
677
683
  if b is not None:
678
684
  try:
679
685
  b.destroy()
@@ -681,10 +687,61 @@ def reset_kernel():
681
687
  pass
682
688
 
683
689
 
690
+ kernel = get
691
+ reset_kernel = reset
692
+
693
+
694
+ def require(name):
695
+ return get().require(name)
696
+
697
+
698
+ def eval(src): # noqa: A001 — session eval, pythonia-shaped
699
+ return get().eval(src)
700
+
701
+
702
+ def eval_cell(src, bindings=None):
703
+ return get().eval_cell(src, bindings)
704
+
705
+
706
+ def import_module(name):
707
+ return get().import_module(name)
708
+
709
+
710
+ def stats():
711
+ return get().stats()
712
+
713
+
684
714
  def load_ipython_extension(ip):
685
715
  from .magics import load_ipython_extension as _load
686
716
  _load(ip)
717
+ if getattr(ip, "_cc_node_lifecycle", False):
718
+ return
719
+ ip._cc_node_lifecycle = True
720
+ orig = getattr(ip, "reset", None)
721
+ if orig is not None:
722
+ def _reset(*args, **kwargs):
723
+ reset()
724
+ return orig(*args, **kwargs)
725
+ ip.reset = _reset
687
726
 
688
727
 
689
728
  def unload_ipython_extension(ip):
690
- reset_kernel()
729
+ reset()
730
+
731
+
732
+ def _boot_ipython():
733
+ # Already in a kernel: IPython is in sys.modules. Do not import it
734
+ # just because the extra is installed — scripts stay spawn-free.
735
+ if "IPython" not in sys.modules:
736
+ return
737
+ try:
738
+ from IPython import get_ipython
739
+ except ImportError:
740
+ return
741
+ ip = get_ipython()
742
+ if ip is None:
743
+ return
744
+ load_ipython_extension(ip)
745
+
746
+
747
+ _boot_ipython()
@@ -6,8 +6,9 @@ Real Node (this package)
6
6
  `require('fs')`, native addons, the npm in cwd, crash isolation.
7
7
  Honest alternatives: drive a `node` child yourself. Spawn-per-call
8
8
  (`node -e`) is what people write first. A persistent JSON-stdio loop
9
- is the DIY that actually competes. PyExecJS was that, abandoned.
10
- `pythonodejs` claims an embed; treat as optional if importable.
9
+ is the DIY that actually competes. pythonia (PyPI `javascript`,
10
+ JSPyBridge) is the packaged peer with the same DX — `require()` on
11
+ import, one child. PyExecJS was eval-a-string; abandoned.
11
12
 
12
13
  A JS engine inside CPython (not Node)
13
14
  pythonmonkey — SpiderMonkey + CommonJS `require` of *pure JS*.
@@ -218,6 +219,77 @@ def bench_cc_node():
218
219
  js.destroy()
219
220
 
220
221
 
222
+ def bench_pythonia():
223
+ """JSPyBridge: PyPI `javascript`, npm twin `pythonia`. Real Node child.
224
+ A Python callable the JS side sees is a Promise (await in the child)."""
225
+ try:
226
+ t0 = time.perf_counter()
227
+ from javascript import eval_js, require
228
+ eval_js("return 1")
229
+ _result("pythonia.spawn_ms", round((time.perf_counter() - t0) * 1000))
230
+ except ImportError:
231
+ _result("pythonia.rtt_us", "SKIP not installed")
232
+ _cap("pythonia.require_path", False, "not installed")
233
+ _cap("pythonia.require_fs", False, "not installed")
234
+ return
235
+
236
+ f = eval_js("return (x) => x")
237
+ f(1)
238
+ dt = _time_loop(500, f)
239
+ _result("pythonia.rtt_us", _us(dt, 500))
240
+
241
+ try:
242
+ g = eval_js(
243
+ "return async (cb) => { const v = await cb(21); return v * 2 }")
244
+ if g(lambda x: x + 1) != 44:
245
+ raise RuntimeError("callback mismatch")
246
+ dt = _time_loop(200, lambda _i: g(lambda x: x + 1))
247
+ _result("pythonia.callback_roundtrip_us", _us(dt, 200))
248
+ _cap("pythonia.python_callback", True,
249
+ "JS sees a Promise; await in the child")
250
+ except Exception as e:
251
+ _result("pythonia.callback_roundtrip_us",
252
+ "SKIP %s" % type(e).__name__)
253
+ _cap("pythonia.python_callback", False, str(e)[:80])
254
+
255
+ _result("pythonia.bulk_8mb_shm_ms", "SKIP JSON IPC, no shm")
256
+ try:
257
+ biglist = _bulk_list()
258
+ sm = eval_js("return " + SUM_JS)
259
+ got = sm(biglist)
260
+ expect = _bulk_sum_expect(biglist)
261
+ if abs(float(got) - expect) > 1e-3:
262
+ raise RuntimeError("pythonia bulk sum mismatch")
263
+ t0 = time.perf_counter()
264
+ for _ in range(3):
265
+ sm(biglist)
266
+ _result("pythonia.bulk_8mb_json_list_ms",
267
+ round((time.perf_counter() - t0) / 3 * 1000))
268
+ except Exception as e:
269
+ _result("pythonia.bulk_8mb_json_list_ms",
270
+ "SKIP %s" % type(e).__name__)
271
+
272
+ _cap("pythonia.require_path",
273
+ require("path").join("a", "b") == "a/b")
274
+ fd, tmp = tempfile.mkstemp()
275
+ try:
276
+ os.write(fd, b"hi")
277
+ os.close(fd)
278
+ got = require("fs").readFileSync(tmp, "utf8")
279
+ _cap("pythonia.require_fs", got == "hi")
280
+ finally:
281
+ try:
282
+ os.unlink(tmp)
283
+ except OSError:
284
+ pass
285
+ try:
286
+ then = eval_js(
287
+ "return async (x) => { await Promise.resolve(); return x * 2 }")
288
+ _cap("pythonia.thenable", then(21) == 42)
289
+ except Exception as e:
290
+ _cap("pythonia.thenable", False, str(e)[:80])
291
+
292
+
221
293
  def bench_diy_stdio():
222
294
  t0 = time.perf_counter()
223
295
  diy = DiyStdio()
@@ -408,6 +480,7 @@ def main():
408
480
  print("SKIP need node on PATH")
409
481
  return 1
410
482
  bench_cc_node()
483
+ bench_pythonia()
411
484
  bench_diy_stdio()
412
485
  bench_spawn_each()
413
486
  bench_pythonmonkey()
@@ -2,7 +2,7 @@
2
2
  from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
3
3
  from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
4
4
 
5
- from . import JsError, kernel, reset_kernel
5
+ from . import JsError, get, reset
6
6
 
7
7
 
8
8
  def _bind_map(names, ns):
@@ -30,7 +30,7 @@ class CcNodeMagics(Magics):
30
30
  @argument("code", nargs="*", help="JS source (line magic)")
31
31
  @line_cell_magic
32
32
  def js(self, line, cell=None):
33
- """Eval JavaScript on the kernel-scoped domain.
33
+ """Eval JavaScript on the session domain (`cc_node.get()`).
34
34
 
35
35
  Line: %js 1 + 1
36
36
  Cell: %%js then a body; last expression is the result.
@@ -45,20 +45,20 @@ class CcNodeMagics(Magics):
45
45
  raise JsError("cc-node: empty %js / %%js")
46
46
  ns = self.shell.user_ns
47
47
  bindings = _bind_map(args.bind, ns)
48
- result = kernel().eval_cell(src, bindings or None)
48
+ result = get().eval_cell(src, bindings or None)
49
49
  if args.to:
50
50
  ns[args.to] = result
51
51
  return result
52
52
 
53
53
  @line_magic
54
54
  def js_reset(self, line):
55
- """Destroy the kernel-scoped Node child. Next %%js spawns again."""
56
- reset_kernel()
55
+ """Destroy the session Node child. Next %%js / get() spawns again."""
56
+ reset()
57
57
 
58
58
  @line_magic
59
59
  def js_stats(self, line):
60
- """Handle-table size of the kernel domain (spawns if needed)."""
61
- return kernel().stats()
60
+ """Handle-table size of the session domain (spawns if needed)."""
61
+ return get().stats()
62
62
 
63
63
 
64
64
  def load_ipython_extension(ip):
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+ /* Long-lived child: inherited stdout is often a pipe (Jupyter, nohup).
3
+ * Node then block-buffers console.log until the buffer fills or exit.
4
+ * Blocking writes make print land in the cell as it happens. TTY is
5
+ * already blocking — this is a no-op there. */
6
+ function block(s) {
7
+ try {
8
+ if (s && s._handle && typeof s._handle.setBlocking === 'function')
9
+ s._handle.setBlocking(true);
10
+ } catch (e) { /* ignore */ }
11
+ }
12
+ block(process.stdout);
13
+ block(process.stderr);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.22.1
3
+ Version: 0.23.1
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
@@ -28,15 +28,19 @@ bridge):
28
28
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
29
29
 
30
30
  ```python
31
- import cc_node
32
-
33
- js = cc_node.create() # always a child `node` process
34
- _ = js.require('lodash') # cwd node_modules
31
+ from cc_node import require # one session child (lazy)
32
+ _ = require('lodash') # cwd node_modules
35
33
  _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
34
+ ```
36
35
 
36
+ A private child (`create()`) is still there when you want N Nodes or
37
+ an explicit lifetime — not required for the first call.
38
+
39
+ ```python
40
+ import cc_node
41
+ js = cc_node.create() # a private Node, not the session
37
42
  semver = js.require('semver')
38
43
  semver.satisfies('1.2.3', '^1.0.0') # True
39
-
40
44
  js.destroy() # or: with cc_node.create() as js:
41
45
  ```
42
46
 
@@ -44,12 +48,13 @@ The other direction (Python from Node):
44
48
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
45
49
  (in-process by default; vs pymport / ncp / pythonia in that README).
46
50
 
47
- Every `create()` here is a separate Node — real addons, crash isolation,
48
- measurable wire. N domains = N processes.
51
+ Every `create()` is a separate Node — real addons, crash isolation,
52
+ measurable wire. N domains = N processes. `require()` / `get()` share
53
+ one session for the process (the Jupyter kernel).
49
54
 
50
55
  | | this package | CC hosted (`cc_js_new(false, …)`) |
51
56
  |---|---|---|
52
- | API | `cc_node.create()` | `.ccs` program |
57
+ | API | `require()` / `create()` | `.ccs` program |
53
58
  | Where | child `node` | libnode in-process |
54
59
  | Hot call | ~105µs RTT | sub-µs (needs libnode) |
55
60
  | Bulk | shm (~9.5ms / 8MB) | in-process |
@@ -60,10 +65,12 @@ measurable wire. N domains = N processes.
60
65
 
61
66
  - Always a child `node`. A call blocks until JS answers; thenables wait
62
67
  in the child. No `{ async: true }`.
68
+ - `from cc_node import require` is the session. `create()` is a private
69
+ child. `reset()` / `%js_reset` / `%reset` / atexit tear the session down.
63
70
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
71
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `%load_ext cc_node` then `%%js` — one kernel domain
66
- (`cc_node.kernel()`), not a child per cell.
72
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
73
+ session as `require()`.
67
74
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
68
75
  cwd `require` once.
69
76
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -73,47 +80,58 @@ measurable wire. N domains = N processes.
73
80
 
74
81
  ```
75
82
  pip install concurrent-c-node # needs node on PATH
76
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
83
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
77
84
  python -m cc_node.examples.use_node
78
85
  python -m cc_node.examples.bench_wire
79
86
  python -m cc_node.benchmarks.multi_domain
80
- python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
87
+ python -m cc_node.benchmarks.vs_alts # vs pythonia / DIY node / pythonmonkey / mini-racer
81
88
  ```
82
89
 
83
90
  ## Jupyter / Colab
84
91
 
85
- Colab and the usual Jupyter kernel are **Python** — this package. Same
86
- calling convention as a script: a cell blocks until Node answers;
87
- thenables wait in the child. Magics share **one** domain for the
88
- kernel, not a spawn per cell (~28ms).
92
+ Same verb as pythonia: `require`. No `%load_ext`, no `create()`, no
93
+ `destroy()` for the happy path. One session child for the kernel;
94
+ `console.log` lands in the cell.
89
95
 
90
96
  ```python
91
97
  %pip install concurrent-c-node
92
98
  # if `node` is missing (typical Colab):
93
99
  !apt-get install -y nodejs
94
- %load_ext cc_node
95
100
 
101
+ from cc_node import require
102
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
103
+ ```
104
+
105
+ `import cc_node` also registers `%%js` (IPython already running; the
106
+ `[jupyter]` extra is only if you want magics without IPython already
107
+ installed).
108
+
109
+ ```python
96
110
  %%js
97
- const path = require('path')
98
- path.join('a', 'b') // last expression comes back as Python
111
+ console.log('hi') # shows in the cell
112
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
99
113
  ```
100
114
 
101
115
  ```python
102
116
  xs = [1, 2, 3, 4]
103
117
 
104
118
  %%js -b xs -t chunks
105
- xs.map(x => x * 2) // wire types only; no pickle fallback
119
+ xs.map(x => x * 2) # wire types only; no pickle fallback
106
120
  ```
107
121
 
108
122
  | | |
109
123
  |---|---|
110
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
111
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
124
+ | `from cc_node import require` | session `require`; spawns on first call |
125
+ | `import cc_node` | registers magics; does **not** spawn until first `require()` / `%%js` |
126
+ | `%load_ext cc_node` | same, idempotent; not required |
127
+ | `%js 1+1` / `%%js` | eval on the same session; last expression is the result |
112
128
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
113
129
  | `-t chunks` / `--to` | store the result in the notebook namespace |
114
130
  | `%js_stats` | handle-table size (spawns if needed) |
115
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
116
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
131
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
132
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
133
+ | `cc_node.kernel()` | alias of `get()` |
134
+ | `cc_node.require('fs')` | `get().require('fs')` |
117
135
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
118
136
 
119
137
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -151,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
151
169
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
152
170
  0700 dir, 0600 files, removed with the bridge.
153
171
 
154
- ### Vs pythonmonkey / mini-racer / DIY node
172
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
155
173
 
156
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
157
- 1M floats (`.length` on an in-process wrapper is free and lies). Snapshot:
174
+ Most “JS from Python” libraries are **not Node**. pythonia (PyPI
175
+ [`javascript`](https://pypi.org/project/javascript/), JSPyBridge) is the
176
+ packaged peer that is: `require()` on import, one child. Bulk is a
177
+ **sum** over 1M floats (`.length` on an in-process wrapper is free and
178
+ lies). Snapshot:
158
179
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
159
180
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
160
181
 
161
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
162
- |---|---|---|---|---|---|
163
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
164
- | callback | **36µs** | — | — | 1µs | — |
165
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
166
- | `require('fs')` | yes | yes | yes | no | no |
167
- | process | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
182
+ | | cc-node | pythonia | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
183
+ |---|---|---|---|---|---|---|
184
+ | identity RTT | **20µs** | 42µs | 19µs | 23ms | **<1µs** | 104µs |
185
+ | callback | **40µs** | 106µs | — | — | 1µs | — |
186
+ | 8MB typed / list | **7.5ms shm** / 266ms | — / 481ms | — / 155ms | — | — / 609ms | — / 78ms |
187
+ | `require('fs')` | yes | yes | yes | yes | no | no |
188
+ | process | child `node` | child `node` | child `node` | new process/call | SpiderMonkey in-process | V8 isolate |
168
189
 
169
190
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
170
191
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
171
- package. Isolated `node -e` per call is ~1000× a persistent child.
172
- Optional engines SKIP if not importable — not package deps.
192
+ package — ~2× pythonia on identity, shm for bulk, Python callables are
193
+ sync (pythonia’s JS side sees a Promise). Isolated `node -e` per call is
194
+ ~1000× a persistent child. Optional engines SKIP if not importable —
195
+ not package deps.
173
196
 
174
197
  The other direction (Python from Node):
175
198
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -184,6 +207,7 @@ vs pymport / ncp / pythonia.
184
207
  `destroy()`; afterwards: `bridge is closed`.
185
208
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
186
209
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
210
+ `get()` / `require()` / `eval()` at module level share that session.
187
211
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
188
212
  in-flight CPU-bound call finishes or you kill the child
189
213
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -222,9 +246,11 @@ total(array.array('d', range(1_000_000)))
222
246
  ## Common issues
223
247
 
224
248
  **`Cannot find module`.** `require` / `import` resolve from the Python
225
- process cwd (`node_modules` next to your program), not from this wheel’s
226
- site-packages. `npm install lodash` in the project directory is the fix;
227
- or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
249
+ process cwd (`node_modules` next to your notebook or program), not from
250
+ this wheel’s site-packages, and this package does **not** `npm install`
251
+ on a miss (pythonia does). `npm install lodash` in that directory is
252
+ the fix; or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on
253
+ `PATH`.
228
254
 
229
255
  ### Empty `{}` stays a handle
230
256
 
@@ -282,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
282
308
 
283
309
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
284
310
  `benchmarks.vs_alts`.
285
- Jupyter: `%load_ext cc_node`.
311
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
286
312
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
287
313
  Own hot path in C/CC → native module (40–90ns) —
288
314
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  cc_node/__init__.py
4
4
  cc_node/broker.cjs
5
5
  cc_node/magics.py
6
+ cc_node/stdio_line.cjs
6
7
  cc_node/benchmarks/__init__.py
7
8
  cc_node/benchmarks/multi_domain.py
8
9
  cc_node/benchmarks/vs_alts.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.22.1"
7
+ version = "0.23.1"
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"
@@ -26,6 +26,6 @@ Documentation = "https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-nod
26
26
  packages = ["cc_node", "cc_node.examples", "cc_node.benchmarks"]
27
27
 
28
28
  [tool.setuptools.package-data]
29
- cc_node = ["broker.cjs"]
29
+ cc_node = ["broker.cjs", "stdio_line.cjs"]
30
30
  "cc_node.examples" = ["*.py"]
31
31
  "cc_node.benchmarks" = ["*.py"]