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
| Operation | Description | Value type |
|---|---|---|
replace | Overwrites the entire value | Any |
merge | Deep-merges into existing object | Object |
counter.add | Increments a number | Number |
set.add | Adds items to a set (no duplicates) | Array |
set.remove | Removes items from a set | Array |
list.add | Appends items to an ordered list | Array |
list.remove | Removes items from a list | Array |
delete | Deletes the key entirely | None |
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)
}
}