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