concurrent-c-node 0.22.0__tar.gz → 0.22.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.
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/PKG-INFO +113 -7
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/README.md +107 -5
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/__init__.py +221 -13
- concurrent_c_node-0.22.1/cc_node/benchmarks/vs_alts.py +420 -0
- concurrent_c_node-0.22.1/cc_node/magics.py +65 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/concurrent_c_node.egg-info/PKG-INFO +113 -7
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/concurrent_c_node.egg-info/SOURCES.txt +3 -0
- concurrent_c_node-0.22.1/concurrent_c_node.egg-info/requires.txt +3 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/pyproject.toml +9 -2
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/benchmarks/__init__.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/benchmarks/multi_domain.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/broker.cjs +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.22.0 → concurrent_c_node-0.22.1}/setup.cfg +0 -0
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: concurrent-c-node
|
|
3
|
-
Version: 0.22.
|
|
3
|
+
Version: 0.22.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
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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,84 @@ 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
|
+
- `%load_ext cc_node` then `%%js` — one kernel domain
|
|
66
|
+
(`cc_node.kernel()`), not a child per cell.
|
|
67
|
+
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
68
|
+
cwd `require` once.
|
|
69
|
+
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
70
|
+
types only; no pickle). Missing names refuse.
|
|
71
|
+
- First Ctrl-C finishes the in-flight call (wire stays in sync).
|
|
72
|
+
Interrupt again to kill the child.
|
|
73
|
+
|
|
52
74
|
```
|
|
53
|
-
pip install concurrent-c-node
|
|
75
|
+
pip install concurrent-c-node # needs node on PATH
|
|
76
|
+
pip install 'concurrent-c-node[jupyter]' # magics (IPython)
|
|
54
77
|
python -m cc_node.examples.use_node
|
|
55
78
|
python -m cc_node.examples.bench_wire
|
|
56
79
|
python -m cc_node.benchmarks.multi_domain
|
|
80
|
+
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Jupyter / Colab
|
|
84
|
+
|
|
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).
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
%pip install concurrent-c-node
|
|
92
|
+
# if `node` is missing (typical Colab):
|
|
93
|
+
!apt-get install -y nodejs
|
|
94
|
+
%load_ext cc_node
|
|
95
|
+
|
|
96
|
+
%%js
|
|
97
|
+
const path = require('path')
|
|
98
|
+
path.join('a', 'b') // last expression comes back as Python
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
xs = [1, 2, 3, 4]
|
|
103
|
+
|
|
104
|
+
%%js -b xs -t chunks
|
|
105
|
+
xs.map(x => x * 2) // wire types only; no pickle fallback
|
|
57
106
|
```
|
|
58
107
|
|
|
108
|
+
| | |
|
|
109
|
+
|---|---|
|
|
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 |
|
|
112
|
+
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
113
|
+
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
114
|
+
| `%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 |
|
|
117
|
+
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
118
|
+
|
|
119
|
+
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
120
|
+
install cwd `require` once so cells look like Node (`require('path')`).
|
|
121
|
+
`--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
|
|
122
|
+
types (`DataFrame`, a set) fail articulately. Reserved names
|
|
123
|
+
(`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
|
|
124
|
+
Node.
|
|
125
|
+
|
|
126
|
+
**Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
|
|
127
|
+
would desync the wire and drop callbacks). The call finishes, the result
|
|
128
|
+
is discarded, the domain stays up. Interrupt again to **kill** the
|
|
129
|
+
child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
|
|
130
|
+
honesty as the rest of the bridge.
|
|
131
|
+
|
|
132
|
+
JS kernels (`tslab`, Deno Jupyter) are
|
|
133
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
|
|
134
|
+
Colab is not that. There, default `create()` blocks the kernel thread —
|
|
135
|
+
`py.task` / `{ isolated: true }` when the loop must stay live.
|
|
136
|
+
|
|
59
137
|
## Measured
|
|
60
138
|
|
|
61
139
|
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
@@ -73,6 +151,30 @@ python -m cc_node.benchmarks.multi_domain
|
|
|
73
151
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
74
152
|
0700 dir, 0600 files, removed with the bridge.
|
|
75
153
|
|
|
154
|
+
### Vs pythonmonkey / mini-racer / DIY node
|
|
155
|
+
|
|
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:
|
|
158
|
+
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
159
|
+
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
160
|
+
|
|
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 |
|
|
168
|
+
|
|
169
|
+
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
170
|
+
(`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.
|
|
173
|
+
|
|
174
|
+
The other direction (Python from Node):
|
|
175
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
176
|
+
vs pymport / ncp / pythonia.
|
|
177
|
+
|
|
76
178
|
## Surface
|
|
77
179
|
|
|
78
180
|
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
@@ -80,6 +182,8 @@ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
|
80
182
|
`String()`). Non-finite floats are tagged.
|
|
81
183
|
- Handles are per-domain. `stats()` / `release()` / idempotent
|
|
82
184
|
`destroy()`; afterwards: `bridge is closed`.
|
|
185
|
+
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
186
|
+
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
83
187
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
84
188
|
in-flight CPU-bound call finishes or you kill the child
|
|
85
189
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -176,7 +280,9 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
176
280
|
# fallbacks: --npm-local / --pypi-twine
|
|
177
281
|
```
|
|
178
282
|
|
|
179
|
-
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain
|
|
283
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
284
|
+
`benchmarks.vs_alts`.
|
|
285
|
+
Jupyter: `%load_ext cc_node`.
|
|
180
286
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
181
287
|
Own hot path in C/CC → native module (40–90ns) —
|
|
182
288
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -25,9 +25,12 @@ semver.satisfies('1.2.3', '^1.0.0') # True
|
|
|
25
25
|
js.destroy() # or: with cc_node.create() as js:
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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.
|
|
31
34
|
|
|
32
35
|
| | this package | CC hosted (`cc_js_new(false, …)`) |
|
|
33
36
|
|---|---|---|
|
|
@@ -38,13 +41,84 @@ real addons, crash isolation, measurable wire. N domains = N processes.
|
|
|
38
41
|
| Parallelism | N children | one process |
|
|
39
42
|
| Crash | child dies; parent lives | shared fate |
|
|
40
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
|
+
- `%load_ext cc_node` then `%%js` — one kernel domain
|
|
51
|
+
(`cc_node.kernel()`), not a child per cell.
|
|
52
|
+
- `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
|
|
53
|
+
cwd `require` once.
|
|
54
|
+
- `--bind` is `Object.assign(globalThis, …)` of names you name (wire
|
|
55
|
+
types only; no pickle). Missing names refuse.
|
|
56
|
+
- First Ctrl-C finishes the in-flight call (wire stays in sync).
|
|
57
|
+
Interrupt again to kill the child.
|
|
58
|
+
|
|
41
59
|
```
|
|
42
|
-
pip install concurrent-c-node
|
|
60
|
+
pip install concurrent-c-node # needs node on PATH
|
|
61
|
+
pip install 'concurrent-c-node[jupyter]' # magics (IPython)
|
|
43
62
|
python -m cc_node.examples.use_node
|
|
44
63
|
python -m cc_node.examples.bench_wire
|
|
45
64
|
python -m cc_node.benchmarks.multi_domain
|
|
65
|
+
python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / mini-racer
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Jupyter / Colab
|
|
69
|
+
|
|
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).
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
%pip install concurrent-c-node
|
|
77
|
+
# if `node` is missing (typical Colab):
|
|
78
|
+
!apt-get install -y nodejs
|
|
79
|
+
%load_ext cc_node
|
|
80
|
+
|
|
81
|
+
%%js
|
|
82
|
+
const path = require('path')
|
|
83
|
+
path.join('a', 'b') // last expression comes back as Python
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
xs = [1, 2, 3, 4]
|
|
88
|
+
|
|
89
|
+
%%js -b xs -t chunks
|
|
90
|
+
xs.map(x => x * 2) // wire types only; no pickle fallback
|
|
46
91
|
```
|
|
47
92
|
|
|
93
|
+
| | |
|
|
94
|
+
|---|---|
|
|
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 |
|
|
97
|
+
| `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
|
|
98
|
+
| `-t chunks` / `--to` | store the result in the notebook namespace |
|
|
99
|
+
| `%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 |
|
|
102
|
+
| `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
|
|
103
|
+
|
|
104
|
+
`eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
|
|
105
|
+
install cwd `require` once so cells look like Node (`require('path')`).
|
|
106
|
+
`--bind` is `Object.assign(globalThis, …)` — missing names and non-wire
|
|
107
|
+
types (`DataFrame`, a set) fail articulately. Reserved names
|
|
108
|
+
(`require`, `process`, `globalThis`, …) refuse so a bind cannot shadow
|
|
109
|
+
Node.
|
|
110
|
+
|
|
111
|
+
**Interrupt.** First Ctrl-C does not abandon the in-flight reply (that
|
|
112
|
+
would desync the wire and drop callbacks). The call finishes, the result
|
|
113
|
+
is discarded, the domain stays up. Interrupt again to **kill** the
|
|
114
|
+
child — cooperative `destroy()` cannot stop a JS `for (;;) {}`. Same
|
|
115
|
+
honesty as the rest of the bridge.
|
|
116
|
+
|
|
117
|
+
JS kernels (`tslab`, Deno Jupyter) are
|
|
118
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python);
|
|
119
|
+
Colab is not that. There, default `create()` blocks the kernel thread —
|
|
120
|
+
`py.task` / `{ isolated: true }` when the loop must stay live.
|
|
121
|
+
|
|
48
122
|
## Measured
|
|
49
123
|
|
|
50
124
|
[`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
|
|
@@ -62,6 +136,30 @@ python -m cc_node.benchmarks.multi_domain
|
|
|
62
136
|
Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
63
137
|
0700 dir, 0600 files, removed with the bridge.
|
|
64
138
|
|
|
139
|
+
### Vs pythonmonkey / mini-racer / DIY node
|
|
140
|
+
|
|
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:
|
|
143
|
+
[`cc_node_vs_alts_20260813.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_vs_alts_20260813.txt)
|
|
144
|
+
· harness: [`benchmarks/vs_alts.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/benchmarks/vs_alts.py).
|
|
145
|
+
|
|
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 |
|
|
153
|
+
|
|
154
|
+
Tiny scalars: pythonmonkey’s in-process SM beats a child. Real Node
|
|
155
|
+
(`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.
|
|
158
|
+
|
|
159
|
+
The other direction (Python from Node):
|
|
160
|
+
[`concurrent-c-python`](https://www.npmjs.com/package/concurrent-c-python)
|
|
161
|
+
vs pymport / ncp / pythonia.
|
|
162
|
+
|
|
65
163
|
## Surface
|
|
66
164
|
|
|
67
165
|
- Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
|
|
@@ -69,6 +167,8 @@ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
|
|
|
69
167
|
`String()`). Non-finite floats are tagged.
|
|
70
168
|
- Handles are per-domain. `stats()` / `release()` / idempotent
|
|
71
169
|
`destroy()`; afterwards: `bridge is closed`.
|
|
170
|
+
- `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
|
|
171
|
+
`require` once, optional `globalThis` binds; `eval()` stays one RTT.
|
|
72
172
|
- Crash isolation, not a sandbox. `destroy()` is cooperative; an
|
|
73
173
|
in-flight CPU-bound call finishes or you kill the child
|
|
74
174
|
([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
|
|
@@ -165,7 +265,9 @@ Both bridges (npm OIDC + PyPI OIDC):
|
|
|
165
265
|
# fallbacks: --npm-local / --pypi-twine
|
|
166
266
|
```
|
|
167
267
|
|
|
168
|
-
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain
|
|
268
|
+
Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
|
|
269
|
+
`benchmarks.vs_alts`.
|
|
270
|
+
Jupyter: `%load_ext cc_node`.
|
|
169
271
|
Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
|
|
170
272
|
Own hot path in C/CC → native module (40–90ns) —
|
|
171
273
|
[JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
|
|
@@ -18,6 +18,9 @@ exceptions cross back as JS errors and vice versa, messages intact.
|
|
|
18
18
|
Handles never cross domains; every door after destroy() answers
|
|
19
19
|
articulately; destroy is idempotent and `with cc_node.create() as js:`
|
|
20
20
|
scopes it.
|
|
21
|
+
|
|
22
|
+
Notebooks (Jupyter / Colab): `%load_ext cc_node` then `%%js` — one
|
|
23
|
+
kernel-scoped domain (`cc_node.kernel()`), same calling convention.
|
|
21
24
|
"""
|
|
22
25
|
import array
|
|
23
26
|
import atexit
|
|
@@ -25,12 +28,29 @@ import base64
|
|
|
25
28
|
import json
|
|
26
29
|
import math
|
|
27
30
|
import os
|
|
31
|
+
import re
|
|
32
|
+
import select
|
|
28
33
|
import shutil
|
|
29
34
|
import subprocess
|
|
35
|
+
import sys
|
|
30
36
|
import tempfile
|
|
31
37
|
|
|
32
|
-
__all__ = [
|
|
33
|
-
|
|
38
|
+
__all__ = [
|
|
39
|
+
"create", "kernel", "reset_kernel", "JsError", "JsHandle",
|
|
40
|
+
"load_ipython_extension", "unload_ipython_extension", "__version__",
|
|
41
|
+
]
|
|
42
|
+
__version__ = "0.22.1"
|
|
43
|
+
|
|
44
|
+
_NO_NODE = (
|
|
45
|
+
"cc-node: no node executable (install Node, or set CC_NODE_BIN). "
|
|
46
|
+
"Colab/Jupyter: `!apt-get install -y nodejs` then retry, or pass "
|
|
47
|
+
"create(node=...) / CC_NODE_BIN."
|
|
48
|
+
)
|
|
49
|
+
_BIND_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
|
50
|
+
_BIND_RESERVED = frozenset({
|
|
51
|
+
"require", "globalThis", "global", "process", "module", "exports",
|
|
52
|
+
"window", "self",
|
|
53
|
+
})
|
|
34
54
|
|
|
35
55
|
# Typed buffers cross as typed arrays; big ones spill through shared
|
|
36
56
|
# memory (tmpfs where available) — one memcpy per side, receiver
|
|
@@ -97,9 +117,20 @@ class JsHandle:
|
|
|
97
117
|
return self._d._req("str", h=self._h)
|
|
98
118
|
|
|
99
119
|
def __repr__(self):
|
|
120
|
+
# Cheap: a wire str() is a round trip and can hang a display hook.
|
|
100
121
|
return "<JsHandle #%d%s>" % (self._h,
|
|
101
122
|
" (closed)" if self._d.closed else "")
|
|
102
123
|
|
|
124
|
+
def _repr_html_(self):
|
|
125
|
+
state = " closed" if self._d.closed else ""
|
|
126
|
+
return (
|
|
127
|
+
'<code title="domain-owned JS proxy; str() / attrs cross the wire">'
|
|
128
|
+
"JsHandle #%d%s</code>" % (self._h, state)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _repr_pretty_(self, p, cycle):
|
|
132
|
+
p.text(repr(self))
|
|
133
|
+
|
|
103
134
|
def __del__(self):
|
|
104
135
|
# Never nest a sync wire op from GC into an in-flight _req — that
|
|
105
136
|
# steals the outer reply (e.g. returning a callback arg handle).
|
|
@@ -117,6 +148,7 @@ class Bridge:
|
|
|
117
148
|
broker = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
118
149
|
"broker.cjs")
|
|
119
150
|
node = node or os.environ.get("CC_NODE_BIN", "node")
|
|
151
|
+
self._node_bin = node
|
|
120
152
|
# Spills live in a private 0700 per-bridge directory (predictable
|
|
121
153
|
# names in a shared /dev/shm invite pre-creation races and
|
|
122
154
|
# umask-dependent exposure); the child writes its spills there
|
|
@@ -143,13 +175,14 @@ class Bridge:
|
|
|
143
175
|
for fd in (req_r, req_w, resp_r, resp_w):
|
|
144
176
|
os.close(fd)
|
|
145
177
|
shutil.rmtree(self._shm_dir, ignore_errors=True)
|
|
146
|
-
raise JsError(
|
|
147
|
-
"cc-node: no node executable (install Node, or set "
|
|
148
|
-
"CC_NODE_BIN)") from None
|
|
178
|
+
raise JsError(_NO_NODE) from None
|
|
149
179
|
os.close(req_r)
|
|
150
180
|
os.close(resp_w)
|
|
151
181
|
self._wire_w = os.fdopen(req_w, "wb")
|
|
152
|
-
|
|
182
|
+
# Raw reply fd + leftover buffer: select then read, so SIGINT can
|
|
183
|
+
# land without mixing select() with a buffered file object.
|
|
184
|
+
self._resp_fd = resp_r
|
|
185
|
+
self._resp_buf = bytearray()
|
|
153
186
|
self.closed = False
|
|
154
187
|
self._nid = 1
|
|
155
188
|
self._cbs = {}
|
|
@@ -160,6 +193,8 @@ class Bridge:
|
|
|
160
193
|
self._pending_release = []
|
|
161
194
|
self._close_pending = False
|
|
162
195
|
self._destroy_done = False
|
|
196
|
+
self._cell_require_ready = False
|
|
197
|
+
self._cell_runner = None
|
|
163
198
|
_live.append(self)
|
|
164
199
|
|
|
165
200
|
# ---- wire ----
|
|
@@ -211,12 +246,84 @@ class Bridge:
|
|
|
211
246
|
raise JsError(msg["e"])
|
|
212
247
|
return self._decode_result(msg)
|
|
213
248
|
|
|
249
|
+
def _read_chunk(self):
|
|
250
|
+
fd = self._resp_fd
|
|
251
|
+
if fd is None:
|
|
252
|
+
return b""
|
|
253
|
+
if sys.platform == "win32":
|
|
254
|
+
try:
|
|
255
|
+
return os.read(fd, 65536)
|
|
256
|
+
except OSError:
|
|
257
|
+
return b""
|
|
258
|
+
while True:
|
|
259
|
+
try:
|
|
260
|
+
select.select([fd], [], [])
|
|
261
|
+
except InterruptedError:
|
|
262
|
+
continue
|
|
263
|
+
except OSError:
|
|
264
|
+
return b""
|
|
265
|
+
try:
|
|
266
|
+
return os.read(fd, 65536)
|
|
267
|
+
except InterruptedError:
|
|
268
|
+
continue
|
|
269
|
+
except OSError:
|
|
270
|
+
return b""
|
|
271
|
+
|
|
272
|
+
def _read_line(self):
|
|
273
|
+
"""One wire line (without the LF), or b'' on EOF."""
|
|
274
|
+
while True:
|
|
275
|
+
nl = self._resp_buf.find(b"\n")
|
|
276
|
+
if nl >= 0:
|
|
277
|
+
line = bytes(self._resp_buf[:nl])
|
|
278
|
+
del self._resp_buf[:nl + 1]
|
|
279
|
+
return line
|
|
280
|
+
chunk = self._read_chunk()
|
|
281
|
+
if not chunk:
|
|
282
|
+
return b""
|
|
283
|
+
self._resp_buf.extend(chunk)
|
|
284
|
+
|
|
285
|
+
def _kill_child(self):
|
|
286
|
+
"""Hard death from inside an in-flight wait. Cooperative close
|
|
287
|
+
cannot interrupt a JS CPU loop; SIGKILL of the child is the
|
|
288
|
+
second-interrupt door. `_req`'s finally runs `_finish_destroy`."""
|
|
289
|
+
self.closed = True
|
|
290
|
+
self._close_pending = True
|
|
291
|
+
try:
|
|
292
|
+
if self._p.poll() is None:
|
|
293
|
+
self._p.kill()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
def _complete_reply(self, msg, hits):
|
|
298
|
+
if hits:
|
|
299
|
+
try:
|
|
300
|
+
self._take_reply(msg)
|
|
301
|
+
except JsError:
|
|
302
|
+
raise
|
|
303
|
+
raise KeyboardInterrupt(
|
|
304
|
+
"cc-node: call finished after interrupt; result discarded"
|
|
305
|
+
) from None
|
|
306
|
+
return self._take_reply(msg)
|
|
307
|
+
|
|
214
308
|
def _wait_reply(self, rid):
|
|
215
|
-
|
|
216
|
-
if parked is not None:
|
|
217
|
-
return self._take_reply(parked)
|
|
309
|
+
hits = 0
|
|
218
310
|
while True:
|
|
219
|
-
|
|
311
|
+
parked = self._parked.pop(rid, None)
|
|
312
|
+
if parked is not None:
|
|
313
|
+
return self._complete_reply(parked, hits)
|
|
314
|
+
try:
|
|
315
|
+
line = self._read_line()
|
|
316
|
+
except KeyboardInterrupt:
|
|
317
|
+
hits += 1
|
|
318
|
+
if hits == 1:
|
|
319
|
+
sys.stderr.write(
|
|
320
|
+
"cc-node: JS still running — interrupt again to "
|
|
321
|
+
"destroy this domain\n")
|
|
322
|
+
sys.stderr.flush()
|
|
323
|
+
continue
|
|
324
|
+
self._kill_child()
|
|
325
|
+
raise KeyboardInterrupt(
|
|
326
|
+
"cc-node: interrupted; domain destroyed") from None
|
|
220
327
|
if not line:
|
|
221
328
|
self.closed = True
|
|
222
329
|
raise JsError("cc-node: the node child exited")
|
|
@@ -226,7 +333,7 @@ class Bridge:
|
|
|
226
333
|
continue
|
|
227
334
|
mid = msg.get("id")
|
|
228
335
|
if mid == rid:
|
|
229
|
-
return self.
|
|
336
|
+
return self._complete_reply(msg, hits)
|
|
230
337
|
if mid is not None:
|
|
231
338
|
# Nested _req (GC release, etc.) can overtake; park by id.
|
|
232
339
|
self._parked[mid] = msg
|
|
@@ -407,6 +514,59 @@ class Bridge:
|
|
|
407
514
|
def eval(self, src):
|
|
408
515
|
return self._req("eval", src=src)
|
|
409
516
|
|
|
517
|
+
def eval_cell(self, src, bindings=None):
|
|
518
|
+
"""Eval JS in this domain. Optional `bindings` are published on
|
|
519
|
+
`globalThis` for the call (`Object.assign`). Installs cwd
|
|
520
|
+
`require` once so a notebook cell can `require('path')` like
|
|
521
|
+
Node. `eval()` itself does not install `require` and does not
|
|
522
|
+
take bindings — one RTT, unchanged."""
|
|
523
|
+
if self.closed:
|
|
524
|
+
raise JsError("cc-node: bridge is closed")
|
|
525
|
+
self._ensure_cell_require()
|
|
526
|
+
if not bindings:
|
|
527
|
+
return self.eval(src)
|
|
528
|
+
names = {}
|
|
529
|
+
for k, v in bindings.items():
|
|
530
|
+
self._check_bind_name(k)
|
|
531
|
+
names[k] = v
|
|
532
|
+
runner = self._cell_runner
|
|
533
|
+
if runner is None:
|
|
534
|
+
runner = self.eval(
|
|
535
|
+
"(b, src) => { Object.assign(globalThis, b); "
|
|
536
|
+
"return (0, eval)(src); }")
|
|
537
|
+
self._cell_runner = runner
|
|
538
|
+
return runner(names, src)
|
|
539
|
+
|
|
540
|
+
def _ensure_cell_require(self):
|
|
541
|
+
if self._cell_require_ready or self.closed:
|
|
542
|
+
return
|
|
543
|
+
install = self.eval(
|
|
544
|
+
"(m) => { globalThis.require = "
|
|
545
|
+
"m.createRequire(process.cwd() + '/'); }")
|
|
546
|
+
mod = self.require("module")
|
|
547
|
+
try:
|
|
548
|
+
install(mod)
|
|
549
|
+
self._cell_require_ready = True
|
|
550
|
+
finally:
|
|
551
|
+
try:
|
|
552
|
+
self.release(mod)
|
|
553
|
+
except Exception:
|
|
554
|
+
pass
|
|
555
|
+
try:
|
|
556
|
+
self.release(install)
|
|
557
|
+
except Exception:
|
|
558
|
+
pass
|
|
559
|
+
|
|
560
|
+
@staticmethod
|
|
561
|
+
def _check_bind_name(name):
|
|
562
|
+
if not isinstance(name, str) or not _BIND_NAME.match(name):
|
|
563
|
+
raise JsError(
|
|
564
|
+
"cc-node: --bind name %r is not a JS identifier" % (name,))
|
|
565
|
+
if name in _BIND_RESERVED:
|
|
566
|
+
raise JsError(
|
|
567
|
+
"cc-node: --bind name %r is reserved (would shadow a "
|
|
568
|
+
"Node global)" % (name,))
|
|
569
|
+
|
|
410
570
|
def release(self, handle):
|
|
411
571
|
if not isinstance(handle, JsHandle) or handle._d is not self:
|
|
412
572
|
raise JsError("cc-node: handle belongs to another bridge")
|
|
@@ -449,7 +609,7 @@ class Bridge:
|
|
|
449
609
|
# land — closing the reply fd under its write is an EPIPE crash
|
|
450
610
|
# in the child.
|
|
451
611
|
try:
|
|
452
|
-
while self.
|
|
612
|
+
while self._read_line():
|
|
453
613
|
pass
|
|
454
614
|
except Exception:
|
|
455
615
|
pass
|
|
@@ -461,9 +621,12 @@ class Bridge:
|
|
|
461
621
|
except Exception:
|
|
462
622
|
pass
|
|
463
623
|
try:
|
|
464
|
-
self.
|
|
624
|
+
if self._resp_fd is not None:
|
|
625
|
+
os.close(self._resp_fd)
|
|
465
626
|
except Exception:
|
|
466
627
|
pass
|
|
628
|
+
self._resp_fd = None
|
|
629
|
+
self._cell_runner = None
|
|
467
630
|
shutil.rmtree(self._shm_dir, ignore_errors=True)
|
|
468
631
|
if self in _live:
|
|
469
632
|
_live.remove(self)
|
|
@@ -480,3 +643,48 @@ class Bridge:
|
|
|
480
643
|
|
|
481
644
|
def create(node=None):
|
|
482
645
|
return Bridge(node=node)
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
_kernel = None
|
|
649
|
+
|
|
650
|
+
|
|
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
|
|
656
|
+
if b is not None and not b.closed:
|
|
657
|
+
if node is not None and node != b._node_bin:
|
|
658
|
+
raise JsError(
|
|
659
|
+
"cc-node: kernel() already live with a different node; "
|
|
660
|
+
"reset_kernel() / %js_reset first")
|
|
661
|
+
return b
|
|
662
|
+
b = create(node=node)
|
|
663
|
+
try:
|
|
664
|
+
b._ensure_cell_require()
|
|
665
|
+
except Exception:
|
|
666
|
+
b.destroy()
|
|
667
|
+
raise
|
|
668
|
+
_kernel = b
|
|
669
|
+
return b
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def reset_kernel():
|
|
673
|
+
"""Destroy the kernel-scoped domain. Next `kernel()` / `%%js` spawns."""
|
|
674
|
+
global _kernel
|
|
675
|
+
b = _kernel
|
|
676
|
+
_kernel = None
|
|
677
|
+
if b is not None:
|
|
678
|
+
try:
|
|
679
|
+
b.destroy()
|
|
680
|
+
except Exception:
|
|
681
|
+
pass
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def load_ipython_extension(ip):
|
|
685
|
+
from .magics import load_ipython_extension as _load
|
|
686
|
+
_load(ip)
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
def unload_ipython_extension(ip):
|
|
690
|
+
reset_kernel()
|