@damurka/jovian 0.1.0 → 0.1.2

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.
@@ -0,0 +1,80 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ const runCommand = (command, args) => {
3
+ try {
4
+ const output = execFileSync(command, args, {
5
+ encoding: 'utf8',
6
+ timeout: 10_000,
7
+ stdio: ['ignore', 'pipe', 'ignore'],
8
+ windowsHide: true
9
+ });
10
+ const lines = output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
11
+ return lines[lines.length - 1];
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ };
17
+ const defaultContext = () => ({ run: runCommand, env: process.env, platform: process.platform });
18
+ /**
19
+ * Where R lives, when the caller did not say: $R_HOME, else what `R RHOME`
20
+ * prints (R's own answer, valid on every platform, if R is on PATH), else on
21
+ * Windows the install path R's installer records in the registry.
22
+ */
23
+ export function discoverRHome(context = defaultContext()) {
24
+ if (context.env.R_HOME)
25
+ return context.env.R_HOME;
26
+ const fromR = context.run('R', ['RHOME']);
27
+ if (fromR)
28
+ return fromR;
29
+ if (context.platform === 'win32') {
30
+ for (const hive of ['HKLM', 'HKCU']) {
31
+ const line = context.run('reg', ['query', `${hive}\\SOFTWARE\\R-core\\R`, '/v', 'InstallPath']);
32
+ const match = line?.match(/InstallPath\s+REG_SZ\s+(.+)$/);
33
+ if (match?.[1])
34
+ return match[1].trim();
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ /**
40
+ * Which Python to embed, when the caller did not say: $PYTHONHOME, else the
41
+ * installation prefix of the first python on PATH. sys.base_prefix, not
42
+ * sys.prefix: inside a virtual environment the latter is the venv, which has
43
+ * no libpython to load.
44
+ */
45
+ export function discoverPythonHome(context = defaultContext()) {
46
+ if (context.env.PYTHONHOME)
47
+ return context.env.PYTHONHOME;
48
+ const script = 'import sys; print(sys.base_prefix)';
49
+ const candidates = [
50
+ ['python3', ['-c', script]],
51
+ ['python', ['-c', script]]
52
+ ];
53
+ if (context.platform === 'win32')
54
+ candidates.push(['py', ['-3', '-c', script]]);
55
+ for (const [command, args] of candidates) {
56
+ const home = context.run(command, args);
57
+ if (home)
58
+ return home;
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * The options with rHome (R sessions) or pythonHome (Python sessions) filled
64
+ * in when the caller left them out and the runtime could be found. Anything
65
+ * the caller passed is kept as it is; if nothing is found the field stays
66
+ * unset, and the kernel reports what it could not find.
67
+ */
68
+ export function withDiscoveredRuntime(options, context = defaultContext()) {
69
+ if (options.kernelType === 'python') {
70
+ if (options.pythonHome)
71
+ return options;
72
+ const pythonHome = discoverPythonHome(context);
73
+ return pythonHome ? { ...options, pythonHome } : options;
74
+ }
75
+ if (options.rHome)
76
+ return options;
77
+ const rHome = discoverRHome(context);
78
+ return rHome ? { ...options, rHome } : options;
79
+ }
80
+ //# sourceMappingURL=runtimes.js.map
@@ -1,5 +1,5 @@
1
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';
2
+ import type { CommInfoReplyContent, CompleteReplyContent, EngineOptions, ExecutionHistoryEntry, ExecutionOptions, ExecutionResult, InspectReplyContent, IsCompleteReplyContent, KernelHistoryEntry, KernelHistoryOptions, KernelInfoReplyContent, LoggerFunction, LogThreshold, SessionManagerOptions, SessionStatusInfo, ShinyAppHandle, ShinyAppOptions } from '../types/index.js';
3
3
  import type { ExecutionState } from '../types/messages.js';
4
4
  import { SupervisorClient, type SessionConnectionInfo } from './supervisor-client.js';
5
5
  import { Comm } from './comm.js';
@@ -27,7 +27,10 @@ export declare class Session extends EventEmitter {
27
27
  private readonly executionHistory;
28
28
  private readonly executionHistoryByMsgId;
29
29
  private readonly historyStreamChars;
30
- constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient);
30
+ constructor(info: SessionConnectionInfo, options: EngineOptions, supervisor: SupervisorClient, logging?: {
31
+ level?: LogThreshold | undefined;
32
+ logger?: LoggerFunction | undefined;
33
+ });
31
34
  /**
32
35
  * (Re)establishes the WebSocket to this.info's session and resolves
33
36
  * once it's ready. Used both by the constructor and by restart() --
@@ -122,7 +125,7 @@ export declare class Session extends EventEmitter {
122
125
  * execute() timed out) first.
123
126
  */
124
127
  interrupt(options?: {
125
- timeout?: number;
128
+ timeout?: number | undefined;
126
129
  }): Promise<boolean>;
127
130
  /**
128
131
  * The latest iopub `status` the kernel reported ('busy' while it is
@@ -145,7 +148,7 @@ export declare class Session extends EventEmitter {
145
148
  * shutdown_request (stop()/restart()) -- the supervisor rejects those.
146
149
  */
147
150
  request<T = any>(msgType: string, content?: Record<string, unknown>, options?: {
148
- timeout?: number;
151
+ timeout?: number | undefined;
149
152
  }): Promise<T>;
150
153
  private trackExecutionState;
151
154
  private routeComm;
@@ -209,11 +212,20 @@ export declare class Session extends EventEmitter {
209
212
  kill(): void;
210
213
  }
211
214
  export declare class SessionManager {
215
+ private readonly logLevel;
216
+ private readonly customLogger;
217
+ private readonly logger;
212
218
  private readonly supervisor;
213
219
  private readonly sessions;
214
220
  private exitHandlerRegistered;
221
+ /**
222
+ * Quiet by default: only one-time setup notices, warnings and errors are
223
+ * printed. See SessionManagerOptions for `logLevel`, `logger` and
224
+ * `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
225
+ */
226
+ constructor(options?: SessionManagerOptions);
215
227
  /** Creates a new R session in its own OS process and waits for it to be ready. */
216
- createSession(options?: EngineOptions): Promise<Session>;
228
+ createSession(requested?: EngineOptions): Promise<Session>;
217
229
  /** Gracefully stops every session managed by this instance. */
218
230
  stopAll(): Promise<void>;
219
231
  /**
@@ -1,6 +1,6 @@
1
1
  import { EventEmitter } from 'events';
2
2
  import { randomUUID } from 'crypto';
3
- import { Logger } from '../utils/logger.js';
3
+ import { Logger, defaultLogLevel } from '../utils/logger.js';
4
4
  import { MessageRouter } from '../messaging/message-router.js';
5
5
  import { ExecutionQueue } from '../execution/execution-queue.js';
6
6
  import { MiddlewareChain } from '../middleware/middleware-chain.js';
@@ -12,6 +12,9 @@ import { ErrorHandler } from '../handlers/error-handler.js';
12
12
  import { DisplayHandler } from '../handlers/display-handler.js';
13
13
  import { findFreePort, waitForPort } from '../utils/network.js';
14
14
  import { SupervisorClient } from './supervisor-client.js';
15
+ import { withDiscoveredRuntime } from './runtimes.js';
16
+ import { ensureRPackages } from './r-setup.js';
17
+ import { bundledHeraSource } from './native-paths.js';
15
18
  import { Comm } from './comm.js';
16
19
  const DEFAULT_REQUEST_TIMEOUT_MS = 10000;
17
20
  // How long stop() waits for the kernel's shutdown_reply after the
@@ -86,12 +89,12 @@ export class Session extends EventEmitter {
86
89
  executionHistory = [];
87
90
  executionHistoryByMsgId = new Map();
88
91
  historyStreamChars = new WeakMap();
89
- constructor(info, options, supervisor) {
92
+ constructor(info, options, supervisor, logging = {}) {
90
93
  super();
91
94
  this.info = info;
92
95
  this.currentOptions = options;
93
96
  this.supervisor = supervisor;
94
- this.logger = new Logger(options.logger);
97
+ this.logger = new Logger(options.logger ?? logging.logger, logging.level);
95
98
  this.on('message', (message) => {
96
99
  this.recordExecutionHistory(message);
97
100
  this.settleRequest(message);
@@ -213,7 +216,7 @@ export class Session extends EventEmitter {
213
216
  // The supervisor replaces a session's options wholesale, so send the
214
217
  // merge -- a restart that only switches rHome must keep the
215
218
  // workingDirectory, rLibs, ... the session was created with.
216
- const mergedOptions = options ? { ...this.currentOptions, ...options } : undefined;
219
+ const mergedOptions = options ? withDiscoveredRuntime({ ...this.currentOptions, ...options }) : undefined;
217
220
  // Reassigned synchronously, before awaiting anything below, so a
218
221
  // concurrent execute()/createShiny() call that reads this.readyPromise
219
222
  // while the restart is still in flight waits for the new connection
@@ -222,6 +225,8 @@ export class Session extends EventEmitter {
222
225
  const shutdownReply = this.watchFor('shutdown_reply');
223
226
  this.readyPromise = (async () => {
224
227
  try {
228
+ if (mergedOptions)
229
+ await ensureRPackages(mergedOptions, this.logger);
225
230
  await this.supervisor.restartSession(this.info, mergedOptions);
226
231
  if (mergedOptions) {
227
232
  this.currentOptions = mergedOptions;
@@ -776,13 +781,34 @@ export class Session extends EventEmitter {
776
781
  }
777
782
  }
778
783
  export class SessionManager {
779
- supervisor = new SupervisorClient(new Logger());
784
+ logLevel;
785
+ customLogger;
786
+ logger;
787
+ supervisor;
780
788
  sessions = new Set();
781
789
  exitHandlerRegistered = false;
790
+ /**
791
+ * Quiet by default: only one-time setup notices, warnings and errors are
792
+ * printed. See SessionManagerOptions for `logLevel`, `logger` and
793
+ * `kernelOutput` (and the JOVIAN_LOG_LEVEL / JOVIAN_KERNEL_OUTPUT variables).
794
+ */
795
+ constructor(options = {}) {
796
+ this.logLevel = options.logLevel ?? defaultLogLevel();
797
+ this.customLogger = options.logger;
798
+ this.logger = new Logger(this.customLogger, this.logLevel);
799
+ const verbose = this.logLevel === 'trace' || this.logLevel === 'debug';
800
+ const forwardKernelOutput = options.kernelOutput ?? (Boolean(process.env.JOVIAN_KERNEL_OUTPUT) || verbose);
801
+ this.supervisor = new SupervisorClient(this.logger, { forwardKernelOutput });
802
+ }
782
803
  /** Creates a new R session in its own OS process and waits for it to be ready. */
783
- async createSession(options = {}) {
804
+ async createSession(requested = {}) {
805
+ // Finds R / Python when rHome / pythonHome were not given (see runtimes.ts).
806
+ // An installed package brings its own copy of hera (none in a source checkout).
807
+ const options = withDiscoveredRuntime(requested.heraSrcPath ? requested : { ...requested, heraSrcPath: bundledHeraSource() });
808
+ // First R session only: installs hera and what it needs (see r-setup.ts).
809
+ await ensureRPackages(options, this.logger);
784
810
  const info = await this.supervisor.createSession(options);
785
- const session = new Session(info, options, this.supervisor);
811
+ const session = new Session(info, options, this.supervisor, { level: this.logLevel, logger: this.customLogger });
786
812
  this.sessions.add(session);
787
813
  this.registerExitHandler();
788
814
  try {
@@ -798,7 +824,7 @@ export class SessionManager {
798
824
  async stopAll() {
799
825
  await Promise.all([...this.sessions].map((session) => session.stop()));
800
826
  this.sessions.clear();
801
- this.supervisor.kill();
827
+ this.supervisor.kill(true);
802
828
  }
803
829
  /**
804
830
  * Forcibly terminates every session. Prefer stopAll(), but a session
@@ -10,7 +10,14 @@ export declare class SupervisorClient {
10
10
  private child;
11
11
  private readyPromise;
12
12
  private readonly logger;
13
- constructor(logger: Logger);
13
+ private readonly forwardKernelOutput;
14
+ private readonly recentOutput;
15
+ constructor(logger: Logger, options?: {
16
+ forwardKernelOutput?: boolean;
17
+ });
18
+ private rememberOutput;
19
+ /** The message plus what the kernels said just before, when that was not already printed. */
20
+ private withKernelOutput;
14
21
  private ensureStarted;
15
22
  private spawnSupervisor;
16
23
  createSession(options: EngineOptions): Promise<SessionConnectionInfo>;
@@ -30,7 +37,11 @@ export declare class SupervisorClient {
30
37
  * close this session and create a new one just to pick a different R.
31
38
  */
32
39
  restartSession(info: SessionConnectionInfo, options?: Partial<EngineOptions>): Promise<void>;
33
- /** Skips graceful per-session shutdown -- only for cleanup on the way out. */
34
- kill(): void;
40
+ /**
41
+ * Skips graceful per-session shutdown -- only for cleanup on the way out.
42
+ * `expected` is the normal end of stopAll(), after every session was
43
+ * stopped: not worth a warning.
44
+ */
45
+ kill(expected?: boolean): void;
35
46
  }
36
47
  //# sourceMappingURL=supervisor-client.d.ts.map
@@ -33,8 +33,29 @@ export class SupervisorClient {
33
33
  child;
34
34
  readyPromise;
35
35
  logger;
36
- constructor(logger) {
36
+ forwardKernelOutput;
37
+ // The supervisor's stderr is where every kernel's start-up output ends up
38
+ // ([elara] ..., [carpo] ...). It is kept, not printed, unless asked for,
39
+ // and attached to the error when a kernel fails to start.
40
+ recentOutput = [];
41
+ constructor(logger, options = {}) {
37
42
  this.logger = logger;
43
+ this.forwardKernelOutput = options.forwardKernelOutput ?? false;
44
+ }
45
+ rememberOutput(line) {
46
+ if (!line.trim())
47
+ return;
48
+ this.recentOutput.push(line);
49
+ if (this.recentOutput.length > 200)
50
+ this.recentOutput.shift();
51
+ }
52
+ /** The message plus what the kernels said just before, when that was not already printed. */
53
+ withKernelOutput(message) {
54
+ if (this.forwardKernelOutput || this.recentOutput.length === 0)
55
+ return message;
56
+ const notable = this.recentOutput.filter((line) => /error|fatal|warning|failed|cannot|not found|no such/i.test(line));
57
+ const lines = (notable.length > 0 ? notable : this.recentOutput).slice(-8);
58
+ return `${message}\nKernel output:\n ${lines.join('\n ')}`;
38
59
  }
39
60
  ensureStarted() {
40
61
  if (!this.readyPromise) {
@@ -48,7 +69,11 @@ export class SupervisorClient {
48
69
  this.logger.debug(`Spawning supervisor process from: ${exePath}`);
49
70
  const child = spawn(exePath, [], { stdio: ['ignore', 'pipe', 'pipe'] });
50
71
  this.child = child;
51
- child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
72
+ createInterface({ input: child.stderr }).on('line', (line) => {
73
+ this.rememberOutput(line);
74
+ if (this.forwardKernelOutput)
75
+ process.stderr.write(`${line}\n`);
76
+ });
52
77
  const rl = createInterface({ input: child.stdout });
53
78
  const onLine = (line) => {
54
79
  let message;
@@ -87,7 +112,7 @@ export class SupervisorClient {
87
112
  });
88
113
  const body = await res.json();
89
114
  if (!res.ok || !body.sessionId) {
90
- throw new Error(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`);
115
+ throw new Error(this.withKernelOutput(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`));
91
116
  }
92
117
  return { sessionId: body.sessionId, httpBase, wsBase: `ws://127.0.0.1:${wsPort}` };
93
118
  }
@@ -126,10 +151,18 @@ export class SupervisorClient {
126
151
  throw new Error(body.error ?? `Supervisor failed to restart session ${info.sessionId} (HTTP ${res.status})`);
127
152
  }
128
153
  }
129
- /** Skips graceful per-session shutdown -- only for cleanup on the way out. */
130
- kill() {
154
+ /**
155
+ * Skips graceful per-session shutdown -- only for cleanup on the way out.
156
+ * `expected` is the normal end of stopAll(), after every session was
157
+ * stopped: not worth a warning.
158
+ */
159
+ kill(expected = false) {
131
160
  if (this.child && !this.child.killed) {
132
- this.logger.warn(`Force-killing supervisor process (pid ${this.child.pid})`);
161
+ const message = `Force-killing supervisor process (pid ${this.child.pid})`;
162
+ if (expected)
163
+ this.logger.debug(message);
164
+ else
165
+ this.logger.warn(message);
133
166
  this.child.kill();
134
167
  }
135
168
  }
@@ -1,5 +1,29 @@
1
1
  import type { JupyterMessage } from './messages.js';
2
- export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
2
+ /**
3
+ * 'notice' is for the few things a user should be told even when the library
4
+ * is otherwise quiet (e.g. the one-time install of the R packages, which takes
5
+ * a while); it sits between 'info' and 'warn'.
6
+ */
7
+ export type LogLevel = 'trace' | 'debug' | 'info' | 'notice' | 'warn' | 'error';
8
+ /** What the built-in console logger prints: this level and above; 'silent' prints nothing. */
9
+ export type LogThreshold = LogLevel | 'silent';
10
+ export interface SessionManagerOptions {
11
+ /**
12
+ * How much the library prints to the console (default 'notice': one-time
13
+ * setup messages, warnings and errors; env JOVIAN_LOG_LEVEL sets the
14
+ * default). Ignored for messages sent to `logger`, which receives all of them.
15
+ */
16
+ logLevel?: LogThreshold | undefined;
17
+ /** Receives every log message instead of the console. */
18
+ logger?: LoggerFunction | undefined;
19
+ /**
20
+ * Print the kernels' own start-up output (the `[elara]` / `[carpo]` lines)
21
+ * to stderr as it happens. Off by default -- it is included in the error
22
+ * when a kernel fails to start -- and on when logLevel is 'debug' or
23
+ * 'trace' or env JOVIAN_KERNEL_OUTPUT is set.
24
+ */
25
+ kernelOutput?: boolean | undefined;
26
+ }
3
27
  export type LoggerFunction = (level: LogLevel, message: string, data?: any) => void;
4
28
  export interface EngineOptions {
5
29
  /**
@@ -9,12 +33,13 @@ export interface EngineOptions {
9
33
  * supervisor actually uses (native/src/themisto/session_registry.cpp's
10
34
  * SessionOptions::kernelType) and which kernel executable it spawns.
11
35
  */
12
- kernelType?: 'r' | 'python';
13
- rHome?: string;
14
- rPath?: string;
15
- rLibs?: string;
36
+ kernelType?: 'r' | 'python' | undefined;
37
+ /** R installation (`R RHOME`). Found from $R_HOME, `R RHOME` or the Windows registry when omitted. */
38
+ rHome?: string | undefined;
39
+ rPath?: string | undefined;
40
+ rLibs?: string | undefined;
16
41
  /** Directory containing the pandoc binary, for bundled R installs that don't ship it on PATH. */
17
- pandocPath?: string;
42
+ pandocPath?: string | undefined;
18
43
  /**
19
44
  * Source directory of the 'hera' R package (packages/hera in this repo).
20
45
  * When set, Elara installs it via remotes::install_local() if it is
@@ -23,13 +48,13 @@ export interface EngineOptions {
23
48
  * already installed in the library -- install or update it with
24
49
  * `npm run hera:install`.
25
50
  */
26
- heraSrcPath?: string;
27
- /** Only used when kernelType is 'python' -- Carpo's equivalent of rHome. */
28
- pythonHome?: string;
51
+ heraSrcPath?: string | undefined;
52
+ /** Only used when kernelType is 'python' -- Carpo's equivalent of rHome. Found from $PYTHONHOME or `python3`/`python` on PATH when omitted. */
53
+ pythonHome?: string | undefined;
29
54
  /** Only used when kernelType is 'python' -- Carpo's equivalent of rPath. */
30
- pythonPath?: string;
55
+ pythonPath?: string | undefined;
31
56
  /** Only used when kernelType is 'python' -- not yet consulted by Carpo itself (see carpo::EnvironmentConfig). */
32
- venvPath?: string;
57
+ venvPath?: string | undefined;
33
58
  /**
34
59
  * Directory the kernel process starts in -- what `getwd()` (R) /
35
60
  * `os.getcwd()` (Python) report and what relative paths resolve against.
@@ -37,44 +62,44 @@ export interface EngineOptions {
37
62
  * (i.e. the calling process's), which is rarely what you want for a
38
63
  * notebook/project: set it to the project or document folder.
39
64
  */
40
- workingDirectory?: string;
41
- queueSize?: number;
42
- enableLogging?: boolean;
43
- enableMetrics?: boolean;
44
- logger?: LoggerFunction;
65
+ workingDirectory?: string | undefined;
66
+ queueSize?: number | undefined;
67
+ enableLogging?: boolean | undefined;
68
+ enableMetrics?: boolean | undefined;
69
+ logger?: LoggerFunction | undefined;
45
70
  }
46
71
  export type EngineState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
47
72
  export interface ExecutionOptions {
48
- silent?: boolean;
49
- storeHistory?: boolean;
50
- allowStdin?: boolean;
73
+ silent?: boolean | undefined;
74
+ storeHistory?: boolean | undefined;
75
+ allowStdin?: boolean | undefined;
51
76
  /**
52
77
  * When this execution fails, abort every execute() still waiting behind
53
78
  * it in the queue instead of running them (Jupyter's stop_on_error) --
54
79
  * their results come back with `aborted: true` and nothing having run.
55
80
  * Also forwarded to the kernel in the execute_request itself.
56
81
  */
57
- stopOnError?: boolean;
82
+ stopOnError?: boolean | undefined;
58
83
  /**
59
84
  * Expressions to evaluate in the kernel right after the code runs, as
60
85
  * {name: expression} (Jupyter's user_expressions). Only evaluated when
61
86
  * the code succeeded; each result -- or its own error -- comes back in
62
87
  * `ExecutionResult.userExpressions` under the same name.
63
88
  */
64
- userExpressions?: Record<string, string>;
89
+ userExpressions?: Record<string, string> | undefined;
65
90
  /**
66
91
  * Milliseconds to wait for the execution to finish before giving up
67
92
  * (default 30000; 0 = no timeout, for calls meant to run indefinitely
68
93
  * such as a Shiny app).
69
94
  */
70
- timeout?: number;
95
+ timeout?: number | undefined;
71
96
  /**
72
97
  * When the timeout fires, also send the kernel an interrupt (default
73
98
  * true) so it stops the code instead of carrying on with work no one is
74
99
  * waiting for -- which would otherwise block everything queued behind it.
75
100
  * Set false to leave the kernel running after a timeout.
76
101
  */
77
- interruptOnTimeout?: boolean;
102
+ interruptOnTimeout?: boolean | undefined;
78
103
  }
79
104
  /** One evaluated user expression: its rich value, or the error evaluating it raised. */
80
105
  export type UserExpressionResult = {
@@ -131,19 +156,19 @@ export interface ExecutionHistoryEntry {
131
156
  */
132
157
  export interface KernelHistoryOptions {
133
158
  /** Defaults to 'tail' -- the n most recent executions. */
134
- histAccessType?: 'tail' | 'range' | 'search';
159
+ histAccessType?: 'tail' | 'range' | 'search' | undefined;
135
160
  /** Include each entry's output alongside its input. Defaults to false -- the kernel doesn't actually record output today either way, so this currently only ever comes back empty. */
136
- output?: boolean;
137
- raw?: boolean;
161
+ output?: boolean | undefined;
162
+ raw?: boolean | undefined;
138
163
  /** Max entries to return ('tail'/'search'). Defaults to 100. */
139
- n?: number;
164
+ n?: number | undefined;
140
165
  /** 'range' only. */
141
- session?: number;
142
- start?: number;
143
- stop?: number;
166
+ session?: number | undefined;
167
+ start?: number | undefined;
168
+ stop?: number | undefined;
144
169
  /** 'search' only -- a glob pattern (*, ?). */
145
- pattern?: string;
146
- unique?: boolean;
170
+ pattern?: string | undefined;
171
+ unique?: boolean | undefined;
147
172
  }
148
173
  /**
149
174
  * One entry from a real history_reply: [session, line_number, input], or
@@ -179,13 +204,13 @@ export interface ShinyAppOptions {
179
204
  /** Directory containing the Shiny app (server.R/ui.R or app.R). */
180
205
  appDir: string;
181
206
  /** Defaults to an OS-assigned free port. */
182
- port?: number;
207
+ port?: number | undefined;
183
208
  /** Defaults to '127.0.0.1'. */
184
- host?: string;
209
+ host?: string | undefined;
185
210
  /** Defaults to false -- the caller decides how/where to display the app. */
186
- launchBrowser?: boolean;
211
+ launchBrowser?: boolean | undefined;
187
212
  /** Max time to wait for the app to start accepting connections, in ms. Defaults to 10000. */
188
- readyTimeout?: number;
213
+ readyTimeout?: number | undefined;
189
214
  /**
190
215
  * Environment variables to set (via Sys.setenv()) in the R session
191
216
  * before launching the app -- e.g. rmncah's app.R reads
@@ -195,7 +220,7 @@ export interface ShinyAppOptions {
195
220
  * that reads them after runApp() starts, since Sys.setenv() itself runs
196
221
  * synchronously right before it in the same execute() call.
197
222
  */
198
- env?: Record<string, string>;
223
+ env?: Record<string, string> | undefined;
199
224
  }
200
225
  export interface ShinyAppHandle {
201
226
  host: string;
@@ -1,11 +1,22 @@
1
- import type { LoggerFunction } from '../types/index.js';
1
+ import type { LogLevel, LoggerFunction, LogThreshold } from '../types/index.js';
2
2
  export declare function timestamp(): string;
3
+ /** True when a message at `level` passes `threshold`. */
4
+ export declare function passes(threshold: LogThreshold, level: LogLevel): boolean;
5
+ /** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
6
+ export declare function defaultLogLevel(env?: Record<string, string | undefined>): LogThreshold;
3
7
  export declare class Logger {
4
- private customLogger?;
5
- constructor(customLogger?: LoggerFunction);
8
+ private readonly customLogger;
9
+ private readonly threshold;
10
+ /**
11
+ * A custom logger receives every message (it does its own filtering, as it
12
+ * always has); `threshold` only limits what is printed to the console.
13
+ */
14
+ constructor(customLogger?: LoggerFunction, threshold?: LogThreshold);
15
+ private emit;
6
16
  trace(message: string, data?: any): void;
7
17
  debug(message: string, data?: any): void;
8
18
  info(message: string, data?: any): void;
19
+ notice(message: string, data?: any): void;
9
20
  warn(message: string, data?: any): void;
10
21
  error(message: string, error?: any): void;
11
22
  }
@@ -9,50 +9,58 @@ export function timestamp() {
9
9
  return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
10
10
  `${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
11
11
  }
12
+ const ORDER = { trace: 0, debug: 1, info: 2, notice: 3, warn: 4, error: 5, silent: 6 };
13
+ /** True when a message at `level` passes `threshold`. */
14
+ export function passes(threshold, level) {
15
+ return ORDER[level] >= ORDER[threshold];
16
+ }
17
+ /** The console threshold when none is given: $JOVIAN_LOG_LEVEL if it names one, else 'notice'. */
18
+ export function defaultLogLevel(env = process.env) {
19
+ const wanted = env.JOVIAN_LOG_LEVEL?.trim().toLowerCase();
20
+ return wanted && wanted in ORDER ? wanted : 'notice';
21
+ }
12
22
  export class Logger {
13
23
  customLogger;
14
- constructor(customLogger) {
24
+ threshold;
25
+ /**
26
+ * A custom logger receives every message (it does its own filtering, as it
27
+ * always has); `threshold` only limits what is printed to the console.
28
+ */
29
+ constructor(customLogger, threshold = defaultLogLevel()) {
15
30
  this.customLogger = customLogger;
31
+ this.threshold = threshold;
16
32
  }
17
- trace(message, data) {
33
+ emit(level, message, data) {
18
34
  if (this.customLogger) {
19
- this.customLogger('trace', message, data);
20
- }
21
- else {
22
- console.log(`${timestamp()} [trace] ${message}`, data ? data : '');
35
+ this.customLogger(level, message, data);
36
+ return;
23
37
  }
38
+ if (!passes(this.threshold, level))
39
+ return;
40
+ const print = level === 'warn' ? console.warn : level === 'error' ? console.error : console.log;
41
+ const line = `${timestamp()} [${level}] ${message}`;
42
+ if (data)
43
+ print(line, data);
44
+ else
45
+ print(line);
46
+ }
47
+ trace(message, data) {
48
+ this.emit('trace', message, data);
24
49
  }
25
50
  debug(message, data) {
26
- if (this.customLogger) {
27
- this.customLogger('debug', message, data);
28
- }
29
- else {
30
- console.log(`${timestamp()} [debug] ${message}`, data ? data : '');
31
- }
51
+ this.emit('debug', message, data);
32
52
  }
33
53
  info(message, data) {
34
- if (this.customLogger) {
35
- this.customLogger('info', message, data);
36
- }
37
- else {
38
- console.log(`${timestamp()} [info] ${message}`, data ? data : '');
39
- }
54
+ this.emit('info', message, data);
55
+ }
56
+ notice(message, data) {
57
+ this.emit('notice', message, data);
40
58
  }
41
59
  warn(message, data) {
42
- if (this.customLogger) {
43
- this.customLogger('warn', message, data);
44
- }
45
- else {
46
- console.warn(`${timestamp()} [warn] ${message}`, data ? data : '');
47
- }
60
+ this.emit('warn', message, data);
48
61
  }
49
62
  error(message, error) {
50
- if (this.customLogger) {
51
- this.customLogger('error', message, error);
52
- }
53
- else {
54
- console.error(`${timestamp()} [error] ${message}`, error ? error : '');
55
- }
63
+ this.emit('error', message, error);
56
64
  }
57
65
  }
58
66
  //# sourceMappingURL=logger.js.map