Move adapters and plugins to the session-based multiplayer API, with presence - #94
Merged
Merged
Conversation
…dency The multiplayer API isn't in a published jsPsych release yet. Vendor the preview build of core ad80b345 (preview commit a6e05be0) so packages can import the real types and tests can run the real session. Delete vendor/ and return to npm once a release includes the API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use jsPsych.multiplayer directly; drop the local API copy. - Subscribe with an AbortController signal, render from the synchronous own-write notification (no optimistic render), and read own messages from the slot instead of a local array. - End with ended_by "participant_left" (partner_left, left_participant) when a participant who was connected at the start leaves, unless end_on_participant_left is false; "connection_lost" when the session closes. Roster marks away/left participants. - end_when and sender_label also receive presence. - Tests run the real session over the in-memory backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use jsPsych.multiplayer directly; drop the local API copy.
- wait() takes { timeout, participants }; wait_for gets (group, presence).
Fixes timeouts, which the old positional form silently disabled.
- New participants parameter (default: others still present when the wait
starts). A departure ends the trial with partner_left/left_participant; a
lost connection ends it with connection_lost.
- Tests run the real session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- LocalAdapter holds configuration only; each connect() returns a new LocalConnection that owns its signal handler, heartbeat timer, and page listeners, and reports changes through options.onChange(). - Presence: each tab refreshes mp-presence:<session>:<pid> every heartbeatIntervalMs (default 2 s); connectedParticipants() lists keys fresher than presenceTimeoutMs (default 70 s, covering Chrome's once-a-minute timers in long-hidden tabs). pagehide and disconnect() remove the key at once; a bfcache pageshow restores it. - disconnect() keeps the data slot; presence says who is still here. - An injected signal is never closed by a connection, and ChangeSignal.onChange() returns a remover so closed connections leave no handlers behind. - Drop the local copy of the adapter interface; import types from jspsych. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Split FirebaseAdapter into configuration plus a FirebaseConnection that each connect() creates, so reconnecting with the same adapter shares no state. Notify the core through onChange()/onStatus() instead of adapter-side subscribe/get, and honor the connect() abort signal after every await and during the first-snapshot wait. Presence moves to a sibling <pathPrefix>-presence node, written on connect and on every reconnect and removed by the server's onDisconnect, with a second listener mirroring it into connectedParticipants(). Data slots are no longer removed on disconnect or re-pushed after a blip, so removeOnDisconnect is gone. .info/connected drives reconnecting and connected, and a listener cancelled after connecting now reports closed instead of being silently ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- connect(options) returns a new JatosConnection per call with getAll(), connectedParticipants() (jatos.groupChannels), push(), disconnect(); adapter-side subscribe()/get() and the local interface copy are gone. - Reports onChange() on group session and member join/leave/open/close events, and onStatus() reconnecting/connected when the channel drops and reopens; closed after closeAfterReconnectingMs (default 30000). - Serves the last group session data while jatos.js has it wiped during a reconnect; push() waits for the channel to reopen. - connect() honors the abort signal, uses the joinGroup() promise so a refused channel rejects promptly, and guards the page's single jatos.js group channel; a cancelled join that opens late leaves the group. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Each gate marks readiness with its own key (data_key, default ready-1,
ready-2, … per jsPsych instance), merged with update() so earlier gates'
flags stay. A second gate no longer passes on leftover flags, and a fast
participant can't remove a flag a slower one is still counting.
- push_data now merges into the slot instead of replacing it.
- Counts ignore participants who have left; new participants parameter;
partner_left/left_participant/connection_lost data; data_key recorded.
- Use jsPsych.multiplayer directly and wait(cond, { timeout, participants }),
which fixes timeouts the old positional form silently disabled.
- demo.html uses a new-contract localStorage adapter; tests run the real
session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use jsPsych.multiplayer directly; drop the local API copy. - Subscribe with an AbortController signal; keep the initial full repaint (the incremental painter skips our own slot) and overlay unwritten points on the last delivered snapshot. - End with ended_by "participant_left" (partner_left, left_participant) unless end_on_participant_left is false, and "connection_lost" when the session closes; roster marks away/left participants. - end_when and roster_label also receive presence. - Copy strokes restored from the frozen slot (fixes redo throwing on a restored stroke) and copy strokes into trial data. - Tests run the real session over the in-memory backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ropouts
- data_key defaults to null, generating choice-1, choice-2, … per jsPsych
instance, so a choice from an earlier trial can't count toward a later one.
An explicit data_key is used as-is. The key is recorded as data_key.
- The barrier ignores participants who have left; the outcome still reports
every choice made.
- New participants parameter; a departure or lost connection proceeds with
the partial group, flagged partner_left/left_participant or connection_lost.
- Use jsPsych.multiplayer directly and wait(cond, { timeout, participants }),
which fixes timeouts the old positional form silently disabled.
- Tests run the real session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… presence - Use jsPsych.multiplayer directly; drop the local API copy. - Subscribe with an AbortController signal (fixes the listener leaked when the first callback ended the trial); read own chat and rounds from the latest snapshot instead of a local array; drop the optimistic render and optimistic enterFeedback, which the synchronous own-write notification now covers. - End with ended_by "participant_left" (partner_left, left_participant) when the partner leaves before feedback, unless end_on_participant_left is false; a submission that arrived first still counts. End with "connection_lost" when the session closes. - Partner auto-detect ignores participants who left and prefers connected ones. - save_group stores a copy instead of the frozen snapshot. - Tests run the real session over the in-memory backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Readiness and the partition ignore participants who have left.
- New participants parameter; a departure or lost connection ends the trial
unmatched with partner_left/left_participant or connection_lost.
- A custom ready predicate gets (snapshot, presence); a throw still means
"not ready" but the first one is logged, so a predicate that modifies the
frozen snapshot no longer hangs silently.
- Use jsPsych.multiplayer directly and wait(cond, { timeout, participants });
the 30 s default timeout, silently disabled by the old positional form, works
again.
- Tests run the real session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… never gets ready A predicate that reads data before it arrives throws routinely, so logging the first throw was noise. Report the last error on the unmatched paths instead, where it may explain why the group never became ready. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Readiness and assignment ignore participants who have left.
- New participants parameter; a departure or lost connection ends the trial
with role: null and partner_left/left_participant or connection_lost.
- A custom ready gets (snapshot, presence). Accessors still count a throw as
"not ready", and the last error is now logged if the group never becomes
ready, so an accessor that modifies the frozen snapshot is reported.
- Use jsPsych.multiplayer directly and wait(cond, { timeout, participants });
the 30 s default timeout, silently disabled by the old positional form, works
again.
- Tests run the real session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…outs
- data_key defaults to null, generating scoreboard-1, scoreboard-2, … per
jsPsych instance, so scores from an earlier board can't count toward a later
one. An explicit data_key is used as-is. The key is recorded as data_key.
- The reporter count ignores participants who have left.
- New participants parameter; a departure or lost connection shows the
partial board with partner_left/left_participant or connection_lost and a
note.
- Replace the own-timer race and 2x backstop wait with the core's
wait(cond, { timeout, participants }) and error-name matching.
- Tests run the real session over the in-memory backend.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use jsPsych.multiplayer directly; drop the local API copy. - Remove the subscription with an AbortController aborted at trial end, which also closes a leak when the first delivery ended the trial. - Record connection_lost; the countdown keeps running locally. - save_group reads the group safely when the session is gone. - Tests run the real session over the in-memory backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the start Leftover slots from members who left earlier start out `away` and turn `left` a few seconds after joining, so counting away participants made a gate right after joining fail with partner_left. Match the live plugins, which already count only connected participants. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… from the HTML The JATOS archive scripts matched the examples' jsPsych tags by their exact unpkg URLs, which broke once the examples were pinned to a preview build. They now find the jsPsych script and stylesheet in the HTML, so re-pinning needs no script change, and the caveat printed after a build describes the vendored preview instead of a missing API. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Pin every example to the preview build of core ad80b345 (a6e05be0), including the ones that loaded an unpinned jsPsych without the API. - Count lobbies and barriers by presence, since slots now outlive their participants, and give lobbies participants: [] so a departure doesn't end them. - Drop the pagehide handlers that called the removed localAdapter.disconnect() and read participantId from jsPsych.multiplayer. - Detect partner departures through presence in the ultimatum games, reference games and group quiz; keep timeouts as a backstop for a partner who stays connected but idle. - Rewrite the group-quiz host on jsPsych.multiplayer instead of the raw JATOS adapter. - Point examples/ultimatum-game-tutorial.md at the docs-site tutorial, which it had drifted from. - Replace the examples README's preview-pin guidance with the vendored preview workflow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rewrite the jsPsych.multiplayer reference and the introduction's API table for the new contract, add a Handling dropouts guide covering presence, the plugins' partner_left/connection_lost data, participants defaults, and per-gate keys, add presence details to Choosing an adapter, move both tutorials to presence-based dropout detection, and update the prerelease announcement bar. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Edit pending changesets that described superseded behavior (JSON-copy reads, update() merging onto the last write, the local API mirrors and resolveMultiplayerApi, "any other rejection propagates") so the eventual changelogs are accurate, and delete two that only described internal changes the migration removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
sync is often a lobby, which should keep waiting when someone leaves rather than end with partner_left. Default participants to [] and keep null for the dropout-aware barrier. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each client computes the partition or role assignment on its own from its own view of presence. Counting participants who were only away let a leftover slot be matched or given a role, and let clients assign while their views disagreed. Now the group isn't ready until every remaining participant is connected. Tests: add MemoryHub.addPeer()/removePeer() for connected participants without a jsPsych instance; seed() stays an unconnected (away) slot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JATOS doesn't inject jatos.js; the study page must load it. Also fix the local adapter changelog's stale pluginAPI.connect() reference. Both fixes come from #71 and #70, whose other changes this branch supersedes. Co-authored-by: Hannah Tsukamoto <hannahtsukamoto555@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jodeleeuw
pushed a commit
that referenced
this pull request
Sep 24, 2026
Reapplied onto the session-based multiplayer API (#94); the feature changes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jodeleeuw
pushed a commit
that referenced
this pull request
Sep 24, 2026
Reapplied onto the session-based multiplayer API (#94); the feature changes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jodeleeuw
pushed a commit
that referenced
this pull request
Sep 24, 2026
Reapplied onto the session-based multiplayer API (#94). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jodeleeuw
pushed a commit
that referenced
this pull request
Sep 24, 2026
Reapplied onto the session-based multiplayer API (#94). The serialized write queue is gone: update() merges only the typing key and the session sends one write at a time, so a timestamp can't overwrite chat or round data. The hint also hides while the partner is away or has left. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Without a charset, servers that don't send one (e.g. python -m http.server) decode the pages as Latin-1 and show "…" for "…". The two replication examples are left alone because #69 removes them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Open
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.
Moves every adapter and plugin to the redesigned core multiplayer API in jsPsych#3694, and handles participants who drop out.
Depends on jsPsych#3694. Until a jsPsych release includes the API,
jspsychis a vendored preview build (coread80b345, previewa6e05be0) invendor/jspsych. Refresh it withnpm run vendor-jspsych <preview-sha>, and deletevendor/and return to npm once the API is released.What changed in the core API
connect()returns a session, andjsPsych.multiplayerforwards to the current session.connect()returns a new connection.connected,awayorleft.wait(condition, { timeout, signal, participants })replaces the positional timeout, which the new core rejects.Adapters
pagehide. A crashed tab counts as gone after 70 s (presenceTimeoutMs), because Chrome slows timers in background tabs.jatos.groupChannelsand the member callbacks.closeAfterReconnectingMs) reportsclosed.reconnecting, thenconnected.closedinstead of being silently ignored.In all three,
disconnect()keeps the participant's data slot; presence now tracks who is in the group.Plugins
jsPsych.multiplayerdirectly; the per-package copies of the API types are deleted.partner_left,left_participantandconnection_lost, plusended_by: "participant_left" | "connection_lost"in the live plugins.participantsdefaults to the other participants who are connected when the wait starts.[](ignore departures), because it is often used as a lobby.ready-1,ready-2, …), so flags from an earlier gate can't satisfy a later one."choice"and"scoreboard".end_on_participant_leftparameter (defaulttrue).Tests, examples and docs
test-utils/memory-backend.ts, which runs the real session over an in-memory connection. 652 tests pass.jsPsych.multiplayer, and the JATOS adapter example now loadsjatos.js(from fix: make the JATOS adapter example runnable, and use jsPsych.multiplayer in examples #71).Not verified
The examples haven't been run in a browser yet.
Related PRs
jatos.jstag and a CHANGELOG wording fix. build(deps-dev): Bump jspsych from 8.2.3 to 8.3.0 #78 (jspsych 8.3.0) is also superseded, because the published jspsych has no multiplayer API and this branch vendors the preview.update().🤖 Generated with Claude Code