concurrent-c-node 0.22.0__tar.gz → 0.23.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.
Files changed (20) hide show
  1. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/PKG-INFO +124 -7
  2. concurrent_c_node-0.23.0/README.md +284 -0
  3. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/__init__.py +277 -14
  4. concurrent_c_node-0.23.0/cc_node/benchmarks/vs_alts.py +420 -0
  5. concurrent_c_node-0.23.0/cc_node/magics.py +65 -0
  6. concurrent_c_node-0.23.0/cc_node/stdio_line.cjs +13 -0
  7. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/PKG-INFO +124 -7
  8. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/SOURCES.txt +4 -0
  9. concurrent_c_node-0.23.0/concurrent_c_node.egg-info/requires.txt +3 -0
  10. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/pyproject.toml +10 -3
  11. concurrent_c_node-0.22.0/README.md +0 -171
  12. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/benchmarks/__init__.py +0 -0
  13. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/benchmarks/multi_domain.py +0 -0
  14. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/broker.cjs +0 -0
  15. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/__init__.py +0 -0
  16. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/bench_wire.py +0 -0
  17. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/cc_node/examples/use_node.py +0 -0
  18. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
  19. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
  20. {concurrent_c_node-0.22.0 → concurrent_c_node-0.23.0}/setup.cfg +0 -0
@@ -1,13 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.22.0
3
+ Version: 0.23.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
7
7
  Project-URL: Documentation, https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md
8
- Keywords: javascript,node,npm,interop,concurrent-c
8
+ Keywords: javascript,node,npm,interop,concurrent-c,jupyter
9
+ Classifier: Framework :: IPython
10
+ Classifier: Framework :: Jupyter
9
11
  Requires-Python: >=3.8
10
12
  Description-Content-Type: text/markdown
13
+ Provides-Extra: jupyter
14
+ Requires-Dist: ipython>=7; extra == "jupyter"
11
15
 
12
16
  # concurrent-c-node
13
17
 
@@ -36,9 +40,12 @@ semver.satisfies('1.2.3', '^1.0.0') # True
36
40
  js.destroy() # or: with cc_node.create() as js:
37
41
  ```
38
42
 
39
- Unlike [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
40
- (in-process by default), every `create()` here is a separate Node —
41
- real addons, crash isolation, measurable wire. N domains = N processes.
43
+ The other direction (Python from Node):
44
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
45
+ (in-process by default; vs pymport / ncp / pythonia in that README).
46
+
47
+ Every `create()` here is a separate Node — real addons, crash isolation,
48
+ measurable wire. N domains = N processes.
42
49
 
43
50
  | | this package | CC hosted (`cc_js_new(false, …)`) |
44
51
  |---|---|---|
@@ -49,13 +56,94 @@ real addons, crash isolation, measurable wire. N domains = N processes.
49
56
  | Parallelism | N children | one process |
50
57
  | Crash | child dies; parent lives | shared fate |
51
58
 
59
+ **Cheat sheet**
60
+
61
+ - Always a child `node`. A call blocks until JS answers; thenables wait
62
+ in the child. No `{ async: true }`.
63
+ - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
+ 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')`.
68
+ - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
69
+ cwd `require` once.
70
+ - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
71
+ types only; no pickle). Missing names refuse.
72
+ - First Ctrl-C finishes the in-flight call (wire stays in sync).
73
+ Interrupt again to kill the child.
74
+
52
75
  ```
53
- pip install concurrent-c-node # needs node on PATH
76
+ pip install concurrent-c-node # needs node on PATH
77
+ pip install 'concurrent-c-node[jupyter]' # magics (IPython)
54
78
  python -m cc_node.examples.use_node
55
79
  python -m cc_node.examples.bench_wire
56
80
  python -m cc_node.benchmarks.multi_domain
81
+ python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
82
+ ```
83
+
84
+ ## Jupyter / Colab
85
+
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).
91
+
92
+ ```python
93
+ %pip install concurrent-c-node
94
+ # if `node` is missing (typical Colab):
95
+ !apt-get install -y nodejs
96
+
97
+ import cc_node # magics register; no %load_ext
98
+ path = cc_node.require('path')
99
+ path.join('a', 'b') # 'a/b'
100
+ ```
101
+
102
+ ```python
103
+ %%js
104
+ console.log('hi') # shows in the cell
105
+ require('path').join('a', 'b') # last expression comes back as Python
57
106
  ```
58
107
 
108
+ ```python
109
+ xs = [1, 2, 3, 4]
110
+
111
+ %%js -b xs -t chunks
112
+ xs.map(x => x * 2) # wire types only; no pickle fallback
113
+ ```
114
+
115
+ | | |
116
+ |---|---|
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 |
120
+ | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
121
+ | `-t chunks` / `--to` | store the result in the notebook namespace |
122
+ | `%js_stats` | handle-table size (spawns if needed) |
123
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
124
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
125
+ | `cc_node.kernel()` | alias of `get()` |
126
+ | `cc_node.require('fs')` | `get().require('fs')` |
127
+ | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
128
+
129
+ `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
130
+ install cwd `require` once so cells look like Node (`require('path')`).
131
+ `--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
132
+ types (`DataFrame`, a set) fail articulately. Reserved names
133
+ (`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
134
+ Node.
135
+
136
+ **Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
137
+ would desync the wire and drop callbacks). The call finishes, the result
138
+ is discarded, the domain stays up. Interrupt again to **kill** the
139
+ child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
140
+ honesty as the rest of the bridge.
141
+
142
+ JS kernels (`tslab`, Deno Jupyter) are
143
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
144
+ Colab is not that. There, default `create()` blocks the kernel thread —
145
+ `py.task` / `{ isolated: true }` when the loop must stay live.
146
+
59
147
  ## Measured
60
148
 
61
149
  [`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
@@ -73,6 +161,30 @@ python -m cc_node.benchmarks.multi_domain
73
161
  Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
74
162
  0700 dir, 0600 files, removed with the bridge.
75
163
 
164
+ ### Vs pythonmonkey / mini-racer / DIY node
165
+
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:
168
+ [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
169
+ · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
170
+
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 |
178
+
179
+ Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
180
+ (`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.
183
+
184
+ The other direction (Python from Node):
185
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
186
+ vs pymport / ncp / pythonia.
187
+
76
188
  ## Surface
77
189
 
78
190
  - Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
@@ -80,6 +192,9 @@ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
80
192
  `String()`). Non-finite floats are tagged.
81
193
  - Handles are per-domain. `stats()` / `release()` / idempotent
82
194
  `destroy()`; afterwards: `bridge is closed`.
195
+ - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
196
+ `require` once, optional `globalThis` binds; `eval()` stays one RTT.
197
+ `get()` / `require()` / `eval()` at module level share that session.
83
198
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
84
199
  in-flight CPU-bound call finishes or you kill the child
85
200
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -176,7 +291,9 @@ Both bridges (npm OIDC + PyPI OIDC):
176
291
  # fallbacks: --npm-local / --pypi-twine
177
292
  ```
178
293
 
179
- Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
294
+ Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
295
+ `benchmarks.vs_alts`.
296
+ Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
180
297
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
181
298
  Own hot path in C/CC → native module (40–90ns) —
182
299
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -0,0 +1,284 @@
1
+ # concurrent-c-node
2
+
3
+ Call Node (and npm packages) from Python.
4
+ Native types, exceptions, callbacks, and async all cross the boundary.
5
+
6
+ Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
7
+ strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
8
+ with your host C compiler. (This bridge itself is pure Python stdlib —
9
+ no native build.)
10
+
11
+ Map of the three boundaries (CC hosts JS, native modules, this package
12
+ bridge):
13
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
14
+
15
+ ```python
16
+ import cc_node
17
+
18
+ js = cc_node.create() # always a child `node` process
19
+ _ = js.require('lodash') # cwd node_modules
20
+ _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
21
+
22
+ semver = js.require('semver')
23
+ semver.satisfies('1.2.3', '^1.0.0') # True
24
+
25
+ js.destroy() # or: with cc_node.create() as js:
26
+ ```
27
+
28
+ The other direction (Python from Node):
29
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
30
+ (in-process by default; vs pymport / ncp / pythonia in that README).
31
+
32
+ Every `create()` here is a separate Node — real addons, crash isolation,
33
+ measurable wire. N domains = N processes.
34
+
35
+ | | this package | CC hosted (`cc_js_new(false, …)`) |
36
+ |---|---|---|
37
+ | API | `cc_node.create()` | `.ccs` program |
38
+ | Where | child `node` | libnode in-process |
39
+ | Hot call | ~105µs RTT | sub-µs (needs libnode) |
40
+ | Bulk | shm (~9.5ms / 8MB) | in-process |
41
+ | Parallelism | N children | one process |
42
+ | Crash | child dies; parent lives | shared fate |
43
+
44
+ **Cheat sheet**
45
+
46
+ - Always a child `node`. A call blocks until JS answers; thenables wait
47
+ in the child. No `{ async: true }`.
48
+ - Scalars / `None` materialize; empty `{}` stays a handle; everything
49
+ 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')`.
53
+ - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
54
+ cwd `require` once.
55
+ - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
56
+ types only; no pickle). Missing names refuse.
57
+ - First Ctrl-C finishes the in-flight call (wire stays in sync).
58
+ Interrupt again to kill the child.
59
+
60
+ ```
61
+ pip install concurrent-c-node # needs node on PATH
62
+ pip install 'concurrent-c-node[jupyter]' # magics (IPython)
63
+ python -m cc_node.examples.use_node
64
+ python -m cc_node.examples.bench_wire
65
+ python -m cc_node.benchmarks.multi_domain
66
+ python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
67
+ ```
68
+
69
+ ## Jupyter / Colab
70
+
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).
76
+
77
+ ```python
78
+ %pip install concurrent-c-node
79
+ # if `node` is missing (typical Colab):
80
+ !apt-get install -y nodejs
81
+
82
+ import cc_node # magics register; no %load_ext
83
+ path = cc_node.require('path')
84
+ path.join('a', 'b') # 'a/b'
85
+ ```
86
+
87
+ ```python
88
+ %%js
89
+ console.log('hi') # shows in the cell
90
+ require('path').join('a', 'b') # last expression comes back as Python
91
+ ```
92
+
93
+ ```python
94
+ xs = [1, 2, 3, 4]
95
+
96
+ %%js -b xs -t chunks
97
+ xs.map(x => x * 2) # wire types only; no pickle fallback
98
+ ```
99
+
100
+ | | |
101
+ |---|---|
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 |
105
+ | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
106
+ | `-t chunks` / `--to` | store the result in the notebook namespace |
107
+ | `%js_stats` | handle-table size (spawns if needed) |
108
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
109
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
110
+ | `cc_node.kernel()` | alias of `get()` |
111
+ | `cc_node.require('fs')` | `get().require('fs')` |
112
+ | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
113
+
114
+ `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
115
+ install cwd `require` once so cells look like Node (`require('path')`).
116
+ `--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
117
+ types (`DataFrame`, a set) fail articulately. Reserved names
118
+ (`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
119
+ Node.
120
+
121
+ **Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
122
+ would desync the wire and drop callbacks). The call finishes, the result
123
+ is discarded, the domain stays up. Interrupt again to **kill** the
124
+ child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
125
+ honesty as the rest of the bridge.
126
+
127
+ JS kernels (`tslab`, Deno Jupyter) are
128
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
129
+ Colab is not that. There, default `create()` blocks the kernel thread —
130
+ `py.task` / `{ isolated: true }` when the loop must stay live.
131
+
132
+ ## Measured
133
+
134
+ [`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
135
+ ·
136
+ [`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
137
+
138
+ | what | result |
139
+ |---|---|
140
+ | spawn (first eval) | 28ms |
141
+ | wire RTT | 105µs |
142
+ | Python callback round trip | 153µs |
143
+ | 8MB `array('d')` via shm | 9.5ms |
144
+ | same 8MB as JSON list | 499ms (~52×) |
145
+
146
+ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
147
+ 0700 dir, 0600 files, removed with the bridge.
148
+
149
+ ### Vs pythonmonkey / mini-racer / DIY node
150
+
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:
153
+ [`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
154
+ · harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
155
+
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 |
163
+
164
+ Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
165
+ (`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.
168
+
169
+ The other direction (Python from Node):
170
+ [`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
171
+ vs pymport / ncp / pythonia.
172
+
173
+ ## Surface
174
+
175
+ - Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
176
+ value; else a domain-owned handle (attrs, calls, `str()` →
177
+ `String()`). Non-finite floats are tagged.
178
+ - Handles are per-domain. `stats()` / `release()` / idempotent
179
+ `destroy()`; afterwards: `bridge is closed`.
180
+ - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
181
+ `require` once, optional `globalThis` binds; `eval()` stays one RTT.
182
+ `get()` / `require()` / `eval()` at module level share that session.
183
+ - Crash isolation, not a sandbox. `destroy()` is cooperative; an
184
+ in-flight CPU-bound call finishes or you kill the child
185
+ ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
186
+
187
+ ### Promises
188
+
189
+ Awaited in the child before the reply — no `async`/`await` on the
190
+ Python side. Same honesty as `concurrent-c-python`: a call blocks until
191
+ the other runtime answers.
192
+
193
+ ```python
194
+ fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
195
+ fetchish(21) # {'doubled': 42}
196
+ ```
197
+
198
+ ### Callbacks
199
+
200
+ ```python
201
+ mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
202
+ # map passes (value, index, array) — take *rest
203
+ ```
204
+
205
+ Exceptions cross both ways with messages intact.
206
+
207
+ ### Buffers
208
+
209
+ `bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
210
+ inline; large via shm.
211
+
212
+ ```python
213
+ import array
214
+ total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
215
+ total(array.array('d', range(1_000_000)))
216
+ ```
217
+
218
+ ## Common issues
219
+
220
+ **`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`.
224
+
225
+ ### Empty `{}` stays a handle
226
+
227
+ An empty object has to stay on the Node side — a materialized Python
228
+ `dict` would lose later property use that matches Node. So
229
+ `js.eval('({})')` returns a live handle:
230
+
231
+ ```python
232
+ o = js.eval('({})') # JsHandle, not {}
233
+ js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
234
+ js.eval('({a: 1})') # {'a': 1} — data return
235
+ ```
236
+
237
+ Non-empty plain objects still cross as Python `dict`s. Same-domain
238
+ handles chain (`h.update(…).digest(…)`).
239
+
240
+ **Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
241
+ on three numbers loses to pure Python. Prefer Python (or a native CC
242
+ module) for small/hot work; use the bridge when Node/npm owns the kernel.
243
+ Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
244
+ domains here).
245
+
246
+ ## Choosing node
247
+
248
+ 1. `create(node='/path/to/node')`
249
+ 2. `CC_NODE_BIN`
250
+ 3. `node` on `PATH`
251
+
252
+ Packages: cwd `node_modules`, same as Node itself.
253
+
254
+ From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
255
+ (libnode), or `cc_js_new(true, &a)` for this wire —
256
+ [`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
257
+
258
+ ## Publishing
259
+
260
+ **PyPI Trusted Publishing (OIDC)** — no API token:
261
+
262
+ 1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
263
+ owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
264
+ environment `pypi`
265
+ 2. GitHub Environment `pypi`
266
+ 3. Bump `pyproject.toml`, push, then:
267
+
268
+ ```
269
+ gh workflow run publish-cc-node.yml
270
+ ```
271
+
272
+ Both bridges (npm OIDC + PyPI OIDC):
273
+
274
+ ```
275
+ ./scripts/publish_bridges.sh --publish --minor
276
+ # fallbacks: --npm-local / --pypi-twine
277
+ ```
278
+
279
+ Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
280
+ `benchmarks.vs_alts`.
281
+ Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
282
+ Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
283
+ Own hot path in C/CC → native module (40–90ns) —
284
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).