Contents#
This section describes how contents is integrated on a JupyterLite site and how it is used.
Access within the kernel#
By default, contents accessible via the default filebrowser is independent of the contents accessible within an execution kernel. Making the files available to the kernel may depend on how kernels are implemented.
Emscripten kernel#
Kernels using Emscripten (like pyodide
or xeus kernels) relies on the
Emscripten filesystem
to access their contents. For such case, @jupyterlite/services provides a
DriveFS
helper class which can be used to mount files in the Emscripten filesystem:
const mountpoint = '/drive';
const { FS, PATH, ERRNO_CODES } = /* provided by the emscripten module */;
const { baseUrl } = options;
const { DriveFS } = await import('@jupyterlite/services');
const driveFS = new DriveFS({
FS,
PATH,
ERRNO_CODES,
baseUrl, // Website base URL
driveName: 'my-drive', // Any name of your choosing
mountpoint,
});
FS.mkdir(mountpoint);
FS.mount(driveFS, {}, mountpoint);
FS.chdir(mountpoint);
After mounting the drive, the Jupyter Server contents (the one displayed in the
filebrowser) will be available within the kernel under the folder /drive.
The website base URL is required for the DriveFS to request its content from the main
application. Diving into the drive architecture will clarify that.
flowchart LR
subgraph main thread
direction TB
M[Main thread]
M --- C[Contents]
end
subgraph webworker
direction TB
M -.- K[Kernel]
K --- FS[Emscripten FS]
FS --- D[DriveFS]
end
S[Service worker] -.-|BroadcastChannel| M
D -.-|REST API| S
Three threads are at play when running a kernel inside JupyterLite:
The main thread: it executes the main user interface and knows about the filebrowser contents.
The kernel web worker: it executes the kernel (e.g. evaluate the code snippet from a notebook sent by the main thread). It mounts a
DriveFSinto the Emscripten filesystem.The service worker: it serves website assets from cache (to work offline). And it can also capture any other network requests.
Assuming the kernel executes the following python snippet code writing into a text file:
Path("dummy.txt").write_text("Writing on Emscripten filesystem")
Here is a simplification sequence of interaction happening to perform the filesystem operation:
sequenceDiagram
participant P as Python interpreter
participant F as Emscripten FS
participant D as DriveFS
participant S as Service Worker
participant M as Main thread
participant C as Contents manager
P->>+F: Write text into file
F->>+D: Call put
D->>+S: Send HTTP POST /api/drive
S->>+M: Broadcast message via channel
M->>+C: Call `save`
C-->>-M: None
M-->>-S: Response
S-->>-D: Response
D-->>-F: Return
F-->>-P: Done
When the code interacts with the filesystem, it interacts with the
Emscripten virtual filesystem.
This virtual filesystem allows classical code (like the Python snippet in this example)
to run with little or no changes. Moreover, the virtual filesystem enables developers to
provide their own mechanisms for handling filesystem I/O through a
filesystem API.
In the sequence diagram, we simplify the API interaction to a single put operation (in
reality, multiple calls happen when writing a file). As we’ve plugged in a custom drive
implementation DriveFS, the put resolution becomes the responsibility of that code.
The implemented logic initiates a POST HTTP request to the /api/drive endpoint with a
body describing the filesystem operation to be performed. In this case, it looks like:
{
"method": "put",
"path": "/dummy.txt",
"data": { "format": "text", "data": "Writing on Emscripten filesystem" }
}
This request is captured by the service worker (defined in the @jupyterlite/apputils
package). The service worker forwards the HTTP request to the main thread via a message
in a BroadcastChannel named /sw-api.v1. This message is received by the
ServiceWorkerManager that is instantiated in the plugin
@jupyterlite/application-extension:service-worker-manager. The wrapper has access to
the Jupyter contents manager to handle the request. For example, in the case of a put
operation, the save method of the contents manager will be called. The reply is then
propagated back (through the BroadcastChannel, then the network request, and so on) to
the Emscripten filesystem.
Since all open tabs for the same origin may listen to messages on the
BroadcastChannel, the request includes a unique identifier (browsingContextId) as
part of its payload to identify the browsing context (i.e., browser tab) where the
message originates. When the message comes back to the ServiceWorkerManager, it checks
this identifier to ensure the message is for the correct tab.
The need to use an HTTP request arises from the constraint of interfacing a synchronous API (the Emscripten filesystem) with an asynchronous API (the Jupyter contents manager).
This architecture makes it possible for lite kernels to access contents from a custom JupyterLab drive to support multiple sources of contents.