concurrent-c-node 0.12.0__tar.gz → 0.15.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.12.0
3
+ Version: 0.15.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
@@ -62,6 +62,7 @@ event-loop or GIL between them.
62
62
  pip install concurrent-c-node # needs node on PATH (or point at one)
63
63
  python -m cc_node.examples.use_node
64
64
  python -m cc_node.examples.bench_wire
65
+ python -m cc_node.benchmarks.multi_domain # N children, thread-fanned
65
66
  ```
66
67
 
67
68
  ## Measured (separate-process wire)
@@ -99,10 +100,12 @@ Examples ship in the wheel. Same domain model and materialization
99
100
  rules as the npm sibling, pointed the other way:
100
101
 
101
102
  - **Values**: plain data (finite numbers, strings, booleans, `None`,
102
- lists/dicts of the same) crosses by value; everything else is a live
103
- handle owned by the domain — attribute access is property lookup
104
- (methods arrive bound), calls are calls, `str()` is `String()`.
105
- Non-finite floats cross tagged, never silently nulled.
103
+ lists and non-empty dicts/objects of the same) crosses by value; an
104
+ empty `{}` stays a live handle (so bags you mint in JS keep property
105
+ access). Everything else is a live handle owned by the domain —
106
+ attribute access is property lookup (methods arrive bound), calls are
107
+ calls, `str()` is `String()`. Non-finite floats cross tagged, never
108
+ silently nulled.
106
109
  - **The domain rules hold**: handles never cross bridges; `stats()` is
107
110
  the handle ledger and `release()` drops one early; `destroy()` is
108
111
  idempotent, every door answers `bridge is closed` after, and the
@@ -161,6 +164,37 @@ memcpy per side, the receiver consumes the spill file, and the sender
161
164
  sweeps it if the child died first. Nothing strays, and nothing is
162
165
  silently truncated: an unsupported type is an articulate error.
163
166
 
167
+ ## Common issues
168
+
169
+ **`Cannot find module '…'`.** `require` / `import` resolve from the
170
+ **Python process cwd** (`node_modules` next to your program), not from
171
+ the site-packages install of this wheel. `npm install lodash` in the
172
+ project directory is the fix; or pass `create(node='/path/to/node')` /
173
+ `CC_NODE_BIN` when the wrong Node is on `PATH`. Missing-module errors
174
+ name that cwd rule.
175
+
176
+ **Empty `{}` is a live handle.** `js.eval('({})')` stays a `JsHandle`
177
+ so later property use matches Node. Non-empty plain objects still cross
178
+ as Python `dict`s (data returns). Same-domain handles chain
179
+ (`h.update(…).digest(…)`); foreign-domain handles do not.
180
+
181
+ **Thenables are awaited in the child.** Promise-based npm APIs need no
182
+ `async`/`await` on the Python side — the call blocks until settle (or
183
+ raises `JsError` on reject). That is the opposite of
184
+ `concurrent-c-python`'s isolated surface, where every call is already a
185
+ JS Promise you must await.
186
+
187
+ **Wire cost vs tiny work.** Round trip is ~100µs class; a one-line JS
188
+ helper on three numbers loses to pure Python. Prefer Python (or a native
189
+ CC module) for small/hot work; use the bridge when Node/npm owns the
190
+ kernel (crypto, parsers, large buffers via shm). Multi-core: fan across
191
+ `create()` handles from threads —
192
+ `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3 domains here).
193
+
194
+ **Crash isolation, not a sandbox.** The child inherits your environment
195
+ and privileges — do not evaluate untrusted JavaScript. `destroy()` is
196
+ cooperative; CPU-bound JS is not preemptible (wait or kill + new domain).
197
+
164
198
  ## Choosing the node
165
199
 
166
200
  Same ambient-first rule as the rest of the family: the domain runs
@@ -183,16 +217,37 @@ speaks, from CC.
183
217
 
184
218
  ## Publishing
185
219
 
186
- From the Concurrent-C repo root (packs this wheel and the npm sibling):
220
+ **Preferred — PyPI Trusted Publishing (OIDC) from CI** (no API token):
221
+
222
+ 1. One-time on
223
+ [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
224
+ - Owner `sreekotay`, repository `concurrent-c`
225
+ - Workflow name `publish-cc-node.yml`
226
+ - Environment name `pypi`
227
+ 2. Create a GitHub Environment named `pypi` (optional reviewers encouraged).
228
+ 3. Bump `version` in `pyproject.toml`, commit + push, then:
187
229
 
188
230
  ```
189
- ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
190
- ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
231
+ gh workflow run publish-cc-node.yml
232
+ # or: git tag cc-node-vX.Y.Z && git push --tags
233
+ ```
234
+
235
+ Local pack / npm sibling (PyPI defaults to CI OIDC after npm):
236
+
237
+ ```
238
+ ./scripts/publish_bridges.sh --publish --minor
239
+ # npm is live; then commit+push bumps and:
240
+ gh workflow run publish-cc-node.yml
241
+
242
+ # local twine fallback:
243
+ ./scripts/publish_bridges.sh --publish --minor --pypi-twine
191
244
  ```
192
245
 
193
246
  A worked tour (builtin Node modules, chains, callbacks, thenables,
194
247
  buffers — no npm install needed):
195
- `python -m cc_node.examples.use_node`.
248
+ `python -m cc_node.examples.use_node`. Wire RTT / shm:
249
+ `python -m cc_node.examples.bench_wire`. Multi-core domains (threads):
250
+ `python -m cc_node.benchmarks.multi_domain`.
196
251
 
197
252
  Adversarial multi-child storm (escaped closures, cooperative
198
253
  fanout-destroy, abort inject, handle-leak / RSS soaks):
@@ -51,6 +51,7 @@ event-loop or GIL between them.
51
51
  pip install concurrent-c-node # needs node on PATH (or point at one)
52
52
  python -m cc_node.examples.use_node
53
53
  python -m cc_node.examples.bench_wire
54
+ python -m cc_node.benchmarks.multi_domain # N children, thread-fanned
54
55
  ```
55
56
 
56
57
  ## Measured (separate-process wire)
@@ -88,10 +89,12 @@ Examples ship in the wheel. Same domain model and materialization
88
89
  rules as the npm sibling, pointed the other way:
89
90
 
90
91
  - **Values**: plain data (finite numbers, strings, booleans, `None`,
91
- lists/dicts of the same) crosses by value; everything else is a live
92
- handle owned by the domain — attribute access is property lookup
93
- (methods arrive bound), calls are calls, `str()` is `String()`.
94
- Non-finite floats cross tagged, never silently nulled.
92
+ lists and non-empty dicts/objects of the same) crosses by value; an
93
+ empty `{}` stays a live handle (so bags you mint in JS keep property
94
+ access). Everything else is a live handle owned by the domain —
95
+ attribute access is property lookup (methods arrive bound), calls are
96
+ calls, `str()` is `String()`. Non-finite floats cross tagged, never
97
+ silently nulled.
95
98
  - **The domain rules hold**: handles never cross bridges; `stats()` is
96
99
  the handle ledger and `release()` drops one early; `destroy()` is
97
100
  idempotent, every door answers `bridge is closed` after, and the
@@ -150,6 +153,37 @@ memcpy per side, the receiver consumes the spill file, and the sender
150
153
  sweeps it if the child died first. Nothing strays, and nothing is
151
154
  silently truncated: an unsupported type is an articulate error.
152
155
 
156
+ ## Common issues
157
+
158
+ **`Cannot find module '…'`.** `require` / `import` resolve from the
159
+ **Python process cwd** (`node_modules` next to your program), not from
160
+ the site-packages install of this wheel. `npm install lodash` in the
161
+ project directory is the fix; or pass `create(node='/path/to/node')` /
162
+ `CC_NODE_BIN` when the wrong Node is on `PATH`. Missing-module errors
163
+ name that cwd rule.
164
+
165
+ **Empty `{}` is a live handle.** `js.eval('({})')` stays a `JsHandle`
166
+ so later property use matches Node. Non-empty plain objects still cross
167
+ as Python `dict`s (data returns). Same-domain handles chain
168
+ (`h.update(…).digest(…)`); foreign-domain handles do not.
169
+
170
+ **Thenables are awaited in the child.** Promise-based npm APIs need no
171
+ `async`/`await` on the Python side — the call blocks until settle (or
172
+ raises `JsError` on reject). That is the opposite of
173
+ `concurrent-c-python`'s isolated surface, where every call is already a
174
+ JS Promise you must await.
175
+
176
+ **Wire cost vs tiny work.** Round trip is ~100µs class; a one-line JS
177
+ helper on three numbers loses to pure Python. Prefer Python (or a native
178
+ CC module) for small/hot work; use the bridge when Node/npm owns the
179
+ kernel (crypto, parsers, large buffers via shm). Multi-core: fan across
180
+ `create()` handles from threads —
181
+ `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3 domains here).
182
+
183
+ **Crash isolation, not a sandbox.** The child inherits your environment
184
+ and privileges — do not evaluate untrusted JavaScript. `destroy()` is
185
+ cooperative; CPU-bound JS is not preemptible (wait or kill + new domain).
186
+
153
187
  ## Choosing the node
154
188
 
155
189
  Same ambient-first rule as the rest of the family: the domain runs
@@ -172,16 +206,37 @@ speaks, from CC.
172
206
 
173
207
  ## Publishing
174
208
 
175
- From the Concurrent-C repo root (packs this wheel and the npm sibling):
209
+ **Preferred — PyPI Trusted Publishing (OIDC) from CI** (no API token):
210
+
211
+ 1. One-time on
212
+ [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
213
+ - Owner `sreekotay`, repository `concurrent-c`
214
+ - Workflow name `publish-cc-node.yml`
215
+ - Environment name `pypi`
216
+ 2. Create a GitHub Environment named `pypi` (optional reviewers encouraged).
217
+ 3. Bump `version` in `pyproject.toml`, commit + push, then:
176
218
 
177
219
  ```
178
- ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
179
- ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
220
+ gh workflow run publish-cc-node.yml
221
+ # or: git tag cc-node-vX.Y.Z && git push --tags
222
+ ```
223
+
224
+ Local pack / npm sibling (PyPI defaults to CI OIDC after npm):
225
+
226
+ ```
227
+ ./scripts/publish_bridges.sh --publish --minor
228
+ # npm is live; then commit+push bumps and:
229
+ gh workflow run publish-cc-node.yml
230
+
231
+ # local twine fallback:
232
+ ./scripts/publish_bridges.sh --publish --minor --pypi-twine
180
233
  ```
181
234
 
182
235
  A worked tour (builtin Node modules, chains, callbacks, thenables,
183
236
  buffers — no npm install needed):
184
- `python -m cc_node.examples.use_node`.
237
+ `python -m cc_node.examples.use_node`. Wire RTT / shm:
238
+ `python -m cc_node.examples.bench_wire`. Multi-core domains (threads):
239
+ `python -m cc_node.benchmarks.multi_domain`.
185
240
 
186
241
  Adversarial multi-child storm (escaped closures, cooperative
187
242
  fanout-destroy, abort inject, handle-leak / RSS soaks):
@@ -9,10 +9,11 @@
9
9
  The mirror of the cc-python bridge, same rules pointed the other way:
10
10
  attribute access is property lookup (methods arrive bound), a call is a
11
11
  call, plain data (finite numbers, strings, booleans, None, lists and
12
- dicts of the same) crosses by value and everything else stays a live
13
- handle owned by the domain. A thenable result is awaited in the child
14
- before the reply, so async package APIs work with nothing extra. A
15
- Python callable passed as an argument becomes a JS function; its
12
+ non-empty dicts of the same) crosses by value; an empty dict/object
13
+ stays a live handle (bags keep property access). Everything else stays
14
+ a live handle owned by the domain. A thenable result is awaited in the
15
+ child before the reply, so async package APIs work with nothing extra.
16
+ A Python callable passed as an argument becomes a JS function; its
16
17
  exceptions cross back as JS errors and vice versa, messages intact.
17
18
  Handles never cross domains; every door after destroy() answers
18
19
  articulately; destroy is idempotent and `with cc_node.create() as js:`
@@ -29,7 +30,7 @@ import subprocess
29
30
  import tempfile
30
31
 
31
32
  __all__ = ["create", "JsError", "JsHandle", "__version__"]
32
- __version__ = "0.4.0"
33
+ __version__ = "0.13.0"
33
34
 
34
35
  # Typed buffers cross as typed arrays; big ones spill through shared
35
36
  # memory (tmpfs where available) — one memcpy per side, receiver
@@ -304,7 +305,12 @@ class Bridge:
304
305
  enc = self._encode_buffer(a)
305
306
  if enc is not None:
306
307
  return enc
307
- raise JsError("cc-node: unsupported argument type: %r" % type(a))
308
+ raise JsError(
309
+ "cc-node: unsupported argument type: %r "
310
+ "(numbers, str, bool, None, list/tuple, dict with str keys, "
311
+ "bytes/array.array/1-D numpy, callables, or this domain's "
312
+ "JsHandle — same-domain handles chain)"
313
+ % (type(a),))
308
314
 
309
315
  def _encode_buffer(self, a):
310
316
  # Typed buffers cross as typed arrays: bytes/array.array/1-D
@@ -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()
@@ -9,13 +9,15 @@
9
9
  * numbering) says which via CC_WIRE_IN / CC_WIRE_OUT. Handles are
10
10
  * integers into one table; results follow the bridge materialization
11
11
  * rule — plain data (finite numbers, strings, booleans, null, arrays
12
- * and plain objects of the same) crosses as a value, everything else
13
- * stays a handle. A thenable result is awaited before the reply, so
14
- * async package APIs need nothing special from the Python side. A
15
- * Python callable crosses as {$f: id}; invoking it sends a nested `cb`
16
- * request and BLOCKS on a synchronous read for the answer — legal
17
- * because the protocol is strictly alternating, so nothing else can be
18
- * in flight. EOF on the request fd is the host vanishing: exit.
12
+ * and non-empty plain objects of the same) crosses as a value; an
13
+ * empty plain object stays a handle (so `eval('({})')` remains a live
14
+ * JS object — materializing it to Python `{}` dropped property access).
15
+ * A thenable result is awaited before the reply, so async package APIs
16
+ * need nothing special from the Python side. A Python callable crosses
17
+ * as {$f: id}; invoking it sends a nested `cb` request and BLOCKS on a
18
+ * synchronous read for the answer — legal because the protocol is
19
+ * strictly alternating, so nothing else can be in flight. EOF on the
20
+ * request fd is the host vanishing: exit.
19
21
  *
20
22
  * cc/include/ccc/script/js.cch embeds this file verbatim (the CC
21
23
  * isolated tier speaks the same wire); js_iso_smoke pins the two
@@ -151,6 +153,14 @@ function encodeResult(v) {
151
153
  return encodeBuffer(kind,
152
154
  Buffer.from(v.buffer, v.byteOffset, v.byteLength));
153
155
  if (Buffer.isBuffer(v)) return encodeBuffer('u8', v);
156
+ // Empty plain {} / Object.create(null) stay handles — callers mint
157
+ // bags for later property use. Non-empty plain objects still cross
158
+ // by value (data returns).
159
+ const proto = Object.getPrototypeOf(v);
160
+ if (!Array.isArray(v) &&
161
+ (proto === Object.prototype || proto === null) &&
162
+ Object.keys(v).length === 0)
163
+ return { h: put(v) };
154
164
  }
155
165
  if (v === null || isPlain(v, 0)) return { v };
156
166
  return { h: put(v) };
@@ -215,8 +225,34 @@ async function main() {
215
225
  try {
216
226
  let r;
217
227
  switch (req.op) {
218
- case 'require': r = requireCwd(req.name); break;
219
- case 'import': r = await import(req.name); break;
228
+ case 'require':
229
+ try {
230
+ r = requireCwd(req.name);
231
+ } catch (e) {
232
+ if (e && e.code === 'MODULE_NOT_FOUND') {
233
+ throw new Error(
234
+ String(e.message) +
235
+ ' — npm install into this working directory\'s ' +
236
+ 'node_modules (require resolves from cwd), or pass ' +
237
+ 'create(node=...) for a different Node');
238
+ }
239
+ throw e;
240
+ }
241
+ break;
242
+ case 'import':
243
+ try {
244
+ r = await import(req.name);
245
+ } catch (e) {
246
+ const msg = String(e && e.message !== undefined ? e.message : e);
247
+ if (/Cannot find module|ERR_MODULE_NOT_FOUND/i.test(msg)) {
248
+ throw new Error(
249
+ msg +
250
+ ' — install the package for this Node (cwd node_modules ' +
251
+ 'or a path import), or pass create(node=...)');
252
+ }
253
+ throw e;
254
+ }
255
+ break;
220
256
  case 'eval': r = (0, eval)(req.src); break;
221
257
  case 'get': {
222
258
  const o = getH(req.h);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.12.0
3
+ Version: 0.15.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
@@ -62,6 +62,7 @@ event-loop or GIL between them.
62
62
  pip install concurrent-c-node # needs node on PATH (or point at one)
63
63
  python -m cc_node.examples.use_node
64
64
  python -m cc_node.examples.bench_wire
65
+ python -m cc_node.benchmarks.multi_domain # N children, thread-fanned
65
66
  ```
66
67
 
67
68
  ## Measured (separate-process wire)
@@ -99,10 +100,12 @@ Examples ship in the wheel. Same domain model and materialization
99
100
  rules as the npm sibling, pointed the other way:
100
101
 
101
102
  - **Values**: plain data (finite numbers, strings, booleans, `None`,
102
- lists/dicts of the same) crosses by value; everything else is a live
103
- handle owned by the domain — attribute access is property lookup
104
- (methods arrive bound), calls are calls, `str()` is `String()`.
105
- Non-finite floats cross tagged, never silently nulled.
103
+ lists and non-empty dicts/objects of the same) crosses by value; an
104
+ empty `{}` stays a live handle (so bags you mint in JS keep property
105
+ access). Everything else is a live handle owned by the domain —
106
+ attribute access is property lookup (methods arrive bound), calls are
107
+ calls, `str()` is `String()`. Non-finite floats cross tagged, never
108
+ silently nulled.
106
109
  - **The domain rules hold**: handles never cross bridges; `stats()` is
107
110
  the handle ledger and `release()` drops one early; `destroy()` is
108
111
  idempotent, every door answers `bridge is closed` after, and the
@@ -161,6 +164,37 @@ memcpy per side, the receiver consumes the spill file, and the sender
161
164
  sweeps it if the child died first. Nothing strays, and nothing is
162
165
  silently truncated: an unsupported type is an articulate error.
163
166
 
167
+ ## Common issues
168
+
169
+ **`Cannot find module '…'`.** `require` / `import` resolve from the
170
+ **Python process cwd** (`node_modules` next to your program), not from
171
+ the site-packages install of this wheel. `npm install lodash` in the
172
+ project directory is the fix; or pass `create(node='/path/to/node')` /
173
+ `CC_NODE_BIN` when the wrong Node is on `PATH`. Missing-module errors
174
+ name that cwd rule.
175
+
176
+ **Empty `{}` is a live handle.** `js.eval('({})')` stays a `JsHandle`
177
+ so later property use matches Node. Non-empty plain objects still cross
178
+ as Python `dict`s (data returns). Same-domain handles chain
179
+ (`h.update(…).digest(…)`); foreign-domain handles do not.
180
+
181
+ **Thenables are awaited in the child.** Promise-based npm APIs need no
182
+ `async`/`await` on the Python side — the call blocks until settle (or
183
+ raises `JsError` on reject). That is the opposite of
184
+ `concurrent-c-python`'s isolated surface, where every call is already a
185
+ JS Promise you must await.
186
+
187
+ **Wire cost vs tiny work.** Round trip is ~100µs class; a one-line JS
188
+ helper on three numbers loses to pure Python. Prefer Python (or a native
189
+ CC module) for small/hot work; use the bridge when Node/npm owns the
190
+ kernel (crypto, parsers, large buffers via shm). Multi-core: fan across
191
+ `create()` handles from threads —
192
+ `python -m cc_node.benchmarks.multi_domain` (~2.8× on 3 domains here).
193
+
194
+ **Crash isolation, not a sandbox.** The child inherits your environment
195
+ and privileges — do not evaluate untrusted JavaScript. `destroy()` is
196
+ cooperative; CPU-bound JS is not preemptible (wait or kill + new domain).
197
+
164
198
  ## Choosing the node
165
199
 
166
200
  Same ambient-first rule as the rest of the family: the domain runs
@@ -183,16 +217,37 @@ speaks, from CC.
183
217
 
184
218
  ## Publishing
185
219
 
186
- From the Concurrent-C repo root (packs this wheel and the npm sibling):
220
+ **Preferred — PyPI Trusted Publishing (OIDC) from CI** (no API token):
221
+
222
+ 1. One-time on
223
+ [Publishing settings](https://pypi.org/manage/project/concurrent-c-node/settings/publishing/):
224
+ - Owner `sreekotay`, repository `concurrent-c`
225
+ - Workflow name `publish-cc-node.yml`
226
+ - Environment name `pypi`
227
+ 2. Create a GitHub Environment named `pypi` (optional reviewers encouraged).
228
+ 3. Bump `version` in `pyproject.toml`, commit + push, then:
187
229
 
188
230
  ```
189
- ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
190
- ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
231
+ gh workflow run publish-cc-node.yml
232
+ # or: git tag cc-node-vX.Y.Z && git push --tags
233
+ ```
234
+
235
+ Local pack / npm sibling (PyPI defaults to CI OIDC after npm):
236
+
237
+ ```
238
+ ./scripts/publish_bridges.sh --publish --minor
239
+ # npm is live; then commit+push bumps and:
240
+ gh workflow run publish-cc-node.yml
241
+
242
+ # local twine fallback:
243
+ ./scripts/publish_bridges.sh --publish --minor --pypi-twine
191
244
  ```
192
245
 
193
246
  A worked tour (builtin Node modules, chains, callbacks, thenables,
194
247
  buffers — no npm install needed):
195
- `python -m cc_node.examples.use_node`.
248
+ `python -m cc_node.examples.use_node`. Wire RTT / shm:
249
+ `python -m cc_node.examples.bench_wire`. Multi-core domains (threads):
250
+ `python -m cc_node.benchmarks.multi_domain`.
196
251
 
197
252
  Adversarial multi-child storm (escaped closures, cooperative
198
253
  fanout-destroy, abort inject, handle-leak / RSS soaks):
@@ -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.12.0"
7
+ version = "0.15.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"]