Skip to main content

Collaborative Counter

A shared counter that synchronizes across browser tabs. Open in two windows, click increment in one, watch both update. No AI—just the synchronization mechanism that powers all Idyllic applications.

Server Code

import { AgenticSystem, field, action } from 'idyllic';

export default class Counter extends AgenticSystem {
  @field count = 0;
  @field lastUpdatedBy: string | null = null;

  @action()
  async increment(userId?: string) {
    this.count++;
    this.lastUpdatedBy = userId ?? null;
  }

  @action()
  async decrement(userId?: string) {
    this.count--;
    this.lastUpdatedBy = userId ?? null;
  }

  @action()
  async reset() {
    this.count = 0;
    this.lastUpdatedBy = null;
  }
}
When any action modifies state, Idyllic broadcasts to every connected client. You don’t write event-emission code. The framework observes property assignments and handles synchronization.

Client Code

import { useSystem } from '@idyllic/react';
import type { Counter } from '../systems/Counter';
import { useMemo } from 'react';

export default function CounterUI() {
  const { count, lastUpdatedBy, increment, decrement, reset } = useSystem<Counter>();
  const userId = useMemo(() => Math.random().toString(36).slice(2, 8), []);

  return (
    <div className="flex flex-col items-center justify-center min-h-screen">
      <div className="text-6xl font-bold mb-8">{count}</div>

      <div className="flex gap-4">
        <button
          onClick={() => decrement(userId)}
          className="w-16 h-16 text-2xl bg-gray-200 rounded-full"
        >
          -
        </button>
        <button
          onClick={() => increment(userId)}
          className="w-16 h-16 text-2xl bg-blue-600 text-white rounded-full"
        >
          +
        </button>
      </div>

      <button onClick={reset} className="mt-8 text-gray-500 underline">
        Reset
      </button>

      {lastUpdatedBy && (
        <p className="mt-4 text-sm text-gray-400">
          Last updated by: {lastUpdatedBy}
          {lastUpdatedBy === userId && ' (you)'}
        </p>
      )}
    </div>
  );
}
Each client generates a random identifier on mount. The UI shows who made each change.

Running the Example

npx idyllic dev
Open http://localhost:3000 in two browser tabs. Click increment in one—both update. The counter value and “last updated by” identifier match in each tab.

Why This Matters

The same primitives that sync this counter also sync streaming LLM output, conversation history, and multi-agent state. The mechanism transfers directly:
  • Push-based updates: Clients don’t poll. Changes push immediately via WebSocket.
  • Automatic broadcast: No event emission code. The framework intercepts property assignments.
  • Sequential execution: Durable Objects process actions one at a time. No race conditions, no locks needed.