Skip to content

Port the group-quiz demo and JATOS packaging scripts out of jsPsych#3694 - #56

Merged
jodeleeuw merged 3 commits into
mainfrom
port-group-quiz-jatos-packaging
Jul 23, 2026
Merged

Port the group-quiz demo and JATOS packaging scripts out of jsPsych#3694#56
jodeleeuw merged 3 commits into
mainfrom
port-group-quiz-jatos-packaging

Conversation

@htsukamoto5

Copy link
Copy Markdown
Member

Rescues three artifacts from the jspsych/jsPsych#3694 branch before it is stripped to core-only (jsPsych.multiplayer + tests + reference docs) at a maintainer's request. Anything not living here is lost when that happens.

Important

Stacked on #53 (migrate-multiplayer-namespace), which is the base of this PR — so the diff below is just my one commit. It has to be stacked: main's plugins still reach the API via jsPsych.pluginAPI, and current #3694 has no pluginAPI fallback (createJointPluginAPIObject composes only keyboard/timeout/media/simulation; JsPsych.ts adds multiplayer as a separate property). Based on main, the group-quiz demo would die at its first sync barrier. GitHub will retarget this to main once #53 merges.

What this adds

1. examples/group-quiz/ — a live, Kahoot-style quiz. Everyone opens one URL; one person clicks Host (the presenter screen), everyone else clicks Player. It's the repo's demo of the asymmetric pattern — one authoritative driver plus many followers — versus ultimatum-game-jatos.html, where every client runs the same timeline and coordination is by deterministic consensus. The host half is deliberately not a jsPsych timeline: it's vanilla JS driving the adapter directly, because a presenter screen reacts continuously (subscribe) rather than advancing through trials.

The load-bearing idea is the monotonic step counter. The host advances by overwriting a phase field, and JATOS doesn't guarantee a client observes every intermediate snapshot — so the obvious barrier deadlocks:

wait_for: (group) => group[hostId]?.phase === "reveal"   // deadlocks

A lagging player whose snapshot jumps straight from question to leaderboard is left with a permanently unsatisfiable condition and hangs forever. Every host push therefore carries a step that only ever increases, and players wait on hostStepValue(group) >= phaseStep(…) — a >= test against a monotonic value can never be missed. Generalized: on a snapshot-based transport, barrier predicates must be monotone.

2. scripts/ — the repo's first packaging tooling. These assemble an importable .jzip (flat assets + a .jas with groupStudy: true, then zip). There was none before, so ultimatum-game-jatos.html couldn't be uploaded to JATOS at all. Probably the highest-value part of this PR.

3. docs/group-quiz-design.md — rewritten from the fork's group-quiz-plan.md, which was a three-track work-assignment plan for a team that has since built the thing. Kept the durable content (protocol, phase table, scoring, the host-only answer-key rationale) and replaced the task checklists with the step-counter explanation, the composition rationale, and known limitations.

What I rewired

  • Asset resolution. The fork's scripts assumed its monorepo layout (packages/jspsych/dist/…). Here only the multiplayer packages live in packages/, so core and the stock plugins now resolve from node_modules (added as devDependencies). A JATOS study must be self-contained, so the examples' CDN <script src> are resolved to bundled copies and rewritten to flat filenames.
  • Rewrites match the quoted attribute value and fail loudly on a miss. https://unpkg.com/jspsych is a prefix of …/jspsych/css/jspsych.css, so unanchored replacement corrupts one depending on iteration order. And a silently-missed rewrite ships a .jzip whose index.html still points at a CDN — failing only at run time inside JATOS, long after whoever broke it moved on.
  • build-jatos-ultimatum.js targets this repo's example and leaves the batch uncapped. The fork capped it at 2 members, but this repo's version routes extra arrivals to a spectator screen — a 2-member cap makes that documented path unreachable.

Composition decision

The demo hand-rolls its leaderboard, timer, and answer buttons rather than composing this repo's scoreboard/countdown/choice plugins, and I kept it that way. The host view isn't a jsPsych timeline, so half the game can't run plugins at all; countdown resolves min-across-slots consensus while this clock is host-authoritative; choice barriers until everyone picks, but the quiz needs a private answer with a host-timed reveal and speed scoring. Closest call was scoreboard's buildLeaderboard, but it expects {[dataKey]: {score, label}} while this protocol is flat and the host page loads no plugins — reshaping the wire contract to avoid a nine-line sort. Full reasoning in the design doc.

Not ported, deliberately

adapter-multiplayer-jatos and plugin-multiplayer-sync — this repo's copies are strictly ahead (265 vs 145 and 197 vs 148 source lines; studyResultId keying, connect re-entry guard + timeout, onClose handling, subscriber isolation, and sync's wait_error/on_load fixes exist only here). The fork's ultimatum examples are superseded by examples/ultimatum-game-jatos.html.

One remaining gap I found and have not actioned: the fork's docs/developers/adapter-development.md (162 lines — per-method gotchas, a worked example, and a new-adapter checklist including "adapter subscribe() must be future-only, no replay — the API handles it"). We have the interface shape in the reference docs but none of that. Happy to port it separately if we think it won't survive the strip.

Verification

  • 557 tests pass; npm run build clean.
  • Both scripts produce valid archives — contents checked, groupStudy: true, correct dirName/htmlFilePath, and no unrewritten asset references in the packaged HTML.
  • Confirmed in the shipped bundle (not just source) that sync resolves the namespace: dist/group-quiz-jatos/plugin-multiplayer-sync.js contains multiplayer ?? instance.pluginAPI.
  • Every API call site checked against #3694 head c5c4499 (verified as the live PR head via gh api).

Not verified: a live JATOS run. Like the other JATOS examples here, this is illustrative — until #3694 ships, the bundled core lacks jsPsych.multiplayer, so the archive imports cleanly but fails at connect(). Both scripts print that caveat, and the examples carry it in their headers.

🤖 Generated with Claude Code

htsukamoto5 and others added 3 commits July 22, 2026 17:08
… jsPsych#3694

Rescues three artifacts from the jspsych/jsPsych `multiplayer` branch before it is
stripped to core-only (jsPsych.multiplayer + tests + reference docs) upstream.

examples/group-quiz/ — a live, Kahoot-style quiz: one participant hosts, others play.
The host drives phases via a monotonic `step` counter rather than an exact
`phase === X` barrier, which deadlocks under JATOS snapshot skipping (a lagging
client whose snapshot jumps a phase is left with a permanently unsatisfiable
condition). Rewired to this repo's example conventions: unpkg for core/stock plugins,
`../../packages/*/dist/index.browser.min.js` for the multiplayer packages, and the
same honest "illustrative, not runnable until #3694 ships" header caveat as
ultimatum-game-jatos.html. Verified against this repo's sync plugin, which has drifted
ahead of the version the demo was written against.

Kept hand-rolled rather than composed from this repo's scoreboard/countdown/choice
plugins: the host view is not a jsPsych timeline (so it cannot run plugins at all),
and the quiz is host-authoritative where those plugins coordinate peer-to-peer.
Reasoning recorded in docs/group-quiz-design.md.

scripts/ — the repo's first packaging tooling. Assembles an importable .jzip (flat
assets + a .jas with groupStudy: true, then zip), so ultimatum-game-jatos.html can
finally be uploaded to JATOS at all. Asset resolution rewired for this repo's layout:
only the multiplayer packages live in packages/, so jsPsych core and the stock plugins
resolve from node_modules (added as devDependencies) instead. build-jatos-ultimatum.js
points at this repo's ultimatum-game-jatos.html, not the fork's superseded version,
and leaves the batch uncapped so the demo's documented spectator-overflow path stays
reachable. Path rewrites now match on the quoted attribute value and fail loudly if
one finds no match — a silently-missed rewrite would only surface inside JATOS.

Both archives carry a printed caveat: the bundled core is a published release, so it
lacks jsPsych.multiplayer and the study fails at connect() until #3694 ships.

Not ported, per an audit of both repos: adapter-multiplayer-jatos and
plugin-multiplayer-sync (this repo's copies are strictly ahead — spot-checked at
265 vs 145 and 197 vs 148 source lines, with studyResultId keying, connect re-entry
guard + timeout, onClose handling, subscriber isolation, and sync's wait_error/on_load
fixes present only here), and the fork's ultimatum examples (superseded by
examples/ultimatum-game-jatos.html).

Verified: 556 tests pass; both scripts produce valid .jzip archives with correct
groupStudy metadata and no unrewritten asset references.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Pin jspsych 8.2.3 as an explicit root devDependency: the packaging
  scripts hard-require node_modules/jspsych/dist/index.browser.js and
  css/jspsych.css, which previously resolved only via hoisting of
  sub-package devDeps. Exact pin (no caret) since the scripts embed it.
- Reword the nodeModulesAsset error message so it accurately describes
  where jsPsych core and the stock plugins come from and how to fix a
  missing install.
- Add a CI smoke-test step running both build:jatos:* scripts after the
  package build (ubuntu-latest ships the required zip binary).
- Add an Attribution subsection to the group-quiz section of
  examples/README.md, crediting the jsPsych#3694 demo (MIT), matching
  the ultimatum section's format.
- Document two packaging gotchas in examples/README.md: the zip CLI
  requirement on macOS/Linux (PowerShell on Windows), and that each
  build mints fresh JATOS UUIDs so re-imports create a new study.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…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>
@jodeleeuw
jodeleeuw deleted the branch main July 23, 2026 15:15
@jodeleeuw jodeleeuw closed this Jul 23, 2026
@jodeleeuw jodeleeuw reopened this Jul 23, 2026
@jodeleeuw
jodeleeuw changed the base branch from migrate-multiplayer-namespace to main July 23, 2026 15:18
@jodeleeuw
jodeleeuw merged commit bbf5ad6 into main Jul 23, 2026
1 of 7 checks passed
@jodeleeuw
jodeleeuw deleted the port-group-quiz-jatos-packaging branch July 23, 2026 15:20
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.

2 participants