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)
    }
}