Add jsPsych.multiplayer core API (MultiplayerAPI + MultiplayerAdapter interface)#3694
Open
htsukamoto5 wants to merge 37 commits into
Open
Add jsPsych.multiplayer core API (MultiplayerAPI + MultiplayerAdapter interface)#3694htsukamoto5 wants to merge 37 commits into
htsukamoto5 wants to merge 37 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: eddf2f6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/jspsych/dist/index.browser.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/jspsych/css/jspsych.css">
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-keyboard-response/dist/index.browser.min.js"></script>All package URLs
Last updated 2026-07-23 15:09 UTC for PR head |
Wraps the get-own-slot -> shallow-merge -> push sequence that every existing multiplayer plugin currently hand-rolls. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… error swallowing - multiplayer-sync: a push() failure now rejects the trial instead of being recorded as timed_out: true - MultiplayerAPI.subscribe(): a throwing subscriber callback is caught and logged instead of aborting notification of other subscribers - JatosAdapter.push(): retry-exhaustion error now preserves the original failure as `cause` instead of discarding it - JatosAdapter spec: correct a stale comment about replay-on-subscribe being an open question (it's decided and implemented in core) - pin jspsych peer dependency to >=8.3.0 in the new packages - escape player-typed names in the group-quiz demo (XSS via display name) - de-duplicate the two build-jatos-*.js scripts into a shared helper - update the changeset to mention update() Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
>=8.3.0 doesn't exist on npm yet (that bump is only staged via the pending changeset), so npm ci can't resolve it in CI. Every other package in the monorepo pins peer ranges to an already-published version for the same reason.
- wait() now owns predicate error handling: a throwing condition rejects the promise instead of being swallowed by subscribe()'s guard and hanging forever; the TDZ-avoiding fast path is replaced by a settle flag checked during the synchronous replay - Timeout rejections are a typed MultiplayerTimeoutError (exported from jspsych); plugin-multiplayer-sync only treats that as a timeout and rethrows anything else (matched by error name so the check survives two loaded copies of jspsych) - JatosAdapter.disconnect() now calls jatos.leaveGroup(), freeing the participant's maxActiveMembers slot immediately; resolves even on a failed leave since JATOS auto-leave at study end is the backstop - Revert the tsconfig types whitelist and @types/jest devDep on core jspsych — hoisted jest types already cover the test file, and the whitelist disabled auto-inclusion of other @types packages - Document update()'s non-atomicity against overlapping calls; note why the adapter push retry doesn't filter error types; real plugin author Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged
2 tasks
Per maintainer feedback on API surface simplification: communicate() was pure push()+wait() sugar that saved a single await, and its fixed push-then-wait fusion prevented callers from handling the two phases differently — plugin-multiplayer-sync, the primary consumer of this API, deliberately hand-rolled the two calls instead of using it. Callers should use push() followed by wait(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per maintainer suggestion on the PR: the multiplayer methods were flat-merged into jsPsych.pluginAPI, putting generic names (get, push, connect, wait, ...) into a shared namespace and requiring an Object.defineProperty workaround to keep participantId live across the Object.assign copy. The API is now its own stateful module on the JsPsych instance, mirroring jsPsych.data: - Move plugin-api/MultiplayerAPI.ts to modules/multiplayer/ unchanged - Instantiate jsPsych.multiplayer in the JsPsych constructor; remove MultiplayerAPI from the joint pluginAPI object and delete the participantId defineProperty workaround - Re-export GroupSessionData, MultiplayerAdapter, Unsubscribe, and MultiplayerTimeoutError from the new module path (public names from jspsych are unchanged) - Rename all call sites in the sync plugin, JATOS adapter doc comment, docs, and examples: jsPsych.pluginAPI.* -> jsPsych.multiplayer.* - Move the API tests to tests/multiplayer/ - Rewrite the changeset to describe jsPsych.multiplayer.* as a state primitive (push/getAll/subscribe) plus conveniences and lifecycle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- push() worst-case backoff is ~6.5s (sleeps after attempts 0-6 only), not ~13s - Nav title for reference/jspsych-multiplayer.md now reads jsPsych.multiplayer, matching the top-level namespace move Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
htsukamoto5
added a commit
to jspsych/jspsych-dev
that referenced
this pull request
Jul 20, 2026
…pace jsPsych moved the multiplayer API from jsPsych.pluginAPI.connect() to a top-level jsPsych.multiplayer.connect() namespace (jspsych/jsPsych#3694). Update the generated adapter's README, docs template, example, and src/index.ts to reference the new call site.
… is cleaned up on abort Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adapters (JATOS) and multiplayer plugins (sync) are distributed from the jspsych-multiplayer ecosystem repo; keep only the core jsPsych.multiplayer API and the MultiplayerAdapter interface in this PR. Also drop the demos, JATOS packaging scripts, and the internal group-quiz plan doc; narrow the changeset to jspsych and update docs/nav accordingly. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ormatting churn Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
htsukamoto5
added a commit
to jspsych/jspsych-multiplayer
that referenced
this pull request
Jul 23, 2026
…abort Port of jspsych/jsPsych#3694 commit 5512c5b5 by Josh de Leeuw, which landed on the core PR just before plugin-multiplayer-sync was moved out to this repo. Routes the minimum_wait hold through pluginAPI.setTimeout so the pending timeout is cancelled when the experiment aborts, instead of a raw setTimeout that leaks past teardown. The single holdMinimumWait helper covers both the success and timeout paths. Adds pluginAPI.setTimeout to the test double. Co-Authored-By: Josh de Leeuw <josh.deleeuw@gmail.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 tasks
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
Adds
jsPsych.multiplayer— an opt-in, backend-agnostic API for real-time multiplayer experiments. This PR is core-only: it introduces theMultiplayerAPImodule and theMultiplayerAdapterinterface that any network backend (JATOS, Firebase, a custom WebSocket server, etc.) implements. No plugin, adapter implementation, or demo is included here — those live in a sibling repo and will follow in separate PRs once this core surface lands.API surface
jsPsych.multiplayeris a small state primitive plus conveniences, organized likejsPsych.data:push(data),getAll(),subscribe(callback)— one shared group-session "whiteboard", three verbs.subscribe()replays the current snapshot once on registration before forwarding future updates.update(data)(shallow get→merge→push),get(participantId)(single-participant read),wait(condition, timeout?)(promise that resolves when a predicate over the group session becomes true, rejecting withMultiplayerTimeoutErroron timeout).connect(adapter),disconnect(),cancelAllSubscriptions(), and aparticipantIdproperty.The
MultiplayerAdapterinterface is intentionally minimal —connect,push,getAll,get,subscribe,disconnect— so a backend only needs to implement network I/O. All higher-level behavior (subscribe replay, wait fast-path, subscription tracking) lives inMultiplayerAPIso it's consistent across backends.MultiplayerTimeoutErroris a new export from thejspsychpackage, re-exported top-level alongside the other multiplayer types.What's included
packages/jspsych):MultiplayerAPIclass andMultiplayerAdapterinterface atsrc/modules/multiplayer/, exposed asjsPsych.multiplayerpackages/jspsych/tests/multiplayer/multiplayer.test.tsdocs/reference/jspsych-multiplayer.md,docs/developers/adapter-development.md, wired intomkdocs.ymljspsychDesign notes
The adapter's
subscribe()is future-only; replay is handled once inMultiplayerAPI.subscribe()(register-then-replay ordering prevents a TDZ crash whenwait()references its ownunsubscribehandle inside the callback).update(data)shallow-merges into the caller's own participant slot before pushing — conflict-freedom relies on each participant only ever writing their own slot, so no version/timestamp logic is needed in core.A throwing
subscribe()callback is caught and logged rather than aborting notification of other subscribers on the same adapter fan-out.Out of scope here
@jspsych/plugin-multiplayer-syncand@jspsych/adapter-multiplayer-jatos— moved to a sibling repo and will be proposed as follow-up PRs once this core API is stablepublish/onMessage) for transient/high-rate messages, deferred pending a concrete use casecancelAllSubscriptions()intoabortExperiment()teardownTest plan
npm testinpackages/jspsych— multiplayer tests pass🤖 Generated with Claude Code