Process model
Startup, development reloads, graceful shutdown, and app-owned resources.
Murasaki separates the native event loop from Node's event loop. In a packaged application the Rust launcher starts and supervises the bundled Node child; the Node child starts your Main lifecycle and local HTTP server; only then does the host load the renderer.
launch
→ Rust host acquires the per-user app lock
→ host chooses the app-local origin and creates a runtime token
→ Node child loads src/main.ts
→ main.ready(context) finishes
→ local server begins listening
→ registered cold-start URLs/files are delivered to main.openRequested()
→ declared WebViews load their renderer routesIf ready() rejects, startup fails instead of showing a renderer attached to a
half-initialized backend. Use it for resources that must exist before the UI is
usable: opening a database, running migrations, or starting an app-owned
service.
The primary process receives its bounded raw arguments and working directory
as MainContext.launch. During development, only arguments after the standalone
delimiter in murasaki dev -- <app-arguments> enter that snapshot; Murasaki's
CLI flags are never forwarded. See Node Main.
In packaged macOS, Windows, and Linux apps, a second launch does not start another
backend. Its argv / working directory are delivered to
main.secondInstance(), and the host focuses the primary window. Development
mode does not currently provide this lock.
Open requests
Packaged macOS and Windows apps, plus installed Linux .deb packages, can
register custom URL schemes and document
extensions with protocols and fileAssociations in
murasaki.config.ts. After ready() has completed, matching activations are
normalized and delivered to main.openRequested():
activationiscold-start,second-instance, oros-event.transportisargv,open-url, oropen-file.targetscontains{ kind: 'url', url, scheme }or{ kind: 'file', path }values.
On a second launch, secondInstance() still receives the raw process
arguments while openRequested() receives only registered URLs and files.
Use openRequested() for cross-platform open behavior and
secondInstance() for other secondary-launch arguments.
On Linux, the .deb installs the generated .desktop entry. A manually
extracted AppDir or AppImage contains the same metadata but does not register
it with the desktop environment automatically.
URL and file activations are untrusted operating-system input. Registration selects which inputs reach the hook; it does not authenticate the sender or make the URL contents or file contents safe.
Quit sequence
Normal window-close and app-quit requests use the lifecycle below:
quit request
→ beforeQuit(context) may return false ┐
→ context.signal aborts
→ shutdown(context) ┘ bounded together by shutdownTimeoutMs
→ Node child exits
→ native host exits or applies an updatebeforeQuit() may cancel a normal close—for example while a document has
unsaved changes. Forced shutdown paths, including process signals and dev Main
reloads, ignore cancellation. The combined hooks default to a 10-second limit;
shutdown() should stop accepting work first, then flush and close resources.
The OS close control and appWindow.close() hide a secondary, so it can be
shown again with windows.open(). Closing the primary main window is an
application quit request and follows the sequence above. Cancellation is
application-wide; Murasaki does not expose a cancellable per-window close
hook. Node Main windows.destroy(label) releases a secondary target, and
windows.create(label) can recreate its configured declaration.
Development reloads
During murasaki dev, a change to the configured Main entry triggers:
- the current
shutdown({ reason: 'dev-reload' }), - Vite invalidation of that module,
- construction of a fresh lifecycle,
- a new
ready()call.
Changes imported by Main still follow Vite's module graph, but only the exact
configured entry currently triggers the lifecycle restart. Avoid module-level
timers or sockets: create them inside ready() and close them inside
shutdown() so reloads do not leak work.
Application paths
MainContext.paths gives stable, per-appId locations:
| Path | Intended data |
|---|---|
data | Databases, user documents owned by the app, durable state |
cache | Re-creatable caches |
logs | Application logs |
temp | Ephemeral staging files |
Use these instead of writing beside the executable. Packaged resources may be read-only, app locations vary by OS, and updates may replace the application bundle.
Process failures
The host owns and polls the Node child lifetime. If the packaged backend exits unexpectedly, the launcher discards any unconfirmed update handoff, closes its WebViews, terminates the backend process tree, and exits non-zero rather than leaving a dead UI. Windows uses a Job Object; macOS launches Node and its sidecars in a dedicated process group. Murasaki does not yet expose a public crash-restart policy, health-check hook, or multiple independently supervised worker groups. Use the Main sidecar supervisor when a helper needs an explicit restart policy.