Help us shape the future of Go! Take the 2026 Go Developer Survey

bottle

package module
v0.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: MIT Imports: 1 Imported by: 1

README

bottle

A Linux-shaped bottle for wasm ships: the OS layer that lets Go programs run in a browser tab the way they run on a host.

Live demo — bottle is the layer underneath rather than a thing to look at, so its demos are the programs that stand on it: shipwright runs cmd/compile and cmd/link against bottle's jsfs, and shipyard is a whole workstation on it — a shell, go build, processes and pipes, and a Go server the in-tab browser fetches from over bottle's vnet. Between them they exercise all three primitives: the filesystem, the network and the process layer.

Big Go programs assume an operating system: a filesystem under /, config in /etc, localhost ports to listen on and dial. A browser tab has none of that, and Go's wasm_exec.js stubs it all with ENOSYS. bottle fills the gap with a few page-global primitives:

  • jsfs.js — an in-memory filesystem, laid out like a Linux root, installed as globalThis.fs / globalThis.process (the exact contract syscall/fs_js.go calls). Every wasm instance on the page shares it: one program writes /etc/foo.conf, a shell in another instance cats it.

  • vnet.js — a virtual loopback network: a page-global port table with in-memory byte pipes. An http.Server or RPC listener bound to 127.0.0.1:<port> in one instance is dialed from another — or from page JS via vnet.httpFetch(port, method, path, body).

  • proc.js — a process layer: proc.spawn({argv, env, cwd, stdio}) instantiates another wasm module from jsfs as a child that shares the page's fs and vnet, with per-process stdio, a process id, kill() and an exit promise. A tab has no fork/exec, but instantiating a wasm module IS spawning a process — this makes that a primitive, the third leg under a Unix-shaped orchestrator (a shell, and eventually go build). It also does the bookkeeping a process layer owes the other two legs: an exited child's vnet claims are released for it (a dead program cannot unlisten its ports, and a zombie entry fakes liveness and holds the port against a rebind), and proc.registerURL(path, url) binds a program too large to hold as bytes to its path, streaming it into the compiler on first spawn instead of through jsfs. opts.tail keeps a per-process stderr ring in proc.tails, so a crashed child's last words survive it. The proc subpackage is its Go adapter (proc.Command(...).Run(), os/exec-shaped).

  • fsbridge.js — the same filesystem, reachable from a Worker. proc.spawnWorker runs a child off the main thread, so a long compile no longer freezes the tab, and several can run at once. jsfs stays on the thread that owns it; the child blocks in Atomics.wait while the page answers, which is the synchronous syscall contract Go's runtime requires. Needs cross-origin isolation (COOP/COEP) for SharedArrayBuffer; without it spawnWorker refuses and callers fall back to proc.spawn.

The vnet Go subpackage is the adapter: vnet.Listen / vnet.DialTimeout are exactly net.Listen / net.DialTimeout on native builds, and route loopback addresses through the page table under js/wasm — so the same code serves on a host and in a tab.

Use

Embed and serve the scripts ahead of any wasm module (Go captures globalThis.fs when an instance starts):

import "github.com/0magnet/bottle"

page := append(bottle.JSFS(), bottle.VNetJS()...) // then your own JS

Seed application layout after jsfs.js runs, from a page script:

jsfs.mkdirp('/opt/myapp');
jsfs.writeFile('/etc/myapp.conf', 'KEY=value\n');

Hand a subtree to another filesystem, such as a remote one, with jsfs.mount(prefix, provider). The provider answers the fs calls under the prefix with node-style callbacks, whenever it is ready; jsfs.js documents them where the mount layer is defined. jsfs.unmount(prefix) detaches it.

And in the program, listen/dial loopback through the adapter:

import "github.com/0magnet/bottle/vnet"

l, err := vnet.Listen("tcp", "127.0.0.1:8000") // page-shared under js/wasm
c, err := vnet.DialTimeout("tcp", "127.0.0.1:8000", 5*time.Second)

Notes:

  • vnet conns honor SetReadDeadline including waking an already-blocked Read — net/http's response teardown depends on that.
  • In-memory only: page lifetime, no persistence, single JS realm (a MessagePort bridge across Workers can layer on later).

Grown in skycoin/skywire, where the whole skywire binary runs in the docs-site terminal: skywire autoconfig in one terminal starts a visor in the foreground, skywire cli in a second terminal dials its RPC over vnet, and a nested browser fetches the hypervisor UI from http://127.0.0.1:8001 — all inside one tab.

Used by

  • m2 — the store's own web server runs in the tab (serve in the /desk terminal), listening on the vnet loopback; the netscrape browser's second tab reads it back.
  • shipwright — the Go toolchain compiling, linking and running programs against jsfs, three instances on one in-memory disk.
  • websh and tuiwasm load the layer on their pages, so every wasm instance there shares one filesystem and localhost.

Dependency Graph

Made with goda:

# GOOS=js: the import edges of a wasm program live in js/wasm-tagged
# files and are invisible to a host-context run
GOOS=js GOARCH=wasm go run github.com/loov/goda@latest graph github.com/0magnet/bottle/... | dot -Tsvg -o docs/bottle-goda-graph.svg

Dependency Graph

Lines of Code

Made with gocloc (excludes vendor/, node_modules/, .git/):

gocloc --not-match-d='(vendor|node_modules|\.git)' .
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
JavaScript                       4             86            288           1020
Go                               6             60            122            398
YAML                             1              0              7             98
Markdown                         2             22              0             75
HTML                             1              0              2             18
-------------------------------------------------------------------------------
TOTAL                           14            168            419           1609
-------------------------------------------------------------------------------

Documentation

Overview

Package bottle is the OS layer for running Go programs in a browser tab the way they run on Linux — the bottle a wasm ship sails in.

Two page-global primitives, one JS file each:

jsfs.js — an in-memory Linux-layout filesystem installed as
          globalThis.fs / globalThis.process. Go's js/wasm runtime routes
          the entire os package through this contract, so every wasm
          instance on the page shares one POSIX-ish root: one program
          writes /etc files, another reads them back.
fsbridge.js — the same filesystem, reachable from a Worker: a child blocks
          in Atomics.wait while the page answers, which is the synchronous
          syscall contract Go's runtime requires. See proc.spawnWorker.
vnet.js — a virtual loopback network: a page-global port table with
          in-memory byte pipes, so an instance that LISTENS on
          127.0.0.1:<port> can be DIALED from another instance — or from
          page JS via vnet.httpFetch.

The vnet subpackage is the Go adapter: vnet.Listen / vnet.DialTimeout are net.Listen / net.DialTimeout on native builds, and route loopback addresses through the page table under js/wasm.

Load order matters: both scripts must run BEFORE a Go wasm instance starts (Go captures globalThis.fs at instance start). Application-specific filesystem layout is the page's job, via jsfs.mkdirp / jsfs.writeFile, in a script loaded after jsfs.js.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func COIRegisterJS

func COIRegisterJS() []byte

COIRegisterJS returns coi-register.js — the page half of COISWJS. It registers the worker and reloads ONCE after it takes control, because the first navigation came from the server without the headers. Load it before anything that wants SharedArrayBuffer, and keep tolerating crossOriginIsolated being false on first paint.

func COISWJS

func COISWJS() []byte

COISWJS returns coi-sw.js — the service worker that makes a page cross-origin isolated on a host that cannot set headers. It re-serves every response with COOP, COEP and CORP attached, which is what SharedArrayBuffer requires, and therefore what FSBridgeJS and proc.spawnWorker require.

Serve it at the page's own directory (the scope must cover the page) and load COIRegisterJS from the page. It does not conflict with vnet-sw.js: that registers at the narrower /vnet/ scope and keeps serving those URLs, which are same-origin, and COEP require-corp only demands CORP of CROSS-origin subresources.

func FSBridgeJS

func FSBridgeJS() []byte

FSBridgeJS returns fsbridge.js — a blocking, synchronous view of the page's jsfs for code running in a Worker. proc.spawnWorker uses it to run a child off the main thread on the parent's filesystem, so a long build no longer freezes the tab. Serve it beside proc.js; the worker loads it by URL.

Requires cross-origin isolation (COOP/COEP) for SharedArrayBuffer. Without it spawnWorker refuses and callers fall back to proc.spawn.

func JSFS

func JSFS() []byte

JSFS returns jsfs.js — the globalThis.fs / globalThis.process filesystem.

func ProcJS

func ProcJS() []byte

ProcJS returns proc.js — the globalThis.proc process layer: spawn another wasm module from jsfs as a child that shares the page's fs and vnet, with per-process stdio, a process id, kill(), and an exit promise. A program too large to hold as bytes is bound to its path with proc.registerURL and streamed straight into the compiler. Load it after jsfs.js; wasm_exec.js may be loaded ahead of it or left to the first spawn. See the proc subpackage for the Go adapter.

func VNetJS

func VNetJS() []byte

VNetJS returns vnet.js — the globalThis.vnet virtual loopback network.

func VNetSWJS

func VNetSWJS() []byte

VNetSWJS returns vnet-sw.js — the service worker that turns virtual loopback ports into real same-origin URLs (/vnet/<port>/…), so iframes can load in-page servers with native resolution. Serve it at the page's directory as vnet-sw.js and call vnet.enableSW() from the page.

Types

This section is empty.

Directories

Path Synopsis
example
proc/child command
child: reads all of stdin, writes it back upper-cased, exits 3.
child: reads all of stdin, writes it back upper-cased, exits 3.
proc/parent command
parent: spawns /bin/child with a piped stdin, collects its stdout, and reports what came back and the exit code — all inside one browser tab, two wasm instances, one shared filesystem.
parent: spawns /bin/child with a piped stdin, collects its stdout, and reports what came back and the exit code — all inside one browser tab, two wasm instances, one shared filesystem.
Package proc is the Go adapter for bottle's process layer (proc.js): spawn another wasm program from the page filesystem as a child that shares this tab's fs and vnet, wire its stdio, and wait for it to exit.
Package proc is the Go adapter for bottle's process layer (proc.js): spawn another wasm program from the page filesystem as a child that shares this tab's fs and vnet, wire its stdio, and wait for it to exit.
Package vnet vnet/vnet_native.go
Package vnet vnet/vnet_native.go

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL