Skip to content

Rool Svelte

v0.3.12

Svelte 5 bindings for Rool Spaces. Adds reactive state to the SDK using $state runes.

Building a new Rool extension? Start with @rool-dev/extension — it includes a reactive channel and handles hosting for you. This package is for integrating Rool into an existing Svelte application that manages its own auth, routing, and build setup.

Requires Svelte 5. For core concepts (objects, references, AI placeholders, undo/redo), see the SDK documentation.

Installation

Terminal window
npm install @rool-dev/svelte

Quick Start

<script>
import { createRool } from '@rool-dev/svelte';
const rool = createRool();
rool.init();
let channel = $state(null);
</script>
{#if !rool.authenticated}
<button onclick={() => rool.login('My App')}>Login</button>
{:else}
<h1>My Spaces</h1>
{#each rool.spaces ?? [] as space}
<button onclick={async () => channel = await rool.openChannel(space.id, 'main')}>
{space.name}
</button>
{/each}
{#if channel}
<p>Interactions: {channel.interactions.length}</p>
{/if}
{/if}

What It Provides

The Svelte wrapper adds reactive state on top of the SDK:

Reactive PropertyDescription
rool.authenticatedAuth state (null = checking, true/false = known)
rool.spacesList of available spaces
rool.spacesLoadingWhether spaces are loading
rool.spacesErrorError from loading spaces
rool.connectionStateSSE connection state
rool.userStorageUser storage (cross-device preferences)
channel.interactionsChannel interactions (auto-updates)
channel.objectIdsAll object IDs in the space (auto-updates on create/delete)
channel.collectionsCollection names from the schema (auto-updates)
channel.conversationsConversations in this channel (auto-updates on create/delete/rename)
thread.interactionsInteractions for a specific conversation (auto-updates)
watch.objectsObjects matching a filter (auto-updates)
watch.loadingWhether watch is loading

Everything else passes through to the SDK directly. See the SDK documentation for full API details.

API

Lifecycle

const rool = createRool();
rool.init(); // Process auth callbacks (call on app startup)
rool.login('My App'); // Redirect to login page
rool.logout(); // Clear auth state and close all channels
rool.destroy(); // Clean up all resources

Client State

<script>
// All properties are reactive $state
// rool.authenticated → boolean | null
// rool.spaces → RoolSpaceInfo[] | undefined
// rool.spacesLoading → boolean
// rool.spacesError → Error | null
// rool.connectionState → 'connected' | 'disconnected' | 'reconnecting'
// rool.userStorage → Record<string, unknown>
</script>
{#if rool.spacesLoading}
<p>Loading spaces...</p>
{:else if rool.spacesError}
<p>Error: {rool.spacesError.message}</p>
{:else}
{#each rool.spaces ?? [] as space}
<div>{space.name}</div>
{/each}
{/if}

User Storage

Reactive cross-device storage for user preferences. Synced from server on init(), then kept up-to-date via SSE.

<script>
const rool = createRool();
rool.init();
</script>
<!-- Reactive binding to storage values -->
{#if rool.userStorage.onboarding_complete}
<Dashboard />
{:else}
<Onboarding onstep={(step) => rool.setUserStorage('onboarding_step', step)} />
{/if}
<!-- Theme toggle -->
<button onclick={() => rool.setUserStorage('theme',
rool.userStorage.theme === 'dark' ? 'light' : 'dark'
)}>
Toggle theme
</button>

Spaces & Channels

// Open a channel — reactive, with SSE (default conversation)
const channel = await rool.openChannel('space-id', 'my-channel');
// Open a channel with a specific conversation
const channel = await rool.openChannel('space-id', 'my-channel', 'thread-1');
// Create a space, then open a channel on it
const space = await rool.createSpace('My New Space');
const channel = await space.openChannel('main');
// Open a space for admin operations (lightweight, no SSE)
const space = await rool.openSpace('space-id');
await space.rename('New Name');
await space.addUser(userId, 'editor');
// Delete a space
await rool.deleteSpace('space-id');
// Import a space from a zip archive
const space = await rool.importArchive('Imported', archiveBlob);
// Clean up
channel.close();

ReactiveChannel

openChannel returns a ReactiveChannel — the SDK’s RoolChannel with reactive interactions and objectIds:

<script>
let channel = $state(null);
async function open(spaceId) {
channel = await rool.openChannel(spaceId, 'main');
}
</script>
{#if channel}
<!-- Reactive: updates as AI makes tool calls -->
{#each channel.interactions as interaction}
<div>
<strong>{interaction.operation}</strong>: {interaction.output}
</div>
{/each}
<!-- All SDK methods work directly -->
<button onclick={() => channel.prompt('Hello')}>Send</button>
{/if}

Reactive Object

Track a single object by ID with auto-updates:

<script>
let channel = $state(null);
let item = $state(null);
async function open(spaceId, objectId) {
channel = await rool.openChannel(spaceId, 'main');
item = channel.object(objectId);
}
</script>
{#if item}
{#if item.loading}
<p>Loading...</p>
{:else if item.data}
<div>{item.data.title}</div>
{:else}
<p>Object not found</p>
{/if}
{/if}
// Reactive state
item.data // $state<RoolObject | undefined>
item.loading // $state<boolean>
// Methods
item.refresh() // Manual re-fetch
item.close() // Stop listening for updates

Lifecycle: Reactive objects are tied to their channel. Closing the channel stops all updates — existing reactive objects will retain their last data but no longer refresh. Calling channel.object() after close() throws.

Reactive Watches

Create auto-updating watches of objects filtered by field values:

<script>
let channel = $state(null);
let articles = $state(null);
async function open(spaceId) {
channel = await rool.openChannel(spaceId, 'main');
// Create a reactive watch of all objects where type === 'article'
articles = channel.watch({ collection: 'article' });
}
</script>
{#if articles}
{#if articles.loading}
<p>Loading...</p>
{:else}
{#each articles.objects as article}
<div>{article.title}</div>
{/each}
{/if}
{/if}

Watches automatically re-fetch when objects matching the filter are created, updated, or deleted. Since the SDK caches objects locally, re-fetches are typically instant (no network round-trip).

Lifecycle: Watches are tied to their channel. Closing the channel stops all updates — existing watches will retain their last data but no longer refresh. Calling channel.watch() after close() throws.

// Watch options (same as findObjects, but no AI prompt)
const articles = channel.watch({
collection: 'article',
where: { status: 'published' },
order: 'desc', // by modifiedAt (default)
limit: 20,
});
// Reactive state
articles.objects // $state<RoolObject[]>
articles.loading // $state<boolean>
// Methods
articles.refresh() // Manual re-fetch
articles.close() // Stop listening for updates

Reactive Channel List

List channels for a space with auto-updates:

<script>
const channelList = rool.channels('space-id');
// Clean up when done
import { onDestroy } from 'svelte';
onDestroy(() => channelList.close());
</script>
{#if channelList.loading}
<p>Loading channels...</p>
{:else}
{#each channelList.list as ch}
<button onclick={() => openChannel(ch.id)}>{ch.name ?? ch.id}</button>
{/each}
{/if}
// Reactive state
channelList.list // $state<ChannelInfo[]>
channelList.loading // $state<boolean>
// Methods
channelList.refresh() // Manual re-fetch
channelList.close() // Stop listening for updates

Reactive Conversation Handle

For apps with multiple independent interaction threads (e.g., chat with threads), use channel.conversation() to get a handle with reactive interactions:

<script>
let channel = $state(null);
let thread = $state(null);
async function openThread(spaceId, threadId) {
channel = await rool.openChannel(spaceId, 'main');
thread = channel.conversation(threadId);
}
</script>
{#if thread}
{#each thread.interactions as interaction}
<div>{interaction.output}</div>
{/each}
<button onclick={() => thread.prompt('Hello')}>Send</button>
{/if}
// Reactive state
thread.interactions // $state<Interaction[]> — auto-updates via SSE
// All conversation-scoped methods
await thread.prompt('Hello')
await thread.createObject({ data: { text: 'Note' } })
await thread.setSystemInstruction('Respond in haiku')
await thread.rename('Research Thread')
thread.getInteractions() // Manual read
thread.getSystemInstruction()
// Cleanup
thread.close() // Stop listening for updates

Conversations are auto-created on first interaction. All conversations share one SSE connection per channel.

Channel Management

// Rename a channel (thin GraphQL call, no SSE needed)
await rool.renameChannel('space-id', 'channel-id', 'New Name');
// Delete a channel
await rool.deleteChannel('space-id', 'channel-id');
// Rename from within a channel
await channel.rename('New Name');

Using the SDK

All RoolChannel methods and properties are available on ReactiveChannel:

// Properties
channel.id
channel.name
channel.role
channel.channelId
// Object operations
await channel.getObject(id)
await channel.createObject({ data: { text: 'Hello' } })
await channel.updateObject(id, { data: { text: 'Updated' } })
await channel.deleteObjects([id])
await channel.findObjects({ collection: 'note' })
// AI
await channel.prompt('Summarize everything')
// Schema
channel.getSchema()
await channel.createCollection('article', [
{ name: 'title', type: { kind: 'string' } },
{ name: 'status', type: { kind: 'enum', values: ['draft', 'published'] } },
])
await channel.alterCollection('article', [...updatedProps])
await channel.dropCollection('article')
// Undo/Redo
await channel.checkpoint('Before edit')
await channel.undo()
await channel.redo()
// Interaction history & conversations
await channel.setSystemInstruction('You are helpful')
channel.getInteractions()
channel.getConversations()
await channel.deleteConversation('old-thread')
await channel.renameConversation('Research')
// Conversation handles (reactive interactions for specific conversations)
const thread = channel.conversation('thread-42');
await thread.prompt('Hello'); // Uses thread-42's interaction history
// thread.interactions is reactive $state — auto-updates via SSE
thread.close(); // Stop listening when done
// Channel admin
await channel.rename('New Name')

See the SDK documentation for complete API details.

Utilities

import { generateId } from '@rool-dev/svelte';
// Generate a 6-character alphanumeric ID
const id = generateId();

Exported Types

// Package types
import type { Rool, ReactiveChannel, ReactiveConversationHandle, ReactiveObject, ReactiveWatch, WatchOptions, ReactiveChannelList } from '@rool-dev/svelte';
// Re-exported from @rool-dev/sdk
import type {
RoolClient,
RoolClientConfig,
RoolChannel,
RoolSpace,
RoolSpaceInfo,
RoolObject,
RoolObjectStat,
RoolUserRole,
ConnectionState,
ChannelInfo,
Conversation,
ConversationInfo,
CurrentUser,
Interaction,
FindObjectsOptions,
PromptOptions,
CreateObjectOptions,
UpdateObjectOptions,
FieldType,
FieldDef,
CollectionDef,
SpaceSchema,
SpaceMember,
UserResult,
PublishedExtensionInfo,
PublishExtensionOptions,
ExtensionManifest,
FindExtensionsOptions,
} from '@rool-dev/svelte';

Examples

  • soft-sql — SQL-style natural language queries with live tool call progress
  • flashcards — Spaced repetition with AI-generated cards

License

MIT - see LICENSE for details.