Documentation
¶
Overview ¶
Package wasmhost runs an app's HTTP services (typically Connect handlers) as WebAssembly inside a browser Web Worker, answering the same generated clients a server answers.
A page pushes files in by name before it asks anything, and the handler reads them through one fs.FS (Root) whose top-level directories are the mounts. The page side lives in tsappkit (@panyam/tsappkit/wasmhost), which starts the worker and hands the Connect transport a fetch that posts to it.
The rules it encodes, each with a failure behind it (goapplib issue 32):
- Never block inside a js.FuncOf callback. Every export returns a Promise and does its work on a goroutine, because a callback that waits on something the JS event loop must deliver (a fetch, a timer, a Promise) deadlocks the worker.
- Push the files in before the request, never pull them during it. fs.FS is synchronous and every browser source of bytes is asynchronous, so the files go into memory first. State a service builds for itself is different: it reads and writes a Cache (BrowserCache in the browser, over the Origin Private File System), whose calls wait on the browser and so are made from a handler's goroutine, which is allowed to wait, never from a js.FuncOf callback.
- Speak the app's existing wire protocol, so its generated clients don't know whether a server or the worker answered.
Host holds the platform-neutral part (mount table, handler, in-process requests) and builds and tests natively. Serve, ServeRebuild and Host.Export, which talk to JavaScript, are js && wasm only.
Index ¶
- Variables
- func CacheKey(parts ...[]byte) string
- type Cache
- type Chunk
- type Host
- func (h *Host) Add(name string, files map[string][]byte) error
- func (h *Host) Do(ctx context.Context, req Request) (Response, error)
- func (h *Host) DoStream(ctx context.Context, req Request, onChunk func(Chunk)) (res Response, err error)
- func (h *Host) Handle(handler http.Handler)
- func (h *Host) Mount(name string, files map[string][]byte) error
- func (h *Host) Namespace() string
- func (h *Host) Rebuild(build func(root fs.FS) (http.Handler, error)) error
- func (h *Host) Root() fs.FS
- func (h *Host) Unmount(name string) error
- type MemCache
- type Request
- type Response
Constants ¶
This section is empty.
Variables ¶
var ErrMiss = errors.New("wasmhost: not in the cache")
ErrMiss is what Get returns for a key the cache has nothing under.
var ErrNoCache = errors.New("wasmhost: no cache in this browser")
ErrNoCache is what BrowserCache's methods return when the browser has no storage to offer the worker (no Origin Private File System, or a worker started without one). A service treats it as a miss and rebuilds.
var ErrNoHandler = errors.New("wasmhost: no handler")
ErrNoHandler is what Do returns before Handle or Rebuild has given the host a handler.
Functions ¶
Types ¶
type Cache ¶
type Cache interface {
// Get returns a copy of the blob under key, or ErrMiss. A key that isn't one CacheKey could
// make is an error rather than a miss.
Get(ctx context.Context, key string) ([]byte, error)
// Put stores b under key, replacing what was there. A reader never sees half a blob: it gets
// the old one, the new one, or ErrMiss. The cache keeps its own copy of b.
Put(ctx context.Context, key string, b []byte) error
}
Cache keeps state a service builds from its inputs and would rather not build again, such as a parsed design, a model or an index, so a reload or a restarted worker can skip the rebuild. Keys name the inputs (see CacheKey), not the blob, so a service computes the key from a request before it has anything cached, and the same inputs always find the same blob. In the browser it's BrowserCache, over the Origin Private File System, which outlives a reload; natively it's DirCache or MemCache.
Every worker the page starts for the same namespace sees the same BrowserCache, so a throwaway worker (tsappkit's oneShotFetch) can build a result through a large peak and Put it, and the long-lived worker Gets it without ever holding the peak.
It's a cache, not a datastore: anything in it must be rebuildable from its inputs, because a miss, ErrNoCache, a browser clearing the origin's storage, or any other error just means the service builds it again. Data a user would lose belongs in the app's server-side storage.
Get and Put may wait on the browser, so call them from a handler or another goroutine, never from inside a js.FuncOf callback (see the package doc). Implementations are safe for concurrent use.
type Chunk ¶
Chunk is part of a response, sent when the handler calls Flush (http.Flusher). The first chunk of a response carries its status and header; later ones carry only body.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is a mount table and the handler that serves requests over it.
A Host is in one of two modes. After Handle it serves one fixed handler, which reads the live Root and so sees each mount change on its next read. After Rebuild it calls the build function again on every Mount and Unmount, for apps whose handler snapshots the files when it is made. All methods are safe for concurrent use.
func New ¶
New returns an empty host whose JavaScript exports, once Export or Serve installs them, live on globalThis[ns].
func (*Host) Add ¶
Add puts files into the mount name, keeping the files it already holds; a file at a path the mount already has replaces it. A missing mount is created, as Mount would. It takes ownership of the byte slices, and in rebuild mode it rebuilds once for the whole batch.
Add checks the whole batch before writing anything, so an error (an invalid path, a path that is already a directory, a file under a file) leaves the mount as it was. With a fixed handler (Handle), a request running at the same moment may see some of the batch and not the rest; in rebuild mode the new handler is built after the last file is in.
func (*Host) Do ¶
Do serves req in-process and returns the buffered response. An HTTP error status is a Response, not an error. The error is for no handler (ErrNoHandler), a failed rebuild, an unparseable request, or a handler panic, which is recovered so one bad request can't kill the worker.
func (*Host) DoStream ¶
func (h *Host) DoStream(ctx context.Context, req Request, onChunk func(Chunk)) (res Response, err error)
DoStream is Do for a handler that streams: each time it calls Flush, the bytes it has written since the last Flush go to onChunk, synchronously and in order, while the handler is still running. The returned Response holds only what was written after the last Flush; if anything was flushed, its Status and Header repeat the first chunk's. A nil onChunk buffers the whole response, as Do does. onChunk is called on the handler's goroutine and must not block on it.
func (*Host) Handle ¶
Handle makes handler the host's one fixed handler, leaving rebuild mode if it was in it.
func (*Host) Mount ¶
Mount replaces the mount name with files, keyed by slash-separated path. It takes ownership of the byte slices. In rebuild mode it returns the build's error, and the mount stays in place either way.
func (*Host) Rebuild ¶
Rebuild puts the host in rebuild mode: build is called now with Root and again after every Mount and Unmount, and its handler serves until the next change. If build fails, requests fail with its error until a later build succeeds. The error from this first build is returned.
type MemCache ¶
type MemCache struct {
// contains filtered or unexported fields
}
MemCache is a Cache in memory, for tests and for a service with nowhere to persist. It forgets everything when the process ends.