@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,41 @@
|
|
|
1
|
+
import { MessageParser } from './message-parser.js';
|
|
2
|
+
export class MessageRouter {
|
|
3
|
+
handlers;
|
|
4
|
+
emitter;
|
|
5
|
+
constructor(emitter) {
|
|
6
|
+
this.emitter = emitter;
|
|
7
|
+
this.handlers = new Map();
|
|
8
|
+
}
|
|
9
|
+
async route(rawMessage) {
|
|
10
|
+
const message = MessageParser.parse(rawMessage);
|
|
11
|
+
// Emit raw message event
|
|
12
|
+
this.emitter.emit('*', message.msgType, message.content);
|
|
13
|
+
this.emitter.emit('message', message);
|
|
14
|
+
// Route to specific handler (dispatch on the plain msg_type, e.g.
|
|
15
|
+
// "stream" / "execute_result" — `topic` is a kernel-namespaced
|
|
16
|
+
// string like "kernel_core.<id>.stream" and isn't matched here)
|
|
17
|
+
const handler = this.findHandler(message.msgType);
|
|
18
|
+
if (handler) {
|
|
19
|
+
await handler.handle(message, this.emitter);
|
|
20
|
+
}
|
|
21
|
+
// Always emit msg_type-specific event
|
|
22
|
+
this.emitter.emit(message.msgType, message.content);
|
|
23
|
+
}
|
|
24
|
+
findHandler(msgType) {
|
|
25
|
+
// Exact match
|
|
26
|
+
if (this.handlers.has(msgType)) {
|
|
27
|
+
return this.handlers.get(msgType);
|
|
28
|
+
}
|
|
29
|
+
// Prefix match (e.g., "stream" matches "stream.stdout")
|
|
30
|
+
for (const [key, handler] of this.handlers) {
|
|
31
|
+
if (msgType.startsWith(key)) {
|
|
32
|
+
return handler;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
registerHandler(msgType, handler) {
|
|
38
|
+
this.handlers.set(msgType, handler);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=message-router.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export class MiddlewareChain {
|
|
2
|
+
middlewares = [];
|
|
3
|
+
use(middleware) {
|
|
4
|
+
this.middlewares.push(middleware);
|
|
5
|
+
}
|
|
6
|
+
async process(message) {
|
|
7
|
+
let result = message;
|
|
8
|
+
for (const middleware of this.middlewares) {
|
|
9
|
+
result = await middleware.process(result);
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=middleware-chain.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class LoggingMiddleware {
|
|
2
|
+
name = 'logging';
|
|
3
|
+
process(message) {
|
|
4
|
+
const timestamp = new Date().toISOString();
|
|
5
|
+
console.log(`[${timestamp}] Message: ${message.substring(0, 100)}...`);
|
|
6
|
+
return message;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=logging-plugin.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class MetricsMiddleware {
|
|
2
|
+
name = 'metrics';
|
|
3
|
+
messageCount = 0;
|
|
4
|
+
startTime = Date.now();
|
|
5
|
+
process(message) {
|
|
6
|
+
this.messageCount++;
|
|
7
|
+
const elapsed = (Date.now() - this.startTime) / 1000;
|
|
8
|
+
const rate = this.messageCount / elapsed;
|
|
9
|
+
console.log(`Messages/sec: ${rate.toFixed(2)}`);
|
|
10
|
+
return message;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=metrics-plugin.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
/**
|
|
3
|
+
* The parts of a Session a Comm needs -- kept as an interface so this file
|
|
4
|
+
* doesn't import session-manager (which imports it).
|
|
5
|
+
*/
|
|
6
|
+
export interface CommTransport {
|
|
7
|
+
commMsg(commId: string, data: Record<string, unknown>): Promise<string>;
|
|
8
|
+
commClose(commId: string, data: Record<string, unknown>): Promise<string>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One open comm: a named, bidirectional message channel between this client
|
|
12
|
+
* and a target registered inside the kernel (in R, `hera::CommManager$
|
|
13
|
+
* register_comm_target()`).
|
|
14
|
+
*
|
|
15
|
+
* Get one either from `session.openComm(target)` (client-initiated) or from
|
|
16
|
+
* the session's `'comm'` event (kernel-initiated, e.g. R calling
|
|
17
|
+
* `CommManager$new_comm()` then `$open()`).
|
|
18
|
+
*
|
|
19
|
+
* Events: `'message'` (data) for every comm_msg the kernel sends over it and
|
|
20
|
+
* `'close'` (data) once it is closed by either side -- or because the kernel
|
|
21
|
+
* restarted or the session ended, in which case `data.reason` says so.
|
|
22
|
+
*/
|
|
23
|
+
export declare class Comm extends EventEmitter {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly targetName: string;
|
|
26
|
+
private readonly transport;
|
|
27
|
+
private isClosed;
|
|
28
|
+
constructor(id: string, targetName: string, transport: CommTransport);
|
|
29
|
+
get closed(): boolean;
|
|
30
|
+
/** Sends `data` to the kernel-side handler. Rejects if the comm is closed. */
|
|
31
|
+
send(data?: Record<string, unknown>): Promise<string>;
|
|
32
|
+
/** Closes the comm (the kernel is told; no-op if it is already closed). */
|
|
33
|
+
close(data?: Record<string, unknown>): Promise<void>;
|
|
34
|
+
/** @internal Called by Session for the kernel's own comm_msg / comm_close. */
|
|
35
|
+
receiveMessage(data: Record<string, unknown>): void;
|
|
36
|
+
/** @internal */
|
|
37
|
+
receiveClose(data: Record<string, unknown>): void;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=comm.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
/**
|
|
3
|
+
* One open comm: a named, bidirectional message channel between this client
|
|
4
|
+
* and a target registered inside the kernel (in R, `hera::CommManager$
|
|
5
|
+
* register_comm_target()`).
|
|
6
|
+
*
|
|
7
|
+
* Get one either from `session.openComm(target)` (client-initiated) or from
|
|
8
|
+
* the session's `'comm'` event (kernel-initiated, e.g. R calling
|
|
9
|
+
* `CommManager$new_comm()` then `$open()`).
|
|
10
|
+
*
|
|
11
|
+
* Events: `'message'` (data) for every comm_msg the kernel sends over it and
|
|
12
|
+
* `'close'` (data) once it is closed by either side -- or because the kernel
|
|
13
|
+
* restarted or the session ended, in which case `data.reason` says so.
|
|
14
|
+
*/
|
|
15
|
+
export class Comm extends EventEmitter {
|
|
16
|
+
id;
|
|
17
|
+
targetName;
|
|
18
|
+
transport;
|
|
19
|
+
isClosed = false;
|
|
20
|
+
constructor(id, targetName, transport) {
|
|
21
|
+
super();
|
|
22
|
+
this.id = id;
|
|
23
|
+
this.targetName = targetName;
|
|
24
|
+
this.transport = transport;
|
|
25
|
+
}
|
|
26
|
+
get closed() {
|
|
27
|
+
return this.isClosed;
|
|
28
|
+
}
|
|
29
|
+
/** Sends `data` to the kernel-side handler. Rejects if the comm is closed. */
|
|
30
|
+
send(data = {}) {
|
|
31
|
+
if (this.isClosed) {
|
|
32
|
+
return Promise.reject(new Error(`Comm ${this.id} (${this.targetName}) is closed`));
|
|
33
|
+
}
|
|
34
|
+
return this.transport.commMsg(this.id, data);
|
|
35
|
+
}
|
|
36
|
+
/** Closes the comm (the kernel is told; no-op if it is already closed). */
|
|
37
|
+
async close(data = {}) {
|
|
38
|
+
if (this.isClosed) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
this.isClosed = true;
|
|
42
|
+
await this.transport.commClose(this.id, data);
|
|
43
|
+
this.emit('close', data);
|
|
44
|
+
}
|
|
45
|
+
/** @internal Called by Session for the kernel's own comm_msg / comm_close. */
|
|
46
|
+
receiveMessage(data) {
|
|
47
|
+
this.emit('message', data);
|
|
48
|
+
}
|
|
49
|
+
/** @internal */
|
|
50
|
+
receiveClose(data) {
|
|
51
|
+
if (this.isClosed) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
this.isClosed = true;
|
|
55
|
+
this.emit('close', data);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=comm.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** The npm scope the packages are published under (also set in scripts/release.mjs; a test keeps them in step). */
|
|
2
|
+
export declare const PACKAGE_SCOPE = "@damurka";
|
|
3
|
+
export declare const PACKAGE_NAME = "@damurka/jovian";
|
|
4
|
+
/**
|
|
5
|
+
* The prebuilt kernels (themisto, elara, carpo) ship in one small package per
|
|
6
|
+
* platform -- `@scope/jovian-<os>-<cpu>` -- that npm installs alongside
|
|
7
|
+
* the main package only where it matches (each declares `os`/`cpu`). These are
|
|
8
|
+
* the platforms that get a package.
|
|
9
|
+
*/
|
|
10
|
+
export declare const SUPPORTED_PLATFORMS: readonly ['win32-x64', 'linux-x64', 'darwin-arm64'];
|
|
11
|
+
export declare function platformPackageName(platform?: string, arch?: string): string | undefined;
|
|
12
|
+
export interface NativeLocation {
|
|
13
|
+
/** Directory holding themisto, elara and carpo. */
|
|
14
|
+
dir: string;
|
|
15
|
+
source: 'JOVIAN_NATIVE_DIR' | 'platform package' | 'source build';
|
|
16
|
+
}
|
|
17
|
+
export interface NativeLookup {
|
|
18
|
+
env: Record<string, string | undefined>;
|
|
19
|
+
platform: string;
|
|
20
|
+
arch: string;
|
|
21
|
+
exists: (path: string) => boolean;
|
|
22
|
+
/** Absolute path of a package's package.json, or undefined if it is not installed. */
|
|
23
|
+
resolvePackageJson: (packageName: string) => string | undefined;
|
|
24
|
+
/** dist/native/Release of a source checkout. */
|
|
25
|
+
sourceBuildDir: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where the kernels are, in order of precedence:
|
|
29
|
+
* 1. JOVIAN_NATIVE_DIR -- an explicit directory (a copy of the binaries, an
|
|
30
|
+
* app that bundles them elsewhere);
|
|
31
|
+
* 2. the installed `@scope/jovian-<os>-<cpu>` package for this machine;
|
|
32
|
+
* 3. a source checkout's dist/native/Release (development).
|
|
33
|
+
*/
|
|
34
|
+
export declare function locateNativeDirectory(lookup?: NativeLookup): NativeLocation;
|
|
35
|
+
/**
|
|
36
|
+
* npm does not reliably keep the executable bit on files in a tarball, so make
|
|
37
|
+
* sure the kernels can be run (POSIX only; a no-op on Windows).
|
|
38
|
+
*/
|
|
39
|
+
export declare function ensureExecutable(dir: string, platform?: string): void;
|
|
40
|
+
/**
|
|
41
|
+
* The copy of the 'hera' R package that ships inside the npm package, used to
|
|
42
|
+
* install/refresh it in R on a session's first start when the caller gave no
|
|
43
|
+
* heraSrcPath. Only when running from an installed package (under
|
|
44
|
+
* node_modules): a source checkout leaves hera alone so development and CI
|
|
45
|
+
* control which one is loaded.
|
|
46
|
+
*/
|
|
47
|
+
export declare function bundledHeraSource(baseDir?: string, exists?: (path: string) => boolean): string | undefined;
|
|
48
|
+
//# sourceMappingURL=native-paths.d.ts.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { chmodSync, existsSync } from 'node:fs';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { dirname, join, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
/** The npm scope the packages are published under (also set in scripts/release.mjs; a test keeps them in step). */
|
|
6
|
+
export const PACKAGE_SCOPE = '@damurka';
|
|
7
|
+
export const PACKAGE_NAME = `${PACKAGE_SCOPE}/jovian`;
|
|
8
|
+
/**
|
|
9
|
+
* The prebuilt kernels (themisto, elara, carpo) ship in one small package per
|
|
10
|
+
* platform -- `@scope/jovian-<os>-<cpu>` -- that npm installs alongside
|
|
11
|
+
* the main package only where it matches (each declares `os`/`cpu`). These are
|
|
12
|
+
* the platforms that get a package.
|
|
13
|
+
*/
|
|
14
|
+
export const SUPPORTED_PLATFORMS = ['win32-x64', 'linux-x64', 'darwin-arm64'];
|
|
15
|
+
export function platformPackageName(platform = process.platform, arch = process.arch) {
|
|
16
|
+
const key = `${platform}-${arch}`;
|
|
17
|
+
return SUPPORTED_PLATFORMS.includes(key) ? `${PACKAGE_NAME}-${key}` : undefined;
|
|
18
|
+
}
|
|
19
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
function defaultLookup() {
|
|
21
|
+
const require = createRequire(import.meta.url);
|
|
22
|
+
return {
|
|
23
|
+
env: process.env,
|
|
24
|
+
platform: process.platform,
|
|
25
|
+
arch: process.arch,
|
|
26
|
+
exists: existsSync,
|
|
27
|
+
resolvePackageJson: (name) => {
|
|
28
|
+
try {
|
|
29
|
+
return require.resolve(`${name}/package.json`);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
// dist/lib/session -> dist/native/Release
|
|
36
|
+
sourceBuildDir: join(moduleDir, '../../native/Release')
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Where the kernels are, in order of precedence:
|
|
41
|
+
* 1. JOVIAN_NATIVE_DIR -- an explicit directory (a copy of the binaries, an
|
|
42
|
+
* app that bundles them elsewhere);
|
|
43
|
+
* 2. the installed `@scope/jovian-<os>-<cpu>` package for this machine;
|
|
44
|
+
* 3. a source checkout's dist/native/Release (development).
|
|
45
|
+
*/
|
|
46
|
+
export function locateNativeDirectory(lookup = defaultLookup()) {
|
|
47
|
+
const exe = lookup.platform === 'win32' ? 'themisto.exe' : 'themisto';
|
|
48
|
+
const has = (dir) => lookup.exists(join(dir, exe));
|
|
49
|
+
const override = lookup.env.JOVIAN_NATIVE_DIR;
|
|
50
|
+
if (override) {
|
|
51
|
+
if (!has(override)) {
|
|
52
|
+
throw new Error(`jovian: JOVIAN_NATIVE_DIR is set to '${override}' but there is no ${exe} in it.`);
|
|
53
|
+
}
|
|
54
|
+
return { dir: override, source: 'JOVIAN_NATIVE_DIR' };
|
|
55
|
+
}
|
|
56
|
+
const packageName = platformPackageName(lookup.platform, lookup.arch);
|
|
57
|
+
if (packageName) {
|
|
58
|
+
const packageJson = lookup.resolvePackageJson(packageName);
|
|
59
|
+
if (packageJson) {
|
|
60
|
+
const dir = join(dirname(packageJson), 'bin');
|
|
61
|
+
if (has(dir)) {
|
|
62
|
+
return { dir, source: 'platform package' };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (has(lookup.sourceBuildDir)) {
|
|
67
|
+
return { dir: lookup.sourceBuildDir, source: 'source build' };
|
|
68
|
+
}
|
|
69
|
+
if (!packageName) {
|
|
70
|
+
throw new Error(`jovian: there are no prebuilt kernels for ${lookup.platform}-${lookup.arch} ` +
|
|
71
|
+
`(supported: ${SUPPORTED_PLATFORMS.join(', ')}). Build them from source and point ` +
|
|
72
|
+
`JOVIAN_NATIVE_DIR at the output directory.`);
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`jovian: the kernel binaries were not found. Expected the '${packageName}' package ` +
|
|
75
|
+
`(installed automatically as an optional dependency -- reinstall without --omit=optional / ` +
|
|
76
|
+
`--no-optional), or a source build at ${lookup.sourceBuildDir}, or JOVIAN_NATIVE_DIR.`);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* npm does not reliably keep the executable bit on files in a tarball, so make
|
|
80
|
+
* sure the kernels can be run (POSIX only; a no-op on Windows).
|
|
81
|
+
*/
|
|
82
|
+
export function ensureExecutable(dir, platform = process.platform) {
|
|
83
|
+
if (platform === 'win32')
|
|
84
|
+
return;
|
|
85
|
+
for (const name of ['themisto', 'elara', 'carpo']) {
|
|
86
|
+
try {
|
|
87
|
+
chmodSync(join(dir, name), 0o755);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// Absent (carpo is optional) or not ours to change: running it will say so.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The copy of the 'hera' R package that ships inside the npm package, used to
|
|
96
|
+
* install/refresh it in R on a session's first start when the caller gave no
|
|
97
|
+
* heraSrcPath. Only when running from an installed package (under
|
|
98
|
+
* node_modules): a source checkout leaves hera alone so development and CI
|
|
99
|
+
* control which one is loaded.
|
|
100
|
+
*/
|
|
101
|
+
export function bundledHeraSource(baseDir = moduleDir, exists = existsSync) {
|
|
102
|
+
if (!baseDir.split(sep).includes('node_modules'))
|
|
103
|
+
return undefined;
|
|
104
|
+
// <package>/lib/session -> <package>/packages/hera (the published layout)
|
|
105
|
+
const candidate = join(baseDir, '../../packages/hera');
|
|
106
|
+
return exists(join(candidate, 'DESCRIPTION')) ? candidate : undefined;
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=native-paths.js.map
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
|
|
3
|
+
import type { ExecutionState } from '../types/messages.js';
|
|
4
|
+
import { SupervisorClient, type SessionConnectionInfo } from './supervisor-client.js';
|
|
5
|
+
import { Comm } from './comm.js';
|
|
6
|
+
export type { ShinyAppHandle };
|
|
7
|
+
export declare class Session extends EventEmitter {
|
|
8
|
+
private ws;
|
|
9
|
+
readonly info: SessionConnectionInfo;
|
|
10
|
+
private currentOptions;
|
|
11
|
+
private readonly supervisor;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
private readonly router;
|
|
14
|
+
private readonly middleware;
|
|
15
|
+
private readonly queue;
|
|
16
|
+
/**
|
|
17
|
+
* The options this session is currently running with: what it was
|
|
18
|
+
* created with, updated by any `restart(options)` that changed them.
|
|
19
|
+
*/
|
|
20
|
+
get options(): EngineOptions;
|
|
21
|
+
private readyPromise;
|
|
22
|
+
private stopped;
|
|
23
|
+
private readonly comms;
|
|
24
|
+
private readonly busyRequests;
|
|
25
|
+
private readonly pendingRequests;
|
|
26
|
+
private kernelExecutionState;
|
|
27
|
+
private readonly executionHistory;
|
|
28
|
+
private readonly executionHistoryByMsgId;
|
|
29
|
+
private readonly historyStreamChars;
|
|
30
|
+
constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient);
|
|
31
|
+
/**
|
|
32
|
+
* (Re)establishes the WebSocket to this.info's session and resolves
|
|
33
|
+
* once it's ready. Used both by the constructor and by restart() --
|
|
34
|
+
* info.sessionId/httpBase/wsBase don't change across a restart
|
|
35
|
+
* (SessionRegistry::restartSession() replaces the kernel in place under
|
|
36
|
+
* the same id), so reconnecting to the exact same URL is enough to pick
|
|
37
|
+
* back up a session the supervisor just gave a fresh kernel.
|
|
38
|
+
*/
|
|
39
|
+
private connect;
|
|
40
|
+
/**
|
|
41
|
+
* Replaces this session's kernel process in place, keeping the same
|
|
42
|
+
* session id -- recovers a crashed session (kernelExit/unexpected close
|
|
43
|
+
* leaves the Session object itself alive but every execute() rejecting
|
|
44
|
+
* forever otherwise), and doubles as Jupyter's "Restart Kernel" for a
|
|
45
|
+
* still-healthy one. Not available after an explicit stop()/kill(): at
|
|
46
|
+
* that point the caller's intent was to end the session, not reset it
|
|
47
|
+
* -- create a new one instead via SessionManager.createSession().
|
|
48
|
+
*
|
|
49
|
+
* `options`, if given, switches this session's R installation on the
|
|
50
|
+
* restart (rHome/rPath/etc) instead of reusing whatever it was created
|
|
51
|
+
* with -- e.g. flip from R 4.4 to R 4.6 on the fly, without closing
|
|
52
|
+
* this session and opening a new one (a different session id/WS URL)
|
|
53
|
+
* just to pick a different R.
|
|
54
|
+
*/
|
|
55
|
+
restart(options?: Partial<EngineOptions>): Promise<void>;
|
|
56
|
+
private send;
|
|
57
|
+
private recordExecutionHistory;
|
|
58
|
+
private boundStreamHistory;
|
|
59
|
+
private handleFrame;
|
|
60
|
+
/** Resolves once this session's R interpreter has started. */
|
|
61
|
+
ready(): Promise<void>;
|
|
62
|
+
execute(code: string, options?: ExecutionOptions): Promise<ExecutionResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Answers a pending input_request -- this session emits one (see the
|
|
65
|
+
* 'input_request' event, content: {prompt, password}) whenever the
|
|
66
|
+
* kernel calls input()/readline()/scan() during an execute() that was
|
|
67
|
+
* given { allowStdin: true }, and genuinely blocks its single execution
|
|
68
|
+
* thread until this arrives (ServerZmqImpl::sendStdin() in
|
|
69
|
+
* native/src/adrastea/transport/server/server_zmq_impl.cpp does a real,
|
|
70
|
+
* untimed ZMQ recv underneath). Fire-and-forget like interrupt(): the
|
|
71
|
+
* reply that eventually unblocks the kernel surfaces through the
|
|
72
|
+
* *execute_request's own* execute_reply/stream messages, not through a
|
|
73
|
+
* reply to this call.
|
|
74
|
+
*/
|
|
75
|
+
sendInputReply(value: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* This session's own local record of every execute() call it has made
|
|
78
|
+
* and what each one produced (code + every iopub message), for as long
|
|
79
|
+
* as this Session object has been alive. Purely in-memory and
|
|
80
|
+
* process-local -- gone if the process holding this Session restarts,
|
|
81
|
+
* same as the Session object itself. Useful for e.g. rebuilding a UI's
|
|
82
|
+
* transcript after some *other* thing (not this process) reconnects to
|
|
83
|
+
* it, or for inspecting what actually ran without threading your own
|
|
84
|
+
* bookkeeping through every execute() call site.
|
|
85
|
+
*
|
|
86
|
+
* Not the same thing as queryKernelHistory(): this is this session's
|
|
87
|
+
* own bookkeeping (full fidelity -- includes actual output, which the
|
|
88
|
+
* kernel's own history manager doesn't track), while that one asks the
|
|
89
|
+
* *kernel itself* what it remembers running (input code only,
|
|
90
|
+
* authoritative even if some other client executed it, but capped by
|
|
91
|
+
* this process's own historical view of it, and lost across a kernel
|
|
92
|
+
* restart the same as the kernel's own memory of it is).
|
|
93
|
+
*/
|
|
94
|
+
getHistory(): ExecutionHistoryEntry[];
|
|
95
|
+
/**
|
|
96
|
+
* Sends a real Jupyter history_request and resolves with the kernel's
|
|
97
|
+
* own history_reply (KernelCore::historyRequest() ->
|
|
98
|
+
* HistoryManager::processRequest(), native/src/adrastea/core/history/)
|
|
99
|
+
* -- the kernel's own authoritative record of what it has executed,
|
|
100
|
+
* independent of which client (or how many, over how many reconnects)
|
|
101
|
+
* actually ran it. Defaults to the 100 most recent executions ('tail').
|
|
102
|
+
* See KernelHistoryOptions' own doc comment for the other access modes,
|
|
103
|
+
* and getHistory()'s doc comment for how this differs from that.
|
|
104
|
+
*/
|
|
105
|
+
queryKernelHistory(options?: KernelHistoryOptions): Promise<KernelHistoryEntry[]>;
|
|
106
|
+
/**
|
|
107
|
+
* Sends interrupt_request over the control channel and resolves true if
|
|
108
|
+
* the kernel acknowledged it (interrupt_reply, status ok), false if it
|
|
109
|
+
* didn't within `options.timeout` (default 5s) or the session is gone --
|
|
110
|
+
* never rejects, since a caller typically fires this from a "stop"
|
|
111
|
+
* button and has nothing useful to do with an error.
|
|
112
|
+
*
|
|
113
|
+
* A real interrupt: the kernel services its control channel on a
|
|
114
|
+
* separate thread while code runs, so this is answered immediately even
|
|
115
|
+
* mid-execution, and the running code is broken out of exactly as Ctrl-C
|
|
116
|
+
* would (R: an interrupt condition; Python: KeyboardInterrupt). The
|
|
117
|
+
* interrupted execute() resolves with success: false. Interrupting an
|
|
118
|
+
* idle kernel does nothing. Limits: code blocked inside a native call
|
|
119
|
+
* that never returns to the interpreter (a long C extension call, a
|
|
120
|
+
* blocking socket read) is only interrupted once it does, and a kernel
|
|
121
|
+
* waiting on an input() / readline() reply must be answered (or its
|
|
122
|
+
* execute() timed out) first.
|
|
123
|
+
*/
|
|
124
|
+
interrupt(options?: {
|
|
125
|
+
timeout?: number;
|
|
126
|
+
}): Promise<boolean>;
|
|
127
|
+
/**
|
|
128
|
+
* The latest iopub `status` the kernel reported ('busy' while it is
|
|
129
|
+
* handling a request, 'idle' between them) -- undefined until the first
|
|
130
|
+
* one arrives. Also available as the 'status' event.
|
|
131
|
+
*/
|
|
132
|
+
get executionState(): ExecutionState | undefined;
|
|
133
|
+
/**
|
|
134
|
+
* Sends any of the Jupyter requests that have a plain request/reply
|
|
135
|
+
* shape and resolves with the kernel's reply content: complete_request,
|
|
136
|
+
* inspect_request, is_complete_request, kernel_info_request,
|
|
137
|
+
* history_request, comm_info_request (shell) and interrupt_request
|
|
138
|
+
* (control) -- the typed methods below (complete(), inspect(), ...) are
|
|
139
|
+
* this with the right message type and content filled in. Rejects on a
|
|
140
|
+
* reply whose status is 'error'/'aborted', when the supervisor refuses
|
|
141
|
+
* the request, on timeout, or if the session goes away first.
|
|
142
|
+
*
|
|
143
|
+
* Deliberately not for execute_request (use execute(): it owns the
|
|
144
|
+
* queue/timeout/stdin semantics), input_reply (sendInputReply()) or
|
|
145
|
+
* shutdown_request (stop()/restart()) -- the supervisor rejects those.
|
|
146
|
+
*/
|
|
147
|
+
request<T = any>(msgType: string, content?: Record<string, unknown>, options?: {
|
|
148
|
+
timeout?: number;
|
|
149
|
+
}): Promise<T>;
|
|
150
|
+
private trackExecutionState;
|
|
151
|
+
private routeComm;
|
|
152
|
+
private closeAllComms;
|
|
153
|
+
private settleRequest;
|
|
154
|
+
private rejectPendingRequests;
|
|
155
|
+
private watchFor;
|
|
156
|
+
/**
|
|
157
|
+
* What the supervisor knows about this session's kernel process right
|
|
158
|
+
* now: lifecycle status, pid, memory, working directory and the
|
|
159
|
+
* heartbeat (round-trip time of the last ping, missed pings). The
|
|
160
|
+
* heartbeat is answered by a kernel thread separate from the one that
|
|
161
|
+
* runs code, so it stays live while the kernel is busy -- unlike
|
|
162
|
+
* kernelInfo(), which would wait for the running code to finish.
|
|
163
|
+
*/
|
|
164
|
+
status(): Promise<SessionStatusInfo>;
|
|
165
|
+
/** complete_request: completions for the code at `cursorPos` (default: the end of `code`). */
|
|
166
|
+
complete(code: string, cursorPos?: number): Promise<CompleteReplyContent>;
|
|
167
|
+
/** inspect_request: documentation/details for the symbol at `cursorPos` (default: the end of `code`). */
|
|
168
|
+
inspect(code: string, cursorPos?: number, detailLevel?: 0 | 1): Promise<InspectReplyContent>;
|
|
169
|
+
/**
|
|
170
|
+
* is_complete_request: whether `code` is a complete statement, needs more
|
|
171
|
+
* lines ('incomplete', with an `indent` hint when the kernel has one),
|
|
172
|
+
* or can never parse ('invalid') -- what a console needs to decide
|
|
173
|
+
* between "run it" and "keep prompting".
|
|
174
|
+
*/
|
|
175
|
+
isComplete(code: string): Promise<IsCompleteReplyContent>;
|
|
176
|
+
/** kernel_info_request: what the kernel is -- implementation, language and its version, protocol version, banner. */
|
|
177
|
+
kernelInfo(): Promise<KernelInfoReplyContent>;
|
|
178
|
+
/** comm_info_request: the comms currently open in the kernel, optionally only those for one target. */
|
|
179
|
+
commInfo(targetName?: string): Promise<CommInfoReplyContent>;
|
|
180
|
+
private sendComm;
|
|
181
|
+
/** Opens a comm to a kernel-side `targetName`; resolves with its comm id and the msg id it was sent under. */
|
|
182
|
+
commOpen(targetName: string, data?: Record<string, unknown>, commId?: string): Promise<{
|
|
183
|
+
commId: string;
|
|
184
|
+
msgId: string;
|
|
185
|
+
}>;
|
|
186
|
+
/**
|
|
187
|
+
* Opens a comm to a kernel-side `targetName` and returns it as a Comm
|
|
188
|
+
* object (send()/close(), 'message'/'close' events). If the kernel has
|
|
189
|
+
* no such target it answers with a comm_close, so the returned comm
|
|
190
|
+
* emits 'close' shortly after. Kernel-initiated comms arrive as this
|
|
191
|
+
* session's 'comm' event instead: `session.on('comm', (comm, data) => ...)`.
|
|
192
|
+
*/
|
|
193
|
+
openComm(targetName: string, data?: Record<string, unknown>): Promise<Comm>;
|
|
194
|
+
/** Sends `data` over an open comm. */
|
|
195
|
+
commMsg(commId: string, data?: Record<string, unknown>): Promise<string>;
|
|
196
|
+
/** Closes a comm. */
|
|
197
|
+
commClose(commId: string, data?: Record<string, unknown>): Promise<string>;
|
|
198
|
+
/**
|
|
199
|
+
* Launches a Shiny app in this session's R process and resolves once
|
|
200
|
+
* it's actually accepting connections. shiny::runApp() blocks the R
|
|
201
|
+
* session for as long as the app runs, so -- unlike execute() --
|
|
202
|
+
* resolving here does not mean the app is done; that's what the
|
|
203
|
+
* returned `done` promise is for.
|
|
204
|
+
*/
|
|
205
|
+
createShiny(options: ShinyAppOptions): Promise<ShinyAppHandle>;
|
|
206
|
+
/** Stops the R session and waits for its process to exit. */
|
|
207
|
+
stop(): Promise<void>;
|
|
208
|
+
/** Skips the graceful shutdown protocol -- only for cleanup on the way out. */
|
|
209
|
+
kill(): void;
|
|
210
|
+
}
|
|
211
|
+
export declare class SessionManager {
|
|
212
|
+
private readonly supervisor;
|
|
213
|
+
private readonly sessions;
|
|
214
|
+
private exitHandlerRegistered;
|
|
215
|
+
/** Creates a new R session in its own OS process and waits for it to be ready. */
|
|
216
|
+
createSession(options?: EngineOptions): Promise<Session>;
|
|
217
|
+
/** Gracefully stops every session managed by this instance. */
|
|
218
|
+
stopAll(): Promise<void>;
|
|
219
|
+
/**
|
|
220
|
+
* Forcibly terminates every session. Prefer stopAll(), but a session
|
|
221
|
+
* whose R interpreter is blocked in a long-running call (e.g.
|
|
222
|
+
* shiny::runApp()) can't process a graceful shutdown until that call
|
|
223
|
+
* returns -- callers wanting a bounded-time exit (e.g. a Ctrl+C
|
|
224
|
+
* handler) should race stopAll() against a timeout and fall back to this.
|
|
225
|
+
*/
|
|
226
|
+
killAll(): void;
|
|
227
|
+
private registerExitHandler;
|
|
228
|
+
}
|
|
229
|
+
//# sourceMappingURL=session-manager.d.ts.map
|