Skip to content

Packages

@surfjs/svelte

Svelte stores and actions for Surf — local commands, reactive state, and Surf Live

@surfjs/svelte

Svelte integration for Surf, wrapping @surfjs/web. Register local command handlers, sync state via stores, manage channels, and connect to Surf Live. Supports Svelte 4 and 5.

Installation#

Bash
npm install @surfjs/svelte

@surfjs/web is included as a dependency — no separate install needed.

Peer dependencies: svelte ^4.0.0 || ^5.0.0

Provider Setup#

Unlike React/Vue, Svelte doesn't have a built-in component provider pattern. Instead, call createSurfProvider in your root component and pass it to children via Svelte's context API:

svelte
<!-- App.svelte -->
<script lang="ts">
import { createSurfProvider, setSurfContext } from '@surfjs/svelte'
 
const provider = createSurfProvider({
url: 'wss://myapp.com/surf/ws',
endpoint: 'https://myapp.com',
auth: 'your-token',
channels: ['project-123'],
})
 
setSurfContext(provider)
</script>
 
<slot />

CreateSurfProviderOptions

| Option | Type | Description | |--------|------|-------------| | url | string | WebSocket URL to connect to (required) | | endpoint | string? | Server URL for HTTP fallback. Also registers window.surf | | auth | string? | Auth token sent on connect | | channels | string[]? | Channels to subscribe to on connect |

Auto-Reconnect

The provider automatically reconnects with exponential backoff: 1s → 2s → 4s → 8s → max 30s.

Channel subscriptions are restored on reconnect.

window.surf Integration

When endpoint is provided, createSurfProvider automatically initialises window.surf with a WebSocket-backed server executor. This means @surfjs/web functions like registerCommand route through the active WebSocket instead of HTTP fallback.

Cleanup

The provider returns a destroy() function for teardown:

svelte
<script lang="ts">
import { onDestroy } from 'svelte'
import { createSurfProvider, setSurfContext } from '@surfjs/svelte'
 
const provider = createSurfProvider({
url: 'wss://myapp.com/surf/ws',
})
 
setSurfContext(provider)
 
onDestroy(() => {
provider.destroy()
})
</script>

getSurfContext#

Access the Surf context in child components:

svelte
<script lang="ts">
import { getSurfContext } from '@surfjs/svelte'
 
const { execute, status, subscribe } = getSurfContext()
 
async function search(query: string) {
const result = await execute('search', { query })
if (result.ok) {
console.log(result.result)
}
}
</script>
 
<button on:click={() => search('laptop')}>Search</button>

Returns

| Property | Type | Description | |----------|------|-------------| | execute | (command: string, params?: Record<string, unknown>) => Promise<SurfResult> | Execute a command via WebSocket | | status | Writable<ConnectionStatus> | Current connection status store | | subscribe | (event: string, cb: EventCallback) => () => void | Subscribe to Surf events | | subscribeChannel | (channelId: string) => void | Subscribe to a channel | | unsubscribeChannel | (channelId: string) => void | Unsubscribe from a channel | | destroy | () => void | Tear down the connection |

Throws if used outside a component with setSurfContext.

Reactive Status

status is a Svelte writable store — use the $ prefix to read it reactively:

svelte
<script lang="ts">
import { getSurfContext } from '@surfjs/svelte'
 
const { status } = getSurfContext()
</script>
 
{#if $status === 'connected'}
<span class="badge green">Connected</span>
{:else if $status === 'reconnecting'}
<span class="badge yellow">Reconnecting…</span>
{:else}
<span class="badge red">Disconnected</span>
{/if}

surfCommands#

Register local command handlers with window.surf. Handlers run in the browser — no server roundtrip for mode: 'local' commands:

svelte
<script lang="ts">
import { surfCommands } from '@surfjs/svelte'
 
surfCommands({
'theme.toggle': {
mode: 'local',
run: () => {
document.documentElement.classList.toggle('dark')
return { ok: true }
},
},
'canvas.addCircle': {
mode: 'local',
run: (params) => {
addCircleToCanvas(params as { x: number; y: number; radius: number })
return { ok: true }
},
},
'doc.save': {
mode: 'sync',
run: (params) => {
saveLocally(params)
return { ok: true }
},
},
})
</script>

Handler Modes

| Mode | Behaviour | |------|-----------| | local | Runs only in the browser. No server call. | | sync | Runs locally AND posts to server in the background for persistence. |

Works with or without the Surf context — registers directly with window.surf via @surfjs/web. Handlers are registered immediately and cleaned up via onDestroy.

surfState#

Reactive Svelte store synced from Surf Live events. Returns a writable store that auto-updates when the server sends surf:state or surf:patch events:

svelte
<script lang="ts">
import { surfState } from '@surfjs/svelte'
 
interface Metrics {
users: number
events: number
latency: number
}
 
const metrics = surfState<Metrics>('metrics', {
users: 0,
events: 0,
latency: 0,
})
</script>
 
<div class="dashboard">
<span>Online: {$metrics.users}</span>
<span>Events: {$metrics.events}</span>
<span>Latency: {$metrics.latency}ms</span>
</div>

How it works

  • surf:state — Full state replacement for the given key
  • surf:patch — Deep merge of partial updates into existing state

Both carry a version number. Out-of-order messages with stale versions are ignored automatically.

Parameters

| Param | Type | Description | |-------|------|-------------| | key | string | State key (matches against channel name in events) | | initialState | T | Initial value before any server events arrive |

Returns Writable<T>. Requires getSurfContext — must be used within a component that has setSurfContext.

surfExecute#

Convenience wrapper around window.surf.execute() for use without the provider context:

svelte
<script lang="ts">
import { surfExecute } from '@surfjs/svelte'
 
async function search(query: string) {
const result = await surfExecute('search', { query })
if (result.ok) {
console.log(result.result)
}
}
</script>
 
<button on:click={() => search('laptop')}>Search</button>

This calls ensureSurf() to guarantee window.surf exists, then delegates to the local handler registry. Useful when you've registered commands via surfCommands or set up window.surf with initSurf directly.

Dynamic Channels#

Manage channel subscriptions at runtime via the context:

svelte
<script lang="ts">
import { getSurfContext } from '@surfjs/svelte'
 
const { subscribeChannel, unsubscribeChannel } = getSurfContext()
 
function joinProject(id: string) {
subscribeChannel(`project-${id}`)
}
 
function leaveProject(id: string) {
unsubscribeChannel(`project-${id}`)
}
</script>

SSR Considerations#

SvelteKit / SSR frameworks

All functions are safe for SSR — they guard against window and WebSocket access on the server:

  • createSurfProvider — WebSocket connection and window.surf registration are wrapped in typeof window !== 'undefined' checks. On the server, the provider is created but no connection is established.
  • surfCommands — Calls registerCommand from @surfjs/web, which no-ops when window is unavailable.
  • surfState — Returns the initialState on the server; hydrates reactively on the client.
  • surfExecute — Should only be called client-side (in event handlers, onMount, etc.).

No special configuration needed. Just use the functions as normal in your SvelteKit pages.

Client-only loading

For components that should only render on the client:

svelte
<!-- +page.svelte -->
<script lang="ts">
import { browser } from '$app/environment'
</script>
 
{#if browser}
<SurfPanel />
{/if}

TypeScript#

All exports are fully typed. Key types you can import:

TypeScript
import type {
// Provider
CreateSurfProviderOptions,
 
// Context & connection
SurfContextValue,
ConnectionStatus, // 'connecting' | 'connected' | 'disconnected' | 'reconnecting'
SurfResult,
EventCallback,
 
// Commands
SurfCommandConfig, // = CommandConfig from @surfjs/web
SurfCommandsMap, // Record<string, SurfCommandConfig>
} from '@surfjs/svelte'

SurfResult

TypeScript
interface SurfResult {
ok: boolean
result?: unknown
error?: { code: string; message: string }
state?: Record<string, unknown>
}

SurfContextValue

TypeScript
interface SurfContextValue {
execute: (command: string, params?: Record<string, unknown>) => Promise<SurfResult>
status: Writable<ConnectionStatus>
subscribe: (event: string, callback: EventCallback) => () => void
subscribeChannel: (channelId: string) => void
unsubscribeChannel: (channelId: string) => void
destroy: () => void
}

Re-exports from @surfjs/web#

For convenience, @surfjs/svelte re-exports the core @surfjs/web functions so you don't need a separate import:

TypeScript
import {
initSurf,
ensureSurf,
registerCommand,
unregisterCommand,
getSurf,
destroySurf,
} from '@surfjs/svelte'

See @surfjs/web for details on these functions.