Repository navigation
Version Packages - #35
Open
github-actions[bot] wants to merge 1 commit into
Open
github-actions[bot] wants to merge 1 commit into
github-actions[bot] wants to merge 1 commit into
Conversation
github-actions
Bot
force-pushed
the
changeset-release/main
branch
4 times, most recently
from
July 13, 2026 18:37
6bc05ab to
5f21215
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
26 times, most recently
from
July 25, 2026 15:24
c860e69 to
c7f09c5
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
July 25, 2026 18:51
c7f09c5 to
642ee5e
Compare
Member
|
@ Once this merges and the
(Noted as a comment rather than in the PR body, since the changesets bot regenerates the description.) |
github-actions
Bot
force-pushed
the
changeset-release/main
branch
14 times, most recently
from
September 25, 2026 18:49
258744f to
6e08055
Compare
github-actions
Bot
force-pushed
the
changeset-release/main
branch
from
September 28, 2026 14:14
6e08055 to
7d6280f
Compare
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
af1f9b6Thanks @jodeleeuw! - Implement the hardened multiplayer adapter contract.getAll()returns each stored payload unchanged, and the connection callsonResumed()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:
pathPrefixis renamed tonamespace, andconnectTimeoutMsis removed: passconnectTimeouttojsPsych.multiplayer.connect()instead. Passing either old option throws.sessionStorage(persistParticipant, defaulttrue), so a reload comes back as the same participant, in the same group, and jsPsych reports it as a restart. PasspersistParticipant: falsefor a new participant on every page load.sessionBindingnow defaults totruein every identity mode.<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.@jspsych-multiplayer/utilsfor session ids, id generation, and id validation. Invalid-id errors now read "must be a non-empty string without any of : / . # $ [ ]".#105
3f2f35cThanks @jodeleeuw! - Report asessionIdon each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report theirsessionIdoption (the?mp_session=value by default); the JATOS adapter reports the JATOS group result ID and rejectsconnect()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
565ff2fThanks @htsukamoto5! - Addadapter-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
MultiplayerAdaptercontract 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 throughonDisconnect()when a participant's connection drops, which is how jsPsych detects dropouts; data slots are kept..info/connecteddrives the connection's ownreconnecting/connectedstatus, and a listener cancelled after connecting reportsclosed.An optional
useUidAsParticipantIdmode 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 amp-sessions-memberships/<uid> = sessionIdrecord 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 asdatabase.rules.json(with afirebase.jsonfor the emulator suite).firebaseis a peer dependency, and the adapter is unit-tested against an in-memory backend fake with no credentials.#94
ffebbe6Thanks @jodeleeuw! - Adopt the redesigned jsPsych multiplayer adapter contract (jsPsych#3694):connect(options)returns a new connection each time, change notifications go throughonChange()/onStatus()instead of adapter-sidesubscribe(), andconnect()honors the cancellation signal. Presence now lives in a separate<pathPrefix>-presencenode (add it to your security rules; see the README), anddisconnect()or a dropped connection no longer deletes the participant's data slot. TheremoveOnDisconnectoption is removed.#106
bba1dc2Thanks @jodeleeuw! - Addmatchmaking: { lobby, groupSize }: everyone opens the same link, and the adapter fills groups ofgroupSizeas 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, withjsPsych.multiplayer.sealGroup()), and the adapter reports it through the core'sgroup(), sojsPsych.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 gainmp-sessions-lobbyandmp-sessions-groupsentries; redeploy them to use matchmaking.Patch Changes
#103
c875c70Thanks @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
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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/adapter-multiplayer-jatos@0.2.0
Minor Changes
#107
af1f9b6Thanks @jodeleeuw! - Port to the hardened multiplayer adapter contract.connectTimeoutMsandcloseAfterReconnectingMsoptions 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 toconnect(), which also ends a join jatos.js keeps refusing while an old socket closes.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 reportssealed: truewith 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 everyonejatos.groupMemberslists 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 nosetGroupFixed(), sojsPsych.multiplayer.sealGroup()reportsunsupportedinstead 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.validateIdfrom@jspsych-multiplayer/utils, a new dependency.#105
3f2f35cThanks @jodeleeuw! - Report asessionIdon each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report theirsessionIdoption (the?mp_session=value by default); the JATOS adapter reports the JATOS group result ID and rejectsconnect()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
cba1c47Thanks @jodeleeuw! - Report the JATOS group through the core'sgroup()andsealGroup(): the size is the batch'smaxActiveMembers, the members arejatos.groupMembers, and sealing fixes the group withjatos.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 withjsPsych.multiplayer.waitForGroup().#103
d54626dThanks @jodeleeuw! - Support rejoining the group from the same page.closeAfterReconnectingMsnow defaults tonull(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 afterdisconnect()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, withinconnectTimeoutMs.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
baf23daThanks @jodeleeuw! - Breaking: implement the redesigned multiplayer adapter contract from jsPsych#3694.connect(options)now returns a newMultiplayerConnectionfor each call, withgetAll(),connectedParticipants(),push(), anddisconnect(). The adapter's ownsubscribe()andget()are gone; the adapter reports changes throughoptions.onChange(), and jsPsych's multiplayer session handles subscriptions.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 asawayand thenleft.reconnectingand a reopened one asconnected. A channel that stays down longer than the newcloseAfterReconnectingMsoption (default 30000 ms) is reported asclosed. 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
403bfc4]:@jspsych-multiplayer/adapter-multiplayer-local@0.2.0
Minor Changes
#107
af1f9b6Thanks @jodeleeuw! - Port to the hardened multiplayer adapter contract.keyPrefixoption is renamednamespace, matching the other adapters. PassingkeyPrefixlogs a warning and is ignored.persistParticipantnow defaults totrue, so a reload keeps the tab's participant ID (insessionStorage) and the group can tell that participant restarted instead of seeing a new stranger. PasspersistParticipant: falsefor a fresh ID on every page load, as before.onResumed()instead of reporting a syntheticreconnecting→connectedblip.validateIdfrom@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.@jspsych-multiplayer/utils, a new dependency.#94
362774cThanks @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 theonChange()callback instead of its ownsubscribe()/get().Add presence: each tab writes a heartbeat, removes it on
pagehideor disconnect, and drops out afterpresenceTimeoutMs(default 70 s) if it stops. New optionsheartbeatIntervalMsandpresenceTimeoutMs.disconnect()no longer deletes the participant's data slot. An injectedsignalis no longer closed by the adapter, andChangeSignal.onChange()now returns a function that removes the handler.#103
4687f75Thanks @jodeleeuw! - Support rejoining. A tab whose heartbeat lapsed for longer thanpresenceTimeoutMs(a throttled background tab, or a page frozen in the back/forward cache) now reportsreconnectingand thenconnectedwhen its next heartbeat runs, so the other tabs count it as back (onParticipantRejoined) instead of leaving itleft. A refresh is still a restart: withpersistParticipant: truethe same id returns from a new page load and the other tabs report it throughonParticipantRestarted.#105
3f2f35cThanks @jodeleeuw! - Report asessionIdon each connection, as the jsPsych multiplayer contract now requires. The local and Firebase adapters report theirsessionIdoption (the?mp_session=value by default); the JATOS adapter reports the JATOS group result ID and rejectsconnect()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
403bfc4]:@jspsych-multiplayer/plugin-multiplayer-chat@0.2.0
Minor Changes
#107
be66150Thanks @jodeleeuw! - Port chat to the hardened multiplayer API.data_key; give several chat trials the samemultiplayer_scopeto continue one conversation across them.multiplayer_outcome("completed","participant_left","connection_lost", or"cancelled"when the experiment disconnects mid-trial). Removedpartner_leftandconnection_lost.ended_bynow only says which end condition completed the trial ("duration","button", or"condition") and isnullotherwise.@jspsych-multiplayer/utils. The "couldn't send" note is gone: the core retries failed writes.#94
bae51f0Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.jsPsych.multiplayerdirectly, with a clear error on a jsPsych version without it.end_on_participant_left(defaulttrue): 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 fieldspartner_left,left_participant, andconnection_lost;ended_bycan also be"participant_left"or"connection_lost".end_when,sender_label(chat), androster_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.save_groupare copies.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
2650fa9Thanks @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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-choice@0.2.0
Minor Changes
#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#107
be66150Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes,MultiplayerErrorcodes).Breaking:
data_keyparameter and data field are removed, and choices are written underchoicein the trial's data.timed_out,partner_left,connection_lost, andwait_errorare replaced bymultiplayer_outcome("completed","timeout","participant_left", or"connection_lost");left_participantstays.A timeout of
0or less still means no limit. The barrier no longer waits for the backend to confirm this participant's choice before its timeout starts.#37
95ccc8aThanks @Mandyx22! - Addplugin-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 allexpected_playershave 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, withrecord_choices_by_player: falseto keep the recorded data anonymous too (this subsumes the separately-proposedplugin-multiplayer-vote). Includes atimeoutthat degrades to a partial group, an optionalpayoff(choices, me)hook (off by default, so the plugin stays a pure decision primitive),player_label/button_htmldisplay 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 skewn_players), and static access to the pure core (collectChoices/countChosen/tally/plurality).#106
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
0a25a8aThanks @Mandyx22! - Distinguish a genuine barrier timeout from otherwait()rejections. jsPsych#3694 rejects a timeout with a typedMultiplayerTimeoutError; await()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, runson_timeout); a throwing predicate or a backend failure rethrows, so the trial halts loudly instead of masquerading as a timeout.#53
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-countdown@0.2.0
Minor Changes
#41
b4d2faaThanks @htsukamoto5! - Addplugin-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
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#107
be66150Thanks @jodeleeuw! - Port to the hardened multiplayer API. The start timestamp now lives in the trial's own scope of the shared data undercountdown_started_at, so every countdown starts a fresh clock and thenameparameter is removed. To run one clock across several trials, give them the samemultiplayer_scope. The trial recordsmultiplayer_outcome("completed"or"connection_lost") andleft_participantin place ofconnection_lost.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.Updated dependencies [
403bfc4]:@jspsych-multiplayer/plugin-multiplayer-draw@0.2.0
Minor Changes
#34
d37bcb3Thanks @htsukamoto5! - Addplugin-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-chatpushes 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'ssubscribeprimitive at a genuinely high rate. Full repaints (triggered by undo or a canvas resize) paint strokes in a global timestamp order so the eraser'sdestination-outcompositing behaves consistently across clients. Requires a jsPsych with the multiplayer API from jsPsych#3694.#107
be66150Thanks @jodeleeuw! - Port draw to the hardened multiplayer API.data_key; give several draw trials the samemultiplayer_scopeto keep drawing on one canvas.multiplayer_outcome("completed","participant_left","connection_lost", or"cancelled"when the experiment disconnects mid-trial). Removedpartner_leftandconnection_lost.ended_bynow only says which end condition completed the trial ("duration","button", or"condition") and isnullotherwise.@jspsych-multiplayer/utils. The "connection trouble" note is gone: the core retries failed writes.#94
bae51f0Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.jsPsych.multiplayerdirectly, with a clear error on a jsPsych version without it.end_on_participant_left(defaulttrue): 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 fieldspartner_left,left_participant, andconnection_lost;ended_bycan also be"participant_left"or"connection_lost".end_when,sender_label(chat), androster_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.save_groupare copies.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-match@0.2.0
Minor Changes
#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#105
3f2f35cThanks @jodeleeuw! - The"random"strategy now shuffles withjsPsych.multiplayer.shuffle, so it is seeded by the session ID (or therandomSeedconnect option) and each group of participants gets its own grouping.seednow picks a different grouping within the session.#107
be66150Thanks @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.joinedAtis written once to the session scope, sojoin_orderstays stable across rounds.push_datais renamedwrite_data. The trial recordsmultiplayer_outcomeandleft_participantin place oftimed_out,partner_left, andconnection_lost, and atimeoutof0now means no limit. The match accessors now read the last match trial's data instead of a module-level store; they take an optionaljsPsychinstance for pages that run several.#38
cda5411Thanks @Mandyx22! - Addplugin-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 withplugin-multiplayer-role(assign roles within a group viaposition). Runs as a short barrier (likeplugin-multiplayer-role), supportsordered/join_order/random(seeded, per-round) pairing strategies anderror/spectator/smaller_groupleftover 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
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
cd7f8feThanks @Mandyx22! - Adopt the push-then-wait pattern aftercommunicate()was removed from the jsPsych multiplayer API (jsPsych#3694). The match barrier now callspush()thenwait(), 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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.Updated dependencies [
403bfc4]:@jspsych-multiplayer/plugin-multiplayer-ready@0.1.0
Minor Changes
#30
d54c569Thanks @Mandyx22! - Addplugin-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 anypush_data) into the participant's slot when they click, display a waiting message, and end the trial onceexpected_playersmembers are ready (or an optionaltimeoutelapses while waiting for the rest of the group). Unlikeplugin-multiplayer-sync, it owns the check-in UI and the "everyone is ready" condition, and standardizes on aready: trueflag so other plugins and examples can reliably gate on group readiness. Requires a jsPsych with the multiplayer API from jsPsych#3694.#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#106
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.#107
be66150Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes,MultiplayerErrorcodes).Breaking:
data_keyparameter and data field are removed, and the plugin writesready: truein the trial's data. The session-wideready: trueflag is no longer written.push_datais renamedwrite_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 withjsPsych.multiplayer.update(data, { scope: "session" }).grouponly with the newsave_group: trueparameter.timed_out,partner_left,connection_lost, andwait_errorare replaced bymultiplayer_outcome("completed","timeout","participant_left", or"connection_lost");left_participantstays.A timeout of
0or 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
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
21e0909Thanks @htsukamoto5! - Fixwait_error/timed_outmislabeling a non-timeoutwait()failure as a timeout. Both plugins previously treated everywait()rejection as a timeout (a leftover from before jsPsych#3694 exported a typedMultiplayerTimeoutError), so a throwingwait_forpredicate or an adapter/backend error would silently finish the trial withtimed_out: trueand callon_timeout, hiding the real failure inwait_error's message.Now only a rejection whose
error.name === "MultiplayerTimeoutError"is recorded as a timeout. A throwingwait_forpredicate or a backend error propagates and fails the trial, matching how apush()failure is already handled.#53
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-reference-game@0.2.0
Minor Changes
#42
a836cd6Thanks @Mandyx22! - Addplugin-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 —
stimulilength andtargetslength — so one configurable plugin covers both, plus everything in between. Likeplugin-multiplayer-chatit 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 withplugin-multiplayer-role(director/matcher) andplugin-multiplayer-sync(lobby). Requires a jsPsych with the multiplayer API from jsPsych#3694.#94
bae51f0Thanks @jodeleeuw! - Move chat, draw, and reference-game to the session-based jsPsych multiplayer API (jsPsych#3694) and handle participants leaving.jsPsych.multiplayerdirectly, with a clear error on a jsPsych version without it.end_on_participant_left(defaulttrue): 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 fieldspartner_left,left_participant, andconnection_lost;ended_bycan also be"participant_left"or"connection_lost".end_when,sender_label(chat), androster_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.save_groupare copies.#107
be66150Thanks @jodeleeuw! - Port reference-game to the hardened multiplayer API.data_keyandtyping_key. Withchat_persists, the chat log is kept in the session scope. The stale-replay guard now catches rounds that share amultiplayer_scope.multiplayer_outcome("completed","timeout","participant_left","connection_lost", or"cancelled"). Removedended_by,partner_left, andconnection_lost.show_running_scoresumsn_correctfrom this participant's earlier reference-game trials in the jsPsych data, plus the current round.save_groupsaves this round's shared data. Chat messages from a partner who leaves stay in the transcript.@jspsych-multiplayer/utils. The send-error note is gone: the core retries failed writes.#80
e4c9a0bThanks @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 sameslot for both players. Plain
"independent"only guarantees the two layouts are not identical, soaround 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_contentmay now be keyed by role —{ director: {...}, matcher: {...} }— so the twoplayers 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
d6926f8Thanks @htsukamoto5! - Add arequire_message_before_responseparameter. 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 behindmessageSent, guaranteeing a referring expression on every trial. Defaults tofalse(unchanged behavior); inert with a warning whenchat_enabledis false (gating with no channel would deadlock the matcher). Applies to both theclickandassign_slotsresponse 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 asgated_clickevents ininteraction_historywhensave_interaction_historyis on.Chat messages now also carry the
roundthey were sent during. This makes the gate exact underchat_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). Savedchat_transcriptentries gain the sameroundfield; transcripts written by earlier versions still merge and render.#105
3f2f35cThanks @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 therandomSeedconnect option.seedstill picks different arrangements within a session.#92
e0fec98Thanks @htsukamoto5! - Add atyping_indicatoroption (off by default) showing a "partner is typing…" hint driven by atimestamp each client keeps in its own slot (
typing_key, throttled bytyping_throttle, hiddentyping_ttlafter the last keystroke, labelled viatyping_label). Hint only — it never gatestrial 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
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-role@0.2.0
Minor Changes
#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#106
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.#105
3f2f35cThanks @jodeleeuw! - Therandomstrategy 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 therandomSeedconnect option.seednow picks a different assignment within the session.#107
be66150Thanks @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.joinedAtis written once to the session scope, sojoin_orderstays stable across rounds.push_datais renamedwrite_dataand is no longer nested underrounds[round](readentry.score, notentry.rounds[round].score). The trial recordsmultiplayer_outcomeandleft_participantin place oftimed_out,partner_left, andconnection_lost, and atimeoutof0now means no limit. The role accessors now read the last role trial's data instead of a module-level store; they take an optionaljsPsychinstance for pages that run several.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
fc1a842Thanks @htsukamoto5! - Update for jsPsych#3694's removal ofMultiplayerAPI.communicate(): the plugin now callspush()followed bywait()directly instead of the removed fused convenience method. Also fixes the same timeout-mislabeling bug already patched inplugin-multiplayer-sync/plugin-multiplayer-ready— only a rejection whoseerror.name === "MultiplayerTimeoutError"is now recorded astimed_out: true; a backend orpush()failure propagates and fails the trial loudly instead.#53
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.Updated dependencies [
403bfc4]:@jspsych-multiplayer/plugin-multiplayer-scoreboard@0.2.0
Minor Changes
#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#106
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.#33
33d337eThanks @Mandyx22! - Addplugin-multiplayer-scoreboard: an end-of-game scoreboard for multiplayer experiments. Each client contributes its finalscore, 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, anon_timeouthook, timeouts that degrade to a partial board, and static accessors (getMyRank/getMyScore/getLeaderboard) for branching downstream trials.#107
be66150Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes,MultiplayerErrorcodes).Breaking:
data_keyparameter and data field are removed, and scores are written underscoreboardin the trial's data.timed_out,partner_left, andconnection_lostare replaced bymultiplayer_outcome("completed","timeout","participant_left", or"connection_lost";nullwhen reporting failed some other way, whicherrordescribes);left_participantanderrorstay.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
0or 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
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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/plugin-multiplayer-sync@0.2.0
Minor Changes
#94
91ff22cThanks @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 withoutjsPsych.multiplayerthey throwan error saying so.
wait(condition, timeout)form silently meant "notimeout" under the redesigned core; every configured
timeout(sync, ready, choice, match, role,scoreboard) is honored again.
participantsparameter: the participants the barrier depends on. It defaults to every otherparticipant 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 theway a timeout would, and records
partner_left: trueandleft_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.
connection_lost: trueinstead of failing the trial.Countdown keeps running locally and records
connection_lost.data_keydefaults toready-N/choice-N/scoreboard-N, counting that plugin's trials in theorder this participant reaches them, so flags or choices from an earlier trial can't count toward a
later one. An explicit
data_keyis used as-is. The key used is recorded asdata_key. A trial thatonly some participants reach needs an explicit key, and the count restarts on a page reload.
push_dataand the gate flag into the slot withupdate()instead of replacingthe slot with
push(), so earlier gates' flags and other data survive. It still setsready: true.wait_for,ready,rank_by,role_from,display_label,…) receive frozen data. Predicates receive
(snapshot, presence). In match and role, a throwingpredicate or accessor still means "not ready", and the last error is logged if the group never
becomes ready.
shows a note on boards revealed after a departure or a lost connection.
#106
8b89cacThanks @jodeleeuw! - Use the sealed group when there is one (jsPsych.multiplayer.group(), from an adapter that forms groups):expected_players(ready, choice, match) andgroup_size(role, scoreboard) default to the sealed group's members who haven't left.expected_playerson 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: nullmeans the rest of the sealed group's members who haven't left, including a member who is onlyawayat that moment, instead of only the participants connected when the trial starts. Without a sealed group it is unchanged.#107
be66150Thanks @jodeleeuw! - Port to the hardened multiplayer API (trial scopes,MultiplayerErrorcodes).Breaking:
push_datais renamedwrite_data, and it is merged into this participant's data withupdate()instead of replacing it. It goes to the trial's own part of the shared data, which is also whatwait_forsees, so values written in an earlier trial never satisfy a later barrier.participantsnow defaults tonull: the other members of a sealed group who haven't left, or else the others who are connected when the wait starts. Passparticipants: []to keep the old behavior of ignoring departures (e.g. in a lobby).grouponly with the newsave_group: trueparameter.timed_out,partner_left,connection_lost, andwait_errorare replaced bymultiplayer_outcome("completed","timeout","participant_left", or"connection_lost");left_participantstays.A timeout of
0or less still means no limit. The barrier no longer waits for the backend to confirmwrite_databefore its timeout starts.Patch Changes
#90
e698fc5Thanks @jodeleeuw! - Stop cleanly when the experiment ends or is aborted.A pending
wait()now rejects with aMultiplayerCancelledErrorwhen the experiment ends or isaborted, 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-scoreboardpreviously logged an error and drew a final board over thecleared display, leaving a Continue button that called
finishTrialafter the run had ended.Fixes found alongside that work:
plugin-multiplayer-countdownandplugin-multiplayer-drawregistered their repeating timers with a raw
setInterval, which survivedabortExperiment()— thecountdown could call
finishTrialafter the run ended and draw kept writing to the session forever;both now tick through jsPsych's own timer registry.
plugin-multiplayer-chatandplugin-multiplayer-reference-gamecould drop a message when a participant sent two in quicksuccession; they no longer do.
adapter-multiplayer-firebasemarked itself connected before armingits 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
21e0909Thanks @htsukamoto5! - Fixwait_error/timed_outmislabeling a non-timeoutwait()failure as a timeout. Both plugins previously treated everywait()rejection as a timeout (a leftover from before jsPsych#3694 exported a typedMultiplayerTimeoutError), so a throwingwait_forpredicate or an adapter/backend error would silently finish the trial withtimed_out: trueand callon_timeout, hiding the real failure inwait_error's message.Now only a rejection whose
error.name === "MultiplayerTimeoutError"is recorded as a timeout. A throwingwait_forpredicate or a backend error propagates and fails the trial, matching how apush()failure is already handled.#53
57ea69dThanks @htsukamoto5! - Read the multiplayer API fromjsPsych.multiplayer(jsPsych#3694's namespace), and throw an error that says so when it is absent. Builds that exposed these methods onjsPsych.pluginAPIpredate the current API and are not supported.#45
a587023Thanks @htsukamoto5! - Finish gracefully whengetAll()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 astimed_out: true. The snapshot read now falls back to an empty group session, matching thesafeGetAllguardplugin-multiplayer-readyalready had.#62
d1552c0Thanks @jodeleeuw! - Register trial timers throughjsPsych.pluginAPI.setTimeoutso they are cancelled when a trial is ended externally (abortExperiment,endCurrentTimeline, forcedfinishTrial), instead of firing into a finished trial. The plugins previously used baresetTimeoutand 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
Minor Changes
403bfc4Thanks @jodeleeuw! - Add@jspsych-multiplayer/utils, the helpers the multiplayer plugins and adapters share:isMultiplayerError,outcomeOfand theMultiplayerOutcometype,pluginTimeout,remainingParticipants,sealedGroupSize,withoutLeft, thewaitForAllbarrier, and the adapter helperssessionIdFromUrl,tabId,generateId, andvalidateId.