State & sync
A plugin’s shared state is a typed schema mapped onto a Yjs structure, so concurrent edits from many devices merge automatically.
The schema
Section titled “The schema”Build state shapes with schema/t:
import { schema, t, type Infer } from "@liebstoeckel/plugin-sdk";
const pollSchema = schema({ question: t.string, options: t.array(t.string), votes: t.record(t.string), // participantId → chosen option closed: t.boolean,});type PollState = Infer<typeof pollSchema>;| Builder | Type | Yjs mapping |
|---|---|---|
t.string t.number t.boolean | primitives | plain values |
t.array(item) | T[] | Y.Array |
t.record(value) | Record<string, V> | Y.Map (concurrent-friendly) |
t.object({…}) / schema({…}) | nested object | Y.Map |
The schema gives runtime validation, a default value, and static types via Infer.
The PluginState API
Section titled “The PluginState API”ClientProps.state is a typed accessor over the shared doc:
state.snapshot(); // current state as plain JS (defaults filled in)state.set("closed", true); // replace a whole top-level fieldstate.recordSet("votes", pid, "Yes"); // set one entry of a record fieldstate.recordDelete("votes", pid); // remove one entrystate.ensureDefaults({ question, options }); // seed once, only if emptyconst off = state.subscribe((snap) => …); // observe deep changesFor example, the Q&A plugin needs per-question, per-participant votes. Rather than nesting, it keys a flat record by `${questionId}|${participantId}`:
const qaSchema = schema({ questions: t.record(t.object({ text: t.string, author: t.string, ts: t.number })), votes: t.record(t.boolean), // key: `${qid}|${pid}` answered: t.record(t.boolean), // key: qid dismissed: t.record(t.boolean), // key: qid});Then derive views with pure functions (rankedQuestions, voteCount, …) that you can unit-test without a browser.
Seeding from author props
Section titled “Seeding from author props”The presenter typically seeds initial content from the placement’s props, once:
useEffect(() => { if (p.role === "presenter" && p.snapshot.options.length === 0) { p.state.ensureDefaults({ question, options }); }}, [/* … */]);ensureDefaults is a no-op once the state is non-empty, so it’s safe to run on every presenter render.
Ephemeral state
Section titled “Ephemeral state”For transient effects (e.g. reactions floating up), store entries in a record keyed by crypto.randomUUID() and prune old ones on a timer with recordDelete, plus a per-participant rate limit. This converges across clients and keeps the doc small without needing a separate awareness channel.
Code-first presentations your agent can author. One file out, no server.
Comments