concurrent-c-node 0.14.0__tar.gz → 0.16.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.
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: concurrent-c-node
3
+ Version: 0.16.0
4
+ Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/sreekotay/concurrent-c
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
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # concurrent-c-node
13
+
14
+ Call Node (and npm packages) from Python.
15
+ Native types, exceptions, callbacks, and async all cross the boundary.
16
+
17
+ Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
18
+ strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
19
+ with your host C compiler. (This bridge itself is pure Python stdlib —
20
+ no native build.)
21
+
22
+ Map of the three boundaries (CC hosts JS, native modules, this package
23
+ bridge):
24
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
25
+
26
+ ```python
27
+ import cc_node
28
+
29
+ js = cc_node.create() # always a child `node` process
30
+ _ = js.require('lodash') # cwd node_modules
31
+ _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
32
+
33
+ semver = js.require('semver')
34
+ semver.satisfies('1.2.3', '^1.0.0') # True
35
+
36
+ js.destroy() # or: with cc_node.create() as js:
37
+ ```
38
+
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.
42
+
43
+ | | this package | CC hosted (`cc_js_new(false, …)`) |
44
+ |---|---|---|
45
+ | API | `cc_node.create()` | `.ccs` program |
46
+ | Where | child `node` | libnode in-process |
47
+ | Hot call | ~105µs RTT | sub-µs (needs libnode) |
48
+ | Bulk | shm (~9.5ms / 8MB) | in-process |
49
+ | Parallelism | N children | one process |
50
+ | Crash | child dies; parent lives | shared fate |
51
+
52
+ ```
53
+ pip install concurrent-c-node # needs node on PATH
54
+ python -m cc_node.examples.use_node
55
+ python -m cc_node.examples.bench_wire
56
+ python -m cc_node.benchmarks.multi_domain
57
+ ```
58
+
59
+ ## Measured
60
+
61
+ [`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
62
+ ·
63
+ [`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
64
+
65
+ | what | result |
66
+ |---|---|
67
+ | spawn (first eval) | 28ms |
68
+ | wire RTT | 105µs |
69
+ | Python callback round trip | 153µs |
70
+ | 8MB `array('d')` via shm | 9.5ms |
71
+ | same 8MB as JSON list | 499ms (~52×) |
72
+
73
+ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
74
+ 0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
75
+ sandbox — don’t eval untrusted JS.
76
+
77
+ ## Surface
78
+
79
+ - Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
80
+ value. Empty `{}` stays a live handle. Else: domain-owned handle
81
+ (attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
82
+ - Handles stay in one domain. `stats()` / `release()` / idempotent
83
+ `destroy()`; after close: `bridge is closed`. Teardown is cooperative;
84
+ CPU-bound JS isn’t cancelable — wait or kill
85
+ ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
86
+
87
+ ### Promises
88
+
89
+ Awaited in the child before the reply — no `async`/`await` on the
90
+ Python side:
91
+
92
+ ```python
93
+ fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
94
+ fetchish(21) # {'doubled': 42}
95
+ ```
96
+
97
+ ### Callbacks
98
+
99
+ ```python
100
+ mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
101
+ # map passes (value, index, array) — take *rest
102
+ ```
103
+
104
+ Exceptions cross both ways with messages intact.
105
+
106
+ ### Buffers
107
+
108
+ `bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
109
+ inline; large via shm.
110
+
111
+ ```python
112
+ import array
113
+ total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
114
+ total(array.array('d', range(1_000_000)))
115
+ ```
116
+
117
+ ## Common issues
118
+
119
+ **`Cannot find module`.** `require` / `import` resolve from the Python
120
+ process cwd (`node_modules` next to your program), not from this wheel’s
121
+ site-packages. `npm install lodash` in the project directory is the fix;
122
+ or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
123
+ Missing-module errors name that cwd rule.
124
+
125
+ ### Empty `{}` stays a handle
126
+
127
+ An empty object has to stay on the Node side — a materialized Python
128
+ `{}`/`dict` would lose later property use that matches Node. So
129
+ `js.eval('({})')` returns a live handle:
130
+
131
+ ```python
132
+ o = js.eval('({})') # JsHandle, not {}
133
+ js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
134
+ js.eval('({a: 1})') # {'a': 1} — data return
135
+ ```
136
+
137
+ Non-empty plain objects still cross as Python `dict`s. Same-domain
138
+ handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
139
+
140
+ **Thenables settle in the child.** Promise-based npm APIs need no
141
+ `async`/`await` on the Python side — the call blocks until settle (or
142
+ raises on reject). Opposite of `concurrent-c-python` isolated, where
143
+ every call is already a JS Promise you must await.
144
+
145
+ **Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
146
+ on three numbers loses to pure Python. Prefer Python (or a native CC
147
+ module) for small/hot work; use the bridge when Node/npm owns the kernel.
148
+ Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
149
+ domains here).
150
+
151
+ **Crash isolation, not a sandbox.** The child inherits your environment
152
+ — don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
153
+ not preemptible (wait or kill + new domain).
154
+
155
+ ## Choosing node
156
+
157
+ 1. `create(node='/path/to/node')`
158
+ 2. `CC_NODE_BIN`
159
+ 3. `node` on `PATH`
160
+
161
+ Packages: cwd `node_modules`, same as Node itself.
162
+
163
+ From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
164
+ (libnode), or `cc_js_new(true, &a)` for this wire —
165
+ [`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
166
+
167
+ ## Publishing
168
+
169
+ **PyPI Trusted Publishing (OIDC)** — no API token:
170
+
171
+ 1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
172
+ owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
173
+ environment `pypi`
174
+ 2. GitHub Environment `pypi`
175
+ 3. Bump `pyproject.toml`, push, then:
176
+
177
+ ```
178
+ gh workflow run publish-cc-node.yml
179
+ ```
180
+
181
+ Local / npm sibling:
182
+
183
+ ```
184
+ ./scripts/publish_bridges.sh --publish --minor
185
+ gh workflow run publish-cc-node.yml
186
+ # twine fallback: … --pypi-twine
187
+ ```
188
+
189
+ Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
190
+ Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
191
+ Own hot path in C/CC → native module (40–90ns) instead of the wire —
192
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -0,0 +1,181 @@
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
+ Unlike [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
29
+ (in-process by default), every `create()` here is a separate Node —
30
+ real addons, crash isolation, measurable wire. N domains = N processes.
31
+
32
+ | | this package | CC hosted (`cc_js_new(false, …)`) |
33
+ |---|---|---|
34
+ | API | `cc_node.create()` | `.ccs` program |
35
+ | Where | child `node` | libnode in-process |
36
+ | Hot call | ~105µs RTT | sub-µs (needs libnode) |
37
+ | Bulk | shm (~9.5ms / 8MB) | in-process |
38
+ | Parallelism | N children | one process |
39
+ | Crash | child dies; parent lives | shared fate |
40
+
41
+ ```
42
+ pip install concurrent-c-node # needs node on PATH
43
+ python -m cc_node.examples.use_node
44
+ python -m cc_node.examples.bench_wire
45
+ python -m cc_node.benchmarks.multi_domain
46
+ ```
47
+
48
+ ## Measured
49
+
50
+ [`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
51
+ ·
52
+ [`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
53
+
54
+ | what | result |
55
+ |---|---|
56
+ | spawn (first eval) | 28ms |
57
+ | wire RTT | 105µs |
58
+ | Python callback round trip | 153µs |
59
+ | 8MB `array('d')` via shm | 9.5ms |
60
+ | same 8MB as JSON list | 499ms (~52×) |
61
+
62
+ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
63
+ 0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
64
+ sandbox — don’t eval untrusted JS.
65
+
66
+ ## Surface
67
+
68
+ - Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
69
+ value. Empty `{}` stays a live handle. Else: domain-owned handle
70
+ (attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
71
+ - Handles stay in one domain. `stats()` / `release()` / idempotent
72
+ `destroy()`; after close: `bridge is closed`. Teardown is cooperative;
73
+ CPU-bound JS isn’t cancelable — wait or kill
74
+ ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
75
+
76
+ ### Promises
77
+
78
+ Awaited in the child before the reply — no `async`/`await` on the
79
+ Python side:
80
+
81
+ ```python
82
+ fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
83
+ fetchish(21) # {'doubled': 42}
84
+ ```
85
+
86
+ ### Callbacks
87
+
88
+ ```python
89
+ mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
90
+ # map passes (value, index, array) — take *rest
91
+ ```
92
+
93
+ Exceptions cross both ways with messages intact.
94
+
95
+ ### Buffers
96
+
97
+ `bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
98
+ inline; large via shm.
99
+
100
+ ```python
101
+ import array
102
+ total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
103
+ total(array.array('d', range(1_000_000)))
104
+ ```
105
+
106
+ ## Common issues
107
+
108
+ **`Cannot find module`.** `require` / `import` resolve from the Python
109
+ process cwd (`node_modules` next to your program), not from this wheel’s
110
+ site-packages. `npm install lodash` in the project directory is the fix;
111
+ or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
112
+ Missing-module errors name that cwd rule.
113
+
114
+ ### Empty `{}` stays a handle
115
+
116
+ An empty object has to stay on the Node side — a materialized Python
117
+ `{}`/`dict` would lose later property use that matches Node. So
118
+ `js.eval('({})')` returns a live handle:
119
+
120
+ ```python
121
+ o = js.eval('({})') # JsHandle, not {}
122
+ js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
123
+ js.eval('({a: 1})') # {'a': 1} — data return
124
+ ```
125
+
126
+ Non-empty plain objects still cross as Python `dict`s. Same-domain
127
+ handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
128
+
129
+ **Thenables settle in the child.** Promise-based npm APIs need no
130
+ `async`/`await` on the Python side — the call blocks until settle (or
131
+ raises on reject). Opposite of `concurrent-c-python` isolated, where
132
+ every call is already a JS Promise you must await.
133
+
134
+ **Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
135
+ on three numbers loses to pure Python. Prefer Python (or a native CC
136
+ module) for small/hot work; use the bridge when Node/npm owns the kernel.
137
+ Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
138
+ domains here).
139
+
140
+ **Crash isolation, not a sandbox.** The child inherits your environment
141
+ — don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
142
+ not preemptible (wait or kill + new domain).
143
+
144
+ ## Choosing node
145
+
146
+ 1. `create(node='/path/to/node')`
147
+ 2. `CC_NODE_BIN`
148
+ 3. `node` on `PATH`
149
+
150
+ Packages: cwd `node_modules`, same as Node itself.
151
+
152
+ From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
153
+ (libnode), or `cc_js_new(true, &a)` for this wire —
154
+ [`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
155
+
156
+ ## Publishing
157
+
158
+ **PyPI Trusted Publishing (OIDC)** — no API token:
159
+
160
+ 1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
161
+ owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
162
+ environment `pypi`
163
+ 2. GitHub Environment `pypi`
164
+ 3. Bump `pyproject.toml`, push, then:
165
+
166
+ ```
167
+ gh workflow run publish-cc-node.yml
168
+ ```
169
+
170
+ Local / npm sibling:
171
+
172
+ ```
173
+ ./scripts/publish_bridges.sh --publish --minor
174
+ gh workflow run publish-cc-node.yml
175
+ # twine fallback: … --pypi-twine
176
+ ```
177
+
178
+ Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
179
+ Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
180
+ Own hot path in C/CC → native module (40–90ns) instead of the wire —
181
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -30,7 +30,7 @@ import subprocess
30
30
  import tempfile
31
31
 
32
32
  __all__ = ["create", "JsError", "JsHandle", "__version__"]
33
- __version__ = "0.12.1"
33
+ __version__ = "0.13.0"
34
34
 
35
35
  # Typed buffers cross as typed arrays; big ones spill through shared
36
36
  # memory (tmpfs where available) — one memcpy per side, receiver
@@ -0,0 +1 @@
1
+ # Measurement scripts for concurrent-c-node (RESULT lines, machine-comparable).
@@ -0,0 +1,60 @@
1
+ """N node children on N cores — measured with real overlap.
2
+
3
+ python -m cc_node.benchmarks.multi_domain
4
+
5
+ Wire RTT / shm live in ``cc_node.examples.bench_wire``. This file only
6
+ answers the multi-domain question: sequential calls on one child vs the
7
+ same work fanned across threads into separate children.
8
+ """
9
+ import time
10
+ from concurrent.futures import ThreadPoolExecutor
11
+
12
+ import cc_node
13
+
14
+ WORK = """
15
+ (iterations) => {
16
+ let s = 0;
17
+ for (let i = 0; i < iterations; i++)
18
+ s += Math.sqrt(i) * Math.sin(i / 1000);
19
+ return s;
20
+ }
21
+ """
22
+
23
+
24
+ def main():
25
+ n_dom = 3
26
+ iters = 40_000_000
27
+
28
+ with cc_node.create() as js:
29
+ f = js.eval(WORK)
30
+ f(iters // 20)
31
+ t0 = time.perf_counter()
32
+ for _ in range(n_dom):
33
+ f(iters)
34
+ seq = time.perf_counter() - t0
35
+ print("RESULT seq_s %.3f" % seq)
36
+
37
+ domains = [cc_node.create() for _ in range(n_dom)]
38
+ try:
39
+ fns = [d.eval(WORK) for d in domains]
40
+ for f in fns:
41
+ f(iters // 20)
42
+
43
+ def run(f):
44
+ return f(iters)
45
+
46
+ t0 = time.perf_counter()
47
+ with ThreadPoolExecutor(max_workers=n_dom) as pool:
48
+ list(pool.map(run, fns))
49
+ par = time.perf_counter() - t0
50
+ finally:
51
+ for d in domains:
52
+ d.destroy()
53
+
54
+ print("RESULT par_s %.3f" % par)
55
+ print("RESULT speedup %.2f" % (seq / par if par else 0))
56
+ print("done")
57
+
58
+
59
+ if __name__ == "__main__":
60
+ main()
@@ -0,0 +1,192 @@
1
+ Metadata-Version: 2.4
2
+ Name: concurrent-c-node
3
+ Version: 0.16.0
4
+ Summary: JavaScript and npm packages from Python over the Concurrent-C bridge: one spawned Node child per domain, host-controlled lifetime.
5
+ License: MIT
6
+ Project-URL: Repository, https://github.com/sreekotay/concurrent-c
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
9
+ Requires-Python: >=3.8
10
+ Description-Content-Type: text/markdown
11
+
12
+ # concurrent-c-node
13
+
14
+ Call Node (and npm packages) from Python.
15
+ Native types, exceptions, callbacks, and async all cross the boundary.
16
+
17
+ Part of [Concurrent-C](https://github.com/sreekotay/concurrent-c) — a
18
+ strict C11-superset preprocessor: `.ccs` lowers to plain C and compiles
19
+ with your host C compiler. (This bridge itself is pure Python stdlib —
20
+ no native build.)
21
+
22
+ Map of the three boundaries (CC hosts JS, native modules, this package
23
+ bridge):
24
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
25
+
26
+ ```python
27
+ import cc_node
28
+
29
+ js = cc_node.create() # always a child `node` process
30
+ _ = js.require('lodash') # cwd node_modules
31
+ _.chunk([1, 2, 3, 4, 5], 2) # [[1, 2], [3, 4], [5]]
32
+
33
+ semver = js.require('semver')
34
+ semver.satisfies('1.2.3', '^1.0.0') # True
35
+
36
+ js.destroy() # or: with cc_node.create() as js:
37
+ ```
38
+
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.
42
+
43
+ | | this package | CC hosted (`cc_js_new(false, …)`) |
44
+ |---|---|---|
45
+ | API | `cc_node.create()` | `.ccs` program |
46
+ | Where | child `node` | libnode in-process |
47
+ | Hot call | ~105µs RTT | sub-µs (needs libnode) |
48
+ | Bulk | shm (~9.5ms / 8MB) | in-process |
49
+ | Parallelism | N children | one process |
50
+ | Crash | child dies; parent lives | shared fate |
51
+
52
+ ```
53
+ pip install concurrent-c-node # needs node on PATH
54
+ python -m cc_node.examples.use_node
55
+ python -m cc_node.examples.bench_wire
56
+ python -m cc_node.benchmarks.multi_domain
57
+ ```
58
+
59
+ ## Measured
60
+
61
+ [`bench_wire`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/bench_wire.py)
62
+ ·
63
+ [`cc_node_bridge_py_20260810.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260810.txt):
64
+
65
+ | what | result |
66
+ |---|---|
67
+ | spawn (first eval) | 28ms |
68
+ | wire RTT | 105µs |
69
+ | Python callback round trip | 153µs |
70
+ | 8MB `array('d')` via shm | 9.5ms |
71
+ | same 8MB as JSON list | 499ms (~52×) |
72
+
73
+ Wire: line-JSON on dedicated fds (stdio stays yours). Bulk spill: private
74
+ 0700 dir, 0600 files, removed with the bridge. Crash isolation, not a
75
+ sandbox — don’t eval untrusted JS.
76
+
77
+ ## Surface
78
+
79
+ - Plain data (numbers, str, bool, `None`, lists, non-empty dicts) by
80
+ value. Empty `{}` stays a live handle. Else: domain-owned handle
81
+ (attrs, calls, `str()` → `String()`). Non-finite floats are tagged.
82
+ - Handles stay in one domain. `stats()` / `release()` / idempotent
83
+ `destroy()`; after close: `bridge is closed`. Teardown is cooperative;
84
+ CPU-bound JS isn’t cancelable — wait or kill
85
+ ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
86
+
87
+ ### Promises
88
+
89
+ Awaited in the child before the reply — no `async`/`await` on the
90
+ Python side:
91
+
92
+ ```python
93
+ fetchish = js.eval('async (x) => { return { doubled: x * 2 } }')
94
+ fetchish(21) # {'doubled': 42}
95
+ ```
96
+
97
+ ### Callbacks
98
+
99
+ ```python
100
+ mapped = js.eval('(f) => [1, 2, 3].map(f)')(lambda x, *rest: x * 10)
101
+ # map passes (value, index, array) — take *rest
102
+ ```
103
+
104
+ Exceptions cross both ways with messages intact.
105
+
106
+ ### Buffers
107
+
108
+ `bytes` / `array.array` / 1-D numpy → typed arrays (and back). Small
109
+ inline; large via shm.
110
+
111
+ ```python
112
+ import array
113
+ total = js.eval('(a) => a.reduce((s, x) => s + x, 0)')
114
+ total(array.array('d', range(1_000_000)))
115
+ ```
116
+
117
+ ## Common issues
118
+
119
+ **`Cannot find module`.** `require` / `import` resolve from the Python
120
+ process cwd (`node_modules` next to your program), not from this wheel’s
121
+ site-packages. `npm install lodash` in the project directory is the fix;
122
+ or `create(node=…)` / `CC_NODE_BIN` when the wrong Node is on `PATH`.
123
+ Missing-module errors name that cwd rule.
124
+
125
+ ### Empty `{}` stays a handle
126
+
127
+ An empty object has to stay on the Node side — a materialized Python
128
+ `{}`/`dict` would lose later property use that matches Node. So
129
+ `js.eval('({})')` returns a live handle:
130
+
131
+ ```python
132
+ o = js.eval('({})') # JsHandle, not {}
133
+ js.eval('(o) => { o.x = 1; return o.x }')(o) # 1
134
+ js.eval('({a: 1})') # {'a': 1} — data return
135
+ ```
136
+
137
+ Non-empty plain objects still cross as Python `dict`s. Same-domain
138
+ handles chain (`h.update(…).digest(…)`); foreign-domain handles do not.
139
+
140
+ **Thenables settle in the child.** Promise-based npm APIs need no
141
+ `async`/`await` on the Python side — the call blocks until settle (or
142
+ raises on reject). Opposite of `concurrent-c-python` isolated, where
143
+ every call is already a JS Promise you must await.
144
+
145
+ **Wire cost vs tiny work.** Round trip is ~100µs; a one-line JS helper
146
+ on three numbers loses to pure Python. Prefer Python (or a native CC
147
+ module) for small/hot work; use the bridge when Node/npm owns the kernel.
148
+ Multi-core: `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3
149
+ domains here).
150
+
151
+ **Crash isolation, not a sandbox.** The child inherits your environment
152
+ — don’t eval untrusted JS. `destroy()` is cooperative; CPU-bound JS is
153
+ not preemptible (wait or kill + new domain).
154
+
155
+ ## Choosing node
156
+
157
+ 1. `create(node='/path/to/node')`
158
+ 2. `CC_NODE_BIN`
159
+ 3. `node` on `PATH`
160
+
161
+ Packages: cwd `node_modules`, same as Node itself.
162
+
163
+ From Concurrent-C (not Python): `cc_js_new(false, &a)` hosted
164
+ (libnode), or `cc_js_new(true, &a)` for this wire —
165
+ [`jsdemo.shcc`](https://github.com/sreekotay/concurrent-c/blob/main/examples/js/jsdemo.shcc).
166
+
167
+ ## Publishing
168
+
169
+ **PyPI Trusted Publishing (OIDC)** — no API token:
170
+
171
+ 1. [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
172
+ owner `sreekotay`, repo `concurrent-c`, workflow `publish-cc-node.yml`,
173
+ environment `pypi`
174
+ 2. GitHub Environment `pypi`
175
+ 3. Bump `pyproject.toml`, push, then:
176
+
177
+ ```
178
+ gh workflow run publish-cc-node.yml
179
+ ```
180
+
181
+ Local / npm sibling:
182
+
183
+ ```
184
+ ./scripts/publish_bridges.sh --publish --minor
185
+ gh workflow run publish-cc-node.yml
186
+ # twine fallback: … --pypi-twine
187
+ ```
188
+
189
+ Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`.
190
+ Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
191
+ Own hot path in C/CC → native module (40–90ns) instead of the wire —
192
+ [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -2,6 +2,8 @@ README.md
2
2
  pyproject.toml
3
3
  cc_node/__init__.py
4
4
  cc_node/broker.cjs
5
+ cc_node/benchmarks/__init__.py
6
+ cc_node/benchmarks/multi_domain.py
5
7
  cc_node/examples/__init__.py
6
8
  cc_node/examples/bench_wire.py
7
9
  cc_node/examples/use_node.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.14.0"
7
+ version = "0.16.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"
@@ -16,8 +16,9 @@ Repository = "https://github.com/sreekotay/concurrent-c"
16
16
  Documentation = "https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/README.md"
17
17
 
18
18
  [tool.setuptools]
19
- packages = ["cc_node", "cc_node.examples"]
19
+ packages = ["cc_node", "cc_node.examples", "cc_node.benchmarks"]
20
20
 
21
21
  [tool.setuptools.package-data]
22
22
  cc_node = ["broker.cjs"]
23
23
  "cc_node.examples" = ["*.py"]
24
+ "cc_node.benchmarks" = ["*.py"]