concurrent-c-node 0.22.1__tar.gz → 0.23.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.
Files changed (19) hide show
  1. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/PKG-INFO +25 -14
  2. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/README.md +24 -13
  3. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/__init__.py +75 -20
  4. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/magics.py +7 -7
  5. concurrent_c_node-0.23.0/cc_node/stdio_line.cjs +13 -0
  6. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/PKG-INFO +25 -14
  7. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/SOURCES.txt +1 -0
  8. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/pyproject.toml +2 -2
  9. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/benchmarks/__init__.py +0 -0
  10. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/benchmarks/multi_domain.py +0 -0
  11. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/benchmarks/vs_alts.py +0 -0
  12. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/broker.cjs +0 -0
  13. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/examples/__init__.py +0 -0
  14. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/examples/bench_wire.py +0 -0
  15. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/cc_node/examples/use_node.py +0 -0
  16. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/dependency_links.txt +0 -0
  17. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/requires.txt +0 -0
  18. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/concurrent_c_node.egg-info/top_level.txt +0 -0
  19. {concurrent_c_node-0.22.1 → concurrent_c_node-0.23.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.22.1
3
+ Version: 0.23.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,8 +62,9 @@ measurable wire. N domains = N processes.
62
62
  in the child. No `{ async: true }`.
63
63
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
64
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `%load_ext cc_node` then `%%js` — one kernel domain
66
- (`cc_node.kernel()`), not a child per cell.
65
+ - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
+ child per cell. `%load_ext` still works (idempotent).
67
+ - `cc_node.require('path')` is `get().require('path')`.
67
68
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
68
69
  cwd `require` once.
69
70
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -84,36 +85,45 @@ python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / min
84
85
 
85
86
  Colab and the usual Jupyter kernel are **Python** — this package. Same
86
87
  calling convention as a script: a cell blocks until Node answers;
87
- thenables wait in the child. Magics share **one** domain for the
88
- kernel, not a spawn per cell (~28ms).
88
+ thenables wait in the child. Magics and `get()` share **one** session
89
+ for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
+ lands in the cell (inherited stdio, line-buffered).
89
91
 
90
92
  ```python
91
93
  %pip install concurrent-c-node
92
94
  # if `node` is missing (typical Colab):
93
95
  !apt-get install -y nodejs
94
- %load_ext cc_node
95
96
 
97
+ import cc_node # magics register; no %load_ext
98
+ path = cc_node.require('path')
99
+ path.join('a', 'b') # 'a/b'
100
+ ```
101
+
102
+ ```python
96
103
  %%js
97
- const path = require('path')
98
- path.join('a', 'b') // last expression comes back as Python
104
+ console.log('hi') # shows in the cell
105
+ require('path').join('a', 'b') # last expression comes back as Python
99
106
  ```
100
107
 
101
108
  ```python
102
109
  xs = [1, 2, 3, 4]
103
110
 
104
111
  %%js -b xs -t chunks
105
- xs.map(x => x * 2) // wire types only; no pickle fallback
112
+ xs.map(x => x * 2) # wire types only; no pickle fallback
106
113
  ```
107
114
 
108
115
  | | |
109
116
  |---|---|
110
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
111
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
117
+ | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
+ | `%load_ext cc_node` | same, idempotent |
119
+ | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
112
120
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
113
121
  | `-t chunks` / `--to` | store the result in the notebook namespace |
114
122
  | `%js_stats` | handle-table size (spawns if needed) |
115
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
116
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
123
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
124
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
125
+ | `cc_node.kernel()` | alias of `get()` |
126
+ | `cc_node.require('fs')` | `get().require('fs')` |
117
127
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
118
128
 
119
129
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -184,6 +194,7 @@ vs pymport / ncp / pythonia.
184
194
  `destroy()`; afterwards: `bridge is closed`.
185
195
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
186
196
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
197
+ `get()` / `require()` / `eval()` at module level share that session.
187
198
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
188
199
  in-flight CPU-bound call finishes or you kill the child
189
200
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -282,7 +293,7 @@ Both bridges (npm OIDC + PyPI OIDC):
282
293
 
283
294
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
284
295
  `benchmarks.vs_alts`.
285
- Jupyter: `%load_ext cc_node`.
296
+ Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
286
297
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
287
298
  Own hot path in C/CC → native module (40–90ns) —
288
299
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -47,8 +47,9 @@ measurable wire. N domains = N processes.
47
47
  in the child. No `{ async: true }`.
48
48
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
49
49
  else is a `JsHandle` until `str()` / attrs / a call.
50
- - `%load_ext cc_node` then `%%js` — one kernel domain
51
- (`cc_node.kernel()`), not a child per cell.
50
+ - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
51
+ child per cell. `%load_ext` still works (idempotent).
52
+ - `cc_node.require('path')` is `get().require('path')`.
52
53
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
53
54
  cwd `require` once.
54
55
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -69,36 +70,45 @@ python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / min
69
70
 
70
71
  Colab and the usual Jupyter kernel are **Python** — this package. Same
71
72
  calling convention as a script: a cell blocks until Node answers;
72
- thenables wait in the child. Magics share **one** domain for the
73
- kernel, not a spawn per cell (~28ms).
73
+ thenables wait in the child. Magics and `get()` share **one** session
74
+ for the kernel, not a spawn per cell (~28ms). Child `console.log`
75
+ lands in the cell (inherited stdio, line-buffered).
74
76
 
75
77
  ```python
76
78
  %pip install concurrent-c-node
77
79
  # if `node` is missing (typical Colab):
78
80
  !apt-get install -y nodejs
79
- %load_ext cc_node
80
81
 
82
+ import cc_node # magics register; no %load_ext
83
+ path = cc_node.require('path')
84
+ path.join('a', 'b') # 'a/b'
85
+ ```
86
+
87
+ ```python
81
88
  %%js
82
- const path = require('path')
83
- path.join('a', 'b') // last expression comes back as Python
89
+ console.log('hi') # shows in the cell
90
+ require('path').join('a', 'b') # last expression comes back as Python
84
91
  ```
85
92
 
86
93
  ```python
87
94
  xs = [1, 2, 3, 4]
88
95
 
89
96
  %%js -b xs -t chunks
90
- xs.map(x => x * 2) // wire types only; no pickle fallback
97
+ xs.map(x => x * 2) # wire types only; no pickle fallback
91
98
  ```
92
99
 
93
100
  | | |
94
101
  |---|---|
95
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
96
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
102
+ | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
103
+ | `%load_ext cc_node` | same, idempotent |
104
+ | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
97
105
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
98
106
  | `-t chunks` / `--to` | store the result in the notebook namespace |
99
107
  | `%js_stats` | handle-table size (spawns if needed) |
100
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
101
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
108
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
109
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
110
+ | `cc_node.kernel()` | alias of `get()` |
111
+ | `cc_node.require('fs')` | `get().require('fs')` |
102
112
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
103
113
 
104
114
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -169,6 +179,7 @@ vs pymport / ncp / pythonia.
169
179
  `destroy()`; afterwards: `bridge is closed`.
170
180
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
171
181
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
182
+ `get()` / `require()` / `eval()` at module level share that session.
172
183
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
173
184
  in-flight CPU-bound call finishes or you kill the child
174
185
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -267,7 +278,7 @@ Both bridges (npm OIDC + PyPI OIDC):
267
278
 
268
279
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
269
280
  `benchmarks.vs_alts`.
270
- Jupyter: `%load_ext cc_node`.
281
+ Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
271
282
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
272
283
  Own hot path in C/CC → native module (40–90ns) —
273
284
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -19,8 +19,8 @@ Handles never cross domains; every door after destroy() answers
19
19
  articulately; destroy is idempotent and `with cc_node.create() as js:`
20
20
  scopes it.
21
21
 
22
- Notebooks (Jupyter / Colab): `%load_ext cc_node` then `%%js` — one
23
- kernel-scoped domain (`cc_node.kernel()`), same calling convention.
22
+ Notebooks (Jupyter / Colab): `import cc_node` then `%%js` or
23
+ `cc_node.get()` — one session domain, same calling convention.
24
24
  """
25
25
  import array
26
26
  import atexit
@@ -36,10 +36,12 @@ import sys
36
36
  import tempfile
37
37
 
38
38
  __all__ = [
39
- "create", "kernel", "reset_kernel", "JsError", "JsHandle",
39
+ "create", "get", "reset", "kernel", "reset_kernel",
40
+ "require", "eval", "eval_cell", "import_module", "stats",
41
+ "JsError", "JsHandle",
40
42
  "load_ipython_extension", "unload_ipython_extension", "__version__",
41
43
  ]
42
- __version__ = "0.22.1"
44
+ __version__ = "0.23.0"
43
45
 
44
46
  _NO_NODE = (
45
47
  "cc-node: no node executable (install Node, or set CC_NODE_BIN). "
@@ -161,12 +163,14 @@ class Bridge:
161
163
  # numbers in the child, so the broker learns them from the env.
162
164
  req_r, req_w = os.pipe()
163
165
  resp_r, resp_w = os.pipe()
166
+ preload = os.path.join(os.path.dirname(os.path.abspath(__file__)),
167
+ "stdio_line.cjs")
164
168
  env = dict(os.environ,
165
169
  CC_WIRE_IN=str(req_r), CC_WIRE_OUT=str(resp_w),
166
170
  CC_NODE_SHM_DIR=self._shm_dir)
167
171
  try:
168
172
  self._p = subprocess.Popen(
169
- [node, broker],
173
+ [node, "-r", preload, broker],
170
174
  pass_fds=(req_r, resp_w),
171
175
  env=env,
172
176
  cwd=os.getcwd(),
@@ -645,19 +649,19 @@ def create(node=None):
645
649
  return Bridge(node=node)
646
650
 
647
651
 
648
- _kernel = None
652
+ _session = None
649
653
 
650
654
 
651
- def kernel(node=None):
652
- """Process-wide domain for notebooks. Lazy; magics share it.
653
- Scripts that want a private child still call `create()`."""
654
- global _kernel
655
- b = _kernel
655
+ def get(node=None):
656
+ """Session domain: one child for this process (the Jupyter kernel).
657
+ Lazy. Magics share it. `create()` is still a private child."""
658
+ global _session
659
+ b = _session
656
660
  if b is not None and not b.closed:
657
661
  if node is not None and node != b._node_bin:
658
662
  raise JsError(
659
- "cc-node: kernel() already live with a different node; "
660
- "reset_kernel() / %js_reset first")
663
+ "cc-node: get() already live with a different node; "
664
+ "reset() / %js_reset first")
661
665
  return b
662
666
  b = create(node=node)
663
667
  try:
@@ -665,15 +669,15 @@ def kernel(node=None):
665
669
  except Exception:
666
670
  b.destroy()
667
671
  raise
668
- _kernel = b
672
+ _session = b
669
673
  return b
670
674
 
671
675
 
672
- def reset_kernel():
673
- """Destroy the kernel-scoped domain. Next `kernel()` / `%%js` spawns."""
674
- global _kernel
675
- b = _kernel
676
- _kernel = None
676
+ def reset():
677
+ """Destroy the session domain. Next get() / %%js spawns."""
678
+ global _session
679
+ b = _session
680
+ _session = None
677
681
  if b is not None:
678
682
  try:
679
683
  b.destroy()
@@ -681,10 +685,61 @@ def reset_kernel():
681
685
  pass
682
686
 
683
687
 
688
+ kernel = get
689
+ reset_kernel = reset
690
+
691
+
692
+ def require(name):
693
+ return get().require(name)
694
+
695
+
696
+ def eval(src): # noqa: A001 — session eval, pythonia-shaped
697
+ return get().eval(src)
698
+
699
+
700
+ def eval_cell(src, bindings=None):
701
+ return get().eval_cell(src, bindings)
702
+
703
+
704
+ def import_module(name):
705
+ return get().import_module(name)
706
+
707
+
708
+ def stats():
709
+ return get().stats()
710
+
711
+
684
712
  def load_ipython_extension(ip):
685
713
  from .magics import load_ipython_extension as _load
686
714
  _load(ip)
715
+ if getattr(ip, "_cc_node_lifecycle", False):
716
+ return
717
+ ip._cc_node_lifecycle = True
718
+ orig = getattr(ip, "reset", None)
719
+ if orig is not None:
720
+ def _reset(*args, **kwargs):
721
+ reset()
722
+ return orig(*args, **kwargs)
723
+ ip.reset = _reset
687
724
 
688
725
 
689
726
  def unload_ipython_extension(ip):
690
- reset_kernel()
727
+ reset()
728
+
729
+
730
+ def _boot_ipython():
731
+ # Already in a kernel: IPython is in sys.modules. Do not import it
732
+ # just because the extra is installed — scripts stay spawn-free.
733
+ if "IPython" not in sys.modules:
734
+ return
735
+ try:
736
+ from IPython import get_ipython
737
+ except ImportError:
738
+ return
739
+ ip = get_ipython()
740
+ if ip is None:
741
+ return
742
+ load_ipython_extension(ip)
743
+
744
+
745
+ _boot_ipython()
@@ -2,7 +2,7 @@
2
2
  from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
3
3
  from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring
4
4
 
5
- from . import JsError, kernel, reset_kernel
5
+ from . import JsError, get, reset
6
6
 
7
7
 
8
8
  def _bind_map(names, ns):
@@ -30,7 +30,7 @@ class CcNodeMagics(Magics):
30
30
  @argument("code", nargs="*", help="JS source (line magic)")
31
31
  @line_cell_magic
32
32
  def js(self, line, cell=None):
33
- """Eval JavaScript on the kernel-scoped domain.
33
+ """Eval JavaScript on the session domain (`cc_node.get()`).
34
34
 
35
35
  Line: %js 1 + 1
36
36
  Cell: %%js then a body; last expression is the result.
@@ -45,20 +45,20 @@ class CcNodeMagics(Magics):
45
45
  raise JsError("cc-node: empty %js / %%js")
46
46
  ns = self.shell.user_ns
47
47
  bindings = _bind_map(args.bind, ns)
48
- result = kernel().eval_cell(src, bindings or None)
48
+ result = get().eval_cell(src, bindings or None)
49
49
  if args.to:
50
50
  ns[args.to] = result
51
51
  return result
52
52
 
53
53
  @line_magic
54
54
  def js_reset(self, line):
55
- """Destroy the kernel-scoped Node child. Next %%js spawns again."""
56
- reset_kernel()
55
+ """Destroy the session Node child. Next %%js / get() spawns again."""
56
+ reset()
57
57
 
58
58
  @line_magic
59
59
  def js_stats(self, line):
60
- """Handle-table size of the kernel domain (spawns if needed)."""
61
- return kernel().stats()
60
+ """Handle-table size of the session domain (spawns if needed)."""
61
+ return get().stats()
62
62
 
63
63
 
64
64
  def load_ipython_extension(ip):
@@ -0,0 +1,13 @@
1
+ 'use strict';
2
+ /* Long-lived child: inherited stdout is often a pipe (Jupyter, nohup).
3
+ * Node then block-buffers console.log until the buffer fills or exit.
4
+ * Blocking writes make print land in the cell as it happens. TTY is
5
+ * already blocking — this is a no-op there. */
6
+ function block(s) {
7
+ try {
8
+ if (s && s._handle && typeof s._handle.setBlocking === 'function')
9
+ s._handle.setBlocking(true);
10
+ } catch (e) { /* ignore */ }
11
+ }
12
+ block(process.stdout);
13
+ block(process.stderr);
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: concurrent-c-node
3
- Version: 0.22.1
3
+ Version: 0.23.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,8 +62,9 @@ measurable wire. N domains = N processes.
62
62
  in the child. No `{ async: true }`.
63
63
  - Scalars / `None` materialize; empty `{}` stays a handle; everything
64
64
  else is a `JsHandle` until `str()` / attrs / a call.
65
- - `%load_ext cc_node` then `%%js` — one kernel domain
66
- (`cc_node.kernel()`), not a child per cell.
65
+ - `import cc_node` then `%%js` or `cc_node.get()` — one session, not a
66
+ child per cell. `%load_ext` still works (idempotent).
67
+ - `cc_node.require('path')` is `get().require('path')`.
67
68
  - `eval()` is one RTT, no extra globals. `%%js` / `eval_cell` install
68
69
  cwd `require` once.
69
70
  - `--bind` is `Object.assign(globalThis, …)` of names you name (wire
@@ -84,36 +85,45 @@ python -m cc_node.benchmarks.vs_alts # vs DIY node / pythonmonkey / min
84
85
 
85
86
  Colab and the usual Jupyter kernel are **Python** — this package. Same
86
87
  calling convention as a script: a cell blocks until Node answers;
87
- thenables wait in the child. Magics share **one** domain for the
88
- kernel, not a spawn per cell (~28ms).
88
+ thenables wait in the child. Magics and `get()` share **one** session
89
+ for the kernel, not a spawn per cell (~28ms). Child `console.log`
90
+ lands in the cell (inherited stdio, line-buffered).
89
91
 
90
92
  ```python
91
93
  %pip install concurrent-c-node
92
94
  # if `node` is missing (typical Colab):
93
95
  !apt-get install -y nodejs
94
- %load_ext cc_node
95
96
 
97
+ import cc_node # magics register; no %load_ext
98
+ path = cc_node.require('path')
99
+ path.join('a', 'b') # 'a/b'
100
+ ```
101
+
102
+ ```python
96
103
  %%js
97
- const path = require('path')
98
- path.join('a', 'b') // last expression comes back as Python
104
+ console.log('hi') # shows in the cell
105
+ require('path').join('a', 'b') # last expression comes back as Python
99
106
  ```
100
107
 
101
108
  ```python
102
109
  xs = [1, 2, 3, 4]
103
110
 
104
111
  %%js -b xs -t chunks
105
- xs.map(x => x * 2) // wire types only; no pickle fallback
112
+ xs.map(x => x * 2) # wire types only; no pickle fallback
106
113
  ```
107
114
 
108
115
  | | |
109
116
  |---|---|
110
- | `%load_ext cc_node` | register magics; does **not** spawn until first `%%js` |
111
- | `%js 1+1` / `%%js` | eval on `cc_node.kernel()`; last expression is the result |
117
+ | `import cc_node` | registers magics; does **not** spawn until first `%%js` / `get()` / `require()` |
118
+ | `%load_ext cc_node` | same, idempotent |
119
+ | `%js 1+1` / `%%js` | eval on `cc_node.get()`; last expression is the result |
112
120
  | `-b xs` / `--bind xs,n` | publish those Python names on `globalThis` for the cell |
113
121
  | `-t chunks` / `--to` | store the result in the notebook namespace |
114
122
  | `%js_stats` | handle-table size (spawns if needed) |
115
- | `%js_reset` | `destroy()` the kernel child; next `%%js` spawns again |
116
- | `cc_node.kernel()` | the same domain the magics use; `create()` is still a private child |
123
+ | `%js_reset` / `cc_node.reset()` | `destroy()` the session child; `%reset` does this too |
124
+ | `cc_node.get()` | the session the magics use; `create()` is still a private child |
125
+ | `cc_node.kernel()` | alias of `get()` |
126
+ | `cc_node.require('fs')` | `get().require('fs')` |
117
127
  | `JsHandle` display | cheap `JsHandle #3` — repr does not cross the wire |
118
128
 
119
129
  `eval()` is unchanged (one RTT, no extra globals). `%%js` / `eval_cell`
@@ -184,6 +194,7 @@ vs pymport / ncp / pythonia.
184
194
  `destroy()`; afterwards: `bridge is closed`.
185
195
  - `eval_cell(src, bindings=)` is the notebook door (`%%js`): cwd
186
196
  `require` once, optional `globalThis` binds; `eval()` stays one RTT.
197
+ `get()` / `require()` / `eval()` at module level share that session.
187
198
  - Crash isolation, not a sandbox. `destroy()` is cooperative; an
188
199
  in-flight CPU-bound call finishes or you kill the child
189
200
  ([`bridge_stress.md`](https://github.com/sreekotay/concurrent-c/blob/main/stress/bridge/bridge_stress.md)).
@@ -282,7 +293,7 @@ Both bridges (npm OIDC + PyPI OIDC):
282
293
 
283
294
  Examples: `use_node`, `bench_wire`, `benchmarks.multi_domain`,
284
295
  `benchmarks.vs_alts`.
285
- Jupyter: `%load_ext cc_node`.
296
+ Jupyter: `import cc_node` then `%%js` (or `%load_ext cc_node`).
286
297
  Stress: [`stress/bridge/`](https://github.com/sreekotay/concurrent-c/tree/main/stress/bridge).
287
298
  Own hot path in C/CC → native module (40–90ns) —
288
299
  [JS / Python interop](https://github.com/sreekotay/concurrent-c/blob/main/docs/js-py-modules.md).
@@ -3,6 +3,7 @@ pyproject.toml
3
3
  cc_node/__init__.py
4
4
  cc_node/broker.cjs
5
5
  cc_node/magics.py
6
+ cc_node/stdio_line.cjs
6
7
  cc_node/benchmarks/__init__.py
7
8
  cc_node/benchmarks/multi_domain.py
8
9
  cc_node/benchmarks/vs_alts.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "concurrent-c-node"
7
- version = "0.22.1"
7
+ version = "0.23.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"
@@ -26,6 +26,6 @@ Documentation = "https://github.com/sreekotay/concurrent-c/blob/main/pypi/cc-nod
26
26
  packages = ["cc_node", "cc_node.examples", "cc_node.benchmarks"]
27
27
 
28
28
  [tool.setuptools.package-data]
29
- cc_node = ["broker.cjs"]
29
+ cc_node = ["broker.cjs", "stdio_line.cjs"]
30
30
  "cc_node.examples" = ["*.py"]
31
31
  "cc_node.benchmarks" = ["*.py"]