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 + fallback
ws: 'wss://myapp.com/surf/ws', // Optional: WebSocket for Surf Live
auth: 'your-token', // Optional: auth token
})

After calling initSurf(), window.surf is available globally. Agents can call window.surf.execute(), window.surf.manifest(), etc.

| Option | Type | Description | |--------|------|-------------| | endpoint | string | Server URL for manifest fetch and HTTP fallback | | ws | string? | WebSocket URL for Surf Live connection | | auth | string? | Auth token sent with requests |

registerCommand

Register a local command handler:

javascript
import { registerCommand } from '@surfjs/web'
 
registerCommand('canvas.addCircle', {
mode: 'local',
run: (params) => {
const circle = canvasStore.addCircle(params)
return { ok: true, result: circle }
}
})

When window.surf.execute('canvas.addCircle', ...) is called, the local handler runs immediately — no network request.

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.

destroySurf

Tear down the runtime and remove window.surf:

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>

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.