# Run modules in a page The browser library is `javascript/src`: plain ES modules, no dependencies, no build step. A page imports it through an import map, runs modules in a Worker and renders what they return. The example is `examples/web/hello`: it counts an account's records (signed out), and sets your Statusphere status (signed in, [next page](oauth.md)). ## Serve the page `examples/web/hello/atlibs` links to `javascript/src` and `modules/` to the `cargo wasm` builds, so the directory serves on its own. ```bash run python3 -m http.server 8000 --bind 127.0.0.1 --directory examples/web/hello & ``` ```bash run curl -s localhost:8000/ | grep ' ``` Open http://127.0.0.1:8000/. On Windows, clone with `git config --global core.symlinks true` (and Developer Mode on), or the links check out as text files. ## The import map `examples/web/hello/index.html` ```html hello

hello

``` Outside this repo, `atlibs/` is `https://cdn.jsdelivr.net/npm/@publicdomainrelay/atlibs@0.5.0/src/` ([Use in a page](../start/page.md)). ## Load a module A local build: the wasm and its module.json. ```js run browser import { localModule } from 'atlibs/run-module/index.js' const modules = new URL('./examples/web/hello/modules/', location.href) const hello = await localModule(new URL('hello.wasm', modules), new URL('hello.json', modules)) console.log(JSON.stringify(hello.calls)) ``` ```output ["com.atproto.repo.listRecords"] ``` A published one: its record and blob, fetched from the publisher's PDS and checked against the CID. ```js run=network browser import { createPluginContext } from 'atlibs/plugin-context/index.js' import { loadModule } from 'atlibs/run-module/index.js' const ctx = createPluginContext({ id: 'docs' }) const statusphere = await loadModule(ctx, 'bobvmbuilder.bsky.social/statusphere') console.log(JSON.stringify(statusphere.calls)) ``` ```output ["com.atproto.repo.listRecords","com.atproto.repo.createRecord","com.atproto.repo.deleteRecord"] ``` These run as page modules in headless Chrome under the import map above. On a server, `createNodeContext` takes the place of `createPluginContext` ([Node server](../server/node.md)). ## Run an entry `runModule(ctx, module, entry, input, call)` runs the step loop. `ctx` is the page's PluginContext (storage, fetch, the wasm Worker); `call(nsid, request)` makes each XRPC call the module asks for. Here a stub answers them: ```js run browser import { createPluginContext } from 'atlibs/plugin-context/index.js' import { localModule, runModule } from 'atlibs/run-module/index.js' const ctx = createPluginContext({ id: 'docs' }) const modules = new URL('./examples/web/hello/modules/', location.href) const statusphere = await localModule(new URL('statusphere.wasm', modules), new URL('statusphere.json', modules)) const call = async (nsid, { params, input }) => { console.log(nsid) return nsid === 'com.atproto.repo.listRecords' ? { records: [] } : { uri: `at://${input.repo}/${input.collection}/3k`, cid: 'bafy' } } const { result, error } = await runModule(ctx, statusphere, 'set_status', { did: 'did:plc:alice', status: '🦋' }, call) console.log(JSON.stringify(error ?? result)) ``` ```output com.atproto.repo.listRecords com.atproto.repo.createRecord {"status":"🦋","unchanged":false,"uri":"at://did:plc:alice/xyz.statusphere.status/3k"} ``` It resolves to `{ entry, result, error?, events, calls }`. Module and call errors come back as `error`; `runModule` does not throw for them. `hostCall({ ctx })` makes the calls for real. With no session it makes public reads only; with an OAuth session it acts as the user ([Sign in from a page](oauth.md)). In the example, `main.js` creates the context and the modules and hands them to the cubit: `examples/web/hello/main.js` ```js const cubit = new HelloCubit({ ctx: createPluginContext({ id: 'hello' }), … hello: local('hello'), statusphere: local('statusphere'), }) ``` The cubit runs entries with them: `examples/web/hello/hello-cubit.js` ```js // Signed out: hostCall with no session makes public reads only. async count({ did, collection }) { this.emit({ count: { ...IDLE, status: 'busy' } }) this.emit({ count: await this.#run(this.#hello, 'count', { did, collection }, hostCall({ ctx: this.#ctx })) }) } ``` `examples/web/hello/hello-cubit.js` ```js async #run(module, entry, input, call) { try { const { result, error } = await runModule(this.#ctx, await module, entry, input, call) return error === undefined ? { status: 'done', result, error: null } : { status: 'error', result: null, error } } catch (error) { return { status: 'error', result: null, error: error.message } } } ``` ## Render the result State lives in a `Cubit`: `emit(patch)` replaces it, `subscribe(fn)` renders it. A component keeps its markup in an `.html` file of `