Skip to content

Version Packages - #35

Open
github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main
Open

github-actions[bot] wants to merge 1 commit into
mainfrom
changeset-release/main

Conversation

@github-actions

@github-actions github-actions Bot commented Jul 10, 2026 •

Copy link
Copy Markdown
Contributor

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and publish to npm yourself or setup this action to publish automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@jspsych-multiplayer/adapter-multiplayer-firebase@0.2.0

Minor Changes

  • #107 af1f9b6 Thanks @jodeleeuw! - Implement the hardened multiplayer adapter contract. getAll() returns each stored payload unchanged, and the connection calls onResumed() when the server removed its presence without the connection seeing a drop. With matchmaking, every member now sees the seal and the final roster.

    Breaking changes:

    • pathPrefix is renamed to namespace, and connectTimeoutMs is removed: pass connectTimeout to jsPsych.multiplayer.connect() instead. Passing either old option throws.
    • The default participant id is now kept per tab in sessionStorage (persistParticipant, default true), so a reload comes back as the same participant, in the same group, and jsPsych reports it as a restart. Pass persistParticipant: false for a new participant on every page load.
    • sessionBinding now defaults to true in every identity mode.
    • New security rules. Each connection claims its participant id (<namespace>-owners/<session>/<id> = uid), and only that uid can write the participant's data and presence, whatever the identity mode. Matchmaking groups are stored as per-seat entries tied to the holder's uid, plus a sealed roster that must match the seats. The lobby can only move off a sealed group. Update your database rules from the README (the quick-start rules now cover every node too). Groups formed by an earlier version aren't read.
    • Uses @jspsych-multiplayer/utils for session ids, id generation, and id validation. Invalid-id errors now read "must be a non-empty string without any of : / . # $ [ ]".
  • #105 3f2f35c Thanks @jodeleeuw! - Report a sessionId on each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report their sessionId option (the ?mp_session= value by default); the JATOS adapter reports the JATOS group result ID and rejects connect() if the channel opens without one. jsPsych seeds shared randomness (jsPsych.multiplayer.random() and related methods) with it, so every participant in a group gets the same values.

  • #43 565ff2f Thanks @htsukamoto5! - Add adapter-multiplayer-firebase, a Firebase Realtime Database backend for the jsPsych multiplayer API — real cross-device multiplayer with essentially no server to write or host.

    It implements the same MultiplayerAdapter contract as the local and JATOS adapters, so plugins behave identically on any of them, and sits between them on the infrastructure spectrum: broader reach than the same-browser local adapter, far less setup than a self-hosted JATOS server.

    Each connect() opens an independent connection that mirrors the session node (everyone's data) and a sibling presence node (who is connected), and resolves once both have loaded, rejecting on a rules denial, a timeout, or a cancelled attempt. Each participant's slot is JSON-encoded as a string so pushes round-trip exactly over RTDB's JSON coercion. Presence nodes are removed by the server through onDisconnect() when a participant's connection drops, which is how jsPsych detects dropouts; data slots are kept. .info/connected drives the connection's own reconnecting / connected status, and a listener cancelled after connecting reports closed.

    An optional useUidAsParticipantId mode enables the recommended session-locked security rules: uid-as-key for slots and presence (no participant can write another's) plus first-write-wins session binding — connect() registers a mp-sessions-memberships/<uid> = sessionId record that the server-evaluated rules make immutable and then require on every session read and write, so a client identity can only ever touch the session it first joined. The recommended rules ship as database.rules.json (with a firebase.json for the emulator suite). firebase is a peer dependency, and the adapter is unit-tested against an in-memory backend fake with no credentials.

  • #94 ffebbe6 Thanks @jodeleeuw! - Adopt the redesigned jsPsych multiplayer adapter contract (jsPsych#3694): connect(options) returns a new connection each time, change notifications go through onChange() / onStatus() instead of adapter-side subscribe(), and connect() honors the cancellation signal. Presence now lives in a separate <pathPrefix>-presence node (add it to your security rules; see the README), and disconnect() or a dropped connection no longer deletes the participant's data slot. The removeOnDisconnect option is removed.

  • #106 bba1dc2 Thanks @jodeleeuw! - Add matchmaking: { lobby, groupSize }: everyone opens the same link, and the adapter fills groups of groupSize as participants arrive, instead of grouping them by ?mp_session= link. Each step of joining is a Realtime Database transaction, so two participants who arrive together can't both take the last place. A group is sealed when its last place is taken (or early, with jsPsych.multiplayer.sealGroup()), and the adapter reports it through the core's group(), so jsPsych.multiplayer.waitForGroup() can hold participants in a waiting room. While a group is filling, a participant who leaves gives up their place. The recommended rules gain mp-sessions-lobby and mp-sessions-groups entries; redeploy them to use matchmaking.

Patch Changes

  • #103 c875c70 Thanks @jodeleeuw! - Document and test rejoining: a participant whose connection drops and recovers on the same page rejoins (onParticipantRejoined), while a reload under the same participant id is reported as a restart (onParticipantRestarted). No behavior change was needed.

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/adapter-multiplayer-jatos@0.2.0

Minor Changes

  • #107 af1f9b6 Thanks @jodeleeuw! - Port to the hardened multiplayer adapter contract.

    • Breaking: the connectTimeoutMs and closeAfterReconnectingMs options are removed; the adapter warns and ignores them. Use jsPsych's own options instead: jsPsych.multiplayer.connect(adapter, { connectTimeout, reconnectTimeout }). The adapter now honours the AbortSignal jsPsych passes to connect(), which also ends a join jatos.js keeps refusing while an old socket closes.
    • The adapter now relays a group seal itself. JATOS confirms setGroupFixed() only to the member who asked, so that member writes the sorted final roster into the group session under the reserved key $sealed. Every member that reads a record it trusts reports sealed: true with that roster, and rosters are merged so members who drop out after the seal stay on it. A record is trusted only if its writer and the reader are on it, it includes everyone jatos.groupMembers lists at the time, and everyone on it has been seen as a member or has written data.
    • getAll() returns only participant payloads, unchanged; group session keys starting with $ are left out.
    • sealGroup() is left off the connection when jatos.js has no setGroupFixed(), so jsPsych.multiplayer.sealGroup() reports unsupported instead of the adapter throwing.
    • push() rejects at once while the group channel is down, and after 3 quick attempts (was 8) on repeated version conflicts; jsPsych retries with backoff and resends once the channel reopens. A push no longer waits indefinitely for the channel, which blocked every later write.
    • The participant ID (the study result ID) is checked with validateId from @jspsych-multiplayer/utils, a new dependency.
  • #105 3f2f35c Thanks @jodeleeuw! - Report a sessionId on each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report their sessionId option (the ?mp_session= value by default); the JATOS adapter reports the JATOS group result ID and rejects connect() if the channel opens without one. jsPsych seeds shared randomness (jsPsych.multiplayer.random() and related methods) with it, so every participant in a group gets the same values.

  • #106 cba1c47 Thanks @jodeleeuw! - Report the JATOS group through the core's group() and sealGroup(): the size is the batch's maxActiveMembers, the members are jatos.groupMembers, and sealing fixes the group with jatos.setGroupFixed(). By default the adapter fixes the group once it is full (sealWhenFull: true), so a member who leaves mid-study counts as a dropout instead of freeing their place for a newcomer. Experiments can hold participants in a waiting room with jsPsych.multiplayer.waitForGroup().

  • #103 d54626d Thanks @jodeleeuw! - Support rejoining the group from the same page.

    • Behavior change: closeAfterReconnectingMs now defaults to null (never give up) instead of 30 s, so a participant whose group channel reopens after a long outage rejoins instead of having their session closed. Set a limit to restore the old behavior.
    • connect() right after disconnect() no longer fails while the old socket is still closing: the adapter retries jatos.js's "not in readyState CLOSED" refusal until the socket has closed, within connectTimeoutMs.
    • A connect() made while a closed connection is still leaving the group, or while a cancelled join is still in flight, now waits for it instead of rejecting. The page-wide guard is also held until an opened channel has finished leaving, so a quick reconnect no longer hits jatos.js's "can't open group channel while leaving a group".
  • #94 baf23da Thanks @jodeleeuw! - Breaking: implement the redesigned multiplayer adapter contract from jsPsych#3694.

    • connect(options) now returns a new MultiplayerConnection for each call, with getAll(), connectedParticipants(), push(), and disconnect(). The adapter's own subscribe() and get() are gone; the adapter reports changes through options.onChange(), and jsPsych's multiplayer session handles subscriptions.
    • Presence: connectedParticipants() returns the members with an open group channel (jatos.groupChannels), and member join/leave/open/close events are reported, so jsPsych can mark participants who drop out as away and then left.
    • Connection status: a dropped group channel is reported as reconnecting and a reopened one as connected. A channel that stays down longer than the new closeAfterReconnectingMs option (default 30000 ms) is reported as closed. While the channel is down, reads return the last group session data instead of the empty data jatos.js holds.
    • push() waits for a dropped channel to reopen instead of failing.
    • connect() honors the abort signal, rejects promptly when jatos.js refuses to open a channel, and rejects while another connection on the page is open or still joining (jatos.js supports one group channel per page). If a cancelled connect's channel opens anyway, the adapter leaves the group.

Patch Changes

  • Updated dependencies [403bfc4]:
    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/adapter-multiplayer-local@0.2.0

Minor Changes

  • #107 af1f9b6 Thanks @jodeleeuw! - Port to the hardened multiplayer adapter contract.

    • Breaking: the keyPrefix option is renamed namespace, matching the other adapters. Passing keyPrefix logs a warning and is ignored.
    • Breaking: persistParticipant now defaults to true, so a reload keeps the tab's participant ID (in sessionStorage) and the group can tell that participant restarted instead of seeing a new stranger. Pass persistParticipant: false for a fresh ID on every page load, as before.
    • A lapsed heartbeat now calls the contract's onResumed() instead of reporting a synthetic reconnecting → connected blip.
    • Session and participant IDs are checked with validateId from @jspsych-multiplayer/utils: they may not contain any of : / . # $ [ ] (previously only : was rejected). The namespace may not contain :.
    • getAll() returns each participant's payload exactly as it was pushed.
    • The session ID, participant ID, and tab-ID helpers now come from @jspsych-multiplayer/utils, a new dependency.
  • #94 362774c Thanks @jodeleeuw! - Implement jsPsych's redesigned multiplayer adapter contract (jsPsych#3694): connect() now returns a new, independent connection each time, and the adapter reports changes through the onChange() callback instead of its own subscribe() / get().

    Add presence: each tab writes a heartbeat, removes it on pagehide or disconnect, and drops out after presenceTimeoutMs (default 70 s) if it stops. New options heartbeatIntervalMs and presenceTimeoutMs. disconnect() no longer deletes the participant's data slot. An injected signal is no longer closed by the adapter, and ChangeSignal.onChange() now returns a function that removes the handler.

  • #103 4687f75 Thanks @jodeleeuw! - Support rejoining. A tab whose heartbeat lapsed for longer than presenceTimeoutMs (a throttled background tab, or a page frozen in the back/forward cache) now reports reconnecting and then connected when its next heartbeat runs, so the other tabs count it as back (onParticipantRejoined) instead of leaving it left. A refresh is still a restart: with persistParticipant: true the same id returns from a new page load and the other tabs report it through onParticipantRestarted.

  • #105 3f2f35c Thanks @jodeleeuw! - Report a sessionId on each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report their sessionId option (the ?mp_session= value by default); the JATOS adapter reports the JATOS group result ID and rejects connect() if the channel opens without one. jsPsych seeds shared randomness (jsPsych.multiplayer.random() and related methods) with it, so every participant in a group gets the same values.

Patch Changes

  • Updated dependencies [403bfc4]:
    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-chat@0.2.0

Minor Changes

  • #107 be66150 Thanks @jodeleeuw! - Port chat to the hardened multiplayer API.

    • Messages live in the trial's own scope, so each chat trial starts empty. Removed data_key; give several chat trials the same multiplayer_scope to continue one conversation across them.
    • New data field multiplayer_outcome ("completed", "participant_left", "connection_lost", or "cancelled" when the experiment disconnects mid-trial). Removed partner_left and connection_lost. ended_by now only says which end condition completed the trial ("duration", "button", or "condition") and is null otherwise.
    • With a sealed group, the trial ends when any other member who hasn't left leaves, including one who was only away when it started.
    • Messages from a participant who leaves stay in the transcript.
    • Uses @jspsych-multiplayer/utils. The "couldn't send" note is gone: the core retries failed writes.
  • #94 bae51f0 Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.

    • Use jsPsych.multiplayer directly, with a clear error on a jsPsych version without it.
    • New parameter end_on_participant_left (default true): chat and draw end when a participant who was connected at the start leaves the study; reference-game ends when the partner leaves before feedback. New data fields partner_left, left_participant, and connection_lost; ended_by can also be "participant_left" or "connection_lost".
    • A lost connection ends the trial instead of leaving it waiting, and send errors no longer claim to be retrying after the connection is gone.
    • end_when, sender_label (chat), and roster_label (draw) also receive the presence snapshot; rosters mark participants who are away or have left. Reference-game partner auto-detection ignores participants who left.
    • Trial data no longer holds the API's frozen snapshots: draw's strokes and reference-game's save_group are copies.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #29 2650fa9 Thanks @htsukamoto5! - Fix unreadable chat transcript: the plugin shipped no CSS, so sender and message text rendered as bare unstyled <span>s with nothing between them (e.g. "AliceHello"). Inject minimal scoped styles (boxed log, one message per line, bold sender label with a colon separator, own-message highlight) so the transcript is legible out of the box.

    Also clarifies the chat-room example's name prompt ("Choose a display name — this is what other participants will see you as in the chat") since testers read the original wording as naming the chat room itself.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-choice@0.2.0

Minor Changes

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes, MultiplayerError codes).

    Breaking:

    • Each trial has its own part of the shared data, so choice trials no longer need their own keys: the data_key parameter and data field are removed, and choices are written under choice in the trial's data.
    • timed_out, partner_left, connection_lost, and wait_error are replaced by multiplayer_outcome ("completed", "timeout", "participant_left", or "connection_lost"); left_participant stays.

    A timeout of 0 or less still means no limit. The barrier no longer waits for the backend to confirm this participant's choice before its timeout starts.

  • #37 95ccc8a Thanks @Mandyx22! - Add plugin-multiplayer-choice: a simultaneous group-decision primitive. Each participant picks one of the same options; the trial pushes that choice and waits (a barrier) until all expected_players have chosen, then optionally reveals the outcome. It is the engine under simultaneous-move paradigms (prisoner's dilemma, public-goods contributions, dictator/coordination games), packaging the choose → push → wait → reveal flow as one declarative trial. Two reveal modes: reveal_mode: "players" (default) lists who chose what; reveal_mode: "tally" shows only per-option counts and the plurality winner — an anonymous group poll, with record_choices_by_player: false to keep the recorded data anonymous too (this subsumes the separately-proposed plugin-multiplayer-vote). Includes a timeout that degrades to a partial group, an optional payoff(choices, me) hook (off by default, so the plugin stays a pure decision primitive), player_label/button_html display hooks, always-recorded aggregate data (tally/winner/is_tie/tied_options), a barrier count bounded by the option range (a stale out-of-range pick can neither lift the barrier nor skew n_players), and static access to the pure core (collectChoices/countChosen/tally/plurality).

  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #37 0a25a8a Thanks @Mandyx22! - Distinguish a genuine barrier timeout from other wait() rejections. jsPsych#3694 rejects a timeout with a typed MultiplayerTimeoutError; a wait() can otherwise reject because the condition predicate threw or the backend failed. Only a timeout now proceeds with a partial snapshot as a timeout (timed_out: true, runs on_timeout); a throwing predicate or a backend failure rethrows, so the trial halts loudly instead of masquerading as a timeout.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-countdown@0.2.0

Minor Changes

  • #41 b4d2faa Thanks @htsukamoto5! - Add plugin-multiplayer-countdown, a synchronized group timer (countdown or count-up) for the jsPsych multiplayer API.

    Every participant pushes its own start timestamp into its own slot, and each client derives the displayed time from the minimum timestamp across all slots — a coordination-free consensus (no elected anchor, no single point of failure) in the same spirit as plugin-multiplayer-role's ordering. Late joiners and refreshes resume at the group's actual remaining time for free, and the pure consensus core (startedAtKey / resolveStartedAt / computeRemaining / computeElapsed / formatTime) is exposed as statics on the default export so demos can render their own synced display. Requires a jsPsych with the multiplayer API from jsPsych#3694.

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API. The start timestamp now lives in the trial's own scope of the shared data under countdown_started_at, so every countdown starts a fresh clock and the name parameter is removed. To run one clock across several trials, give them the same multiplayer_scope. The trial records multiplayer_outcome ("completed" or "connection_lost") and left_participant in place of connection_lost.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-draw@0.2.0

Minor Changes

  • #34 d37bcb3 Thanks @htsukamoto5! - Add plugin-multiplayer-draw, a real-time collaborative drawing canvas for the jsPsych multiplayer API.

    Every participant draws on one shared canvas; strokes from everyone appear live on everyone else's screen. Includes pen/eraser tools, a fixed color palette, brush sizes, and undo/redo buttons that only ever act on the participant's own strokes. Where plugin-multiplayer-chat pushes once per message, this plugin pushes continuously while a stroke is active (throttled and point-decimated), making it the first plugin that stresses the multiplayer API's subscribe primitive at a genuinely high rate. Full repaints (triggered by undo or a canvas resize) paint strokes in a global timestamp order so the eraser's destination-out compositing behaves consistently across clients. Requires a jsPsych with the multiplayer API from jsPsych#3694.

  • #107 be66150 Thanks @jodeleeuw! - Port draw to the hardened multiplayer API.

    • Strokes live in the trial's own scope, so each draw trial starts with a blank canvas. Removed data_key; give several draw trials the same multiplayer_scope to keep drawing on one canvas.
    • New data field multiplayer_outcome ("completed", "participant_left", "connection_lost", or "cancelled" when the experiment disconnects mid-trial). Removed partner_left and connection_lost. ended_by now only says which end condition completed the trial ("duration", "button", or "condition") and is null otherwise.
    • With a sealed group, the trial ends when any other member who hasn't left leaves, including one who was only away when it started.
    • Strokes from a participant who leaves stay on the canvas and in the data.
    • Uses @jspsych-multiplayer/utils. The "connection trouble" note is gone: the core retries failed writes.
  • #94 bae51f0 Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.

    • Use jsPsych.multiplayer directly, with a clear error on a jsPsych version without it.
    • New parameter end_on_participant_left (default true): chat and draw end when a participant who was connected at the start leaves the study; reference-game ends when the partner leaves before feedback. New data fields partner_left, left_participant, and connection_lost; ended_by can also be "participant_left" or "connection_lost".
    • A lost connection ends the trial instead of leaving it waiting, and send errors no longer claim to be retrying after the connection is gone.
    • end_when, sender_label (chat), and roster_label (draw) also receive the presence snapshot; rosters mark participants who are away or have left. Reference-game partner auto-detection ignores participants who left.
    • Trial data no longer holds the API's frozen snapshots: draw's strokes and reference-game's save_group are copies.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-match@0.2.0

Minor Changes

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #105 3f2f35c Thanks @jodeleeuw! - The "random" strategy now shuffles with jsPsych.multiplayer.shuffle, so it is seeded by the session ID (or the randomSeed connect option) and each group of participants gets its own grouping. seed now picks a different grouping within the session.

  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API. The trial now writes its data into its own scope of the shared data, and the snapshot it partitions holds the participants who have reached this trial, each with their session data merged under their data from the trial. joinedAt is written once to the session scope, so join_order stays stable across rounds. push_data is renamed write_data. The trial records multiplayer_outcome and left_participant in place of timed_out, partner_left, and connection_lost, and a timeout of 0 now means no limit. The match accessors now read the last match trial's data instead of a module-level store; they take an optional jsPsych instance for pages that run several.

  • #38 cda5411 Thanks @Mandyx22! - Add plugin-multiplayer-match: partition a multiplayer group into matched sub-groups (pairs by default, or triads/larger) by deterministic consensus — every client independently computes the same partition from the shared group-session snapshot, with no coordinator. It is the foundational primitive under pairwise/small-group paradigms (trust game, ultimatum, dyadic negotiation) and composes with plugin-multiplayer-role (assign roles within a group via position). Runs as a short barrier (like plugin-multiplayer-role), supports ordered/join_order/random (seeded, per-round) pairing strategies and error/spectator/smaller_group leftover policies for non-divisible counts, fails loud on timeout, and exposes the pure core (buildMatches) plus partner accessors (getMyPartners/getMyGroup/getMyPosition/getMatchMap) as statics for downstream trials.

  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #38 cd7f8fe Thanks @Mandyx22! - Adopt the push-then-wait pattern after communicate() was removed from the jsPsych multiplayer API (jsPsych#3694). The match barrier now calls push() then wait(), and distinguishes a genuine readiness timeout (MultiplayerTimeoutError, matched by error name) from other rejections: a real timeout ends the trial gracefully (timed_out: true), while a backend or push failure propagates loudly instead of being mislabelled as a timeout.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-ready@0.1.0

Minor Changes

  • #30 d54c569 Thanks @Mandyx22! - Add plugin-multiplayer-ready, a participant-facing ready / check-in barrier for the jsPsych multiplayer API.

    It packages the common lobby / waiting-room pattern into a single declarative trial: show a prompt and a ready button, merge { ready: true } and a key for this gate (plus any push_data) into the participant's slot when they click, display a waiting message, and end the trial once expected_players members are ready (or an optional timeout elapses while waiting for the rest of the group). Unlike plugin-multiplayer-sync, it owns the check-in UI and the "everyone is ready" condition, and standardizes on a ready: true flag so other plugins and examples can reliably gate on group readiness. Requires a jsPsych with the multiplayer API from jsPsych#3694.

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.
  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes, MultiplayerError codes).

    Breaking:

    • Each trial has its own part of the shared data, so gates no longer need their own keys: the data_key parameter and data field are removed, and the plugin writes ready: true in the trial's data. The session-wide ready: true flag is no longer written.
    • push_data is renamed write_data. It is merged into this participant's part of the trial's data, so values in it are not visible in later trials; write those with jsPsych.multiplayer.update(data, { scope: "session" }).
    • The trial's shared data is saved in group only with the new save_group: true parameter.
    • timed_out, partner_left, connection_lost, and wait_error are replaced by multiplayer_outcome ("completed", "timeout", "participant_left", or "connection_lost"); left_participant stays.

    A timeout of 0 or less still means no limit. The gate no longer waits for the backend to confirm the ready flag before its timeout starts.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #45 21e0909 Thanks @htsukamoto5! - Fix wait_error/timed_out mislabeling a non-timeout wait() failure as a timeout. Both plugins previously treated every wait() rejection as a timeout (a leftover from before jsPsych#3694 exported a typed MultiplayerTimeoutError), so a throwing wait_for predicate or an adapter/backend error would silently finish the trial with timed_out: true and call on_timeout, hiding the real failure in wait_error's message.

    Now only a rejection whose error.name === "MultiplayerTimeoutError" is recorded as a timeout. A throwing wait_for predicate or a backend error propagates and fails the trial, matching how a push() failure is already handled.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-reference-game@0.2.0

Minor Changes

  • #42 a836cd6 Thanks @Mandyx22! - Add plugin-multiplayer-reference-game, a repeated referential communication game ("tangrams"; Hawkins, Frank & Goodman 2020) for two players on the jsPsych multiplayer API.

    Two players are paired as a fixed director and matcher and see the same objects, each in an independently scrambled layout; only the director sees which objects are targets (and, for more than one, in what order). They communicate over an integrated free-text chat, the matcher assigns objects to the director's ordered target slots (a single click when there is one target), and both then see feedback with the true answer revealed. The published "sequential" (one target, click) and "unconstrained" (all N objects are ordered targets, reproduce the whole board) conditions are the same task with two parameters turned differently — stimuli length and targets length — so one configurable plugin covers both, plus everything in between. Like plugin-multiplayer-chat it is a continuously-open, subscribe-driven trial: the matcher's submitted assignment is the shared trigger on which both clients score, show feedback, and end. Object/target counts, scramble mode, chat direction and limits, scoring rule, feedback content, and an optional pre-submit interaction log are all parameters. Composes with plugin-multiplayer-role (director/matcher) and plugin-multiplayer-sync (lobby). Requires a jsPsych with the multiplayer API from jsPsych#3694.

  • #94 bae51f0 Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.

    • Use jsPsych.multiplayer directly, with a clear error on a jsPsych version without it.
    • New parameter end_on_participant_left (default true): chat and draw end when a participant who was connected at the start leaves the study; reference-game ends when the partner leaves before feedback. New data fields partner_left, left_participant, and connection_lost; ended_by can also be "participant_left" or "connection_lost".
    • A lost connection ends the trial instead of leaving it waiting, and send errors no longer claim to be retrying after the connection is gone.
    • end_when, sender_label (chat), and roster_label (draw) also receive the presence snapshot; rosters mark participants who are away or have left. Reference-game partner auto-detection ignores participants who left.
    • Trial data no longer holds the API's frozen snapshots: draw's strokes and reference-game's save_group are copies.
  • #107 be66150 Thanks @jodeleeuw! - Port reference-game to the hardened multiplayer API.

    • Each round's shared data (the submission, the chat, typing timestamps) lives in the round's own trial scope. Removed data_key and typing_key. With chat_persists, the chat log is kept in the session scope. The stale-replay guard now catches rounds that share a multiplayer_scope.
    • New data field multiplayer_outcome ("completed", "timeout", "participant_left", "connection_lost", or "cancelled"). Removed ended_by, partner_left, and connection_lost.
    • show_running_score sums n_correct from this participant's earlier reference-game trials in the jsPsych data, plus the current round.
    • Partner auto-detection uses the sealed group's other member, or else the other connected participant.
    • save_group saves this round's shared data. Chat messages from a partner who leaves stay in the transcript.
    • Uses @jspsych-multiplayer/utils. The send-error note is gone: the core retries failed writes.
  • #80 e4c9a0b Thanks @Mandyx22! - Add two options needed to match the original tangrams experiment (hawkrobe/tangrams) exactly:

    • scramble_mode: "disjoint" — like "independent", but guarantees no object occupies the same
      slot for both players. Plain "independent" only guarantees the two layouts are not identical, so
      around a third of objects still coincide by chance and positional reference ("the one in the
      corner") sometimes works by luck. The original re-rolls its layouts until every position differs.
    • feedback_content may now be keyed by role — { director: {...}, matcher: {...} } — so the two
      players can see different feedback. The original shows the director only the object the matcher
      clicked, and the matcher only the true target. A flat object still applies to both roles.
  • #67 d6926f8 Thanks @htsukamoto5! - Add a require_message_before_response parameter. When true, the matcher cannot commit a selection until the director has sent at least one chat message this round — while gated, matcher grid clicks are ignored and a brief hint is shown. This makes the plugin faithful to Hawkins, Frank & Goodman (2020) Exp. 2, whose client blocked the matcher's click behind messageSent, guaranteeing a referring expression on every trial. Defaults to false (unchanged behavior); inert with a warning when chat_enabled is false (gating with no channel would deadlock the matcher). Applies to both the click and assign_slots response modes. Only the partner's messages open the gate, so neither the matcher's own message nor a third participant's counts. Blocked clicks are recorded as gated_click events in interaction_history when save_interaction_history is on.

    Chat messages now also carry the round they were sent during. This makes the gate exact under chat_persists: true, where every round shares one log: an earlier round's message never pre-opens the gate, and the director's message for the current round still counts when it lands before the matcher's trial is constructed (the two clients do not enter a round at the same moment). Saved chat_transcript entries gain the same round field; transcripts written by earlier versions still merge and render.

  • #105 3f2f35c Thanks @jodeleeuw! - Arrangements now come from the session's shared randomness (jsPsych.multiplayer.shuffle), so each group gets its own arrangements, seeded by the session ID or the randomSeed connect option. seed still picks different arrangements within a session.

  • #92 e0fec98 Thanks @htsukamoto5! - Add a typing_indicator option (off by default) showing a "partner is typing…" hint driven by a
    timestamp each client keeps in its own slot (typing_key, throttled by typing_throttle, hidden
    typing_ttl after the last keystroke, labelled via typing_label). Hint only — it never gates
    trial progress, and it hides while the partner is away or has left. The timestamp is written with
    update(), which merges only that key, so it can't overwrite chat or round data.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-role@0.2.0

Minor Changes

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.
  • #105 3f2f35c Thanks @jodeleeuw! - The random strategy now draws from the session's shared randomness (jsPsych.multiplayer.shuffle), so each group gets its own assignment, and you can pin it across groups with the randomSeed connect option. seed now picks a different assignment within the session.

  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API. The trial now writes its data into its own scope of the shared data, and the snapshot the strategies see holds the participants who have reached this trial, each with their session data merged under their data from the trial. joinedAt is written once to the session scope, so join_order stays stable across rounds. push_data is renamed write_data and is no longer nested under rounds[round] (read entry.score, not entry.rounds[round].score). The trial records multiplayer_outcome and left_participant in place of timed_out, partner_left, and connection_lost, and a timeout of 0 now means no limit. The role accessors now read the last role trial's data instead of a module-level store; they take an optional jsPsych instance for pages that run several.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #45 fc1a842 Thanks @htsukamoto5! - Update for jsPsych#3694's removal of MultiplayerAPI.communicate(): the plugin now calls push() followed by wait() directly instead of the removed fused convenience method. Also fixes the same timeout-mislabeling bug already patched in plugin-multiplayer-sync/plugin-multiplayer-ready — only a rejection whose error.name === "MultiplayerTimeoutError" is now recorded as timed_out: true; a backend or push() failure propagates and fails the trial loudly instead.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-scoreboard@0.2.0

Minor Changes

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.
  • #33 33d337e Thanks @Mandyx22! - Add plugin-multiplayer-scoreboard: an end-of-game scoreboard for multiplayer experiments. Each client contributes its final score, the trial waits (a barrier) until the group has reported, then every client independently computes the same ranked leaderboard from the shared group-session snapshot — no coordinator, no extra round-trip — and renders it locally with its own row highlighted. Supports ascending/descending sort, standard/dense tie ranking, an on_timeout hook, timeouts that degrade to a partial board, and static accessors (getMyRank/getMyScore/getLeaderboard) for branching downstream trials.

  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes, MultiplayerError codes).

    Breaking:

    • Each trial has its own part of the shared data, so boards no longer need their own keys: the data_key parameter and data field are removed, and scores are written under scoreboard in the trial's data.
    • timed_out, partner_left, and connection_lost are replaced by multiplayer_outcome ("completed", "timeout", "participant_left", or "connection_lost"; null when reporting failed some other way, which error describes); left_participant and error stay.
    • display_label's second argument is now the session's shared data (what participants wrote with { scope: "session" }, e.g. names chosen in earlier trials) instead of the whole group snapshot.

    A timeout of 0 or less now means no limit, like the other plugins. The board no longer waits for the backend to confirm this client's score before its timeout starts.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/plugin-multiplayer-sync@0.2.0

Minor Changes

  • #94 91ff22c Thanks @jodeleeuw! - Move to jsPsych#3694's session-based multiplayer API, and handle participants who leave.

    Breaking: these plugins need a jsPsych with the redesigned multiplayer API (sessions, presence,
    wait(condition, { timeout, participants })). On a jsPsych without jsPsych.multiplayer they throw
    an error saying so.

    • Timeouts work again. The old positional wait(condition, timeout) form silently meant "no
      timeout" under the redesigned core; every configured timeout (sync, ready, choice, match, role,
      scoreboard) is honored again.
    • Departures. The barrier plugins (sync, ready, choice, match, role, scoreboard) take a
      participants parameter: the participants the barrier depends on. It defaults to every other
      participant who is connected when the wait starts (null), except in sync, where it defaults to
      [] (ignore departures) because sync is often used as a lobby. If one leaves, the trial ends the
      way a timeout would, and records partner_left: true and left_participant.
      Counts, lobbies, and partitions ignore participants who have left. match and role also wait until
      every participant they count is connected, so clients agree on the group and a slot left over from
      an earlier member is never matched or given a role.
    • Lost connections end the wait with connection_lost: true instead of failing the trial.
      Countdown keeps running locally and records connection_lost.
    • One key per gate. ready, choice, and scoreboard now tie each trial's writes to that trial:
      data_key defaults to ready-N / choice-N / scoreboard-N, counting that plugin's trials in the
      order this participant reaches them, so flags or choices from an earlier trial can't count toward a
      later one. An explicit data_key is used as-is. The key used is recorded as data_key. A trial that
      only some participants reach needs an explicit key, and the count restarts on a page reload.
    • ready merges push_data and the gate flag into the slot with update() instead of replacing
      the slot with push(), so earlier gates' flags and other data survive. It still sets ready: true.
    • Frozen snapshots. User callbacks (wait_for, ready, rank_by, role_from, display_label,
      …) receive frozen data. Predicates receive (snapshot, presence). In match and role, a throwing
      predicate or accessor still means "not ready", and the last error is logged if the group never
      becomes ready.
    • scoreboard drops its own timeout race and backstop wait in favor of the core's timeout, and
      shows a note on boards revealed after a departure or a lost connection.
  • #106 8b89cac Thanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):

    • expected_players (ready, choice, match) and group_size (role, scoreboard) default to the sealed group's members who haven't left. expected_players on ready and choice is no longer required when the group is sealed; without a sealed group it still is, and the error says how to fix it.
    • participants: null means the rest of the sealed group's members who haven't left, including a member who is only away at that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.
  • #107 be66150 Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes, MultiplayerError codes).

    Breaking:

    • push_data is renamed write_data, and it is merged into this participant's data with update() instead of replacing it. It goes to the trial's own part of the shared data, which is also what wait_for sees, so values written in an earlier trial never satisfy a later barrier.
    • participants now defaults to null: the other members of a sealed group who haven't left, or else the others who are connected when the wait starts. Pass participants: [] to keep the old behavior of ignoring departures (e.g. in a lobby).
    • The trial's shared data is saved in group only with the new save_group: true parameter.
    • timed_out, partner_left, connection_lost, and wait_error are replaced by multiplayer_outcome ("completed", "timeout", "participant_left", or "connection_lost"); left_participant stays.

    A timeout of 0 or less still means no limit. The barrier no longer waits for the backend to confirm write_data before its timeout starts.

Patch Changes

  • #90 e698fc5 Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.

    A pending wait() now rejects with a MultiplayerCancelledError when the experiment ends or is
    aborted, instead of hanging. The plugins that wait on the group (choice, match, ready, role,
    scoreboard, sync) stop quietly on that error rather than treating it as a timeout or a backend
    failure. plugin-multiplayer-scoreboard previously logged an error and drew a final board over the
    cleared display, leaving a Continue button that called finishTrial after the run had ended.

    Fixes found alongside that work: plugin-multiplayer-countdown and plugin-multiplayer-draw
    registered their repeating timers with a raw setInterval, which survived abortExperiment() — the
    countdown could call finishTrial after the run ended and draw kept writing to the session forever;
    both now tick through jsPsych's own timer registry. plugin-multiplayer-chat and
    plugin-multiplayer-reference-game could drop a message when a participant sent two in quick
    succession; they no longer do. adapter-multiplayer-firebase marked itself connected before arming
    its disconnect cleanup, so a failure there left a connected-looking adapter with a leaked listener
    and a retry that silently did nothing; a failed connect() now releases everything it opened.

  • #45 21e0909 Thanks @htsukamoto5! - Fix wait_error/timed_out mislabeling a non-timeout wait() failure as a timeout. Both plugins previously treated every wait() rejection as a timeout (a leftover from before jsPsych#3694 exported a typed MultiplayerTimeoutError), so a throwing wait_for predicate or an adapter/backend error would silently finish the trial with timed_out: true and call on_timeout, hiding the real failure in wait_error's message.

    Now only a rejection whose error.name === "MultiplayerTimeoutError" is recorded as a timeout. A throwing wait_for predicate or a backend error propagates and fails the trial, matching how a push() failure is already handled.

  • #53 57ea69d Thanks @htsukamoto5! - Read the multiplayer API from jsPsych.multiplayer (jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods on jsPsych.pluginAPI predate the current API and are not supported.

  • #45 a587023 Thanks @htsukamoto5! - Finish gracefully when getAll() throws on the timeout path. On a genuine timeout the adapter may already be torn down (getAll() then throws "connect() must be called…"), which would otherwise escape and reject the trial instead of finishing it as timed_out: true. The snapshot read now falls back to an empty group session, matching the safeGetAll guard plugin-multiplayer-ready already had.

  • #62 d1552c0 Thanks @jodeleeuw! - Register trial timers through jsPsych.pluginAPI.setTimeout so they are cancelled when a trial is ended externally (abortExperiment, endCurrentTimeline, forced finishTrial), instead of firing into a finished trial. The plugins previously used bare setTimeout and only cleared handles on their own end paths, so external termination — exactly what multiplayer sync timeouts and host-ended sessions do — left timers alive.

  • Updated dependencies [403bfc4]:

    • @jspsych-multiplayer/utils@0.1.0

@jspsych-multiplayer/utils@0.1.0

Minor Changes

  • #107 403bfc4 Thanks @jodeleeuw! - Add @jspsych-multiplayer/utils, the helpers the multiplayer plugins and adapters share: isMultiplayerError, outcomeOf and the MultiplayerOutcome type, pluginTimeout, remainingParticipants, sealedGroupSize, withoutLeft, the waitForAll barrier, and the adapter helpers sessionIdFromUrl, tabId, generateId, and validateId.

@github-actions
github-actions Bot force-pushed the changeset-release/main branch 4 times, most recently from 6bc05ab to 5f21215 Compare July 13, 2026 18:37
@github-actions
github-actions Bot force-pushed the changeset-release/main branch 26 times, most recently from c860e69 to c7f09c5 Compare July 25, 2026 15:24
@github-actions
github-actions Bot force-pushed the changeset-release/main branch from c7f09c5 to 642ee5e Compare July 25, 2026 18:51
@htsukamoto5

Copy link
Copy Markdown
Member

@
Post-merge follow-up (not a blocker for this PR):

Once this merges and the @jspsych-multiplayer/* packages actually publish, reconcile the README in jspsych/multiplayer-test-experiments:

  • Its script tags are pinned to 0.1.0 on jsDelivr, but its "Running it" steps still describe building from a local checkout and swapping in relative dist/ paths. The two halves currently disagree; pick whichever is true post-publish and drop the other to a fallback note.
  • Confirm the published version actually is 0.1.0 — if the changesets here bump past it, those pinned URLs are wrong and the files won't load.

(Noted as a comment rather than in the PR body, since the changesets bot regenerates the description.)
@

@github-actions
github-actions Bot force-pushed the changeset-release/main branch from 6e08055 to 7d6280f Compare September 28, 2026 14:14

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant