@damurka/jovian 0.1.0
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.
- package/LICENSE +24 -0
- package/README.md +175 -0
- package/docs/api/README.md +55 -0
- package/docs/api/session.md +143 -0
- package/docs/api/types.md +120 -0
- package/docs/architecture/overview.md +218 -0
- package/docs/cpp-usage.md +60 -0
- package/docs/development.md +105 -0
- package/docs/getting-started.md +127 -0
- package/docs/guides/comms.md +70 -0
- package/docs/guides/environments.md +80 -0
- package/docs/guides/history.md +44 -0
- package/docs/guides/interactive-input.md +48 -0
- package/docs/guides/interrupting.md +45 -0
- package/docs/guides/playground.md +33 -0
- package/docs/guides/sessions-lifecycle.md +70 -0
- package/docs/kernels.md +117 -0
- package/docs/protocol.md +135 -0
- package/docs/releasing.md +73 -0
- package/docs/troubleshooting.md +110 -0
- package/lib/execution/execution-queue.d.ts +20 -0
- package/lib/execution/execution-queue.js +256 -0
- package/lib/handlers/display-handler.d.ts +7 -0
- package/lib/handlers/display-handler.js +10 -0
- package/lib/handlers/error-handler.d.ts +7 -0
- package/lib/handlers/error-handler.js +8 -0
- package/lib/handlers/result-handler.d.ts +7 -0
- package/lib/handlers/result-handler.js +10 -0
- package/lib/handlers/stream-handler.d.ts +7 -0
- package/lib/handlers/stream-handler.js +8 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +5 -0
- package/lib/messaging/message-parser.d.ts +6 -0
- package/lib/messaging/message-parser.js +33 -0
- package/lib/messaging/message-router.d.ts +14 -0
- package/lib/messaging/message-router.js +41 -0
- package/lib/middleware/index.d.ts +5 -0
- package/lib/middleware/index.js +5 -0
- package/lib/middleware/middleware-chain.d.ts +7 -0
- package/lib/middleware/middleware-chain.js +14 -0
- package/lib/middleware/middleware.d.ts +5 -0
- package/lib/middleware/middleware.js +2 -0
- package/lib/middleware/plugins/logging-plugin.d.ts +6 -0
- package/lib/middleware/plugins/logging-plugin.js +9 -0
- package/lib/middleware/plugins/metrics-plugin.d.ts +8 -0
- package/lib/middleware/plugins/metrics-plugin.js +13 -0
- package/lib/session/comm.d.ts +39 -0
- package/lib/session/comm.js +58 -0
- package/lib/session/native-paths.d.ts +48 -0
- package/lib/session/native-paths.js +108 -0
- package/lib/session/session-manager.d.ts +229 -0
- package/lib/session/session-manager.js +842 -0
- package/lib/session/supervisor-client.d.ts +36 -0
- package/lib/session/supervisor-client.js +147 -0
- package/lib/types/engine.d.ts +269 -0
- package/lib/types/engine.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -0
- package/lib/types/messages.d.ts +68 -0
- package/lib/types/messages.js +2 -0
- package/lib/utils/logger.d.ts +12 -0
- package/lib/utils/logger.js +58 -0
- package/lib/utils/network.d.ts +11 -0
- package/lib/utils/network.js +50 -0
- package/package.json +57 -0
- package/packages/hera/DESCRIPTION +29 -0
- package/packages/hera/LICENSE +2 -0
- package/packages/hera/LICENSE.md +21 -0
- package/packages/hera/NAMESPACE +32 -0
- package/packages/hera/NEWS.md +7 -0
- package/packages/hera/R/cell_options.R +13 -0
- package/packages/hera/R/comm.R +228 -0
- package/packages/hera/R/completion.R +54 -0
- package/packages/hera/R/execute.R +199 -0
- package/packages/hera/R/inspect.R +73 -0
- package/packages/hera/R/log.R +14 -0
- package/packages/hera/R/mime_bundle.R +65 -0
- package/packages/hera/R/routines.R +86 -0
- package/packages/hera/R/utils.R +32 -0
- package/packages/hera/R/zzz.R +128 -0
- package/packages/hera/man/Comm.Rd +179 -0
- package/packages/hera/man/CommManager.Rd +215 -0
- package/packages/hera/man/View.Rd +22 -0
- package/packages/hera/man/cell_options.Rd +20 -0
- package/packages/hera/man/clear_output.Rd +23 -0
- package/packages/hera/man/complete.Rd +23 -0
- package/packages/hera/man/display_data.Rd +22 -0
- package/packages/hera/man/is_elara.Rd +18 -0
- package/packages/hera/man/mime_bundle.Rd +25 -0
- package/packages/hera/man/mime_types.Rd +22 -0
- package/packages/hera/man/reexports.Rd +16 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Jovian is published to npm as four packages under the `@damurka` scope (Windows x64, Linux x64 and macOS arm64; see [Adding a platform](#adding-a-platform) for the others):
|
|
4
|
+
|
|
5
|
+
| Package | Contents |
|
|
6
|
+
|---|---|
|
|
7
|
+
| `@damurka/jovian` | The compiled TypeScript library, the `hera` R package, docs. Lists the platform packages below as `optionalDependencies`. This is the one users install. |
|
|
8
|
+
| `@damurka/jovian-win32-x64` | `themisto.exe`, `elara.exe`, `carpo.exe` and the DLLs they need. |
|
|
9
|
+
| `@damurka/jovian-linux-x64`, `-darwin-arm64` | `themisto`, `elara`, `carpo`. |
|
|
10
|
+
|
|
11
|
+
Each platform package declares `os` and `cpu`, so npm installs only the one that matches the machine. At run time the library finds the binaries in that package (see [`lib/session/native-paths.ts`](../lib/session/native-paths.ts): `JOVIAN_NATIVE_DIR`, then the platform package, then a source checkout's `dist/native/Release`). All four packages are published at the **same version**; the main package pins the platform packages to it.
|
|
12
|
+
|
|
13
|
+
The repository's own `package.json` is `"private": true` — it is the development workspace and is never published. Everything published is staged by `scripts/release.mjs` under `dist/release/`.
|
|
14
|
+
|
|
15
|
+
## One-time setup
|
|
16
|
+
|
|
17
|
+
1. **The scope.** `@damurka` must be a user or organization you can publish to on npmjs.com. If you use a different scope, change it in two places — `SCOPE` in `scripts/release.mjs` and `PACKAGE_SCOPE` in `lib/session/native-paths.ts` (a unit test fails if they differ) — plus the names in `README.md`, `docs/`, and the tarball globs in `.github/workflows/release.yml`.
|
|
18
|
+
2. **A token.** On npmjs.com create an *automation* access token (or a granular token with read/write on the `@damurka` packages) and add it to the GitHub repository as the secret **`NPM_TOKEN`** (Settings → Secrets and variables → Actions). If your account enforces 2FA for publishing, the token must be of the automation kind, which bypasses the prompt.
|
|
19
|
+
3. Scoped packages are private by default on npm; the packages carry `publishConfig.access: public`, and the workflow passes `--access public`.
|
|
20
|
+
4. The workflow publishes with **provenance** (`--provenance`, needs the `id-token: write` permission it declares), which requires the GitHub repository to be public.
|
|
21
|
+
|
|
22
|
+
## Cutting a release
|
|
23
|
+
|
|
24
|
+
1. Make sure `main` is green on CI.
|
|
25
|
+
2. Decide the version (semver). Nothing in the repository holds it: it comes from the tag.
|
|
26
|
+
3. Tag and push:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
git tag v0.1.0
|
|
30
|
+
git push origin v0.1.0
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
4. `release.yml` runs. For each platform it builds the native binaries in Release, compiles the library, stages both packages, packs them, and **smoke-tests the packed tarballs**: `scripts/release-smoke.mjs` installs the platform tarball and the main tarball into an empty project (outside the repository, with an empty R library so the bundled `hera` has to install from the package) and starts a real R kernel and a real Python kernel from them.
|
|
34
|
+
5. Only if every platform passed does the `publish` job run: the platform packages first, then the main package (so nothing ever depends on a version that is not there yet).
|
|
35
|
+
|
|
36
|
+
A version with a hyphen (`v0.2.0-rc.1`) is published under the `next` dist-tag, so it does not become what `npm install` picks by default.
|
|
37
|
+
|
|
38
|
+
### Dry run
|
|
39
|
+
|
|
40
|
+
Run the workflow by hand (Actions → Release → Run workflow). It does everything except publish and uploads the tarballs as artifacts, which you can download and `npm install` yourself.
|
|
41
|
+
|
|
42
|
+
### Locally
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run build # native + TypeScript
|
|
46
|
+
node scripts/release.mjs platform --version 0.1.0 # this machine's platform package
|
|
47
|
+
node scripts/release.mjs main --version 0.1.0
|
|
48
|
+
mkdir -p dist/release/tarballs
|
|
49
|
+
(cd dist/release/jovian-win32-x64 && npm pack --pack-destination ../tarballs) # your platform's name
|
|
50
|
+
(cd dist/release/jovian && npm pack --pack-destination ../tarballs)
|
|
51
|
+
node scripts/release-smoke.mjs --dir dist/release/tarballs --python
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Nothing there publishes. Publishing by hand is `npm publish <tarball> --access public`, platform packages first.
|
|
55
|
+
|
|
56
|
+
## Adding a platform
|
|
57
|
+
|
|
58
|
+
Only the platforms that have been built and smoke-tested end to end are published (Windows x64, Ubuntu x64 and macOS arm64, the three the CI matrix covers). To add `linux-arm64` or `darwin-x64`:
|
|
59
|
+
|
|
60
|
+
1. Add the target to `TARGETS` in `scripts/release.mjs` and to `SUPPORTED_PLATFORMS` in `lib/session/native-paths.ts` (a unit test fails if they differ).
|
|
61
|
+
2. Add it to the build matrix in `.github/workflows/release.yml` (`ubuntu-24.04-arm`, `macos-15-intel`); the job `env` already sets `MACOSX_DEPLOYMENT_TARGET: '14.0'` for macOS.
|
|
62
|
+
3. Run the workflow by hand (a dry run) and fix what the smoke test finds before tagging a release.
|
|
63
|
+
|
|
64
|
+
Until a platform is added, its users get `jovian: there are no prebuilt kernels for <os>-<cpu>` and can build from source and set `JOVIAN_NATIVE_DIR`.
|
|
65
|
+
|
|
66
|
+
## Things to know
|
|
67
|
+
|
|
68
|
+
- **hera upgrades.** npm resets file modification times, so the kernel's usual "is the hera source newer than the installed one" check cannot fire for an npm install. The staged `hera` `DESCRIPTION` is stamped with `Config/jovian/release: <version>`, and the kernel reinstalls `hera` when that stamp differs from the installed copy's. Users therefore get the matching `hera` after upgrading the package, once.
|
|
69
|
+
- **Linux binaries and glibc.** They are built on `ubuntu-24.04` (glibc 2.39) and run on any distribution with at least that glibc. Building on an older image would widen compatibility; a `linux-arm64` build would use `ubuntu-24.04-arm`.
|
|
70
|
+
- **macOS.** The build sets `MACOSX_DEPLOYMENT_TARGET=14.0`. The binaries are not code-signed or notarized; binaries installed through npm are not quarantined by Gatekeeper, so this works, but bundling them into a downloaded `.app` would require signing.
|
|
71
|
+
- **Windows.** The DLLs come from vcpkg's `x64-windows` triplet and are copied next to the executables; the binaries link the dynamic Visual C++ runtime (`MSVCP140.dll`, `VCRUNTIME140.dll`, checked with `dumpbin /dependents`), which is **not** bundled: users need the "Microsoft Visual C++ Redistributable" (x64, 2015–2022), and the README's Install section says so. Bundling the runtime DLLs into the package, or linking the runtime statically, would remove that requirement.
|
|
72
|
+
- **Executable bit.** npm does not reliably preserve it; the library `chmod`s the kernels before starting them.
|
|
73
|
+
- **Versions cannot be reused.** npm never lets a published version be republished. If a release fails half-way (say the platform packages published but the main package did not), bump the version and release again.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
Messages below are quoted from the code. **First habit:** a kernel that fails to start reports only a generic error to your code; the real cause is on stderr, prefixed `[elara]` (R) or `[carpo]` (Python) — the supervisor re-prints everything the kernel writes. Read that output first.
|
|
4
|
+
|
|
5
|
+
## Building
|
|
6
|
+
|
|
7
|
+
| Symptom | Cause / fix |
|
|
8
|
+
|---|---|
|
|
9
|
+
| CMake cannot find `nlohmann_json` / `zeromq` / `cppzmq` / `httplib` / `ixwebsocket` | vcpkg's toolchain was not used. Set `VCPKG_ROOT` and re-configure with `-DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake` (`npm run build` does this when `VCPKG_ROOT` is set). |
|
|
10
|
+
| `FindR.cmake requires the following variables to be set: R_COMMAND` | `cmake/FindR.cmake` runs `R RHOME`; put `R` on `PATH` or pass `-DR_COMMAND=<path to R>`. Only R's *headers* are needed to build. |
|
|
11
|
+
| `R.h: No such file or directory` (Debian/Ubuntu) | Install `r-base-dev`. `cmake/FindR.cmake` finds Debian's split headers (`R.h` in `/usr/share/R/include`) via `R CMD config --cppflags`; if your R reports no such directory, add it with `-DCMAKE_CXX_FLAGS=-I<dir containing R.h>`. |
|
|
12
|
+
| `ERR_UNKNOWN_FILE_EXTENSION … ".ts"` running the tests | The Node build has no TypeScript type stripping (Ubuntu's packaged `nodejs`). Use the official build from nodejs.org. |
|
|
13
|
+
| `undefined reference to uuid_generate` (Linux) | Install `uuid-dev` (`sudo apt-get install uuid-dev`). |
|
|
14
|
+
| `LNK1104: cannot open file '…\themisto.exe'` (Windows) | A `themisto` / `elara` / `carpo` process is still running from an earlier run. Kill strays (`tasklist \| findstr /i "themisto elara carpo"`) — or run against a *copy* of `dist/native/Release` via `JOVIAN_NATIVE_DIR`. |
|
|
15
|
+
| C++23 / `/std:c++latest` errors on Windows | Use Visual Studio 2026 (the toolset the repo is built with); older toolsets are untested. |
|
|
16
|
+
| `npm ci` fails on peer dependencies | Use `--legacy-peer-deps` (as CI does): TypeScript 7 conflicts with the `@typescript-eslint` plugin's declared range. |
|
|
17
|
+
| Unit tests fail after editing `lib/` | The tests import `dist/lib`: run `npm run build:lib`. |
|
|
18
|
+
|
|
19
|
+
## Creating a session
|
|
20
|
+
|
|
21
|
+
| Message | Meaning and fix |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `jovian: the kernel binaries were not found. Expected the '@damurka/jovian-<os>-<cpu>' package …` | The platform package was not installed: it is an optional dependency, so `npm install --omit=optional` / `--no-optional` skips it (reinstall without the flag), or a lockfile made on another OS omitted it (`npm install` on this OS). In a source checkout: run `npm run build`, or set `$JOVIAN_NATIVE_DIR`. |
|
|
24
|
+
| `jovian: there are no prebuilt kernels for <os>-<cpu> (supported: …)` | Prebuilt packages exist for `win32-x64`, `linux-x64` and `darwin-arm64` only (Intel Macs and Linux on ARM are not published yet). Elsewhere, [build from source](../README.md#requirements) and set `$JOVIAN_NATIVE_DIR` to `dist/native/Release`. |
|
|
25
|
+
| `version 'GLIBC_2.xx' not found` on Linux | The prebuilt Linux binaries need a glibc at least as new as the one they were built with (Ubuntu 24.04: 2.39). Build from source on the older system. |
|
|
26
|
+
| `Supervisor process exited before it was ready (code 1)` | `themisto` refused to start — typically `[themisto] FATAL: kernel executable not found at … (pass --kernel-exe to override)`: `elara` is missing next to `themisto`. |
|
|
27
|
+
| `no kernel executable is configured for kernelType 'python' …` | `carpo` was not built (or not found beside `themisto`). It is built by default; check `dist/native/Release/carpo[.exe]` and Themisto's `[themisto] NOTE: no Python kernel executable found` line. R sessions are unaffected. |
|
|
28
|
+
| `workingDirectory does not exist or is not a directory: <path>` | Create the directory first, or fix the path. |
|
|
29
|
+
| `Kernel process exited before it could register -- check its stderr output for the actual error.` | The kernel crashed at start-up. See the R / Python sections below and read the `[elara]` / `[carpo]` lines. |
|
|
30
|
+
| `Did not receive kernel configuration within 60s -- the kernel process is still running but never registered. Check its stderr output for what it's doing.` | The kernel started but hung before registering — e.g. R blocked loading packages, a very slow disk, or a security product scanning the process. The stderr shows how far it got. |
|
|
31
|
+
| `WebSocket connection to session <id> failed` / `Session <id> closed before it was ready` | The supervisor accepted the session but the WebSocket could not be established/kept — usually the supervisor died. Look for its exit. |
|
|
32
|
+
|
|
33
|
+
### R
|
|
34
|
+
|
|
35
|
+
| Symptom | Cause / fix |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `Could not load R.dll (…). Is R installed? Checked PATH and R_HOME=… Install R from https://cran.r-project.org, or make sure R_HOME/the R bin directory is configured correctly.` | Windows: `rHome` wrong, or `rPath` (the folder containing `R.dll`, normally `<R_HOME>\bin\x64`) is not right. |
|
|
38
|
+
| `R_HOME is not set -- elara needs a working R installation to run. …` | Linux/macOS: pass `rHome` (find it with `R RHOME`). |
|
|
39
|
+
| `Could not load …/lib/libR.so (…). Is R installed at '…'? If this R was built from source, it needs to have been configured with --enable-R-shlib, or no libR.so exists at all` | Use a distribution/CRAN R, or rebuild R with `--enable-R-shlib`. |
|
|
40
|
+
| No `libR.dylib` on macOS | Point `rHome` at the framework's `Resources` directory (what `R RHOME` prints). |
|
|
41
|
+
|
|
42
|
+
#### `hera` is not installed
|
|
43
|
+
|
|
44
|
+
Symptoms: the kernel log says `WARNING: 'hera' package could not be loaded (status: …)`, then `execute()` results in an error — the reply carries `R evaluation of hera:::hera_call("execute", ...) failed (is the 'hera' package installed?): …`, and completion/inspect fail the same way.
|
|
45
|
+
|
|
46
|
+
On Debian/Ubuntu, `R CMD INSTALL` may fail loading an apt package with `undefined symbol: SETLENGTH` (`rlang`, `vctrs`, `htmltools`, …): those `r-cran-*` packages were built for a different R ABI. Hide the site library and install from CRAN into a private one — `export R_LIBS_SITE=/nonexistent R_LIBS_USER=$HOME/Rlib`, then `install.packages()` the dependencies and run `npm run hera:install` in the same shell, keeping both variables set when you start sessions.
|
|
47
|
+
|
|
48
|
+
Fix: install `hera` and its dependencies into the library the session uses (`npm run hera:install`, i.e. `R CMD INSTALL packages/hera`, after installing `cli`, `evaluate`, `glue`, `IRdisplay`, `jsonlite`, `R6`, `repr`, `rlang` from CRAN), or pass `heraSrcPath: '<repo>/packages/hera'` (needs `remotes`) so Elara installs it. The status names in the warning: `no_source_configured` (not installed and no `heraSrcPath`), `source_not_found` (path does not exist), `remotes_unavailable` (install `remotes`), `install_failed` (see R's error above it — usually a missing dependency or an unwritable library: set `rLibs`).
|
|
49
|
+
|
|
50
|
+
There is **no bundled fallback**: `heraSrcPath` has no default, whatever older comments say.
|
|
51
|
+
|
|
52
|
+
#### R code fails after the kernel started
|
|
53
|
+
|
|
54
|
+
- `there is no package called '…'` — the package is not in `rLibs` / the default library of that R installation.
|
|
55
|
+
|
|
56
|
+
### Python
|
|
57
|
+
|
|
58
|
+
| Message | Cause / fix |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `No python3NN.dll was found directly under python_home ('…'). Is Python installed there? …` | Windows: `pythonHome` must be the install root that directly contains `python3NN.dll`. |
|
|
61
|
+
| `No libpython3.*.so*/.dylib was found under '<home>/lib' (or lib64/, lib/<arch>-linux-gnu/). …` | POSIX: `pythonHome` must be the prefix whose `lib/`, `lib64/` or `lib/<arch>-linux-gnu/` has the shared library (a distribution Python works with `pythonHome: '/usr'`). Some builds (pyenv without `--enable-shared`, some Homebrew/conda layouts) have none. |
|
|
62
|
+
| `Could not load <path> (…)` | The library was found but would not load — a 32/64-bit mismatch or a missing dependency. |
|
|
63
|
+
| Imports from your venv fail | Set `venvPath` **and** keep `pythonHome` at the base install (`sys.base_prefix`). Inside a venv, `sys.prefix` has neither libpython nor the standard library. |
|
|
64
|
+
|
|
65
|
+
## Running code
|
|
66
|
+
|
|
67
|
+
| Symptom | Cause / fix |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `Execution timed out after 30000ms (the kernel was interrupted)` | The default `timeout` fired. The library interrupted the kernel, so it is usable again straight away. Raise the timeout (`execute(code, { timeout })`) or use `0` for none. With `interruptOnTimeout: false` the message has no parenthesis and the kernel is **still running** the code: `interrupt()` or restart it, or new work — and `complete()` / `inspect()` — queue behind it and look hung. |
|
|
70
|
+
| `complete()` / `inspect()` / `kernelInfo()` time out (10 s) | The kernel is busy: it answers requests on the thread that runs code, so they wait until the running execution ends (only `interrupt()` is answered mid-run). Usually a long or timed-out execution left running (`interruptOnTimeout: false`, or a client that gave up without interrupting). `interrupt()` it. |
|
|
71
|
+
| Output of one long R expression (a `for` loop of `print()`s) only appears when it finishes | An older `hera` is installed. Live streaming inside a single top-level expression needs `hera` >= 0.6.0.9001: run `npm run hera:install` (see [Environments](guides/environments.md#the-hera-package-required)). |
|
|
72
|
+
| `Queue cleared` | The session was stopped, restarted, killed or crashed while your `execute()` was queued or running. |
|
|
73
|
+
| `Execution queue is full` | More than `queueSize` (100) queued calls. |
|
|
74
|
+
| `Execution aborted: an earlier execution failed with stopOnError` | Expected: a previous call with `stopOnError` failed (`result.aborted === true`). |
|
|
75
|
+
| `This execution didn't allow interactive input (allow_stdin was false) …` | Pass `{ allowStdin: true }` and answer `'input_request'` — see [Interactive input](guides/interactive-input.md). |
|
|
76
|
+
| The process crashes with `Unhandled 'error' event` | You did not attach `session.on('error', …)`. Node throws for `'error'` without a listener. |
|
|
77
|
+
| A script never exits | Call `await manager.stopAll()` — the supervisor's pipes keep the event loop alive. |
|
|
78
|
+
| `execute()` never resolves while a Shiny app runs | `shiny::runApp()` blocks the kernel; use `createShiny()` (which passes `timeout: 0`) and `interrupt()` to stop it. |
|
|
79
|
+
| `interrupt()` returned `false` | The kernel did not answer within 5 s: it is blocked waiting for `input()`, stuck in native code, or dead. See [Interrupting](guides/interrupting.md#limits). |
|
|
80
|
+
| A request (`complete`, `kernelInfo`, …) times out after 10 s | The kernel is busy running code — requests other than interrupt wait for the running execution. |
|
|
81
|
+
| `Timed out waiting for a <x>_reply after <n>ms` | Same, or the kernel is hung. |
|
|
82
|
+
| Comm messages seem lost | Attach the `'comm'` listener before running the R code that opens the comm; register the target before `new_comm()` (an unregistered target yields `NULL`). Comms only exist in R sessions. |
|
|
83
|
+
|
|
84
|
+
## Crashed and unresponsive sessions
|
|
85
|
+
|
|
86
|
+
| You see | Meaning |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `'exit'` with `kernel process exited unexpectedly (process exited with code 0xc0000005 (STATUS_ACCESS_VIOLATION -- a native crash, e.g. in a compiled R package))` | The kernel process died. Common codes are decoded (`STATUS_STACK_OVERFLOW`, `STATUS_STACK_BUFFER_OVERRUN`). `await session.restart()` recovers the session under the same id. |
|
|
89
|
+
| `'exit'` with `heartbeat gave up waiting for a response (…)` | The process is alive but stopped answering pings for about a minute — deadlocked or stuck in a native call. Restart it. |
|
|
90
|
+
| `session.status().heartbeat` shows `misses > 0` (the playground's HEARTBEAT readout says "no reply") | The kernel process is still there but is not answering pings — it is stuck (deadlocked, or inside a native call that never returns), not merely busy: a busy kernel keeps answering from its heartbeat thread. If it does not recover, the heartbeat gives up after about a minute and you get the `'exit'` event above; restart it earlier if you can't wait. |
|
|
91
|
+
| `'exit'` with `kernel process exited unexpectedly` while the heartbeat looked healthy a moment before | The process died between pings; process exit is detected by the OS process handle within milliseconds and does not wait for the heartbeat. `heartbeat` is a liveness hint, the exit event is authoritative. |
|
|
92
|
+
| `'exit'` with `WebSocket connection to the supervisor closed unexpectedly` | The supervisor process is gone; create a new `SessionManager`. |
|
|
93
|
+
| `Cannot restart session <id>: it was already stopped` | `stop()` is final. Create a new session. |
|
|
94
|
+
| Orphaned `elara`/`carpo` processes after a hard kill of Node | Kernels normally die with the supervisor (a Windows job object kills them when it exits); a supervisor that itself survived (killed test runner) keeps them. End the `themisto` process. |
|
|
95
|
+
|
|
96
|
+
## Interrupt does nothing
|
|
97
|
+
|
|
98
|
+
- Idle kernel: nothing to interrupt (that is normal; `interrupt()` still returns `true`).
|
|
99
|
+
- Long native call (a C extension, a blocking read): interrupted only when it returns to the interpreter.
|
|
100
|
+
- Waiting on `input()`: answer it first.
|
|
101
|
+
- Linux/macOS: the interrupt path there (`pthread_kill`) is newer than the Windows one; if a Python `time.sleep()` does not wake on your platform, please report it with the platform and Python version.
|
|
102
|
+
|
|
103
|
+
## Playground
|
|
104
|
+
|
|
105
|
+
| Symptom | Fix |
|
|
106
|
+
|---|---|
|
|
107
|
+
| `next: not found` / dependencies missing | `npm run playground:install` (the playground has its own `node_modules`). |
|
|
108
|
+
| Sessions fail with the supervisor executable missing | Run `npm run build` first; or set `JOVIAN_NATIVE_DIR`. |
|
|
109
|
+
| R/Python not pre-filled | Set `R_HOME` / `PYTHONHOME`, or enter the paths in the *New Kernel Session* dialog. |
|
|
110
|
+
| Port in use | `PLAYGROUND_PORT=4200 npm run playground`. |
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { EventEmitter } from 'events';
|
|
2
|
+
import type { ExecutionOptions, ExecutionResult } from '../types/index.js';
|
|
3
|
+
import type { Logger } from '../utils/logger.js';
|
|
4
|
+
export declare class ExecutionQueue {
|
|
5
|
+
private queue;
|
|
6
|
+
private executing;
|
|
7
|
+
private addon;
|
|
8
|
+
private maxSize;
|
|
9
|
+
private pending;
|
|
10
|
+
private logger?;
|
|
11
|
+
private onTimeout?;
|
|
12
|
+
constructor(addon: any, emitter: EventEmitter, maxSize?: number, logger?: Logger, onTimeout?: (msgId: string) => void);
|
|
13
|
+
execute(code: string, options?: ExecutionOptions): Promise<ExecutionResult>;
|
|
14
|
+
private processNext;
|
|
15
|
+
private handleMessage;
|
|
16
|
+
private abortQueued;
|
|
17
|
+
clear(): void;
|
|
18
|
+
get size(): number;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=execution-queue.d.ts.map
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
const DEFAULT_TIMEOUT_MS = 30000;
|
|
2
|
+
const ABORTED_MESSAGE = 'Execution aborted: an earlier execution failed with stopOnError';
|
|
3
|
+
export class ExecutionQueue {
|
|
4
|
+
queue = [];
|
|
5
|
+
executing = false;
|
|
6
|
+
addon;
|
|
7
|
+
maxSize;
|
|
8
|
+
pending = new Map();
|
|
9
|
+
logger;
|
|
10
|
+
onTimeout;
|
|
11
|
+
// `onTimeout` is called when an execution times out (unless that
|
|
12
|
+
// execution opted out with interruptOnTimeout: false), so the owner can
|
|
13
|
+
// interrupt the kernel -- otherwise the kernel keeps running code nobody
|
|
14
|
+
// is waiting for, and everything queued behind it (and every complete/
|
|
15
|
+
// inspect request) is stuck behind it.
|
|
16
|
+
constructor(addon, emitter, maxSize = 100, logger, onTimeout) {
|
|
17
|
+
this.addon = addon;
|
|
18
|
+
this.onTimeout = onTimeout;
|
|
19
|
+
this.maxSize = maxSize;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
emitter.on('message', (message) => this.handleMessage(message));
|
|
22
|
+
}
|
|
23
|
+
execute(code, options = {}) {
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
if (this.queue.length >= this.maxSize) {
|
|
26
|
+
this.logger?.error(`Execution queue is full (maxSize=${this.maxSize}); rejecting new request`);
|
|
27
|
+
reject(new Error('Execution queue is full'));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
this.logger?.trace(`Queued execution (queue depth: ${this.queue.length + 1})`, { timeout: options.timeout });
|
|
31
|
+
this.queue.push({
|
|
32
|
+
code,
|
|
33
|
+
options,
|
|
34
|
+
resolve,
|
|
35
|
+
reject,
|
|
36
|
+
timestamp: Date.now()
|
|
37
|
+
});
|
|
38
|
+
// Start processing if not already executing
|
|
39
|
+
if (!this.executing) {
|
|
40
|
+
this.processNext();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
processNext() {
|
|
45
|
+
if (this.queue.length === 0) {
|
|
46
|
+
this.executing = false;
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
this.executing = true;
|
|
50
|
+
const item = this.queue.shift();
|
|
51
|
+
const queuedMs = Date.now() - item.timestamp;
|
|
52
|
+
let msgId;
|
|
53
|
+
try {
|
|
54
|
+
msgId = this.addon.execute(item.code, item.options);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
this.logger?.error('Native addon threw while starting execution', { error, queuedMs });
|
|
58
|
+
item.reject(error);
|
|
59
|
+
this.processNext();
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!msgId) {
|
|
63
|
+
this.logger?.error('Native addon did not return a message id for this execution', { queuedMs });
|
|
64
|
+
item.reject(new Error('Native addon did not return a message id for this execution'));
|
|
65
|
+
this.processNext();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.logger?.trace(`Execution ${msgId} started after ${queuedMs}ms in queue`);
|
|
69
|
+
// A timeout of 0 means "no timeout" -- used for long-running calls
|
|
70
|
+
// that intentionally block the R session until something external
|
|
71
|
+
// stops them (e.g. shiny::runApp(), see Session.createShiny in
|
|
72
|
+
// lib/session/session-manager.ts).
|
|
73
|
+
// Note this also means no *further* queued execute() calls will be
|
|
74
|
+
// sent until this one's execute_reply arrives, since R itself is
|
|
75
|
+
// single-threaded and busy running it.
|
|
76
|
+
const timeoutMs = item.options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
77
|
+
const timer = timeoutMs > 0
|
|
78
|
+
? setTimeout(() => {
|
|
79
|
+
this.logger?.error(`Execution ${msgId} timed out after ${timeoutMs}ms`, { code: previewCode(item.code) });
|
|
80
|
+
this.pending.delete(msgId);
|
|
81
|
+
const interrupting = item.options.interruptOnTimeout !== false && this.onTimeout !== undefined;
|
|
82
|
+
if (interrupting) {
|
|
83
|
+
try {
|
|
84
|
+
this.onTimeout(msgId);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.logger?.error('Interrupting after a timeout failed', { error });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
item.reject(new Error(`Execution timed out after ${timeoutMs}ms` + (interrupting ? ' (the kernel was interrupted)' : '')));
|
|
91
|
+
this.processNext();
|
|
92
|
+
}, timeoutMs)
|
|
93
|
+
: undefined;
|
|
94
|
+
this.pending.set(msgId, {
|
|
95
|
+
stopOnError: item.options.stopOnError === true,
|
|
96
|
+
output: [],
|
|
97
|
+
timer,
|
|
98
|
+
finish: (result) => {
|
|
99
|
+
clearTimeout(timer);
|
|
100
|
+
this.pending.delete(msgId);
|
|
101
|
+
this.logger?.trace(`Execution ${msgId} finished`, { success: result.success, outputMessages: result.output?.length ?? 0 });
|
|
102
|
+
item.resolve(result);
|
|
103
|
+
this.processNext();
|
|
104
|
+
},
|
|
105
|
+
reject: (error) => {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
this.pending.delete(msgId);
|
|
108
|
+
this.logger?.error(`Execution ${msgId} rejected`, { error });
|
|
109
|
+
item.reject(error);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
handleMessage(message) {
|
|
114
|
+
const pending = this.pending.get(message.parentMsgId);
|
|
115
|
+
if (!pending) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
switch (message.msgType) {
|
|
119
|
+
case 'input_request':
|
|
120
|
+
// The kernel is now genuinely blocked waiting on a human to
|
|
121
|
+
// answer a prompt (see Session's own 'input_request' event/
|
|
122
|
+
// sendInputReply()) -- there's no way to know how long that
|
|
123
|
+
// will take, so the fixed execution timeout no longer
|
|
124
|
+
// applies once this arrives. Without this, an execute()
|
|
125
|
+
// with allowStdin: true could time out from nothing more
|
|
126
|
+
// than a user taking longer than DEFAULT_TIMEOUT_MS to
|
|
127
|
+
// answer a prompt, even though the kernel is behaving
|
|
128
|
+
// completely normally and will resume the moment it gets a
|
|
129
|
+
// reply.
|
|
130
|
+
if (pending.timer) {
|
|
131
|
+
clearTimeout(pending.timer);
|
|
132
|
+
pending.timer = undefined;
|
|
133
|
+
}
|
|
134
|
+
break;
|
|
135
|
+
case 'stream':
|
|
136
|
+
case 'execute_result':
|
|
137
|
+
case 'display_data':
|
|
138
|
+
case 'update_display_data':
|
|
139
|
+
case 'clear_output':
|
|
140
|
+
pending.output.push(message);
|
|
141
|
+
break;
|
|
142
|
+
case 'error':
|
|
143
|
+
pending.output.push(message);
|
|
144
|
+
this.logger?.error(`Execution ${message.parentMsgId} reported an R error`, { evalue: message.content?.evalue });
|
|
145
|
+
if (pending.stopOnError) {
|
|
146
|
+
this.abortQueued();
|
|
147
|
+
}
|
|
148
|
+
pending.finish({
|
|
149
|
+
success: false,
|
|
150
|
+
status: 'error',
|
|
151
|
+
output: pending.output,
|
|
152
|
+
error: new Error(message.content?.evalue ?? 'R execution error')
|
|
153
|
+
});
|
|
154
|
+
break;
|
|
155
|
+
case 'execute_reply': {
|
|
156
|
+
const executionCount = message.content?.execution_count;
|
|
157
|
+
const replyStatus = message.content?.status;
|
|
158
|
+
const finishNow = () => {
|
|
159
|
+
if (replyStatus !== 'ok' && pending.stopOnError) {
|
|
160
|
+
this.abortQueued();
|
|
161
|
+
}
|
|
162
|
+
const result = {
|
|
163
|
+
success: replyStatus === 'ok',
|
|
164
|
+
output: pending.output,
|
|
165
|
+
executionCount
|
|
166
|
+
};
|
|
167
|
+
if (replyStatus)
|
|
168
|
+
result.status = replyStatus;
|
|
169
|
+
if (replyStatus === 'aborted') {
|
|
170
|
+
result.aborted = true;
|
|
171
|
+
result.error = new Error(ABORTED_MESSAGE);
|
|
172
|
+
}
|
|
173
|
+
const userExpressions = message.content?.user_expressions;
|
|
174
|
+
if (userExpressions && Object.keys(userExpressions).length > 0) {
|
|
175
|
+
result.userExpressions = userExpressions;
|
|
176
|
+
}
|
|
177
|
+
pending.finish(result);
|
|
178
|
+
};
|
|
179
|
+
// iopub (stream/execute_result/display_data) and shell
|
|
180
|
+
// (execute_reply) are separate ZMQ channels/sockets with no
|
|
181
|
+
// cross-channel delivery-order guarantee -- the kernel
|
|
182
|
+
// publishes iopub content before sending the shell reply
|
|
183
|
+
// (confirmed directly: RInterpreter/PyInterpreter's own
|
|
184
|
+
// executeRequestImpl always calls publishExecutionResult()
|
|
185
|
+
// before invoking the reply callback), but nothing enforces
|
|
186
|
+
// that this client *observes* them in that same order once
|
|
187
|
+
// they've gone through themisto's relay. Confirmed as a
|
|
188
|
+
// real, if rare, flake via CI (a passing execute_reply
|
|
189
|
+
// resolving with empty output, the execute_result iopub
|
|
190
|
+
// message arriving microseconds later, too late to matter).
|
|
191
|
+
// Only a short, bounded wait for output that should exist --
|
|
192
|
+
// an actually-empty-output execution (e.g. a bare
|
|
193
|
+
// assignment) still resolves immediately, since this only
|
|
194
|
+
// triggers on the narrow "ok but nothing collected yet"
|
|
195
|
+
// case, not on every execution.
|
|
196
|
+
if (pending.output.length === 0 && message.content?.status === 'ok') {
|
|
197
|
+
setTimeout(() => {
|
|
198
|
+
if (this.pending.has(message.parentMsgId)) {
|
|
199
|
+
finishNow();
|
|
200
|
+
}
|
|
201
|
+
}, 50);
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
finishNow();
|
|
205
|
+
}
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
default:
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
// stopOnError: an execution that failed takes everything still waiting
|
|
213
|
+
// behind it down with it, unrun -- the client-side half of Jupyter's
|
|
214
|
+
// stop_on_error (the kernel does the same to requests already queued on
|
|
215
|
+
// ITS side, but this queue is single-flight, so anything behind the
|
|
216
|
+
// failure is still here, never sent).
|
|
217
|
+
abortQueued() {
|
|
218
|
+
if (this.queue.length === 0) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
this.logger?.warn(`Aborting ${this.queue.length} queued execution(s) after a failure (stopOnError)`);
|
|
222
|
+
const aborted = this.queue;
|
|
223
|
+
this.queue = [];
|
|
224
|
+
for (const item of aborted) {
|
|
225
|
+
item.resolve({
|
|
226
|
+
success: false,
|
|
227
|
+
status: 'aborted',
|
|
228
|
+
aborted: true,
|
|
229
|
+
output: [],
|
|
230
|
+
error: new Error(ABORTED_MESSAGE)
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
clear() {
|
|
235
|
+
if (this.queue.length > 0 || this.pending.size > 0) {
|
|
236
|
+
this.logger?.warn(`Clearing execution queue (${this.queue.length} queued, ${this.pending.size} in flight)`);
|
|
237
|
+
}
|
|
238
|
+
this.queue.forEach(item => {
|
|
239
|
+
item.reject(new Error('Queue cleared'));
|
|
240
|
+
});
|
|
241
|
+
this.queue = [];
|
|
242
|
+
this.executing = false;
|
|
243
|
+
// Reject in-flight executions too, so callers never end up with a
|
|
244
|
+
// promise that silently hangs forever (or rejects long after the
|
|
245
|
+
// caller stopped caring, once its timeout eventually fires).
|
|
246
|
+
this.pending.forEach(pending => pending.reject(new Error('Queue cleared')));
|
|
247
|
+
}
|
|
248
|
+
get size() {
|
|
249
|
+
return this.queue.length;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function previewCode(code, maxLength = 200) {
|
|
253
|
+
const singleLine = code.replace(/\s+/g, ' ').trim();
|
|
254
|
+
return singleLine.length > maxLength ? `${singleLine.slice(0, maxLength)}…` : singleLine;
|
|
255
|
+
}
|
|
256
|
+
//# sourceMappingURL=execution-queue.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessageHandler } from '../messaging/message-router.js';
|
|
2
|
+
import type { JupyterMessage, DisplayDataContent } from '../types/messages.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
export declare class DisplayHandler implements MessageHandler {
|
|
5
|
+
handle(message: JupyterMessage<DisplayDataContent>, emitter: EventEmitter): void;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=display-handler.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class DisplayHandler {
|
|
2
|
+
handle(message, emitter) {
|
|
3
|
+
if (message.content && message.content.data && message.content.data['text/plain']) {
|
|
4
|
+
const textPlain = message.content.data['text/plain'];
|
|
5
|
+
const resultText = Array.isArray(textPlain) ? textPlain.join('\n') : textPlain;
|
|
6
|
+
emitter.emit('result', resultText);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=display-handler.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessageHandler } from '../messaging/message-router.js';
|
|
2
|
+
import type { JupyterMessage, ErrorContent } from '../types/messages.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
export declare class ErrorHandler implements MessageHandler {
|
|
5
|
+
handle(message: JupyterMessage<ErrorContent>, emitter: EventEmitter): void;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=error-handler.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessageHandler } from '../messaging/message-router.js';
|
|
2
|
+
import type { JupyterMessage, ExecuteResultContent } from '../types/messages.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
export declare class ResultHandler implements MessageHandler {
|
|
5
|
+
handle(message: JupyterMessage<ExecuteResultContent>, emitter: EventEmitter): void;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=result-handler.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class ResultHandler {
|
|
2
|
+
handle(message, emitter) {
|
|
3
|
+
if (message.content && message.content.data && message.content.data['text/plain']) {
|
|
4
|
+
const textPlain = message.content.data['text/plain'];
|
|
5
|
+
const resultText = Array.isArray(textPlain) ? textPlain.join('\n') : textPlain;
|
|
6
|
+
emitter.emit('result', resultText);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=result-handler.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { MessageHandler } from '../messaging/message-router.js';
|
|
2
|
+
import type { JupyterMessage, StreamContent } from '../types/messages.js';
|
|
3
|
+
import { EventEmitter } from 'events';
|
|
4
|
+
export declare class StreamHandler implements MessageHandler {
|
|
5
|
+
handle(message: JupyterMessage<StreamContent>, emitter: EventEmitter): void;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=stream-handler.d.ts.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { Session, SessionManager } from './session/session-manager.js';
|
|
2
|
+
export { Comm } from './session/comm.js';
|
|
3
|
+
export type { ShinyAppHandle as SessionShinyAppHandle } from './session/session-manager.js';
|
|
4
|
+
export * from './types/index.js';
|
|
5
|
+
export * from './middleware/index.js';
|
|
6
|
+
export type { EngineOptions, ExecutionOptions, ExecutionResult, JupyterMessage, MessageTopic } from './types/index.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class MessageParser {
|
|
2
|
+
static parse(raw) {
|
|
3
|
+
let envelope;
|
|
4
|
+
try {
|
|
5
|
+
envelope = JSON.parse(raw);
|
|
6
|
+
}
|
|
7
|
+
catch (error) {
|
|
8
|
+
throw new Error(`Failed to parse message envelope: ${error}`);
|
|
9
|
+
}
|
|
10
|
+
if (!envelope || typeof envelope.msg_type !== 'string') {
|
|
11
|
+
throw new Error(`Invalid message format: ${raw}`);
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
topic: envelope.topic,
|
|
15
|
+
msgType: envelope.msg_type,
|
|
16
|
+
channel: envelope.channel,
|
|
17
|
+
parentMsgId: envelope.parent_msg_id ?? '',
|
|
18
|
+
content: envelope.content,
|
|
19
|
+
timestamp: Date.now(),
|
|
20
|
+
raw
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
static stringify(message) {
|
|
24
|
+
return JSON.stringify({
|
|
25
|
+
channel: message.channel,
|
|
26
|
+
topic: message.topic,
|
|
27
|
+
msg_type: message.msgType,
|
|
28
|
+
parent_msg_id: message.parentMsgId,
|
|
29
|
+
content: message.content
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=message-parser.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { JupyterMessage } from '../types/messages.js';
|
|
3
|
+
export interface MessageHandler {
|
|
4
|
+
handle(message: JupyterMessage, emitter: EventEmitter): Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
export declare class MessageRouter {
|
|
7
|
+
private handlers;
|
|
8
|
+
private emitter;
|
|
9
|
+
constructor(emitter: EventEmitter);
|
|
10
|
+
route(rawMessage: string): Promise<void>;
|
|
11
|
+
private findHandler;
|
|
12
|
+
registerHandler(msgType: string, handler: MessageHandler): void;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=message-router.d.ts.map
|