/ proof-of-concept / jupyterlite-numbl-kernel
Sign in
proof-of-concept / jupyterlite-numbl-kernel
jupyterlite-numbl-kernel / README.md
197 lines · 9.5 KBPreviewCodeBlameHistoryRaw
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
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 }
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- **No `input()`** (stdin): numbl's browser session has no stdin channel in
144 its worker protocol yet, so `input()` is unsupported. (This is now a
145 missing feature, not a headers limitation — the demo is cross-origin
146 isolated for interrupt, so the `SharedArrayBuffer` such a channel would
147 need is available.)
148- **Figures are per-cell** (like inline matplotlib): each cell renders the
149 figures its own commands produce; `hold on` does not span cells.
150- **Named function definitions are not supported inside cells** (a numbl
151 REPL limitation): anonymous functions work, and named functions belong in
152 `.m` files next to the notebook (see `demo/content/statsutils.m`), which
153 this kernel syncs into the session automatically.
154- The `.m`-file sync is **one-way**: deleting a `.m` file from the file
155 browser leaves its function defined until the kernel restarts, and files
156 written by cell code (e.g. via `fopen`) don't appear back in the file
157 browser.
158- **uihtml** components render display-only; the MATLAB↔HTML event bridge
159 is not wired into outputs yet.
160- numbl itself is not MATLAB: it covers a large, tested subset of the
161 language and toolbox surface. See [numbl](https://numbl.org) for scope.
163## Development
165Requires Python ≥ 3.9 and NodeJS ≥ 20, and `numbl >= 0.4.15` on npm — the
166first release with the browser cancellation API (`session.interrupt()` /
167`canInterrupt`) that cell interrupt uses. (numbl `0.4.14` added the
168incremental `session.execute` browser API this kernel is built on.) To
169develop against an unreleased numbl checkout, run `npm pack` there and point
170the `numbl` dependency at the tarball.
172```bash
173python -m venv .venv && source .venv/bin/activate
174pip install "jupyterlab~=4.6.0" "jupyterlite-core==0.8.1"
176jlpm install
177jlpm build # tsc + labextension (dev)
178pip install -e . # editable install, registers the labextension
180# Build and serve the demo site locally
181pip install -r demo/requirements.txt
182jupyter lite build --lite-dir demo --contents content --output-dir demo/_output
183node demo/inject-coi.mjs demo/_output # cross-origin isolation, for interrupt
184python -m http.server -d demo/_output 8000
185# then open http://localhost:8000 — a secure context, required for the
186# service worker (interrupt is a no-op without it)
187```
189`jlpm watch` rebuilds on change during development.
191## License
193Apache-2.0. Built on [numbl](https://numbl.org) and
194the [JupyterLite](https://github.com/jupyterlite/jupyterlite) kernel API;
195scaffolding follows the
196[jupyterlite/echo-kernel](https://github.com/jupyterlite/echo-kernel)
197template.
↑↓ move↵openescclose