Add real-time multiplayer support (MultiplayerAPI + plugin-multiplayer-sync + adapter-multiplayer-jatos)#3694
Open
htsukamoto5 wants to merge 26 commits into
Open
Add real-time multiplayer support (MultiplayerAPI + plugin-multiplayer-sync + adapter-multiplayer-jatos)#3694htsukamoto5 wants to merge 26 commits into
htsukamoto5 wants to merge 26 commits into
Conversation
Adds MultiplayerAPI to plugin-api (Connect/Push/Get/Wait/Subscribe/Communicate primitives, adapter pattern, subscription lifecycle tracking) and a JatosAdapter package that implements the interface via JATOS group studies. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 6 missing test cases: getAll, wait fast-path, subscribe fires, disconnect state reset, pre-connect error guards, and double-connect error. Move "types": ["jest"] into compilerOptions where TypeScript expects it. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- tsconfig: restore @types/node by using ["jest", "node"] so tsc passes - MultiplayerAPI.connect() now rolls back adapter/participantId on failure - add test for failed-connect rollback and retry Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…TOS 3.x - Use .NET ZipFile API on Windows to write forward-slash entry names (Compress-Archive produces backslashes which violate the ZIP spec) - Rename metadata file from jatos_study_metadata.json to .jas extension, which is what JATOS 3.x import actually scans for - Add required uuid fields on study, component, and batch - Add missing JATOS 3.x fields: linearStudy, allowPreview, studyEntryMsg Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Packages the multiplayer push -> wait pattern as a single declarative trial, replacing the call-function (async/done) and NO_KEYS html-keyboard-response + on_start + manual finishTrial idioms. Params: wait_for (required), push_data, message, timeout, on_timeout, minimum_wait. Data: group, wait_time, timed_out. Built on the existing multiplayer plugin API; relies on jsPsych dynamic parameters so push_data/message accept function forms. Includes 4 jest tests (in-memory MockAdapter), README, and a rewritten ultimatum-game example using the plugin for all sync points. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Point the JATOS archive builder at multiplayer-ultimatum-game-sync.html: drop the plugin-call-function bundle (no longer used) and add the plugin-multiplayer-sync bundle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…in-multiplayer-sync
Port the 2-player-cap behavior from multiplayer-ultimatum-game.html: a 3rd+ participant sets gameIsFull in the lobby on_finish, disconnects, and is routed to a "game full" screen; the game timeline is gated on !gameIsFull. Keeps both example versions in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Windows branch that zips via .NET ZipFile with forward-slash entry names (matching build-jatos-ultimatum.js), so the archive imports correctly on Windows. The existing zip -r path still serves macOS/Linux. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bug 1 — players who joined before the host were invisible. The host subscribed to the group session but subscribe() only fires on future updates, so anyone already sitting in the lobby barrier never triggered a render. The host now renders the current snapshot once right after subscribing. Bug 2 — players could be stranded forever on a loading screen. Barriers waited on exact phase equality (e.g. phase === REVEAL), but JATOS does not guarantee a client observes every intermediate snapshot, so a skipped phase made the condition permanently unsatisfiable. Introduce a monotonic step counter (phaseStep) in protocol.js: the host stamps every push with a step that only increases, and players wait with hostStepValue(group) >= phaseStep(...). A >= test on a monotonic value can never be missed. Applied across all three example files (index.html is the one the JATOS build ships; host.html/player.html are the split versions). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Covers the full MultiplayerAdapter contract against a mock of the jatos global: construction (missing-global error, participantId derivation), the connect handshake (resolve on open, reject on error), getAll/get reads, subscribe fan-out + unsubscribe, and push — including the optimistic-concurrency retry path (retries a version conflict to success, and throws after exhausting all 8 attempts), driven with fake timers. Documents that the adapter's subscribe() is intentionally future-only; whether core should replay current state on subscribe is a separate open question deferred pending maintainer input. No changeset: test-only change, no published behavior affected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
subscribe() now emits the current group session snapshot once synchronously on registration (register-then-replay ordering), matching Firebase onSnapshot semantics and eliminating the asymmetry with wait()'s existing fast-path. The replay fires after the unsubscribe handle is built, preventing the TDZ crash that would occur if wait()'s callback tried to call unsubscribe() before it was assigned. Two core tests updated to account for the leading replay emit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Delete host.html and player.html — both were stale divergent copies of the host and player flows that had since been fixed and unified into index.html. The jzip build already shipped index.html as the sole component; the deleted files were never referenced by the build script. Also clarify the adapter.subscribe() comment in index.html (replay is a MultiplayerAPI wrapper concern, not the raw adapter's) and add a stale-dist warning to build-jatos-kahoot.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three new pages: - docs/plugins/multiplayer-sync.md — plugin-style reference with parameters, data-generated, install snippet, and three usage examples - docs/reference/jspsych-multiplayer.md — full MultiplayerAPI method reference (connect, disconnect, push, get, getAll, subscribe, wait, communicate, cancelAllSubscriptions, participantId) - docs/developers/adapter-development.md — adapter authoring guide with the full MultiplayerAdapter interface contract, a minimal in-memory example, a pointer to the JATOS adapter as a real-world reference, and a checklist mkdocs.yml: wired all three pages into Plugins, Reference, and Developers nav. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Renames examples/kahoot → examples/group-quiz, the build script, and the docs plan file. Updates all internal references (study title, dir name, zip name, asset paths, heading). Replaces context-specific trivia questions with general-knowledge questions. Switches the color scheme from Kahoot purple/red/blue/yellow/green to dark teal with blue/amber/violet/emerald answer tiles, and replaces ▲◆●■ symbols with A/B/C/D labels. Removes the stale root-level kahoot-plan.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 01b8ee4 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
jodeleeuw
added a commit
that referenced
this pull request
Jun 26, 2026
The fork-PR fallback used listPullRequestsAssociatedWithCommit, which returns an empty list for fork-PR head commits (they don't live in a base-repo branch). The publish job therefore failed with "No open PR found" before it could push the preview branch or post the comment, so fork PRs (e.g. #3694) never received a preview comment. Look the PR up by its head ref (headOwner:headBranch) from the trusted workflow_run payload via pulls.list instead. The PR number and head SHA are still sourced only from the trusted event payload and validated downstream, so trust guarantees are unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Contributor
📦 Preview build readyBuilt from PR head Changed packages: Quick-start HTML: <script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@c43b3a1f285ef2d899cd43960c889a4a9d630dc9/packages/jspsych/dist/index.browser.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@c43b3a1f285ef2d899cd43960c889a4a9d630dc9/packages/jspsych/css/jspsych.css">
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@c43b3a1f285ef2d899cd43960c889a4a9d630dc9/packages/plugin-multiplayer-sync/dist/index.browser.min.js"></script>All package URLs
Last updated 2026-06-26 15:21 UTC for PR head |
This was referenced Jun 29, 2026
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.
Summary
This PR adds opt-in, real-time multiplayer support to jsPsych through a two-layer architecture:
MultiplayerAPI(in corejspsych) — high-level API onjsPsych.pluginAPIfor pushing data, subscribing to group session changes, and waiting on conditions. Backend-agnostic: all network logic lives in a swappable adapter.@jspsych/plugin-multiplayer-sync— a synchronization-barrier trial plugin. Pushes participant data and waits until await_forpredicate over the group session is satisfied. Packages the common push → wait pattern as a single declarative trial.@jspsych/adapter-multiplayer-jatos— a JATOS group session adapter implementing theMultiplayerAdapterinterface. Handles WebSocket join, group session reads/writes, and optimistic-concurrency retry.The architecture is intentionally minimal: adapters only need to implement network I/O (connect, push, subscribe, getAll). All higher-level behavior (subscribe replay, wait fast-path, subscription tracking) lives in
MultiplayerAPIso it is consistent across backends.What's included
packages/jspsych):MultiplayerAPIclass withconnect,disconnect,push,get,getAll,subscribe(replays current state on registration),wait,communicate,cancelAllSubscriptionspackages/plugin-multiplayer-sync):multiplayer-synctrial withpush_data,wait_for,message,timeout,on_timeout,minimum_waitparams; savesgroup,wait_time,timed_outpackages/adapter-multiplayer-jatos): JATOS group session adapter with exponential-backoff retry on version conflictsexamples/group-quiz/): live quiz game demonstrating the full host+player flow in a single JATOS component; includes a build script (scripts/build-jatos-group-quiz.js) that produces an importable.jzipdocs/plugins/multiplayer-sync.md,docs/reference/jspsych-multiplayer.md,docs/developers/adapter-development.md, all wired intomkdocs.ymlDesign notes
The
MultiplayerAdapterinterface is intentionally minimal so dropping in a second adapter (Firebase, a custom WebSocket server) requires no changes to core or plugin code. The adapter'ssubscribe()is future-only; replay is handled once inMultiplayerAPI.subscribe()(register-then-replay ordering prevents a TDZ crash whenwait()references its ownunsubscribehandle inside the callback).Test plan
npm testinpackages/jspsych,packages/plugin-multiplayer-sync, andpackages/adapter-multiplayer-jatos— all 32 tests should passnode scripts/build-jatos-group-quiz.js(afternpm run buildon the three multiplayer packages)dist/group-quiz-jatos.jzipinto a JATOS instance, run as a group study with 2+ participants — one selects Host, others select Player🤖 Generated with Claude Code