1# jupyterlite-numbl-kernel
3Run [**numbl**](https://numbl.org), numerical computing with **MATLAB
4syntax**, in [JupyterLite](https://jupyterlite.readthedocs.io/) notebooks,
5**entirely in the browser**, with no server, no kernel process, and nothing
6for the reader to install.
8numbl is an open-source numerical-computing engine, written in TypeScript,
9that uses MATLAB syntax, so `.m` code runs unchanged. This kernel runs a
10numbl session in a Web Worker in the page: variables persist across cells,
11console output streams into the running cell, plots render as figures in cell
12outputs (including interactive 3-D), the `mip` package manager can install
13numbl packages from GitHub, and `.m` files next to the notebook are part of
14the workspace (named functions, called from cells). Everything runs
15client-side.
17**Demo site:**
18<https://concept-collection.github.io/jupyterlite-numbl-kernel/> (deployed
19from this repo via GitHub Pages; see `.github/workflows/deploy.yml`)
21## Why
23This kernel is a proof of concept that a numbl notebook can be a static web
24page: hostable on GitHub Pages, shareable as a link, and executable by anyone
25with a browser. Because numbl uses MATLAB syntax and needs no MATLAB/Octave
26install (or any server process), the same `.m` code that would otherwise
27require a licensed product behind a server just runs in the tab.
29## How it works
31Three small pieces, all in this repo:
33- **Kernel** (`src/kernel.ts`): implements JupyterLite's `BaseKernel` from
34 `@jupyterlite/services`. Before each `execute_request`, `.m` files in the
35 notebook's directory (read via the JupyterLite contents manager) are
36 synced into the numbl session; the cell source then runs against the
37 session's persistent workspace (`createNumblSession` /
38 `session.execute` from `numbl/browser`, a Web Worker that numbl manages).
39 Output streams back as `stream` messages; the run's plot instructions are
40 published as `display_data` with the mime type
41 `application/vnd.numbl.figure+json`.
42- **Figure renderer** (`src/mime.tsx`): a JupyterLab mime renderer for that
43 mime type: it replays the instructions through numbl's figures reducer and
44 mounts numbl's React `FigureView` (from `numbl/graphics`). Outputs are
45 plain JSON, so saved notebooks re-render wherever the extension is
46 installed.
47- **Kernel registration** (`src/index.ts`): registers the kernelspec with
48 JupyterLite's `IKernelSpecs`.
50## Build a site with it
52```bash
53pip install jupyterlite-core jupyterlite-numbl-kernel
54jupyter lite build --contents my-notebooks --output-dir dist
55# dist/ is a static site; serve it anywhere
56```
58The `demo/` directory in this repo contains the demo site sources
59(notebooks + requirements); `.github/workflows/deploy.yml` builds and
60deploys it to GitHub Pages. `demo/content/` is a numbered walkthrough: an
61intro (with plotting), a systematic language tour (data types, matrices,
62control flow, linear algebra, data structures, numerical methods,
63plotting), the advanced MATLAB object model (classes and OOP, namespaces
64and packages), and finally installing packages with `mip`. The class and
65package `.m` files live next to the notebooks (e.g. `Vec2.m`, `+geom/`,
66`@Poly/`) and are synced into the session recursively.
68### Always-fresh content (demo choice)
70JupyterLite caches content in two layers that both defeat redeploys:
721. It copies notebooks into the browser's IndexedDB on first visit, and that
73 local copy then wins over the deployed files **even after a redeploy**.
742. Its **service worker** is an offline caching proxy for the app itself and
75 for content files, so it can keep serving the old app and old notebooks
76 after a redeploy until it happens to update.
78Since this is a demo, `demo/jupyter-lite.json` neutralizes both: it uses
79JupyterLite's in-memory storage (so every reload re-seeds the latest
80deployed notebooks) and disables the service-worker plugin (which the numbl
81kernel doesn't need, since it reads content on the main thread, not via the
82service worker's kernel drive):
84```json
85{
86 "jupyter-config-data": {
87 "enableMemoryStorage": true,
88 "contentsStorageDrivers": ["memoryStorageDriver"],
89 "settingsStorageDrivers": ["memoryStorageDriver"],
90 "workspacesStorageDrivers": ["memoryStorageDriver"],
91 "disabledExtensions": [
92 "@jupyterlite/application-extension:service-worker-manager"
93 ]
94 }
95}
96```
98The trade-off is that a visitor's edits live only for the session and are
99discarded on reload. A visitor who loaded the site **before** the service
100worker was disabled still has it registered and must clear browser data
101(or `Help > Clear Browser Data`) once to get past it. For a real deployment where users should keep their
102work, omit these keys (the default persistent storage) and bump
103`contentsStorageName` when you want to force-refresh shipped content.
104numbl's own package cache (installed via `mip`) lives in a separate
105IndexedDB store and is unaffected, so `mip`-installed packages still
106persist across reloads.
108### Cross-origin isolation (for interrupt)
110Cell interrupt needs a `SharedArrayBuffer`, which browsers only expose on a
111**cross-origin-isolated** page (served with `Cross-Origin-Opener-Policy:
112same-origin` and `Cross-Origin-Embedder-Policy: credentialless`). GitHub Pages
113serves static files and can't set those headers, so the demo synthesizes them
114client-side with a small service worker, `demo/coi-serviceworker.js` (based on
115[coi-serviceworker](https://github.com/niccokunzmann/coi-serviceworker)). It
116only rewrites **same-origin** responses, so numbl's cross-origin `mip` download
117from its GitHub release still works.
119`demo/inject-coi.mjs` runs after `jupyter lite build`: it copies the worker to
120the site root and adds a `<script>` registering it to every generated page's
121`<head>` (the deploy workflow does this automatically). The worker adds no
122caching — it only injects headers — so it doesn't undermine the always-fresh
123content choice above. Service workers require a secure context, so view the
124site over `https://` or `http://localhost` / `http://127.0.0.1`; an `http://`
125LAN IP or an embedded/preview browser has no service worker, and interrupt
126degrades to a no-op there.
128## Limitations (proof of concept)
130- **Interrupt** works cooperatively: the Stop button aborts the running cell
131 at the next loop iteration or function/builtin call, reports it as a
132 `KeyboardInterrupt`, and leaves the workspace intact (variables from before
133 the cell survive). It relies on a `SharedArrayBuffer` cancel flag that numbl
134 polls during execution, so the page must be **cross-origin isolated**
135 (`COOP`/`COEP`). Plain GitHub Pages can't set those headers, so the demo
136 ships a `coi-serviceworker` that synthesizes them (see [Cross-origin
137 isolation](#cross-origin-isolation-for-interrupt)). Two caveats: on a
138 deployment that is **not** cross-origin isolated the interrupt silently
139 falls back to a no-op (a runaway cell can then only be stopped by
140 restarting the kernel), and a **tight loop with no function or builtin
141 calls** (e.g. `while true; x = x + 1; end`) is JIT-compiled straight
142 through with no cancellation checkpoint, so it too needs a restart.
143- **`input()`** (stdin) works: a cell that calls `input()` prompts in the
144 notebook and blocks until you answer, then resumes with your entry —
145 numeric (`n = input('n? ')`) or, with `input(prompt, 's')`, a string. Like
146 interrupt it uses a `SharedArrayBuffer`, so it needs the page to be
147 cross-origin isolated; on a non-isolated deployment `input()` raises
148 ("input() is not available in this environment") rather than prompting.
149- **Figures are per-cell** (like inline matplotlib): each cell renders the
150 figures its own commands produce; `hold on` does not span cells.
151- **Named function definitions are not supported inside cells** (a numbl
152 REPL limitation): anonymous functions work, and named functions belong in
153 `.m` files next to the notebook (see `demo/content/statsutils.m`), which
154 this kernel syncs into the session automatically.
155- The `.m`-file sync is **one-way**: deleting a `.m` file from the file
156 browser leaves its function defined until the kernel restarts, and files
157 written by cell code (e.g. via `fopen`) don't appear back in the file
158 browser.
159- **uihtml** components render display-only; the MATLAB↔HTML event bridge
160 is not wired into outputs yet.
161- numbl itself is not MATLAB: it covers a large, tested subset of the
162 language and toolbox surface. See [numbl](https://numbl.org) for scope.
164## Development
166Requires Python ≥ 3.9 and NodeJS ≥ 20, and `numbl >= 0.4.16` on npm — the
167release with the browser stdin API (`onInputRequest` / `provideInput` /
168`canInput`) behind `input()`; it also carries the `session.interrupt()` /
169`canInterrupt` cancellation API for cell interrupt (added in 0.4.15) and the
170incremental `session.execute` browser API this kernel is built on (0.4.14).
171To develop against an unreleased numbl checkout, run `npm pack` there and
172point the `numbl` dependency at the tarball (as `package.json` does while
1730.4.16 is being published).
175```bash
176python -m venv .venv && source .venv/bin/activate
177pip install "jupyterlab~=4.6.0" "jupyterlite-core==0.8.1"
179jlpm install
180jlpm build # tsc + labextension (dev)
181pip install -e . # editable install, registers the labextension
183# Build and serve the demo site locally
184pip install -r demo/requirements.txt
185jupyter lite build --lite-dir demo --contents content --output-dir demo/_output
186node demo/inject-coi.mjs demo/_output # cross-origin isolation, for interrupt
187python -m http.server -d demo/_output 8000
188# then open http://localhost:8000 — a secure context, required for the
189# service worker (interrupt is a no-op without it)
190```
192`jlpm watch` rebuilds on change during development.
194## License
196Apache-2.0. Built on [numbl](https://numbl.org) and
197the [JupyterLite](https://github.com/jupyterlite/jupyterlite) kernel API;
198scaffolding follows the
199[jupyterlite/echo-kernel](https://github.com/jupyterlite/echo-kernel)
200template.