@enclosurejs/platform-web
v1.1.0
Published
> [!IMPORTANT] > This package bridges `@enclosurejs/core` to the browser. It provides `WebPlatformBackend` (the `BackendAdapter`), and concrete implementations for 20 capability contracts using standard Web APIs. Your application code never imports this p
Readme
@enclosurejs/platform-web — Browser Backend + 20 Capability Implementations
[!IMPORTANT] This package bridges
@enclosurejs/coreto the browser. It providesWebPlatformBackend(theBackendAdapter), and concrete implementations for 20 capability contracts using standard Web APIs. Your application code never imports this package directly — it consumes capability tokens from@enclosurejs/core.
The Problem
Modern browsers expose rich APIs — Origin Private File System, Web Serial, WebUSB, BroadcastChannel, WebAssembly, Web Crypto — but each has its own surface, error model, and availability story. Wiring 20 capabilities to these APIs by hand means scattered feature detection, inconsistent error handling, and callsites tightly coupled to the browser platform. If you later want to switch to Tauri or Electron, every navigator.* and window.* call must be rewritten.
@enclosurejs/platform-web solves this by mapping every applicable @enclosurejs/core capability token to a browser implementation behind a single registration call. Application code talks to abstract interfaces via DI; switching to Tauri means swapping the import — zero changes to business logic.
Architecture
┌──────────────── Browser Tab (JS) ─────────────────────┐
│ │
│ App code → context.use(FileSystemToken) │
│ │ │
│ WebFileSystem.readTextFile(path) │
│ │ │
│ navigator.storage.getDirectory() → OPFS │
│ │ (same-process, no IPC) │
└────────────────────────────────────────────────────────┘Single layer, single process — no IPC bridge, no preload, no native process:
| Layer | Package | Runs in | Responsibility |
| --------------- | ------------------------- | ----------- | ---------------------------------------------------- |
| JS Services | @enclosurejs/platform-web | Browser tab | WebPlatformBackend + 20 service classes (Web APIs) |
How It Works
No native side — unlike Tauri (Rust) or Electron (Node.js main process), the web platform runs entirely in the browser. All capabilities use standard Web APIs directly.
WebPlatformBackendextendsWebBackendfrom@enclosurejs/core— in-processinvoke()andpushEvent()for commands and events. No serialization overhead.Registration —
registerWebCapabilities(ctx)binds 18 service classes unconditionally, plus 2 conditionally (UsbToken,SerialToken) based on browser API availability.Application code calls
context.use(ShortcutToken)and gets aShortcutServiceinterface. ReplaceWebPlatformBackendwithTauriBackendand every service swaps automatically.
Capabilities Not Available on Web
Six capabilities from @enclosurejs/core have no browser equivalent and are intentionally omitted:
| Capability | Why unavailable |
| --------------------- | -------------------------------------------------- |
| ShellService | No subprocess access in browser sandbox |
| TrayService | No system tray API |
| AutoLaunchService | No start-on-login capability |
| UpdaterService | No auto-update binary mechanism |
| FileWatcherService | OPFS does not support fs.watch-style observation |
| PowerMonitorService | Battery API deprecated and unreliable |
Check with context.has(ShellToken) before using — returns false on web.
Quick Start
import { createApp, FileSystemToken } from '@enclosurejs/core';
import { WebPlatformBackend, registerWebCapabilities } from '@enclosurejs/platform-web';
const app = createApp({
backend: new WebPlatformBackend(),
frontend: {
mount({ container, context }) {
const fs = context.use(FileSystemToken);
fs.readTextFile('hello.txt').then((text) => {
container.textContent = text;
});
},
},
container: document.getElementById('app')!,
modules: [
{
id: 'web-caps',
install(ctx) {
registerWebCapabilities(ctx.context);
},
},
],
});
await app.start();API
Entrypoints
| Import path | Export | Purpose |
| ---------------------------------- | ------------------------------ | --------------------------------------------- |
| @enclosurejs/platform-web | WebPlatformBackend | BackendAdapter for browser |
| | registerWebCapabilities(ctx) | Registers 18–20 capability tokens in one call |
| | Web* (20 classes) | Individual service implementations |
| @enclosurejs/platform-web/register | registerWebCapabilities(ctx) | Same as main export (direct import) |
WebPlatformBackend
Extends WebBackend from @enclosurejs/core with platform = 'web':
| Member | Type | Description |
| ----------------- | ----------------------------------------------------------- | ------------------------------------------ |
| platform | 'web' | Platform identifier |
| invoke | (cmd: string, args?: Record<string, unknown>) => Promise | In-process command dispatch (no IPC) |
| on | (event: string, handler: (payload) => void) => Disposable | In-process pub/sub, idempotent dispose() |
| once | (event: string) => Promise | One-shot event — resolves on first payload |
| registerCommand | (cmd: string, handler: CommandHandler) => void | Bind a command handler for invoke |
| pushEvent | (event: string, payload: unknown) => void | Emit an event to all on subscribers |
No async gap — on() is synchronous, dispose() is immediate. No leaked listeners possible.
registerWebCapabilities(ctx)
Binds 18 service classes unconditionally, plus 2 conditionally:
UsbToken— registered only if'usb' in navigator(WebUSB API available)SerialToken— registered only if'serial' in navigator(Web Serial API available)
Throws CoreError if called twice on the same context (token-already-provided).
Capability Implementations (20)
| Service class | Token | Web API |
| ------------------ | ------------------- | ------------------------------------------------------------- |
| WebAppLifecycle | AppLifecycleToken | visibilitychange, focus/blur, beforeunload |
| WebClipboard | ClipboardToken | navigator.clipboard (Clipboard API) |
| WebDatabase | DatabaseToken | sql.js (SQLite via WASM, optional sql.js peer dep) |
| WebDeepLink | DeepLinkToken | location.search/hash, registerProtocolHandler |
| WebDialogs | DialogToken | File System Access API, <input> fallback, alert/confirm |
| WebDisplay | DisplayToken | window.screen, devicePixelRatio |
| WebFileSystem | FileSystemToken | Origin Private File System (OPFS) |
| WebHttp | HttpClientToken | fetch (with AbortController, progress streams) |
| WebIpc | IpcToken | BroadcastChannel (inter-tab messaging) |
| WebKeychain | KeychainToken | IndexedDB + Web Crypto API (AES-256-GCM, PBKDF2) |
| WebNotifications | NotificationToken | Web Notifications API |
| WebPath | PathToken | Pure JS POSIX path operations |
| WebPrint | PrintToken | window.print(), iframe-based printUrl/printHtml |
| WebSerial | SerialToken | Web Serial API (navigator.serial) |
| WebShortcuts | ShortcutToken | document.addEventListener('keydown') + accelerator parsing |
| WebStorage | StorageToken | localStorage (prefixed keys, JSON serialization) |
| WebSystemInfo | SystemInfoToken | navigator.userAgent, hardwareConcurrency, deviceMemory |
| WebTheme | ThemeToken | matchMedia('prefers-color-scheme') + localStorage |
| WebUsb | UsbToken | WebUSB API (navigator.usb) |
| WebWindows | WindowToken | window.open(), popup management |
Configuration
Near-zero configuration. The package uses browser-native APIs directly:
- Registration:
registerWebCapabilities(ctx)— one call, no options. - Conditional tokens:
UsbTokenandSerialTokenare auto-detected via feature check. - Database WASM:
WebDatabaseaccepts optionalWebDatabaseConfigwithlocateFileto override the default jsDelivr CDN URL for the sql.js WASM file. - Other services: wrap browser globals directly. No env vars, no config objects.
Browser compatibility considerations
| API | Used by | Availability |
| -------------------------- | ---------- | -------------------------------------------- |
| navigator.clipboard | Clipboard | All modern browsers (HTTPS required) |
| Origin Private File System | FileSystem | Chrome 86+, Firefox 111+, Safari 15.2+ |
| BroadcastChannel | IPC | All modern browsers |
| Web Crypto API | Keychain | All modern browsers (HTTPS required) |
| WebAssembly | Database | All modern browsers (required for sql.js) |
| Web Serial API | Serial | Chrome 89+ (experimental, Chromium only) |
| WebUSB API | USB | Chrome 61+ (experimental, Chromium only) |
| File System Access API | Dialogs | Chrome 86+ (fallback to <input> elsewhere) |
| registerProtocolHandler | DeepLink | Firefox, Chrome (not Safari) |
| window.open | Windows | All browsers (popup blockers may interfere) |
Types Exported
Types other packages and application code depend on:
| Type | Used by |
| ------------------------- | ----------------------------------------------------- |
| WebPlatformBackend | App entry point (browser) |
| All 20 Web* classes | Direct use when bypassing DI (not recommended) |
| WebDatabaseConfig | Optional config for WebDatabase (WASM locateFile) |
| registerWebCapabilities | Module install() for DI registration |
Entrypoint separation:
| Import path | Contains |
| ---------------------------------- | ------------------------------------ |
| @enclosurejs/platform-web | Backend + all 20 services + register |
| @enclosurejs/platform-web/register | registerWebCapabilities only |
Both entrypoints use standard Web APIs — this package is usable in any modern browser context.
Safety
Lifecycle Safety
WebPlatformBackendinheritsWebBackend—on()returnsDisposablewith idempotentdispose(). Double-dispose is safe.registerWebCapabilities()throwsCoreErroron double-call — prevents silent token shadowing.- All event-based services (IPC, shortcuts, theme, windows) properly unsubscribe on dispose.
WebSerialconnectiononDataread loop stops cleanly onclose()— no leaked readers.
Error Safety
WebHttpwraps allfetchfailures inCoreErrorwithHTTP_REQUEST_FAILEDcode.WebClipboard.writeImage()wraps failures inCoreErrorwithCLIPBOARD_WRITE_IMAGEcode.WebDatabasethrowsCoreErrorwithDB_INIT_FAILEDwhen sql.js WASM fails to load, andDB_TRANSACTION_FAILEDon transaction rollback.WebDialogsthrowsCoreErrorwithDIALOG_UNSUPPORTEDwhen experimental APIs are unavailable.WebSerialthrowsCoreErrorwithSERIAL_NOT_WRITABLEwhen port has no writable stream.WebUsbthrowsCoreErrorwithUSB_NO_INTERFACEwhen no interface owns the target endpoint.WebFileSystemthrowsCoreErrorwithFS_INVALID_PATHfor empty or root-only paths.WebStorage.get()returnsundefinedfor corrupt JSON — no throws on data corruption.
Data Safety
WebKeychainencrypts secrets with AES-256-GCM (PBKDF2 key derivation,location.originas salt) — stored in IndexedDB, scoped to origin. Database connection is closed after every operation.WebStorageprefixes all keys with__enclosure_store_— no collision with otherlocalStorageusers.WebDatabaseuses sql.js in-memory SQLite — eachopen()creates an isolated database with no IndexedDB collision risk.WebThemepersists user preference tolocalStorageunder__enclosure_theme— survives page reload.
Sandbox Safety
- All services operate within the browser sandbox — no filesystem access outside OPFS, no process spawning, no system-level modifications.
WebSerialandWebUsbrequire user gesture (browser permission prompt) — cannot enumerate or connect silently.
Platform Specifics
Single-Process, No IPC
Unlike Tauri (webview + Rust) or Electron (renderer + main + preload), the web platform runs in a single JS context. WebPlatformBackend extends WebBackend — invoke() dispatches to in-process command handlers, pushEvent() delivers to in-process subscribers. Zero serialization overhead.
File System — Origin Private File System (OPFS)
| Detail | Value |
| ----------------- | --------------------------------------------------------------------- |
| Backend | navigator.storage.getDirectory() → FileSystemDirectoryHandle |
| Scope | Origin-private — invisible to OS file manager, sandboxed per origin |
| watch() | No-op — returns a Disposable that does nothing (OPFS has no events) |
| rename() | Implemented as read → write → delete (no native rename in OPFS) |
| Invalid paths | '' and '/' throw CoreError with FS_INVALID_PATH |
Storage — localStorage with Prefix
| Detail | Value |
| --------------------------- | ------------------------------------------------------------------- |
| Backend | window.localStorage |
| Key prefix | __enclosure_store_ |
| Persistence | Per-origin, survives restart |
| size() | Approximate UTF-16 estimate (key.length*2 + value.length*2) |
| get() on corrupt JSON | Returns undefined (silent JSON.parse catch) |
| clear() | Removes prefixed keys, fires undefined to all onChange watchers |
Database — sql.js (SQLite via WASM)
| Detail | Value |
| ---------------- | ----------------------------------------------------------------------------- |
| Backend | sql.js (SQLite compiled to WASM), loaded on first open() call |
| WASM source | jsDelivr CDN by default, override with WebDatabaseConfig.locateFile |
| Peer dep | sql.js (optional) — dynamic import, tree-shakes when unused |
| Migrations | DbOpenOptions.migrations applied via PRAGMA user_version versioning |
| Transactions | Real SQL transactions (BEGIN / COMMIT / ROLLBACK) |
| Errors | DB_INIT_FAILED when WASM fails to load, DB_TRANSACTION_FAILED on rollback |
Keychain — IndexedDB + Web Crypto
| Detail | Value |
| -------------- | ------------------------------------------------------------ |
| Storage | IndexedDB (__enclosure_keychain database, secrets store) |
| Encryption | AES-256-GCM with PBKDF2-derived key (100,000 iterations) |
| Salt | location.origin — keys are origin-scoped |
| Key format | {service}::{account} — composite key |
IPC — BroadcastChannel
| Detail | Value |
| ---------------------- | -------------------------------------------------------------------------------- |
| Backend | BroadcastChannel — same-origin inter-tab messaging |
| send() target | Targeted delivery — filtered by windowId on the receiving side |
| broadcast() | Delivers to local handlers AND remote tabs |
| getLastMessage() | Per-channel cache, updated only when delivered through an active on() listener |
| Sender ID | windowId from WebWindows (URL __windowId param or 'main') |
Path — Pure JS POSIX
WebPath implements POSIX-style path operations with / separator and : delimiter. All methods are pure JS — no platform detection, no native calls. This is correct for the web platform where all paths (OPFS, URLs) use forward slashes.
Windows — window.open() Popups
| Detail | Value |
| ------------------------ | ------------------------------------------------------------------ |
| open() | window.open() with features string (width, height, left, top) |
| openModal() | Polls win.closed every 200ms — resolves undefined if no result |
| getCurrent() | Promise<AppWindow> — proxy to the current window object |
| Stubs (no-op) | minimize, maximize, hide, setAlwaysOnTop, setKiosk, etc. |
| Closed window access | Throws CoreError with WINDOW_CLOSED |
Shortcuts — Accelerator Parsing
WebShortcuts parses Electron/Tauri-style accelerator strings (CmdOrCtrl+Shift+P) into modifier+key combinations and listens on document.addEventListener('keydown', ..., true). Supports Ctrl, Shift, Alt/Option, Meta/Cmd/Command/Super, and CmdOrCtrl/CommandOrControl (auto-detects Mac via navigator.userAgent).
Theme — matchMedia + localStorage
| Detail | Value |
| -------------------- | ------------------------------------------------------------------- |
| System detection | matchMedia('(prefers-color-scheme: dark)') with change listener |
| User override | Stored in localStorage under __enclosure_theme |
| set('system') | Clears stored override, reverts to system detection |
| Source tracking | getSource() returns 'system' or 'user' based on last action |
SystemInfo — Best-Effort Browser Detection
| Method | Source |
| ------------ | -------------------------------------------------------------------------------------- |
| arch() | Regex on navigator.userAgent (x86_64, aarch64, arm, x86) |
| platform() | Regex on navigator.userAgent (android → ios → windows → macos → linux) |
| hostname() | location.hostname (fallback: 'localhost') |
| cpus() | Single entry: { model: 'Browser (estimated)', speed: 0, cores: hardwareConcurrency } |
| memory() | navigator.deviceMemory × 1GB (fallback: 4GB), free: total - usedJSHeapSize |
| uptime() | Math.floor(performance.now() / 1000) (tab uptime, not OS) |
| homedir() | Always '/' |
| tmpdir() | Always '/tmp' |
Print — Mixed Implementation
| Method | Implementation |
| ------------- | ------------------------------------------------------------ |
| print() | window.print() — browser print dialog, ignores options |
| printUrl() | Hidden iframe, loads URL, calls contentWindow.print() |
| printHtml() | Hidden iframe, sets srcdoc to HTML, calls print() |
Comparison with platform-tauri / platform-electron
| Aspect | Web | Tauri | Electron |
| ------------------- | ---------------------------------------- | ------------------------------------------ | ------------------------------------- |
| Architecture | Single JS context (no IPC) | 1 JS process (webview) + Rust core | 3 processes (main, preload, renderer) |
| Capabilities | 20 of 26 (6 unavailable) | 26 of 26 | 26 of 26 |
| Storage backend | localStorage (prefix-based) | LazyStore (file-based, auto-flushed) | localStorage (prefix-based) |
| Keychain | IndexedDB + Web Crypto (origin-scoped) | OS native keyring (custom Rust plugin) | Encrypted JSON file (safeStorage) |
| Database | sql.js (SQLite via WASM, in-memory) | @tauri-apps/plugin-sql (SQLite via Rust) | better-sqlite3 (native SQLite) |
| File System | OPFS (sandboxed, no watch) | Native FS via plugin | node:fs/promises |
| Path style | POSIX only (/) | OS-native (auto-detect sep) | OS-native (userAgent-based) |
| HTTP | Browser fetch() | @tauri-apps/plugin-http (Rust-backed) | Renderer fetch() |
| IPC | BroadcastChannel (inter-tab) | @tauri-apps/api/event (inter-webview) | ipcMain/ipcRenderer |
| Serial/USB | Web Serial / WebUSB (Chromium only) | Custom Rust plugins | serialport / usb (native npm) |
| Windows | window.open() popups | WebviewWindow (native) | BrowserWindow (native) |
| Shortcuts | keydown listener + accelerator parsing | @tauri-apps/plugin-global-shortcut | electron.globalShortcut |
Benchmarks
Not applicable. @enclosurejs/platform-web is a platform bridge — its performance is dominated by the underlying browser APIs (OPFS access time, sql.js query speed, fetch overhead). The service layer adds negligible overhead (argument forwarding, error wrapping). Real-world performance depends on:
- Browser engine implementation of Web APIs (V8, SpiderMonkey, WebKit)
- OPFS access latency (~0.1–1ms per operation depending on browser)
- sql.js WASM initialization time (one-time, cached after first
open()) - Network latency for
fetch-based operations
For production profiling, use browser DevTools Performance tab.
Bundle Size
| Output | File | Size |
| ------------ | --------------- | --------- |
| Runtime (JS) | index.js | 57.84 KB |
| | register.js | 57.49 KB |
| Types (DTS) | index.d.ts | 14.17 KB |
| | register.d.ts | 0.76 KB |
| Total JS | | 115.33 KB |
| Total | | 130.26 KB |
sql.js is a dynamic import (peer dependency, optional) — not bundled into index.js/register.js. index.js and register.js share most code (20 service classes). DTS output includes all 20 service classes and the WebPlatformBackend type.
Quality
| Metric | Value |
| ------------------- | ------------------------------------------------------------------ |
| Unit tests | 446 (all pass) |
| Test files | 22 (one per service + backend + register) |
| Source files | 20 services + backend + register + barrels = 23 |
| Dependencies | @enclosurejs/core |
| Peer dependencies | sql.js (optional — for WebDatabase) |
| Dev dependencies | sql.js, tsup |
| Coverage thresholds | statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90% |
Quality Layers
Layer 1: STATIC ANALYSIS (every commit)
tsc --noEmit strict mode, zero errors
eslint ESLint 9 flat config, zero warnings
prettier --check formatting
Layer 2: UNIT TESTS (every commit)
446 tests backend, register, 20 services (one file each)
covers lifecycle, dispose, errors, edge cases
all browser APIs mocked via vi.fn / Object.defineProperty
v8 coverage statements >= 90%, branches >= 85%, functions >= 95%, lines >= 90%
Layer 3: BENCHMARKS
N/A platform bridge — performance dominated by browser APIs
Layer 4: PACKAGE HEALTH
1 peer dep sql.js (optional, for WebDatabase)
tsup build ESM + DTS output, 2 entrypointsFile Structure
packages/platform-web/
├── src/
│ ├── index.ts WebPlatformBackend, re-exports services + register
│ ├── register.ts registerWebCapabilities (18+2 tokens → DI)
│ ├── sql-js.d.ts Module declaration for sql.js (no bundled types)
│ ├── services/
│ │ ├── index.ts Barrel for all 20 Web* service classes
│ │ ├── app-lifecycle.ts WebAppLifecycle
│ │ ├── clipboard.ts WebClipboard
│ │ ├── database.ts WebDatabase
│ │ ├── deep-link.ts WebDeepLink
│ │ ├── dialogs.ts WebDialogs
│ │ ├── display.ts WebDisplay
│ │ ├── fs.ts WebFileSystem
│ │ ├── http.ts WebHttp
│ │ ├── ipc.ts WebIpc
│ │ ├── keychain.ts WebKeychain
│ │ ├── notifications.ts WebNotifications
│ │ ├── path.ts WebPath
│ │ ├── print.ts WebPrint
│ │ ├── serial.ts WebSerial
│ │ ├── shortcuts.ts WebShortcuts
│ │ ├── storage.ts WebStorage
│ │ ├── system-info.ts WebSystemInfo
│ │ ├── theme.ts WebTheme
│ │ ├── usb.ts WebUsb
│ │ └── windows.ts WebWindows
│ └── __tests__/
│ ├── app-lifecycle.test.ts 22 tests — AppLifecycle (state, quit, relaunch)
│ ├── backend.test.ts 8 tests — WebPlatformBackend adapter
│ ├── clipboard.test.ts 11 tests — ClipboardService (navigator.clipboard)
│ ├── database.test.ts 18 tests — DatabaseService (sql.js, migrations, tx)
│ ├── deep-link.test.ts 18 tests — DeepLink (register, hash/search parsing)
│ ├── dialogs.test.ts 20 tests — DialogService (FSA API, fallback, alert)
│ ├── display.test.ts 9 tests — DisplayService (screen, devicePixelRatio)
│ ├── fs.test.ts 30 tests — FileSystem (OPFS, read, write, stat, copy)
│ ├── http.test.ts 29 tests — HttpClient (fetch, progress, errors)
│ ├── ipc.test.ts 28 tests — IpcService (BroadcastChannel, send, on)
│ ├── keychain.test.ts 18 tests — Keychain (IndexedDB + Web Crypto)
│ ├── notifications.test.ts 10 tests — Notifications (Web Notification API)
│ ├── path.test.ts 39 tests — PathService (POSIX join, parse, resolve)
│ ├── print.test.ts 6 tests — PrintService (window.print, iframe)
│ ├── register.test.ts 7 tests — registerWebCapabilities (18+2 tokens)
│ ├── serial.test.ts 12 tests — SerialService (Web Serial API)
│ ├── shortcuts.test.ts 24 tests — ShortcutService (accelerator parsing)
│ ├── storage.test.ts 23 tests — StorageService (localStorage, onChange)
│ ├── system-info.test.ts 24 tests — SystemInfo (userAgent parsing, memory)
│ ├── theme.test.ts 16 tests — ThemeService (matchMedia, localStorage)
│ ├── usb.test.ts 19 tests — UsbService (WebUSB, transfers, events)
│ └── windows.test.ts 55 tests — WindowService (open, modal, popups)
├── package.json
├── tsconfig.json
├── tsup.config.ts
└── README.mdLicense
MIT
