or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

authentication.mdclient-runtime.mddatabase.mde2e-testing.mdindex.mdrealtime.mdrouting.mdsynced-state.mdturnstile.mdvite-plugin.mdworker-runtime.md
tile.json

synced-state.mddocs/

Synced State

Cross-client state synchronization using Durable Objects with React hooks for seamless state updates across connected clients.

Capabilities

Synced State Hook (Client)

React hook for synchronized state across clients, similar to useState but synced via Durable Objects.

/**
 * React hook for synced state across clients
 * @param initialValue - Initial state value
 * @param key - Unique key for this state (used for synchronization)
 * @returns Tuple of [state, setState] like useState
 */
function useSyncedState<T>(
  initialValue: T,
  key: string
): [T, (value: T | ((prev: T) => T)) => void];

Usage Example:

import { useSyncedState } from 'rwsdk/use-synced-state/client';

function Counter() {
  const [count, setCount] = useSyncedState(0, 'global-counter');

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
      <button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
    </div>
  );
}

// All clients viewing this component will see the same count
// Changes from any client are immediately reflected in all others

Custom Synced State Hook

Creates a custom synced state hook with specific configuration.

/**
 * Creates a custom synced state hook
 * @param options - Configuration options
 * @returns Custom useSyncedState hook
 */
function createSyncedStateHook(options?: {
  /** Custom endpoint URL for synced state server */
  url?: string;
  /** Custom React hooks (for testing) */
  hooks?: HookDeps;
}): typeof useSyncedState;

interface HookDeps {
  useState: typeof import('react').useState;
  useEffect: typeof import('react').useEffect;
}

Synced State Client (Client)

Low-level client interface for synced state operations.

/**
 * Gets or creates a synced state client instance
 * @param endpoint - Optional custom endpoint
 * @returns Synced state client
 */
function getSyncedStateClient(endpoint?: string): SyncedStateClient;

/**
 * Initializes a new synced state client
 * @param options - Initialization options
 * @returns Synced state client
 */
function initSyncedStateClient(options?: {
  endpoint?: string;
}): SyncedStateClient;

/**
 * Sets client instance for testing
 * @param client - Mock client
 * @param endpoint - Endpoint key
 */
function setSyncedStateClientForTesting(
  client: SyncedStateClient,
  endpoint?: string
): void;

interface SyncedStateClient {
  /**
   * Gets state value by key
   * @param key - State key
   * @returns Current state value
   */
  getState(key: string): Promise<unknown>;

  /**
   * Sets state value by key
   * @param value - New state value
   * @param key - State key
   */
  setState(value: unknown, key: string): Promise<void>;

  /**
   * Subscribes to state changes
   * @param key - State key to watch
   * @param handler - Callback for state changes
   */
  subscribe(key: string, handler: (value: unknown) => void): Promise<void>;

  /**
   * Unsubscribes from state changes
   * @param key - State key
   * @param handler - Handler to remove
   */
  unsubscribe(key: string, handler: (value: unknown) => void): Promise<void>;
}

Synced State Server (Worker)

Durable Object for managing synced state on the server.

/**
 * Durable Object for synced state management
 */
class SyncedStateServer {
  /**
   * Registers a key transformation handler (static)
   * @param handler - Function to transform keys
   */
  static registerKeyHandler(
    handler: (key: string) => Promise<string>
  ): void;

  /**
   * Gets the registered key handler (static)
   * @returns Current key handler or null
   */
  static getKeyHandler(): ((key: string) => Promise<string>) | null;

  /**
   * Registers a setState interceptor (static)
   * @param handler - Handler called before setState
   */
  static registerSetStateHandler(handler: OnSetHandler | null): void;

  /**
   * Registers a getState interceptor (static)
   * @param handler - Handler called before getState
   */
  static registerGetStateHandler(handler: OnGetHandler | null): void;

  /**
   * Gets state by key
   * @param key - State key
   * @returns Current state value
   */
  getState(key: string): SyncedStateValue;

  /**
   * Sets state by key and notifies subscribers
   * @param value - New state value
   * @param key - State key
   */
  setState(value: SyncedStateValue, key: string): void;

  /**
   * Subscribes a client to state changes
   * @param key - State key
   * @param client - RPC stub for the client
   */
  subscribe(key: string, client: RpcStub): void;

  /**
   * Unsubscribes a client from state changes
   * @param key - State key
   * @param client - RPC stub for the client
   */
  unsubscribe(key: string, client: RpcStub): void;

  /**
   * Handles HTTP requests
   * @param request - Incoming request
   * @returns Response
   */
  fetch(request: Request): Promise<Response>;
}

type SyncedStateValue = unknown;
type OnSetHandler = (key: string, value: unknown) => Promise<void>;
type OnGetHandler = (key: string) => Promise<unknown>;

Synced State Routes

Creates route definitions for synced state endpoints.

/**
 * Creates route definitions for synced state
 * @param getNamespace - Function to get Durable Object namespace
 * @param options - Route configuration
 * @returns Array of route definitions
 */
function syncedStateRoutes(
  getNamespace: (env: any) => DurableObjectNamespace<SyncedStateServer>,
  options?: SyncedStateRouteOptions
): Route<any>[];

interface SyncedStateRouteOptions {
  /** Base path for routes (default: "/synced-state") */
  basePath?: string;
  /** Durable Object name (default: "default") */
  durableObjectName?: string;
}

Complete Synced State Example

Worker Setup

// worker.tsx
import { defineApp, route, render } from 'rwsdk/worker';
import { syncedStateRoutes, SyncedStateServer } from 'rwsdk/use-synced-state/worker';

function Document({ children }) {
  return (
    <html>
      <head>
        <title>Synced State App</title>
      </head>
      <body>{children}</body>
    </html>
  );
}

function Counter() {
  'use client';
  const [count, setCount] = useSyncedState(0, 'app-counter');

  return (
    <div>
      <p>Shared Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

export default defineApp([
  // Add synced state routes
  ...syncedStateRoutes((env) => env.SYNCED_STATE_DO),

  // Regular routes
  render(Document, [
    route('/', Counter),
  ]),
]);

// Export Durable Object
export { SyncedStateServer };

Client Setup

// client.tsx
import { initClient } from 'rwsdk/client';

initClient();

// useSyncedState is automatically configured
// No additional client-side setup needed

Wrangler Configuration

# wrangler.toml
name = "synced-state-app"

[[durable_objects.bindings]]
name = "SYNCED_STATE_DO"
class_name = "SyncedStateServer"
script_name = "synced-state-app"

Advanced Usage

Custom Key Transformation

import { SyncedStateServer } from 'rwsdk/use-synced-state/worker';

// Transform keys to include user-specific prefixes
SyncedStateServer.registerKeyHandler(async (key) => {
  const userId = await getCurrentUserId();
  return `${userId}:${key}`;
});

State Persistence

import { SyncedStateServer } from 'rwsdk/use-synced-state/worker';

// Persist state changes to KV
SyncedStateServer.registerSetStateHandler(async (key, value) => {
  await env.KV.put(`state:${key}`, JSON.stringify(value));
});

// Load state from KV
SyncedStateServer.registerGetStateHandler(async (key) => {
  const stored = await env.KV.get(`state:${key}`);
  return stored ? JSON.parse(stored) : undefined;
});

Multiple State Groups

// Different base paths for different state groups
const userStateRoutes = syncedStateRoutes(
  (env) => env.USER_STATE_DO,
  { basePath: '/user-state' }
);

const appStateRoutes = syncedStateRoutes(
  (env) => env.APP_STATE_DO,
  { basePath: '/app-state' }
);

export default defineApp([
  ...userStateRoutes,
  ...appStateRoutes,
  // ... other routes
]);

Complex State Objects

import { useSyncedState } from 'rwsdk/use-synced-state/client';

interface TodoList {
  items: Array<{ id: string; text: string; done: boolean }>;
}

function TodoApp() {
  const [todos, setTodos] = useSyncedState<TodoList>(
    { items: [] },
    'todo-list'
  );

  const addTodo = (text: string) => {
    setTodos((prev) => ({
      items: [
        ...prev.items,
        { id: crypto.randomUUID(), text, done: false },
      ],
    }));
  };

  const toggleTodo = (id: string) => {
    setTodos((prev) => ({
      items: prev.items.map((item) =>
        item.id === id ? { ...item, done: !item.done } : item
      ),
    }));
  };

  return (
    <div>
      <ul>
        {todos.items.map((item) => (
          <li key={item.id} onClick={() => toggleTodo(item.id)}>
            {item.done ? '✓' : '○'} {item.text}
          </li>
        ))}
      </ul>
      <button onClick={() => addTodo('New task')}>Add Todo</button>
    </div>
  );
}

Use Cases

  • Collaborative Editing: Multiple users editing the same document
  • Shared Counters: Vote counts, like buttons, view counts
  • Live Dashboards: Real-time metrics viewed by multiple users
  • Multi-tab State: Synchronize state across multiple browser tabs
  • Collaborative Games: Shared game state for multiplayer games
  • Chat Applications: Shared conversation state