Skip to content

Packages

@surfjs/web

Framework-agnostic browser runtime — window.surf, local handler registry, and command dispatcher

@surfjs/web

The core browser runtime for Surf. Provides window.surf, the local handler registry, and the command dispatcher. Framework-agnostic — works with React, Vue, Svelte, or plain JavaScript.

If you're using React, you probably want @surfjs/react — it wraps @surfjs/web with React hooks and lifecycle management. Everything on this page still applies, but you won't call these functions directly.

Installation#

Bash
npm install @surfjs/web

Architecture#

@surfjs/web is the foundation of Surf's browser runtime:

TypeScript
@surfjs/web ← You are here
├─ @surfjs/react ← React hooks (useSurfCommands, SurfProvider)
├─ @surfjs/vue ← Vue composables (useSurfCommands, SurfProvider)
└─ @surfjs/svelte ← Svelte stores (surfCommands, surfState)

Framework packages are thin lifecycle wrappers. @surfjs/react's useSurfCommands calls registerCommand and cleans up on unmount. SurfProvider calls initSurf and manages WebSocket setup via React context. The actual runtime — window.surf, handler dispatch, server fallback — all lives here.

initSurf

Initialize the window.surf runtime:

JavaScript
import { initSurf } from '@surfjs/web'
 
initSurf({
endpoint: 'https://myapp.com', // Server URL for manifest + HTTP fallback
})

After calling initSurf(), window.surf is available globally. Agents can call window.surf.execute(), window.surf.manifest(), etc. Idempotent — safe to call multiple times.

| Option | Type | Description | |--------|------|-------------| | endpoint | string? | Server URL for manifest fetch and HTTP fallback |

💡 Tip: WebSocket transport and auth are handled by framework wrappers like SurfProvider (@surfjs/react), not by initSurf directly. Use setServerExecutor and setServerStatus (framework integration helpers) for custom transport setups.

registerCommand

Register a local command handler:

JavaScript
import { registerCommand } from '@surfjs/web'
 
// Returns an unregister function
const unregister = registerCommand('canvas.addCircle', {
mode: 'local',
run: (params) => {
const circle = canvasStore.addCircle(params)
return { ok: true, result: circle }
}
})
 
// Later: unregister() to remove the handler

When window.surf.execute('canvas.addCircle', ...) is called, the local handler runs immediately — no network request. registerCommand automatically calls initSurf() if window.surf isn't set up yet.

Modes

| Mode | Behavior | |------|----------| | 'local' | Runs in browser only. No server contact. | | 'sync' | Runs locally first, then POSTs to server in background. |

JavaScript
// Local-only — no server
registerCommand('canvas.clear', {
mode: 'local',
run: () => {
canvasStore.clear()
return { ok: true }
}
})
 
// Local + background sync
registerCommand('doc.save', {
mode: 'sync',
run: (params) => {
docStore.updateLocal(params)
return { ok: true }
// Surf automatically POSTs to server after returning
}
})

unregisterCommand

Remove a local handler:

JavaScript
import { unregisterCommand } from '@surfjs/web'
 
unregisterCommand('canvas.addCircle')

In React, useSurfCommands handles registration and cleanup automatically on mount/unmount. You don't need to call unregisterCommand manually.

getSurf

Get the current window.surf instance, or undefined if not initialized:

JavaScript
import { getSurf } from '@surfjs/web'
 
const surf = getSurf()
if (surf) {
const manifest = await surf.manifest()
}

ensureSurf

Ensure the SurfGlobal instance exists (creates it if needed). Used by framework wrappers that need the instance before full initialization:

JavaScript
import { ensureSurf } from '@surfjs/web'
 
const surf = ensureSurf()
// surf is guaranteed to exist, also assigned to window.surf in browser

destroySurf

Tear down the runtime and remove window.surf. Clears all handlers, executor, and cached manifest:

JavaScript
import { destroySurf } from '@surfjs/web'
 
destroySurf()
// window.surf is now undefined

Full Example — Vanilla JS#

HTML
<script type="module">
import { initSurf, registerCommand } from '@surfjs/web'
 
// Initialize runtime
initSurf({ endpoint: 'https://myapp.com' })
 
// Register local handlers
registerCommand('theme.toggle', {
mode: 'local',
run: () => {
document.body.classList.toggle('dark')
const isDark = document.body.classList.contains('dark')
return { ok: true, result: { dark: isDark } }
}
})
 
registerCommand('search', {
mode: 'local',
run: ({ query }) => {
const results = searchIndex.search(query)
return { ok: true, result: results }
}
})
 
// Agents can now use:
// await window.surf.execute('theme.toggle')
// await window.surf.execute('search', { query: 'laptop' })
</script>

Full Example — Vue#

vue
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { initSurf, registerCommand, unregisterCommand, destroySurf } from '@surfjs/web'
 
onMounted(() => {
initSurf({ endpoint: 'https://myapp.com' })
 
registerCommand('counter.increment', {
mode: 'local',
run: () => {
count.value++
return { ok: true, result: { count: count.value } }
}
})
})
 
onUnmounted(() => {
unregisterCommand('counter.increment')
destroySurf()
})
</script>

Framework Integration Helpers#

These functions are used by @surfjs/react, @surfjs/vue, etc. — not typically by end users:

| Function | Description | |----------|-------------| | setServerExecutor(executor) | Set the server fallback function. Returns cleanup function. | | setServerStatus(status) | Update the connection status ('connected', 'disconnected', 'connecting'). | | setManifestUrl(url) | Set the manifest URL directly. Invalidates cache. | | ensureSurf() | Ensure the SurfGlobal instance exists (creates if needed). |

How window.surf Dispatches

When window.surf.execute(command, params) is called:

  1. Local handler registered? → Run it immediately (0ms network)
  2. No local handler + WebSocket connected? → Send via WebSocket
  3. No local handler + no WebSocket? → HTTP POST to {endpoint}/surf/execute

This means adding local handlers via registerCommand progressively upgrades window.surf from an HTTP client to a local runtime. Commands that don't have local handlers still work — they just go to the server.