Skip to content

Store and Sync Shared State

Problem

You need persistent, shared state that survives reconnections — a score counter, a list of items, or a collaborative document — and multiple clients may update it concurrently.

Solution

Use client.state.set() with StateOp operations to read and write shared state. Starfish supports replace, merge, counter, set, and list operations with optimistic concurrency control via versioning.

Code

typescript
import { StarfishClient } from "@starfish/sdk";

const client = new StarfishClient({ server: "ws://localhost:4000" });
await client.connect();
await client.join("my-session");

// Replace: set an entire value
await client.state.set({
  key: "settings",
  scope: "session",
  op: "replace",
  value: { theme: "dark", volume: 80 },
});

// Merge: deep-merge into existing value
await client.state.set({
  key: "settings",
  scope: "session",
  op: "merge",
  value: { volume: 50 },  // only updates volume, keeps theme
});

// Counter: increment a numeric value
await client.state.set({
  key: "score",
  scope: "session",
  op: "counter.add",
  value: 10,
});

// Set: add/remove from a set (no duplicates)
await client.state.set({
  key: "tags",
  scope: "session",
  op: "set.add",
  value: ["urgent", "reviewed"],
});

// List: append to an ordered list
await client.state.set({
  key: "history",
  scope: "session",
  op: "list.add",
  value: [{ action: "moved", x: 10, y: 20 }],
});

// Read current value
const result = await client.state.get({ key: "settings", scope: "session" });
console.log(result.value, "version:", result.version);

// Watch for changes
client.state.keyChanges$("settings").subscribe((result) => {
  console.log("Settings changed:", result.value);
});
python
from starfish import StarfishClient, StarfishClientOptions, SetOptions

client = StarfishClient(StarfishClientOptions(server="ws://localhost:4000"))
await client.connect()
await client.join("my-session")

# Replace: set an entire value
await client.state.set(SetOptions(
    key="settings",
    scope="session",
    op="replace",
    value={"theme": "dark", "volume": 80},
))

# Merge: deep-merge into existing value
await client.state.set(SetOptions(
    key="settings",
    scope="session",
    op="merge",
    value={"volume": 50},  # only updates volume, keeps theme
))

# Counter: increment a numeric value
await client.state.set(SetOptions(
    key="score",
    scope="session",
    op="counter.add",
    value=10,
))

# Set: add/remove from a set (no duplicates)
await client.state.set(SetOptions(
    key="tags",
    scope="session",
    op="set.add",
    value=["urgent", "reviewed"],
))

# List: append to an ordered list
await client.state.set(SetOptions(
    key="history",
    scope="session",
    op="list.add",
    value=[{"action": "moved", "x": 10, "y": 20}],
))

# Read current value
result = await client.state.get("settings", scope="session")
print(result.value, "version:", result.version)

# Watch for changes
client.state.key_stream("settings").subscribe(
    lambda result: print("Settings changed:", result.value)
)
swift
import StarfishClient

let client = StarfishClient(options: StarfishClientOptions(
    server: URL(string: "ws://localhost:4000")!
))
try await client.connect()
try await client.join(session: "my-session")

// Replace: set an entire value
try await client.state.set(SetOptions(
    key: "settings",
    scope: .session,
    op: .replace,
    value: AnyCodable(["theme": "dark", "volume": 80])
))

// Merge: deep-merge into existing value
try await client.state.set(SetOptions(
    key: "settings",
    scope: .session,
    op: .merge,
    value: AnyCodable(["volume": 50])  // only updates volume, keeps theme
))

// Counter: increment a numeric value
try await client.state.set(SetOptions(
    key: "score",
    scope: .session,
    op: .counterAdd,
    value: AnyCodable(10)
))

// Set: add/remove from a set (no duplicates)
try await client.state.set(SetOptions(
    key: "tags",
    scope: .session,
    op: .setAdd,
    value: AnyCodable(["urgent", "reviewed"])
))

// List: append to an ordered list
try await client.state.set(SetOptions(
    key: "history",
    scope: .session,
    op: .listAdd,
    value: AnyCodable([["action": "moved", "x": 10, "y": 20]])
))

// Read current value
let result = try await client.state.get(key: "settings", scope: .session)
print(result.value as Any, "version:", result.version)

// Watch for changes
Task {
    for await result in client.state.changes(forKey: "settings") {
        print("Settings changed:", result.value as Any)
    }
}

Explanation

State operations

OperationDescriptionValue type
replaceOverwrites the entire valueAny
mergeDeep-merges into existing objectObject
counter.addIncrements a numberNumber
set.addAdds items to a set (no duplicates)Array
set.removeRemoves items from a setArray
list.addAppends items to an ordered listArray
list.removeRemoves items from a listArray
deleteDeletes the key entirelyNone

Scopes

  • "session" — shared across all clients in the session. Use for collaborative state.
  • "self" — private to the current client. Use for per-user preferences or draft state.

Concurrency control

Use expectedVersion to prevent lost updates. If the server's version doesn't match, the set is rejected:

typescript
const current = await client.state.get({ key: "score", scope: "session" });

try {
  await client.state.set({
    key: "score",
    scope: "session",
    op: "counter.add",
    value: 1,
    expectedVersion: current.version,
  });
} catch (err) {
  console.log("Conflict — re-read and retry");
}
python
from starfish import ConflictError

current = await client.state.get("score", scope="session")

try:
    await client.state.set(SetOptions(
        key="score",
        scope="session",
        op="counter.add",
        value=1,
        expected_version=current.version,
    ))
except ConflictError as err:
    print(f"Conflict at version {err.current_version} — re-read and retry")
swift
let current = try await client.state.get(key: "score", scope: .session)

do {
    try await client.state.set(SetOptions(
        key: "score",
        scope: .session,
        op: .counterAdd,
        value: AnyCodable(1),
        expectedVersion: current.version
    ))
} catch {
    print("Conflict — re-read and retry")
}

Variations

Delete a key

typescript
await client.state.set({ key: "settings", scope: "session", op: "delete" });
python
await client.state.set(SetOptions(key="settings", scope="session", op="delete"))
swift
try await client.state.set(SetOptions(key: "settings", scope: .session, op: .delete))

Watch all state changes

typescript
client.state.changes$.subscribe((result) => {
  console.log(`Key "${result.key}" changed to:`, result.value);
});
python
client.state.changed.subscribe(
    lambda result: print(f'Key "{result.key}" changed to:', result.value)
)
swift
Task {
    for await result in client.state.changes {
        print("Key \"\(result.key)\" changed to:", result.value as Any)
    }
}