jsPsych.multiplayer
The multiplayer API is a module on the jsPsych instance, alongside jsPsych.data. It is
inert until an adapter is connected.
Most experiments use it only for connect(); the plugins call the rest. It is available
directly for the cases plugins do not cover.
Methods
connect(adapter): Promise<void>
Registers a backend and joins the group session. Must resolve before jsPsych.run(),
because every multiplayer trial reaches the session through the connected adapter.
const jsPsych = initJsPsych();
await jsPsych.multiplayer.connect(new jsPsychAdapterMultiplayerLocal());
jsPsych.run(timeline);
participantId: string
This client's stable ID within the session. Keys into the group session object.
push(data): Promise<void>
Replaces the calling client's slot with data. It does not merge: any key the call
omits is gone, both for that client and for every other client reading the session.
await jsPsych.multiplayer.push({ offer: 4, joinedAt: myJoinedAt });
Whatever fields other clients depend on must be carried forward on every push. This is the most common source of multiplayer bugs; see the ultimatum tutorial for a worked example of the failure.
update(data): Promise<void>
Shallow-merges data into the calling client's slot and pushes the result — the
get → merge → push sequence in one call, for plugins and experiments that only ever
change a few keys of their own slot.
Not atomic against itself: overlapping calls from the same client race, so await each one
before issuing the next.
get(participantId): Record<string, unknown> | undefined
One participant's slot, or undefined if that participant is not in the session.
getAll(): GroupSessionData
Synchronous snapshot of the whole group session — a map from participant ID to slot.
subscribe(callback): Unsubscribe
Registers callback for live updates; returns a function that cancels it. The current
state is replayed immediately on registration, so a component mounting mid-session
renders at once instead of waiting for the next change.
Every subscription is tracked internally, but nothing in jsPsych's lifecycle cancels them
automatically. They are released only when the experiment calls disconnect() (which
cancels them for you) or cancelAllSubscriptions() directly. A subscription that is never
unsubscribed and never disconnected leaks its listener — so a continuous plugin should
release its handle when its trial ends, and an experiment should disconnect() when it
finishes.
const unsubscribe = jsPsych.multiplayer.subscribe((group) => {
render(group);
});
wait(condition, timeout?): Promise<GroupSessionData>
Resolves with the group session once condition(group) returns true.
- Event-driven, built on
subscribe— no polling. - Fast-path: an already-true condition resolves immediately.
- With
timeout(ms), rejects with a typedMultiplayerTimeoutError, so an experiment can detect an abandoned partner instead of hanging forever. - A
conditionthat throws rejects the promise rather than being silently swallowed.
push() followed by wait() is the synchronization barrier most turn-based paradigms
reduce to. plugin-multiplayer-sync packages that pair as one declarative trial, and is
usually the better choice for experiment code than calling these directly.
cancelAllSubscriptions(): void
Releases every subscription registered through subscribe(). Nothing calls this
automatically; call it at experiment end if the experiment does not disconnect().
disconnect(): Promise<void>
Leaves the group session, cancelling all active subscriptions first.
The adapter contract
An adapter is any object implementing:
interface MultiplayerAdapter {
readonly participantId: string;
connect(): Promise<void>;
push(data: Record<string, unknown>): Promise<void>;
getAll(): GroupSessionData;
get(participantId: string): Record<string, unknown> | undefined;
subscribe(cb: (data: GroupSessionData) => void): Unsubscribe;
disconnect(): Promise<void>;
}
Note that getAll and get are synchronous: an adapter over an asynchronous backend
must maintain an in-memory mirror of the session and await its first snapshot during
connect(). update() and wait() are composed by the API on top of these six methods,
so an adapter does not implement them.
The rules of the group session
- A client can write only its own slot, and a write replaces it entirely. Write conflicts are impossible by construction.
- Every client can read every slot, by snapshot or subscription.
- Shared decisions are computed, not negotiated — every client runs the same deterministic function over the same session data and reaches the same conclusion, with no coordinator to disconnect.