concurrent-c-node 0.23.0__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.23.0 → concurrent_c_node-0.23.1}/PKG-INFO +57 -42
  2. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/README.md +56 -41
  3. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/__init__.py +8 -6
  4. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/benchmarks/vs_alts.py +75 -2
  5. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/PKG-INFO +57 -42
  6. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/pyproject.toml +1 -1
  7. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/benchmarks/__init__.py +0 -0
  8. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/benchmarks/multi_domain.py +0 -0
  9. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/broker.cjs +0 -0
  10. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/examples/__init__.py +0 -0
  11. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/examples/bench_wire.py +0 -0
  12. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/examples/use_node.py +0 -0
  13. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/magics.py +0 -0
  14. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/cc_node/stdio_line.cjs +0 -0
  15. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/SOURCES.txt +0 -0
  16. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
  17. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/requires.txt +0 -0
  18. {concurrent_c_node-0.23.0 → concurrent_c_node-0.23.1}/concurrent_c_node.egg-info/top_level.txt +0 -0
  19. {concurrent_c_node-0.23.0 → 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.23.0
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
+ ```
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.
36
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,11 +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
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
- child per cell. `%load_ext` still works (idempotent).
67
- - `cc_node.require('path')` is `get().require('path')`.
72
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
73
+ session as `require()`.
68
74
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
69
75
  cwd `require` once.
70
76
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -74,35 +80,36 @@ measurable wire. N domains = N processes.
74
80
 
75
81
  ```
76
82
  pip install concurrent-c-node # needs node on PATH
77
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
83
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
78
84
  python -m cc_node.examples.use_node
79
85
  python -m cc_node.examples.bench_wire
80
86
  python -m cc_node.benchmarks.multi_domain
81
- 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
82
88
  ```
83
89
 
84
90
  ## Jupyter / Colab
85
91
 
86
- Colab and the usual Jupyter kernel are **Python** — this package. Same
87
- calling convention as a script: a cell blocks until Node answers;
88
- thenables wait in the child. Magics and `get()` share **one** session
89
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
- lands in the cell (inherited stdio, line-buffered).
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.
91
95
 
92
96
  ```python
93
97
  %pip install concurrent-c-node
94
98
  # if `node` is missing (typical Colab):
95
99
  !apt-get install -y nodejs
96
100
 
97
- import cc_node # magics register; no %load_ext
98
- path = cc_node.require('path')
99
- path.join('a', 'b') # 'a/b'
101
+ from cc_node import require
102
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
100
103
  ```
101
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
+
102
109
  ```python
103
110
  %%js
104
111
  console.log('hi') # shows in the cell
105
- require('path').join('a', 'b') # last expression comes back as Python
112
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
106
113
  ```
107
114
 
108
115
  ```python
@@ -114,9 +121,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
114
121
 
115
122
  | | |
116
123
  |---|---|
117
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
- | `%load_ext cc_node` | same, idempotent |
119
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; 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 |
120
128
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
121
129
  | `-t chunks` / `--to` | store the result in the notebook namespace |
122
130
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -161,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
161
169
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
162
170
  0700 dir, 0600 files, removed with the bridge.
163
171
 
164
- ### Vs pythonmonkey / mini-racer / DIY node
172
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
165
173
 
166
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
167
- 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:
168
179
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
169
180
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
170
181
 
171
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
172
- |---|---|---|---|---|---|
173
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
174
- | callback | **36µs** | — | — | 1µs | — |
175
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
176
- | `require('fs')` | yes | yes | yes | no | no |
177
- | 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 |
178
189
 
179
190
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
180
191
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
181
- package. Isolated `node -e` per call is ~1000× a persistent child.
182
- 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.
183
196
 
184
197
  The other direction (Python from Node):
185
198
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -233,9 +246,11 @@ total(array.array('d', range(1_000_000)))
233
246
  ## Common issues
234
247
 
235
248
  **`Cannot find module`.** `require` / `import` resolve from the Python
236
- process cwd (`node_modules` next to your program), not from this wheel’s
237
- site-packages. `npm install lodash` in the project directory is the fix;
238
- 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`.
239
254
 
240
255
  ### Empty `{}` stays a handle
241
256
 
@@ -293,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
293
308
 
294
309
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
295
310
  `benchmarks.vs_alts`.
296
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
311
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
297
312
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
298
313
  Own hot path in C/CC → native module (40–90ns) —
299
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
+ ```
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.
21
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,11 +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
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
51
- child per cell. `%load_ext` still works (idempotent).
52
- - `cc_node.require('path')` is `get().require('path')`.
57
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
58
+ session as `require()`.
53
59
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
54
60
  cwd `require` once.
55
61
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -59,35 +65,36 @@ measurable wire. N domains = N processes.
59
65
 
60
66
  ```
61
67
  pip install concurrent-c-node # needs node on PATH
62
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
68
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
63
69
  python -m cc_node.examples.use_node
64
70
  python -m cc_node.examples.bench_wire
65
71
  python -m cc_node.benchmarks.multi_domain
66
- 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
67
73
  ```
68
74
 
69
75
  ## Jupyter / Colab
70
76
 
71
- Colab and the usual Jupyter kernel are **Python** — this package. Same
72
- calling convention as a script: a cell blocks until Node answers;
73
- thenables wait in the child. Magics and `get()` share **one** session
74
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
75
- lands in the cell (inherited stdio, line-buffered).
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.
76
80
 
77
81
  ```python
78
82
  %pip install concurrent-c-node
79
83
  # if `node` is missing (typical Colab):
80
84
  !apt-get install -y nodejs
81
85
 
82
- import cc_node # magics register; no %load_ext
83
- path = cc_node.require('path')
84
- path.join('a', 'b') # 'a/b'
86
+ from cc_node import require
87
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
85
88
  ```
86
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
+
87
94
  ```python
88
95
  %%js
89
96
  console.log('hi') # shows in the cell
90
- require('path').join('a', 'b') # last expression comes back as Python
97
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
91
98
  ```
92
99
 
93
100
  ```python
@@ -99,9 +106,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
99
106
 
100
107
  | | |
101
108
  |---|---|
102
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
103
- | `%load_ext cc_node` | same, idempotent |
104
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; 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 |
105
113
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
106
114
  | `-t chunks` / `--to` | store the result in the notebook namespace |
107
115
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -146,25 +154,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
146
154
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
147
155
  0700 dir, 0600 files, removed with the bridge.
148
156
 
149
- ### Vs pythonmonkey / mini-racer / DIY node
157
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
150
158
 
151
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
152
- 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:
153
164
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
154
165
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
155
166
 
156
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
157
- |---|---|---|---|---|---|
158
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
159
- | callback | **36µs** | — | — | 1µs | — |
160
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
161
- | `require('fs')` | yes | yes | yes | no | no |
162
- | 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 |
163
174
 
164
175
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
165
176
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
166
- package. Isolated `node -e` per call is ~1000× a persistent child.
167
- 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.
168
181
 
169
182
  The other direction (Python from Node):
170
183
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -218,9 +231,11 @@ total(array.array('d', range(1_000_000)))
218
231
  ## Common issues
219
232
 
220
233
  **`Cannot find module`.** `require` / `import` resolve from the Python
221
- process cwd (`node_modules` next to your program), not from this wheel’s
222
- site-packages. `npm install lodash` in the project directory is the fix;
223
- 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`.
224
239
 
225
240
  ### Empty `{}` stays a handle
226
241
 
@@ -278,7 +293,7 @@ Both bridges (npm OIDC + PyPI OIDC):
278
293
 
279
294
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
280
295
  `benchmarks.vs_alts`.
281
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
296
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
282
297
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
283
298
  Own hot path in C/CC → native module (40–90ns) —
284
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): `import cc_node` then `%%js` or
23
- `cc_node.get()` — one session domain, 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
@@ -41,7 +43,7 @@ __all__ = [
41
43
  "JsError", "JsHandle",
42
44
  "load_ipython_extension", "unload_ipython_extension", "__version__",
43
45
  ]
44
- __version__ = "0.23.0"
46
+ __version__ = "0.23.1"
45
47
 
46
48
  _NO_NODE = (
47
49
  "cc-node: no node executable (install Node, or set CC_NODE_BIN). "
@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.23.0
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
+ ```
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.
36
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,11 +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
- - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
- child per cell. `%load_ext` still works (idempotent).
67
- - `cc_node.require('path')` is `get().require('path')`.
72
+ - Notebook: `import cc_node` registers `%%js` (no `%load_ext`). Same
73
+ session as `require()`.
68
74
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
69
75
  cwd `require` once.
70
76
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -74,35 +80,36 @@ measurable wire. N domains = N processes.
74
80
 
75
81
  ```
76
82
  pip install concurrent-c-node # needs node on PATH
77
- pip install 'concurrent-c-node[jupyter]' # magics (IPython)
83
+ pip install 'concurrent-c-node[jupyter]' # IPython (%%js); require() does not need this
78
84
  python -m cc_node.examples.use_node
79
85
  python -m cc_node.examples.bench_wire
80
86
  python -m cc_node.benchmarks.multi_domain
81
- 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
82
88
  ```
83
89
 
84
90
  ## Jupyter / Colab
85
91
 
86
- Colab and the usual Jupyter kernel are **Python** — this package. Same
87
- calling convention as a script: a cell blocks until Node answers;
88
- thenables wait in the child. Magics and `get()` share **one** session
89
- for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
- lands in the cell (inherited stdio, line-buffered).
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.
91
95
 
92
96
  ```python
93
97
  %pip install concurrent-c-node
94
98
  # if `node` is missing (typical Colab):
95
99
  !apt-get install -y nodejs
96
100
 
97
- import cc_node # magics register; no %load_ext
98
- path = cc_node.require('path')
99
- path.join('a', 'b') # 'a/b'
101
+ from cc_node import require
102
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
100
103
  ```
101
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
+
102
109
  ```python
103
110
  %%js
104
111
  console.log('hi') # shows in the cell
105
- require('path').join('a', 'b') # last expression comes back as Python
112
+ require('lodash').chunk([1, 2, 3, 4, 5], 2)
106
113
  ```
107
114
 
108
115
  ```python
@@ -114,9 +121,10 @@ xs.map(x => x * 2) # wire types only; no pickle fallback
114
121
 
115
122
  | | |
116
123
  |---|---|
117
- | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
- | `%load_ext cc_node` | same, idempotent |
119
- | `%js 1+1` / `%%js` | eval on `cc_node.get()`; 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 |
120
128
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
121
129
  | `-t chunks` / `--to` | store the result in the notebook namespace |
122
130
  | `%js_stats` | handle-table size (spawns if needed) |
@@ -161,25 +169,30 @@ Colab is not that. There, default `create()` blocks the kernel thread —
161
169
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
162
170
  0700 dir, 0600 files, removed with the bridge.
163
171
 
164
- ### Vs pythonmonkey / mini-racer / DIY node
172
+ ### Vs pythonia / pythonmonkey / mini-racer / DIY node
165
173
 
166
- Most “JS from Python” libraries are **not Node**. Bulk is a **sum** over
167
- 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:
168
179
  [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
169
180
  · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
170
181
 
171
- | | cc-node | DIY JSON stdio | `node -e` each | pythonmonkey | mini-racer |
172
- |---|---|---|---|---|---|
173
- | identity RTT | **20µs** | 39µs | 25ms | **<1µs** | 114µs |
174
- | callback | **36µs** | — | — | 1µs | — |
175
- | 8MB typed / list | **6.2ms shm** / 359ms | — / 197ms | — | — / 630ms proxy | — / 78ms |
176
- | `require('fs')` | yes | yes | yes | no | no |
177
- | 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 |
178
189
 
179
190
  Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
180
191
  (`require('fs')`, native addons, callbacks, stdout stays yours): this
181
- package. Isolated `node -e` per call is ~1000× a persistent child.
182
- 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.
183
196
 
184
197
  The other direction (Python from Node):
185
198
  [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
@@ -233,9 +246,11 @@ total(array.array('d', range(1_000_000)))
233
246
  ## Common issues
234
247
 
235
248
  **`Cannot find module`.** `require` / `import` resolve from the Python
236
- process cwd (`node_modules` next to your program), not from this wheel’s
237
- site-packages. `npm install lodash` in the project directory is the fix;
238
- 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`.
239
254
 
240
255
  ### Empty `{}` stays a handle
241
256
 
@@ -293,7 +308,7 @@ Both bridges (npm OIDC + PyPI OIDC):
293
308
 
294
309
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
295
310
  `benchmarks.vs_alts`.
296
- Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
311
+ Jupyter: `from cc_node import require` (or `import cc_node` then `%%js`).
297
312
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
298
313
  Own hot path in C/CC → native module (40–90ns) —
299
314
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.23.0"
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"