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.
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/PKG-INFO +64 -9
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/README.md +63 -8
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/cc_node/__init__.py +12 -6
- concurrent_c_node-0.15.0/cc_node/benchmarks/__init__.py +1 -0
- concurrent_c_node-0.15.0/cc_node/benchmarks/multi_domain.py +60 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/cc_node/broker.cjs +45 -9
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/concurrent_c_node.egg-info/PKG-INFO +64 -9
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/concurrent_c_node.egg-info/SOURCES.txt +2 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/pyproject.toml +3 -2
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/cc_node/examples/__init__.py +0 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/cc_node/examples/bench_wire.py +0 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/cc_node/examples/use_node.py +0 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
- {concurrent_c_node-0.12.0 → concurrent_c_node-0.15.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.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/
|
|
103
|
-
handle
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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/
|
|
92
|
-
handle
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
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
|
-
|
|
179
|
-
|
|
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
|
|
13
|
-
handle
|
|
14
|
-
|
|
15
|
-
|
|
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.
|
|
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(
|
|
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
|
|
13
|
-
* stays a handle
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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':
|
|
219
|
-
|
|
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.
|
|
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/
|
|
103
|
-
handle
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
|
|
190
|
-
|
|
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):
|
|
@@ -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.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"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{concurrent_c_node-0.12.0 → concurrent_c_node-0.15.0}/concurrent_c_node.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|