Skip to content

Add jsPsych.multiplayer core API (MultiplayerAPI + MultiplayerAdapter interface)#3694

Open
htsukamoto5 wants to merge 37 commits into
jspsych:mainfrom
htsukamoto5:multiplayer
Open

Add jsPsych.multiplayer core API (MultiplayerAPI + MultiplayerAdapter interface)#3694
htsukamoto5 wants to merge 37 commits into
jspsych:mainfrom
htsukamoto5:multiplayer

Conversation

@htsukamoto5

@htsukamoto5 htsukamoto5 commented Jun 25, 2026

Copy link
Copy Markdown
Member

Summary

Adds jsPsych.multiplayer — an opt-in, backend-agnostic API for real-time multiplayer experiments. This PR is core-only: it introduces the MultiplayerAPI module and the MultiplayerAdapter interface 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.multiplayer is a small state primitive plus conveniences, organized like jsPsych.data:

  • State primitive: push(data), getAll(), subscribe(callback) — one shared group-session "whiteboard", three verbs. subscribe() replays the current snapshot once on registration before forwarding future updates.
  • Conveniences built on the primitive: 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 with MultiplayerTimeoutError on timeout).
  • Lifecycle: connect(adapter), disconnect(), cancelAllSubscriptions(), and a participantId property.

The MultiplayerAdapter interface 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 in MultiplayerAPI so it's consistent across backends.

MultiplayerTimeoutError is a new export from the jspsych package, re-exported top-level alongside the other multiplayer types.

What's included

  • Core API (packages/jspsych): MultiplayerAPI class and MultiplayerAdapter interface at src/modules/multiplayer/, exposed as jsPsych.multiplayer
  • Tests: core API tests in packages/jspsych/tests/multiplayer/multiplayer.test.ts
  • Docs: docs/reference/jspsych-multiplayer.md, docs/developers/adapter-development.md, wired into mkdocs.yml
  • Changeset: minor bump for jspsych

Design notes

The adapter's subscribe() is future-only; replay is handled once in MultiplayerAPI.subscribe() (register-then-replay ordering prevents a TDZ crash when wait() references its own unsubscribe handle 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-sync and @jspsych/adapter-multiplayer-jatos — moved to a sibling repo and will be proposed as follow-up PRs once this core API is stable
  • Participant dropout / presence — no presence primitive yet; a peer disconnecting isn't surfaced to the API
  • A lightweight ephemeral event channel (publish/onMessage) for transient/high-rate messages, deferred pending a concrete use case
  • Wiring cancelAllSubscriptions() into abortExperiment() teardown

Test plan

  • npm test in packages/jspsych — multiplayer tests pass

🤖 Generated with Claude Code

htsukamoto5 and others added 25 commits June 17, 2026 11:38
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>
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-bot

changeset-bot Bot commented Jun 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: eddf2f6

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
jspsych Minor

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>
@github-actions

github-actions Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

📦 Preview build ready

Built from PR head eddf2f6 and published at 151ab52 on branch preview/pr-3694.
URLs below are pinned to an immutable commit SHA, so they are safe to share and are cached permanently by jsDelivr.

Changed packages: jspsych

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
  • @jspsych/extension-mouse-trackinghttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/extension-mouse-tracking/dist/index.browser.min.js
  • @jspsych/extension-record-videohttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/extension-record-video/dist/index.browser.min.js
  • @jspsych/extension-webgazerhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/extension-webgazer/dist/index.browser.min.js
  • jspsychhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/jspsych/dist/index.browser.min.js
  • @jspsych/plugin-animationhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-animation/dist/index.browser.min.js
  • @jspsych/plugin-audio-button-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-audio-button-response/dist/index.browser.min.js
  • @jspsych/plugin-audio-keyboard-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-audio-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-audio-slider-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-audio-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-browser-checkhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-browser-check/dist/index.browser.min.js
  • @jspsych/plugin-call-functionhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-call-function/dist/index.browser.min.js
  • @jspsych/plugin-canvas-button-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-canvas-button-response/dist/index.browser.min.js
  • @jspsych/plugin-canvas-keyboard-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-canvas-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-canvas-slider-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-canvas-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-categorize-animationhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-categorize-animation/dist/index.browser.min.js
  • @jspsych/plugin-categorize-htmlhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-categorize-html/dist/index.browser.min.js
  • @jspsych/plugin-categorize-imagehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-categorize-image/dist/index.browser.min.js
  • @jspsych/plugin-clozehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-cloze/dist/index.browser.min.js
  • @jspsych/plugin-external-htmlhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-external-html/dist/index.browser.min.js
  • @jspsych/plugin-free-sorthttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-free-sort/dist/index.browser.min.js
  • @jspsych/plugin-fullscreenhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-fullscreen/dist/index.browser.min.js
  • @jspsych/plugin-html-audio-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-audio-response/dist/index.browser.min.js
  • @jspsych/plugin-html-button-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-button-response/dist/index.browser.min.js
  • @jspsych/plugin-html-keyboard-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-html-slider-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-html-video-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-html-video-response/dist/index.browser.min.js
  • @jspsych/plugin-iat-htmlhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-iat-html/dist/index.browser.min.js
  • @jspsych/plugin-iat-imagehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-iat-image/dist/index.browser.min.js
  • @jspsych/plugin-image-button-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-image-button-response/dist/index.browser.min.js
  • @jspsych/plugin-image-keyboard-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-image-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-image-slider-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-image-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-initialize-camerahttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-initialize-camera/dist/index.browser.min.js
  • @jspsych/plugin-initialize-microphonehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-initialize-microphone/dist/index.browser.min.js
  • @jspsych/plugin-instructionshttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-instructions/dist/index.browser.min.js
  • @jspsych/plugin-maxdiffhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-maxdiff/dist/index.browser.min.js
  • @jspsych/plugin-mirror-camerahttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-mirror-camera/dist/index.browser.min.js
  • @jspsych/plugin-preloadhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-preload/dist/index.browser.min.js
  • @jspsych/plugin-reconstructionhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-reconstruction/dist/index.browser.min.js
  • @jspsych/plugin-resizehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-resize/dist/index.browser.min.js
  • @jspsych/plugin-same-different-htmlhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-same-different-html/dist/index.browser.min.js
  • @jspsych/plugin-same-different-imagehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-same-different-image/dist/index.browser.min.js
  • @jspsych/plugin-serial-reaction-time-mousehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-serial-reaction-time-mouse/dist/index.browser.min.js
  • @jspsych/plugin-serial-reaction-timehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-serial-reaction-time/dist/index.browser.min.js
  • @jspsych/plugin-sketchpadhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-sketchpad/dist/index.browser.min.js
  • @jspsych/plugin-survey-html-formhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey-html-form/dist/index.browser.min.js
  • @jspsych/plugin-survey-likerthttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey-likert/dist/index.browser.min.js
  • @jspsych/plugin-survey-multi-choicehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey-multi-choice/dist/index.browser.min.js
  • @jspsych/plugin-survey-multi-selecthttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey-multi-select/dist/index.browser.min.js
  • @jspsych/plugin-survey-texthttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey-text/dist/index.browser.min.js
  • @jspsych/plugin-surveyhttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-survey/dist/index.browser.min.js
  • @jspsych/plugin-video-button-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-video-button-response/dist/index.browser.min.js
  • @jspsych/plugin-video-keyboard-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-video-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-video-slider-responsehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-video-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-virtual-chinresthttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-virtual-chinrest/dist/index.browser.min.js
  • @jspsych/plugin-visual-search-circlehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-visual-search-circle/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-calibratehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-webgazer-calibrate/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-init-camerahttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-webgazer-init-camera/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-validatehttps://cdn.jsdelivr.net/gh/jspsych/jsPsych@151ab520542a8e48bcc4d5b21c74cdffae8b48c6/packages/plugin-webgazer-validate/dist/index.browser.min.js

Last updated 2026-07-23 15:09 UTC for PR head eddf2f6.

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>
github-actions Bot pushed a commit that referenced this pull request Jul 15, 2026
htsukamoto5 and others added 2 commits July 15, 2026 13:01
… 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.
github-actions Bot pushed a commit that referenced this pull request Jul 15, 2026
- 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>
htsukamoto5 and others added 2 commits July 16, 2026 11:43
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>
github-actions Bot pushed a commit that referenced this pull request Jul 16, 2026
- 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>
Comment thread packages/plugin-multiplayer-sync/src/index.ts Fixed
github-actions Bot pushed a commit that referenced this pull request Jul 16, 2026
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Jul 16, 2026
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.
jodeleeuw and others added 3 commits July 23, 2026 09:37
… 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>
github-actions Bot pushed a commit that referenced this pull request Jul 23, 2026
@htsukamoto5 htsukamoto5 changed the title Add real-time multiplayer support (MultiplayerAPI + plugin-multiplayer-sync + adapter-multiplayer-jatos) Add jsPsych.multiplayer core API (MultiplayerAPI + MultiplayerAdapter interface) Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants