6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 1import type { Contents, KernelMessage } from '@jupyterlab/services';
3import { BaseKernel } from '@jupyterlite/services';
6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 4import type { IKernel } from '@jupyterlite/services';
6import { createNumblSession } from 'numbl/browser';
7import type { NumblSession } from 'numbl/browser';
9/** Mime type carrying a cell's plot instructions (see src/mime.tsx). */
10export const FIGURE_MIME = 'application/vnd.numbl.figure+json';
12/**
80cd178Update numbl links and tidy copyJeremy Magland 13 * A JupyterLite kernel that runs numbl (a MATLAB-syntax numerical
14 * computing engine) entirely in the browser.
16 * Each kernel owns one numbl session (a Web Worker managed by numbl):
17 * variables persist across cells, console output streams to the running
18 * cell, and figures are published as display_data with the numbl figure
19 * mime type. Restarting the kernel disposes the session, so the next
20 * execution boots a fresh workspace.
22 * Before every execution, `.m` files sitting next to the notebook (in the
23 * JupyterLite contents) are synced into the session, so named functions can
24 * be defined in the file browser, edited in the Jupyter editor, and called
80cd178Update numbl links and tidy copyJeremy Magland 25 * from cells. numbl rescans the working directory on each execution, so
6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 26 * edits apply on the next run. The sync is one-way and additive: deleting a
27 * `.m` file leaves its function defined until the kernel restarts.
29export class NumblKernel extends BaseKernel {
31 * @param options Standard kernel options.
32 * @param contents The (browser-side) contents manager used to read `.m`
33 * workspace files from the notebook's directory.
34 */
35 constructor(options: IKernel.IOptions, contents?: Contents.IManager) {
36 super(options);
37 this._contents = contents ?? null;
38 }
40 * Handle a kernel_info_request message.
41 */
42 async kernelInfoRequest(): Promise<KernelMessage.IInfoReplyMsg['content']> {
43 const content: KernelMessage.IInfoReply = {
44 implementation: 'numbl',
45 implementation_version: '0.1.0',
46 language_info: {
f44f6a3Rebrand numbl-first: this runs numbl (which uses MATLAB syntax)Jeremy Magland 47 // numbl uses MATLAB syntax; the octave/matlab modes drive
48 // highlighting in the notebook and on nbconvert export.
41c072cjupyterlite-numbl-kernel: MATLAB-syntax kernel for JupyterLiteJeremy Magland 49 codemirror_mode: 'octave',
50 file_extension: '.m',
51 mimetype: 'text/x-octave',
f44f6a3Rebrand numbl-first: this runs numbl (which uses MATLAB syntax)Jeremy Magland 52 name: 'numbl',
41c072cjupyterlite-numbl-kernel: MATLAB-syntax kernel for JupyterLiteJeremy Magland 53 nbconvert_exporter: 'script',
54 pygments_lexer: 'matlab',
55 version: 'numbl'
56 },
57 protocol_version: '5.3',
58 status: 'ok',
59 banner:
60 'numbl: MATLAB-syntax numerical computing, running in the browser',
61 help_links: [
62 {
63 text: 'numbl',
66 ]
67 };
68 return content;
69 }
71 /**
72 * Handle an `execute_request` message: run the cell against the numbl
73 * session's persistent workspace.
74 */
75 async executeRequest(
76 content: KernelMessage.IExecuteRequestMsg['content']
77 ): Promise<KernelMessage.IExecuteReplyMsg['content']> {
78 let session: NumblSession;
79 try {
80 session = await this._sessionPromise();
81 } catch (err) {
82 // Boot failure (e.g. the mip download was unreachable). Reset so a
83 // later cell can retry, and report the failure on this cell.
84 this._session = null;
85 const message = err instanceof Error ? err.message : String(err);
86 return this._errorReply(
87 'SessionError',
88 `Failed to start numbl: ${message}`
89 );
90 }
6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 92 await this._syncWorkspaceFiles(session);
41c072cjupyterlite-numbl-kernel: MATLAB-syntax kernel for JupyterLiteJeremy Magland 93 const result = await session.execute(content.code);
95 if (!result.ok) {
96 return this._errorReply('NumblError', result.error ?? 'Unknown error');
97 }
99 if (result.plotInstructions.length > 0) {
100 // Round-trip through JSON so the live render path sees exactly what a
101 // reloaded notebook sees (structured-clone NaNs become nulls; the
102 // renderer restores them).
103 const instructions = JSON.parse(JSON.stringify(result.plotInstructions));
104 this.displayData({
105 data: {
106 [FIGURE_MIME]: { version: 1, plotInstructions: instructions },
107 'text/plain':
80cd178Update numbl links and tidy copyJeremy Magland 108 '<numbl figure (install jupyterlite-numbl-kernel to render)>'
110 metadata: {}
111 });
112 }
114 return {
115 status: 'ok',
116 execution_count: this.executionCount,
117 user_expressions: {}
118 };
119 }
121 /**
122 * Handle a `complete_request` message. Completion is not implemented.
123 */
124 async completeRequest(
125 content: KernelMessage.ICompleteRequestMsg['content']
126 ): Promise<KernelMessage.ICompleteReplyMsg['content']> {
127 return {
128 status: 'ok',
129 matches: [],
130 cursor_start: content.cursor_pos,
131 cursor_end: content.cursor_pos,
132 metadata: {}
133 };
134 }
136 /**
137 * Handle an `inspect_request` message. Inspection is not implemented.
138 */
139 async inspectRequest(
140 content: KernelMessage.IInspectRequestMsg['content']
141 ): Promise<KernelMessage.IInspectReplyMsg['content']> {
142 return { status: 'ok', found: false, data: {}, metadata: {} };
143 }
145 /**
146 * Handle an `is_complete_request` message: treat every submission as a
f44f6a3Rebrand numbl-first: this runs numbl (which uses MATLAB syntax)Jeremy Magland 147 * complete numbl statement (the console runs on Enter).
149 async isCompleteRequest(
150 content: KernelMessage.IIsCompleteRequestMsg['content']
151 ): Promise<KernelMessage.IIsCompleteReplyMsg['content']> {
152 return { status: 'complete' };
153 }
155 /**
156 * Handle a `comm_info_request` message. Comms are not implemented.
157 */
158 async commInfoRequest(
159 content: KernelMessage.ICommInfoRequestMsg['content']
160 ): Promise<KernelMessage.ICommInfoReplyMsg['content']> {
161 return { status: 'ok', comms: {} };
162 }
164 /**
165 * Send an `input_reply` message. stdin is not supported.
166 */
167 inputReply(content: KernelMessage.IInputReplyMsg['content']): void {
168 // no-op
169 }
171 async commOpen(msg: KernelMessage.ICommOpenMsg): Promise<void> {
172 // no-op
173 }
175 async commMsg(msg: KernelMessage.ICommMsgMsg): Promise<void> {
176 // no-op
177 }
179 async commClose(msg: KernelMessage.ICommCloseMsg): Promise<void> {
180 // no-op
181 }
183 /**
184 * Dispose the kernel and its numbl session (worker).
185 */
186 dispose(): void {
187 if (this.isDisposed) {
188 return;
189 }
190 void this._session?.then(s => s.dispose()).catch(() => undefined);
191 this._session = null;
192 super.dispose();
193 }
9ff4e5bRestructure demo into a full guided tour with OOP and namespacesJeremy Magland 196 * Sync `.m` files from the notebook's directory (recursively) into the
197 * session VFS, preserving the relative layout. Recursing matters for
198 * MATLAB's folder-based constructs: `+namespace/`, `@class/`, and
199 * `private/` folders all live in subdirectories and must reach the
200 * session at the right paths. numbl rescans its working directory on
201 * every execution, so a file written here is callable on this same
202 * execute() call, and an edit in the Jupyter editor takes effect on the
203 * next run. Best-effort: a contents-manager error (e.g. no browser drive
204 * mounted) just skips the sync rather than failing the cell.
206 private async _syncWorkspaceFiles(session: NumblSession): Promise<void> {
207 if (!this._contents) {
208 return;
209 }
9ff4e5bRestructure demo into a full guided tour with OOP and namespacesJeremy Magland 210 const root = this.location;
211 const rootPrefix = root ? root.replace(/\/$/, '') + '/' : '';
213 const syncDir = async (dir: string): Promise<void> => {
214 let listing: Contents.IModel;
9ff4e5bRestructure demo into a full guided tour with OOP and namespacesJeremy Magland 216 listing = await this._contents!.get(dir, { content: true });
6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 217 } catch {
9ff4e5bRestructure demo into a full guided tour with OOP and namespacesJeremy Magland 220 const entries = Array.isArray(listing.content) ? listing.content : [];
221 for (const entry of entries as Contents.IModel[]) {
222 if (entry.type === 'directory') {
223 await syncDir(entry.path);
224 continue;
225 }
226 if (entry.type !== 'file' || !entry.name.endsWith('.m')) {
227 continue;
228 }
229 if (this._syncedMTimes.get(entry.path) === entry.last_modified) {
230 continue;
231 }
232 try {
233 const file = await this._contents!.get(entry.path, {
234 content: true,
235 type: 'file',
236 format: 'text'
237 });
238 // Write at the path relative to the notebook directory so that
239 // +pkg/@class/private layouts land correctly under the session root.
240 const rel = entry.path.startsWith(rootPrefix)
241 ? entry.path.slice(rootPrefix.length)
242 : entry.name;
243 session.writeFile(rel, String(file.content));
244 this._syncedMTimes.set(entry.path, entry.last_modified);
245 } catch {
246 // Skip this file; other workspace files still sync.
247 }
248 }
249 };
251 await syncDir(root);
255 * Boot the numbl session lazily on first use, so creating the kernel is
256 * instant and boot progress (mip download, cached-package restore) streams
257 * into the first executed cell.
258 */
259 private _sessionPromise(): Promise<NumblSession> {
260 this._session ??= createNumblSession({
261 onOutput: text => this.stream({ name: 'stdout', text }),
262 onProgress: message =>
263 this.stream({ name: 'stdout', text: `[numbl] ${message}\n` })
264 });
265 return this._session;
266 }
268 private _errorReply(
269 ename: string,
270 formatted: string
271 ): KernelMessage.IExecuteReplyMsg['content'] {
272 const traceback = formatted.split('\n');
273 const evalue = traceback[0] ?? '';
274 this.publishExecuteError({ ename, evalue, traceback });
275 return {
276 status: 'error',
277 execution_count: this.executionCount,
278 ename,
279 evalue,
280 traceback
281 };
282 }
284 private _session: Promise<NumblSession> | null = null;
6b31a9aSync .m workspace files from the notebook directory into the sessionJeremy Magland 285 private readonly _contents: Contents.IManager | null;
286 private readonly _syncedMTimes = new Map<string, string>();