Skip to content

Harden the multiplayer API for 1.0: trial scopes, outcomes, shared utils - #107

Merged
jodeleeuw merged 11 commits into
mainfrom
feat/1.0-hardening
Sep 25, 2026
Merged

jodeleeuw merged 11 commits into
mainfrom
feat/1.0-hardening

Conversation

@jodeleeuw

Copy link
Copy Markdown
Member

Ports the whole ecosystem to the hardened multiplayer core, as agreed in the pre-1.0 API review. The core changes are on jspsych/jsPsych#3694 (commits 18e5fdb2..14b34bf2), and this branch vendors its preview build 4df7fbb6.

What changed in the core (for context)

  • Trial scopes: during a trial, reads, writes, subscribe, and wait use that trial's own part of the shared data. It is named by the trial's position in the timeline, or by the new multiplayer_scope trial parameter. { scope: "session" } is for data that lasts the whole session. Subscriptions and waits made during a trial end with it.
  • Writes: update() is the write; push() becomes replace(). Failed writes are retried with backoff.
  • Presence: left is final and agreed across the group. A participant who reloads sees restarted (it replaces previousInstance).
  • Errors: one MultiplayerError class with a code.
  • Timeouts: they must be a positive number of milliseconds, or null for none. The core now owns connectTimeout and reconnectTimeout.
  • Data IDs: every row recorded while connected gets multiplayer_participant_id and multiplayer_session_id (recordIds, on by default).
  • Groups: adapters relay the seal through group(); peers' data can no longer seal the group. The bookkeeping format is versioned.

This PR

  • New @jspsych-multiplayer/utils: shared helpers (isMultiplayerError, outcomeOf, pluginTimeout, remainingParticipants, sealedGroupSize, waitForAll, and adapter ID/URL helpers). The root build now builds it first.
  • Adapters:
    • keyPrefix/pathPrefix are renamed namespace, and persistParticipant defaults to true.
    • Local: onResumed() replaces the fake reconnect.
    • JATOS: relays the seal to every member, and omits sealGroup when JATOS can't seal.
    • Firebase: rules now tie slots, seats, rosters, and lobbies to auth.uid, and a reload returns to its group.
  • Plugins:
    • The per-plugin shims, gate counters, and data_key parameters are gone.
    • push_data becomes write_data, which merges.
    • Every trial records multiplayer_outcome and left_participant in place of timed_out/partner_left/connection_lost/wait_error.
    • Group snapshots are saved only with save_group.
  • Examples and docs: everything is ported, with a new "Upgrading from 0.x" guide. The docs and example pages pin the new preview.

Breaking changes

Every changed package has a minor changeset describing its breaking changes. Existing Firebase deployments must redeploy the rules.

Testing

  • 823 tests pass across the repo, and npm run build is clean.
  • The docs site builds with no broken links.
  • The local-adapter examples ran end to end in a jsdom smoke harness with 2–4 simulated tabs.
  • Not tested live: the JATOS and Firebase ultimatum pages, and the new Firebase rules against the emulator.

Open question

The default scope comes from a trial's position in the timeline, so role-specific conditional branches (e.g. proposer vs. responder) don't share a scope. Researchers have to set multiplayer_scope for them, as the ultimatum tutorial now shows. Should the default change, or is documenting this enough?

After merge

The new @jspsych-multiplayer/utils package needs its one-time bootstrap publish (npm publish + npm trust) before the Version Packages PR can publish it.

🤖 Generated with Claude Code

jodeleeuw and others added 11 commits September 25, 2026 11:27
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Also moves the in-memory test backend to the new wire format and keeps
non-plugin packages out of the README's plugin table.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Local: onResumed replaces the fake reconnect; keyPrefix -> namespace;
  persistParticipant defaults to true; IDs from @jspsych-multiplayer/utils
- JATOS: relays the seal to every member under $sealed with a trust check;
  sealGroup is omitted when unsupported; connectTimeoutMs and
  closeAfterReconnectingMs give way to the core's timeouts; push rejects
  instead of waiting forever for the channel
- Firebase: pathPrefix -> namespace; tab-persisted participant IDs keep a
  reload in its group; rules tie slots, seats, rosters, and lobbies to
  auth.uid; connectTimeoutMs gives way to the core's connectTimeout

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Use @jspsych-multiplayer/utils; delete the per-plugin multiplayer.ts shims
- Trial scopes replace gate counters and data_key parameters
- push_data -> write_data, which merges; nothing replaces the whole slot
- Record multiplayer_outcome and left_participant in place of timed_out,
  partner_left, connection_lost, and wait_error
- Group snapshots only with save_group; waits default to the remaining
  participants; a timeout of 0 still means no limit
- Values shared across trials use the session scope; role and match
  accessors read jsPsych data instead of module-level stores

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Names and other values shared across trials use the session scope
- Done screens read multiplayer_outcome; pages check restarted and
  handle a failed connect()
- The JATOS ultimatum game waits with waitForGroup (maxActiveMembers: 2)
- group-quiz waits on the host through the session scope

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vendors preview 4df7fbb6 (core 14b34bf2) and points the docs and example
pages at it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jodeleeuw
jodeleeuw merged commit f6b359e into main Sep 25, 2026
6 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

📦 New package — trusted-publishing bootstrap needed

This PR added one or more new packages. npm trusted publishing (OIDC) can't be configured for a package that doesn't exist yet, so a maintainer must do a one-time bootstrap per new package. After that, releases publish automatically via OIDC (publish.yml).

Before running the commands below: npm >=11.15.0 (npm install -g npm@latest) — required for npm trust; older versions fail the trust step with HTTP 400. Also 2FA enabled, logged in (npm login), with publish access to the @jspsych-multiplayer scope.

@jspsych-multiplayer/utils

From a fresh checkout of main:

npm ci && npm run build
npm publish -w @jspsych-multiplayer/utils --access public
npm trust github @jspsych-multiplayer/utils --repo jspsych/jspsych-multiplayer --file publish.yml --allow-publish

Once bootstrapped, bump the version and merge to main — publish.yml publishes future versions tokenlessly via OIDC, with provenance.

@github-actions github-actions Bot mentioned this pull request Sep 25, 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.

1 participant