concurrent-c-node 0.2.0__tar.gz → 0.3.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.2.0
3
+ Version: 0.3.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
@@ -40,9 +40,11 @@ array in **9ms** where the same values as a JSON list take 583ms.
40
40
 
41
41
  ```
42
42
  pip install concurrent-c-node # needs node on PATH (or point at one)
43
+ python -m cc_node.examples.use_node
44
+ python -m cc_node.examples.bench_wire
43
45
  ```
44
46
 
45
- Import stays `import cc_node`. The mirror of
47
+ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
46
48
  [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
47
49
  — same domain model, same materialization rules, pointed the other way:
48
50
 
@@ -117,11 +119,22 @@ And *which packages* it sees is the working directory's
117
119
  Run Python in your project, get your project's packages: `npm install`
118
120
  next to your program is the whole setup.
119
121
 
122
+ ## Publishing
123
+
124
+ From the Concurrent-C repo root (packs this wheel and the npm sibling):
125
+
126
+ ```
127
+ ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
128
+ ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
129
+ ```
130
+
120
131
  ## Measured
121
132
 
122
- From [`examples/bench_wire.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/bench_wire.py)
123
- on a 4-vCPU x86-64 box, node 22 / python 3.11 (dated baselines under
124
- `perf/baselines/` in the repo):
133
+ From `python -m cc_node.examples.bench_wire` (sources under
134
+ [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
135
+ on a 4-vCPU x86-64 box, node 22 / python 3.11
136
+ ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
137
+ catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
125
138
 
126
139
  | what | result |
127
140
  |---|---|
@@ -138,7 +151,12 @@ future work.
138
151
 
139
152
  A worked tour (builtin Node modules, chains, callbacks, thenables,
140
153
  buffers — no npm install needed):
141
- [`examples/use_node.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/use_node.py).
154
+ `python -m cc_node.examples.use_node`.
155
+
156
+ Adversarial multi-child storm (fanout, callback blizzard, shm hail,
157
+ teardown derby): [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge)
158
+ — `./stress/bridge/run.sh` (`CHAOS_SCALE=full` for bigger N; latency demos
159
+ stay in `cc_node/examples/`).
142
160
 
143
161
  And when the hot path is YOUR code rather than an npm package, skip the
144
162
  wire entirely: a page of Concurrent-C (or C) exports as a native module
@@ -29,9 +29,11 @@ array in **9ms** where the same values as a JSON list take 583ms.
29
29
 
30
30
  ```
31
31
  pip install concurrent-c-node # needs node on PATH (or point at one)
32
+ python -m cc_node.examples.use_node
33
+ python -m cc_node.examples.bench_wire
32
34
  ```
33
35
 
34
- Import stays `import cc_node`. The mirror of
36
+ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
35
37
  [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
36
38
  — same domain model, same materialization rules, pointed the other way:
37
39
 
@@ -106,11 +108,22 @@ And *which packages* it sees is the working directory's
106
108
  Run Python in your project, get your project's packages: `npm install`
107
109
  next to your program is the whole setup.
108
110
 
111
+ ## Publishing
112
+
113
+ From the Concurrent-C repo root (packs this wheel and the npm sibling):
114
+
115
+ ```
116
+ ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
117
+ ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
118
+ ```
119
+
109
120
  ## Measured
110
121
 
111
- From [`examples/bench_wire.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/bench_wire.py)
112
- on a 4-vCPU x86-64 box, node 22 / python 3.11 (dated baselines under
113
- `perf/baselines/` in the repo):
122
+ From `python -m cc_node.examples.bench_wire` (sources under
123
+ [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
124
+ on a 4-vCPU x86-64 box, node 22 / python 3.11
125
+ ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
126
+ catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
114
127
 
115
128
  | what | result |
116
129
  |---|---|
@@ -127,7 +140,12 @@ future work.
127
140
 
128
141
  A worked tour (builtin Node modules, chains, callbacks, thenables,
129
142
  buffers — no npm install needed):
130
- [`examples/use_node.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/use_node.py).
143
+ `python -m cc_node.examples.use_node`.
144
+
145
+ Adversarial multi-child storm (fanout, callback blizzard, shm hail,
146
+ teardown derby): [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge)
147
+ — `./stress/bridge/run.sh` (`CHAOS_SCALE=full` for bigger N; latency demos
148
+ stay in `cc_node/examples/`).
131
149
 
132
150
  And when the hot path is YOUR code rather than an npm package, skip the
133
151
  wire entirely: a page of Concurrent-C (or C) exports as a native module
@@ -107,9 +107,11 @@ class JsHandle:
107
107
  " (closed)" if self._d.closed else "")
108
108
 
109
109
  def __del__(self):
110
+ # Never nest a sync wire op from GC into an in-flight _req — that
111
+ # steals the outer reply (e.g. returning a callback arg handle).
110
112
  try:
111
113
  if not self._d.closed:
112
- self._d._req("release", h=self._h)
114
+ self._d._queue_release(self._h)
113
115
  except Exception:
114
116
  pass
115
117
 
@@ -137,6 +139,11 @@ class Bridge:
137
139
  self._cbs = {}
138
140
  self._ncb = 1
139
141
  self._shm_out = []
142
+ self._depth = 0
143
+ self._parked = {}
144
+ self._pending_release = []
145
+ self._close_pending = False
146
+ self._destroy_done = False
140
147
  _live.append(self)
141
148
 
142
149
  # ---- wire ----
@@ -146,6 +153,56 @@ class Bridge:
146
153
  self._p.stdin.write(line.encode("utf-8"))
147
154
  self._p.stdin.flush()
148
155
 
156
+ def _queue_release(self, hid):
157
+ self._pending_release.append(hid)
158
+ if self._depth == 0:
159
+ self._flush_releases()
160
+
161
+ def _flush_releases(self):
162
+ while self._pending_release and not self.closed:
163
+ hid = self._pending_release.pop(0)
164
+ try:
165
+ self._req("release", h=hid)
166
+ except Exception:
167
+ pass
168
+
169
+ def _flush_shm(self):
170
+ # The child unlinks spill files as it decodes; this sweep only
171
+ # matters when it died first (ENOENT is the normal case).
172
+ for path in self._shm_out:
173
+ try:
174
+ os.unlink(path)
175
+ except OSError:
176
+ pass
177
+ del self._shm_out[:]
178
+
179
+ def _take_reply(self, msg):
180
+ if "e" in msg:
181
+ raise JsError(msg["e"])
182
+ return self._decode_result(msg)
183
+
184
+ def _wait_reply(self, rid):
185
+ parked = self._parked.pop(rid, None)
186
+ if parked is not None:
187
+ return self._take_reply(parked)
188
+ while True:
189
+ line = self._p.stdout.readline()
190
+ if not line:
191
+ self.closed = True
192
+ raise JsError("cc-node: the node child exited")
193
+ msg = json.loads(line)
194
+ if "cb" in msg:
195
+ self._serve_callback(msg)
196
+ continue
197
+ mid = msg.get("id")
198
+ if mid == rid:
199
+ return self._take_reply(msg)
200
+ if mid is not None:
201
+ # Nested _req (GC release, etc.) can overtake; park by id.
202
+ self._parked[mid] = msg
203
+ continue
204
+ raise JsError("cc-node: protocol violation (unexpected reply)")
205
+
149
206
  def _req(self, op, **kw):
150
207
  if self.closed:
151
208
  raise JsError("cc-node: bridge is closed")
@@ -153,35 +210,22 @@ class Bridge:
153
210
  self._nid += 1
154
211
  kw["id"] = rid
155
212
  kw["op"] = op
213
+ self._depth += 1
156
214
  try:
157
- self._send(kw)
158
- except (BrokenPipeError, ValueError):
159
- self.closed = True
160
- raise JsError("cc-node: the node child exited") from None
161
- try:
162
- while True:
163
- line = self._p.stdout.readline()
164
- if not line:
165
- self.closed = True
166
- raise JsError("cc-node: the node child exited")
167
- msg = json.loads(line)
168
- if "cb" in msg:
169
- self._serve_callback(msg)
170
- continue
171
- if msg.get("id") == rid:
172
- if "e" in msg:
173
- raise JsError(msg["e"])
174
- return self._decode_result(msg)
175
- raise JsError("cc-node: protocol violation (unexpected reply)")
215
+ try:
216
+ self._send(kw)
217
+ except (BrokenPipeError, ValueError):
218
+ self.closed = True
219
+ raise JsError("cc-node: the node child exited") from None
220
+ return self._wait_reply(rid)
176
221
  finally:
177
- # The child unlinks spill files as it decodes; this sweep only
178
- # matters when it died first (ENOENT is the normal case).
179
- for path in self._shm_out:
180
- try:
181
- os.unlink(path)
182
- except OSError:
183
- pass
184
- del self._shm_out[:]
222
+ self._depth -= 1
223
+ if self._depth == 0:
224
+ self._flush_shm()
225
+ if self._close_pending:
226
+ self._finish_destroy()
227
+ else:
228
+ self._flush_releases()
185
229
 
186
230
  def _serve_callback(self, msg):
187
231
  fn = self._cbs.get(msg["cb"])
@@ -309,13 +353,32 @@ class Bridge:
309
353
  return self._req("stats")
310
354
 
311
355
  def destroy(self):
312
- if self.closed:
356
+ """Idempotent teardown. Nested destroy (e.g. from a Python
357
+ callback while the broker waits on cbr) defers the farewell
358
+ close until the in-flight wire op unwinds — nesting close into
359
+ the cbr slot hangs the child."""
360
+ if self._destroy_done:
361
+ return
362
+ if self._depth > 0:
363
+ self.closed = True
364
+ self._close_pending = True
365
+ return
366
+ self._finish_destroy()
367
+
368
+ def _finish_destroy(self):
369
+ if self._destroy_done:
313
370
  return
371
+ self._destroy_done = True
372
+ self._close_pending = False
373
+ self.closed = True
314
374
  try:
315
- self._req("close")
316
- except JsError:
375
+ if self._p.poll() is None and self._p.stdin \
376
+ and not self._p.stdin.closed:
377
+ rid = self._nid
378
+ self._nid += 1
379
+ self._send({"id": rid, "op": "close"})
380
+ except Exception:
317
381
  pass
318
- self.closed = True
319
382
  try:
320
383
  self._p.stdin.close()
321
384
  except Exception:
@@ -323,7 +386,10 @@ class Bridge:
323
386
  try:
324
387
  self._p.wait(timeout=5)
325
388
  except Exception:
326
- self._p.kill()
389
+ try:
390
+ self._p.kill()
391
+ except Exception:
392
+ pass
327
393
  if self in _live:
328
394
  _live.remove(self)
329
395
 
File without changes
@@ -0,0 +1,47 @@
1
+ """The cc-node wire, measured. RESULT lines are machine-comparable.
2
+
3
+ python -m cc_node.examples.bench_wire
4
+ """
5
+ import array
6
+ import time
7
+
8
+ import cc_node
9
+
10
+ t0 = time.perf_counter()
11
+ js = cc_node.create()
12
+ js.eval("1")
13
+ print("RESULT spawn_ms %d" % round((time.perf_counter() - t0) * 1000))
14
+
15
+ f = js.eval("(x) => x")
16
+ f(1)
17
+ t0 = time.perf_counter()
18
+ for i in range(500):
19
+ f(i)
20
+ print("RESULT rtt_us %d" % round((time.perf_counter() - t0) / 500 * 1e6))
21
+
22
+ g = js.eval("(cb) => cb(21) * 2")
23
+ g(lambda x: x + 1)
24
+ t0 = time.perf_counter()
25
+ for _ in range(200):
26
+ g(lambda x: x + 1)
27
+ print("RESULT callback_roundtrip_us %d"
28
+ % round((time.perf_counter() - t0) / 200 * 1e6))
29
+
30
+ big = array.array("d", [float(i % 97) for i in range(1 << 20)]) # 8MB
31
+ ln = js.eval("(a) => a.length")
32
+ ln(big)
33
+ t0 = time.perf_counter()
34
+ for _ in range(10):
35
+ ln(big)
36
+ print("RESULT bulk_8mb_shm_ms %.1f" % ((time.perf_counter() - t0) / 10 * 1000))
37
+
38
+ biglist = [float(i % 97) for i in range(1 << 20)]
39
+ ln(biglist)
40
+ t0 = time.perf_counter()
41
+ for _ in range(3):
42
+ ln(biglist)
43
+ print("RESULT bulk_8mb_json_list_ms %d"
44
+ % round((time.perf_counter() - t0) / 3 * 1000))
45
+
46
+ js.destroy()
47
+ print("done")
@@ -0,0 +1,32 @@
1
+ """cc-node in one sitting: builtin Node modules (no npm install needed),
2
+ chains, callbacks both ways, and async-for-free.
3
+
4
+ python -m cc_node.examples.use_node
5
+ """
6
+ import cc_node
7
+
8
+ with cc_node.create() as js:
9
+ # Builtin modules: attribute access is property lookup, calls are calls.
10
+ path = js.require("path")
11
+ print("join :", path.join("a", "b", "c.txt"))
12
+
13
+ crypto = js.require("crypto")
14
+ print("sha256 :", crypto.createHash("sha256")
15
+ .update("cc-node").digest("hex")[:16], "...")
16
+
17
+ # eval for a quick lambda; a Python callable crosses as a JS function
18
+ # (JS conventions apply: Array.map passes value, index, array).
19
+ mapped = js.eval("(f) => [1, 2, 3].map(f)")(lambda x, *rest: x * 10)
20
+ print("callback :", mapped)
21
+
22
+ # Async is free: thenables are awaited in the child before the reply.
23
+ fetchish = js.eval("async (x) => { return { doubled: x * 2 } }")
24
+ print("thenable :", fetchish(21))
25
+
26
+ # Typed buffers cross as typed arrays (big ones via shared memory);
27
+ # results come back as numpy arrays when numpy is installed.
28
+ total = js.eval("(a) => a.reduce((s, x) => s + x, 0)")
29
+ import array
30
+ print("buffer sum:", total(array.array("d", [1.5, 2.5, 3.0])))
31
+
32
+ print("handles :", js.stats())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -40,9 +40,11 @@ array in **9ms** where the same values as a JSON list take 583ms.
40
40
 
41
41
  ```
42
42
  pip install concurrent-c-node # needs node on PATH (or point at one)
43
+ python -m cc_node.examples.use_node
44
+ python -m cc_node.examples.bench_wire
43
45
  ```
44
46
 
45
- Import stays `import cc_node`. The mirror of
47
+ Import stays `import cc_node`. Examples ship in the wheel. The mirror of
46
48
  [`concurrent-c-python`](https://github.com/sreekotay/concurrent-c/tree/main/npm/cc-python)
47
49
  — same domain model, same materialization rules, pointed the other way:
48
50
 
@@ -117,11 +119,22 @@ And *which packages* it sees is the working directory's
117
119
  Run Python in your project, get your project's packages: `npm install`
118
120
  next to your program is the whole setup.
119
121
 
122
+ ## Publishing
123
+
124
+ From the Concurrent-C repo root (packs this wheel and the npm sibling):
125
+
126
+ ```
127
+ ./scripts/publish_bridges.sh # → out/pypi/concurrent_c_node-* (+ npm tgz)
128
+ ./scripts/publish_bridges.sh --publish # bump patch, pack, twine + npm publish
129
+ ```
130
+
120
131
  ## Measured
121
132
 
122
- From [`examples/bench_wire.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/bench_wire.py)
123
- on a 4-vCPU x86-64 box, node 22 / python 3.11 (dated baselines under
124
- `perf/baselines/` in the repo):
133
+ From `python -m cc_node.examples.bench_wire` (sources under
134
+ [`cc_node/examples/`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/cc_node/examples/))
135
+ on a 4-vCPU x86-64 box, node 22 / python 3.11
136
+ ([`perf/baselines/cc_node_bridge_py_20260809.txt`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/cc_node_bridge_py_20260809.txt);
137
+ catalog: [`perf/baselines/README.md`](https://github.com/sreekotay/concurrent-c/blob/main/perf/baselines/README.md)):
125
138
 
126
139
  | what | result |
127
140
  |---|---|
@@ -138,7 +151,12 @@ future work.
138
151
 
139
152
  A worked tour (builtin Node modules, chains, callbacks, thenables,
140
153
  buffers — no npm install needed):
141
- [`examples/use_node.py`](https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-node/examples/use_node.py).
154
+ `python -m cc_node.examples.use_node`.
155
+
156
+ Adversarial multi-child storm (fanout, callback blizzard, shm hail,
157
+ teardown derby): [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge)
158
+ — `./stress/bridge/run.sh` (`CHAOS_SCALE=full` for bigger N; latency demos
159
+ stay in `cc_node/examples/`).
142
160
 
143
161
  And when the hot path is YOUR code rather than an npm package, skip the
144
162
  wire entirely: a page of Concurrent-C (or C) exports as a native module
@@ -2,6 +2,9 @@ README.md
2
2
  pyproject.toml
3
3
  cc_node/__init__.py
4
4
  cc_node/broker.cjs
5
+ cc_node/examples/__init__.py
6
+ cc_node/examples/bench_wire.py
7
+ cc_node/examples/use_node.py
5
8
  concurrent_c_node.egg-info/PKG-INFO
6
9
  concurrent_c_node.egg-info/SOURCES.txt
7
10
  concurrent_c_node.egg-info/dependency_links.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.2.0"
7
+ version = "0.3.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,7 +16,8 @@ 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"]
19
+ packages = ["cc_node", "cc_node.examples"]
20
20
 
21
21
  [tool.setuptools.package-data]
22
22
  cc_node = ["broker.cjs"]
23
+ "cc_node.examples" = ["*.py"]