# jupyterlite-numbl-kernel
Run [**numbl**](https://numbl.org), numerical computing with **MATLAB
syntax**, in [JupyterLite](https://jupyterlite.readthedocs.io/) notebooks,
**entirely in the browser**, with no server, no kernel process, and nothing
for the reader to install.
numbl is an open-source numerical-computing engine, written in TypeScript,
that uses MATLAB syntax, so `.m` code runs unchanged. This kernel runs a
numbl session in a Web Worker in the page: variables persist across cells,
console output streams into the running cell, plots render as figures in cell
outputs (including interactive 3-D), the `mip` package manager can install
numbl packages from GitHub, and `.m` files next to the notebook are part of
the workspace (named functions, called from cells). Everything runs
client-side.
**Demo site:**
(deployed
from this repo via GitHub Pages; see `.github/workflows/deploy.yml`)
## Why
This kernel is a proof of concept that a numbl notebook can be a static web
page: hostable on GitHub Pages, shareable as a link, and executable by anyone
with a browser. Because numbl uses MATLAB syntax and needs no MATLAB/Octave
install (or any server process), the same `.m` code that would otherwise
require a licensed product behind a server just runs in the tab.
## How it works
Three small pieces, all in this repo:
- **Kernel** (`src/kernel.ts`): implements JupyterLite's `BaseKernel` from
`@jupyterlite/services`. Before each `execute_request`, `.m` files in the
notebook's directory (read via the JupyterLite contents manager) are
synced into the numbl session; the cell source then runs against the
session's persistent workspace (`createNumblSession` /
`session.execute` from `numbl/browser`, a Web Worker that numbl manages).
Output streams back as `stream` messages; the run's plot instructions are
published as `display_data` with the mime type
`application/vnd.numbl.figure+json`.
- **Figure renderer** (`src/mime.tsx`): a JupyterLab mime renderer for that
mime type: it replays the instructions through numbl's figures reducer and
mounts numbl's React `FigureView` (from `numbl/graphics`). Outputs are
plain JSON, so saved notebooks re-render wherever the extension is
installed.
- **Kernel registration** (`src/index.ts`): registers the kernelspec with
JupyterLite's `IKernelSpecs`.
## Build a site with it
```bash
pip install jupyterlite-core jupyterlite-numbl-kernel
jupyter lite build --contents my-notebooks --output-dir dist
# dist/ is a static site; serve it anywhere
```
The `demo/` directory in this repo contains the demo site sources
(notebooks + requirements); `.github/workflows/deploy.yml` builds and
deploys it to GitHub Pages. `demo/content/` is a numbered walkthrough: an
intro (with plotting), a systematic language tour (data types, matrices,
control flow, linear algebra, data structures, numerical methods,
plotting), the advanced MATLAB object model (classes and OOP, namespaces
and packages), and finally installing packages with `mip`. The class and
package `.m` files live next to the notebooks (e.g. `Vec2.m`, `+geom/`,
`@Poly/`) and are synced into the session recursively.
### Always-fresh content (demo choice)
JupyterLite caches content in two layers that both defeat redeploys:
1. It copies notebooks into the browser's IndexedDB on first visit, and that
local copy then wins over the deployed files **even after a redeploy**.
2. Its **service worker** is an offline caching proxy for the app itself and
for content files, so it can keep serving the old app and old notebooks
after a redeploy until it happens to update.
Since this is a demo, `demo/jupyter-lite.json` neutralizes both: it uses
JupyterLite's in-memory storage (so every reload re-seeds the latest
deployed notebooks) and disables the service-worker plugin (which the numbl
kernel doesn't need, since it reads content on the main thread, not via the
service worker's kernel drive):
```json
{
"jupyter-config-data": {
"enableMemoryStorage": true,
"contentsStorageDrivers": ["memoryStorageDriver"],
"settingsStorageDrivers": ["memoryStorageDriver"],
"workspacesStorageDrivers": ["memoryStorageDriver"],
"disabledExtensions": [
"@jupyterlite/application-extension:service-worker-manager"
]
}
}
```
The trade-off is that a visitor's edits live only for the session and are
discarded on reload. A visitor who loaded the site **before** the service
worker was disabled still has it registered and must clear browser data
(or `Help > Clear Browser Data`) once to get past it. For a real deployment where users should keep their
work, omit these keys (the default persistent storage) and bump
`contentsStorageName` when you want to force-refresh shipped content.
numbl's own package cache (installed via `mip`) lives in a separate
IndexedDB store and is unaffected, so `mip`-installed packages still
persist across reloads.
### Cross-origin isolation (for interrupt)
Cell interrupt needs a `SharedArrayBuffer`, which browsers only expose on a
**cross-origin-isolated** page (served with `Cross-Origin-Opener-Policy:
same-origin` and `Cross-Origin-Embedder-Policy: credentialless`). GitHub Pages
serves static files and can't set those headers, so the demo synthesizes them
client-side with a small service worker, `demo/coi-serviceworker.js` (based on
[coi-serviceworker](https://github.com/niccokunzmann/coi-serviceworker)). It
only rewrites **same-origin** responses, so numbl's cross-origin `mip` download
from its GitHub release still works.
`demo/inject-coi.mjs` runs after `jupyter lite build`: it copies the worker to
the site root and adds a `