@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.
Files changed (91) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +175 -0
  3. package/docs/api/README.md +55 -0
  4. package/docs/api/session.md +143 -0
  5. package/docs/api/types.md +120 -0
  6. package/docs/architecture/overview.md +218 -0
  7. package/docs/cpp-usage.md +60 -0
  8. package/docs/development.md +105 -0
  9. package/docs/getting-started.md +127 -0
  10. package/docs/guides/comms.md +70 -0
  11. package/docs/guides/environments.md +80 -0
  12. package/docs/guides/history.md +44 -0
  13. package/docs/guides/interactive-input.md +48 -0
  14. package/docs/guides/interrupting.md +45 -0
  15. package/docs/guides/playground.md +33 -0
  16. package/docs/guides/sessions-lifecycle.md +70 -0
  17. package/docs/kernels.md +117 -0
  18. package/docs/protocol.md +135 -0
  19. package/docs/releasing.md +73 -0
  20. package/docs/troubleshooting.md +110 -0
  21. package/lib/execution/execution-queue.d.ts +20 -0
  22. package/lib/execution/execution-queue.js +256 -0
  23. package/lib/handlers/display-handler.d.ts +7 -0
  24. package/lib/handlers/display-handler.js +10 -0
  25. package/lib/handlers/error-handler.d.ts +7 -0
  26. package/lib/handlers/error-handler.js +8 -0
  27. package/lib/handlers/result-handler.d.ts +7 -0
  28. package/lib/handlers/result-handler.js +10 -0
  29. package/lib/handlers/stream-handler.d.ts +7 -0
  30. package/lib/handlers/stream-handler.js +8 -0
  31. package/lib/index.d.ts +7 -0
  32. package/lib/index.js +5 -0
  33. package/lib/messaging/message-parser.d.ts +6 -0
  34. package/lib/messaging/message-parser.js +33 -0
  35. package/lib/messaging/message-router.d.ts +14 -0
  36. package/lib/messaging/message-router.js +41 -0
  37. package/lib/middleware/index.d.ts +5 -0
  38. package/lib/middleware/index.js +5 -0
  39. package/lib/middleware/middleware-chain.d.ts +7 -0
  40. package/lib/middleware/middleware-chain.js +14 -0
  41. package/lib/middleware/middleware.d.ts +5 -0
  42. package/lib/middleware/middleware.js +2 -0
  43. package/lib/middleware/plugins/logging-plugin.d.ts +6 -0
  44. package/lib/middleware/plugins/logging-plugin.js +9 -0
  45. package/lib/middleware/plugins/metrics-plugin.d.ts +8 -0
  46. package/lib/middleware/plugins/metrics-plugin.js +13 -0
  47. package/lib/session/comm.d.ts +39 -0
  48. package/lib/session/comm.js +58 -0
  49. package/lib/session/native-paths.d.ts +48 -0
  50. package/lib/session/native-paths.js +108 -0
  51. package/lib/session/session-manager.d.ts +229 -0
  52. package/lib/session/session-manager.js +842 -0
  53. package/lib/session/supervisor-client.d.ts +36 -0
  54. package/lib/session/supervisor-client.js +147 -0
  55. package/lib/types/engine.d.ts +269 -0
  56. package/lib/types/engine.js +2 -0
  57. package/lib/types/index.d.ts +3 -0
  58. package/lib/types/index.js +3 -0
  59. package/lib/types/messages.d.ts +68 -0
  60. package/lib/types/messages.js +2 -0
  61. package/lib/utils/logger.d.ts +12 -0
  62. package/lib/utils/logger.js +58 -0
  63. package/lib/utils/network.d.ts +11 -0
  64. package/lib/utils/network.js +50 -0
  65. package/package.json +57 -0
  66. package/packages/hera/DESCRIPTION +29 -0
  67. package/packages/hera/LICENSE +2 -0
  68. package/packages/hera/LICENSE.md +21 -0
  69. package/packages/hera/NAMESPACE +32 -0
  70. package/packages/hera/NEWS.md +7 -0
  71. package/packages/hera/R/cell_options.R +13 -0
  72. package/packages/hera/R/comm.R +228 -0
  73. package/packages/hera/R/completion.R +54 -0
  74. package/packages/hera/R/execute.R +199 -0
  75. package/packages/hera/R/inspect.R +73 -0
  76. package/packages/hera/R/log.R +14 -0
  77. package/packages/hera/R/mime_bundle.R +65 -0
  78. package/packages/hera/R/routines.R +86 -0
  79. package/packages/hera/R/utils.R +32 -0
  80. package/packages/hera/R/zzz.R +128 -0
  81. package/packages/hera/man/Comm.Rd +179 -0
  82. package/packages/hera/man/CommManager.Rd +215 -0
  83. package/packages/hera/man/View.Rd +22 -0
  84. package/packages/hera/man/cell_options.Rd +20 -0
  85. package/packages/hera/man/clear_output.Rd +23 -0
  86. package/packages/hera/man/complete.Rd +23 -0
  87. package/packages/hera/man/display_data.Rd +22 -0
  88. package/packages/hera/man/is_elara.Rd +18 -0
  89. package/packages/hera/man/mime_bundle.Rd +25 -0
  90. package/packages/hera/man/mime_types.Rd +22 -0
  91. 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,8 @@
1
+ export class ErrorHandler {
2
+ handle(message, emitter) {
3
+ if (message.content && message.content.evalue) {
4
+ emitter.emit('error', message.content.evalue);
5
+ }
6
+ }
7
+ }
8
+ //# sourceMappingURL=error-handler.js.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
@@ -0,0 +1,8 @@
1
+ export class StreamHandler {
2
+ handle(message, emitter) {
3
+ if (message.content && message.content.text) {
4
+ emitter.emit(message.content.name === 'stderr' ? 'stderr' : 'stdout', message.content.text);
5
+ }
6
+ }
7
+ }
8
+ //# sourceMappingURL=stream-handler.js.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,5 @@
1
+ export { Session, SessionManager } from './session/session-manager.js';
2
+ export { Comm } from './session/comm.js';
3
+ export * from './types/index.js';
4
+ export * from './middleware/index.js';
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,6 @@
1
+ import type { JupyterMessage } from '../types/messages.js';
2
+ export declare class MessageParser {
3
+ static parse(raw: string): JupyterMessage;
4
+ static stringify(message: JupyterMessage): string;
5
+ }
6
+ //# sourceMappingURL=message-parser.d.ts.map
@@ -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