Skip to content

feat: add plugin-multiplayer-countdown (synchronized group timer) - #41

Merged
jodeleeuw merged 8 commits into
mainfrom
plugin-multiplayer-countdown
Jul 21, 2026
Merged

feat: add plugin-multiplayer-countdown (synchronized group timer)#41
jodeleeuw merged 8 commits into
mainfrom
plugin-multiplayer-countdown

Conversation

@htsukamoto5

Copy link
Copy Markdown
Member

Summary

Adds plugin-multiplayer-countdown, a synchronized group timer (countdown or count-up) for the jsPsych multiplayer API, plus two illustrative examples.

Every participant pushes its own start timestamp into its own slot on trial start, and each client derives the displayed time from the minimum timestamp across all slots — a coordination-free consensus (no elected anchor, no single point of failure) in the same spirit as plugin-multiplayer-role's ordering. Min is order-independent, so every client converges on the same value with no coordination, and late joiners / refreshes resume at the group's real remaining time for free.

Because push replaces a whole slot, the timestamp is merged into this participant's existing slot (read-own → spread → push) so it never clobbers role/joinedAt metadata, and the push is idempotent on refresh (keep-if-present).

The pure consensus core is exposed as statics on the default export (startedAtKey / resolveStartedAt / computeRemaining / computeElapsed / formatTime) so demos can render their own synced display during another trial.

What's included

  • packages/plugin-multiplayer-countdown/ — plugin + pure core (countdown-core.ts), coded against a local interface mirroring the multiplayer API (jsPsych#3694, unreleased), same seam pattern as the merged chat/ready plugins. 41 tests (24 core + 17 trial), tsc clean, build emits all bundles with statics present.
  • examples/countdown-timer.html — a "barrier sandwich" two-tab demo (sync lobby → countdown → sync barrier → results). Browser-verified.
  • examples/public-goods-local.html — a timed public-goods game rendering a shared contribution deadline from the exported statics (the flagship "synced timer during another trial" use). Chosen over a timed ultimatum because its pacing is duration-bound (simultaneous), unlike the turn-based ultimatum game. Browser-verified two-tab; the min-across-slots consensus resolved starts 2 ms apart and preserved all unrelated slot keys.
  • Changeset (minor) + examples/README.md sections.

Design notes for review

  • Clock skew is a documented v1 limitation. Min is maximally sensitive to the single worst-behind clock: that client sees a normal countdown while everyone else clamps to 0 and ends early. The clamp to [0, duration] is the mitigation (failure direction is "ends early"); a deterministic outlier-filter escape hatch is designed-but-deferred. The README states this plainly, alongside the monotone-not-smooth and slots-outlive-members caveats, and recommends composing behind a ready/sync barrier.
  • Not a barrier. Ends are synchronized only within skew + latency — compose with plugin-multiplayer-sync/ready afterwards for a hard barrier.
  • Accessibility: the visible timer is not a live region (per-second announcements would be noise); a visually-hidden aria-live region announces only the final 5 seconds, the point an SR user needs to know the group deadline is about to auto-end the trial.
  • ⚠️ One decision to confirm: a failed self-registration push is surfaced as a loud console.error and the trial continues off the local-fallback start, rather than being made fatal — a sync subscribe-trial has no trial-promise to reject (unlike ready's await push). I believe continuing is the right call (the display can still run from local time), but flagging it in case you'd prefer it fatal.

🤖 Generated with Claude Code

htsukamoto5 and others added 3 commits July 14, 2026 16:27
A synchronized group timer (countdown or count-up) for the jsPsych
multiplayer API. Every participant pushes its own start timestamp into
its own slot; each client derives the displayed time from the minimum
timestamp across all slots — a coordination-free consensus (no anchor,
no single point of failure) in the spirit of plugin-multiplayer-role.
Late joiners and refreshes resume at the group's real remaining time.

The pure consensus core (startedAtKey / resolveStartedAt /
computeRemaining / computeElapsed / formatTime) is exposed as statics on
the default export so demos can render their own synced display. Built
against a local interface mirroring the multiplayer API (jsPsych#3694),
so it carries no build-time dependency on the unreleased core.

A visually-hidden aria-live region announces the final 5 seconds only,
avoiding per-second screen-reader spam. Ships a README and an
illustrative two-tab local-adapter example (examples/countdown-timer.html).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A synchronized public-goods game (two-tab, local adapter) that showcases
plugin-multiplayer-countdown's exported statics rendering a shared
contribution deadline inside a normal html-button-response — the plugin's
flagship "synced timer during another trial" use. Two players
simultaneously contribute to a multiplied common pool within one timed
window; the deadline ticks identically on both screens off the
minimum-across-slots consensus start. Duration-bound pacing, unlike the
turn-based ultimatum demo. Adds an examples/README.md section for it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…p 5)

Replaces plugin-multiplayer-draw's own per-client duration timeout with
a consensus clock rendered from plugin-multiplayer-countdown's exported
statics, so the room closes for every participant within skew/latency of
each other instead of on independent unsynced timers. Also adds the
draw-room.html README section that was missing since PR #34.
@htsukamoto5
htsukamoto5 force-pushed the plugin-multiplayer-countdown branch from 704c9cc to 0ba146e Compare July 14, 2026 20:35
…rder labels

Strokes on the shared canvas aren't attributed by name anywhere in the
UI, so asking for a display name bought nothing beyond a nicer-looking
roster. Replace it with "Player N" labels derived from the joinedAt
timestamp the lobby already pushes, removing a trial and the
plugin-survey-text dependency.
Updates the jsDelivr preview SHA to 7b1d96a0576032d15bd0d7c434f01b2d614f3a85
so the demos load the update() function and recent fixes from #3694.
Replaces two manual get(id) -> spread -> push sequences (the "I'm done"
button and the draw-start-timestamp registration) with the new update()
convenience method added to #3694.
…ng design doc

countdown-core.ts referenced docs/countdown-plugin-design.md, which is not
part of the package; the analysis lives in the README's Limitations section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…kage

- Drop the draw-room retrofit and the chat-room/ultimatum re-pins from this
  branch: the re-pins are PR #44's scope, and the draw-room rewrite moves to
  its own follow-up PR so this one stays 'add the countdown package' plus
  its two dedicated demos (countdown-timer.html, public-goods-local.html).
- Regenerate package-lock.json from a clean npm install: the previous diff
  stripped resolved/integrity fields from 808 entries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jodeleeuw

Copy link
Copy Markdown
Member

Pushed 458e0bd: merged main and narrowed this PR to its core scope per review — the draw-room retrofit moved to #51 (stacked on this branch), and the chat-room/ultimatum re-pins were dropped as #44's scope. This PR is now purely 'add the countdown package' plus its two dedicated demos (countdown-timer.html, public-goods-local.html), all additions. Also regenerated package-lock.json from a clean npm install (the previous diff had stripped resolved/integrity from 808 entries) and fixed a dangling design-doc reference in countdown-core.ts (2e247f8). The min-of-wall-clocks sync model has been reviewed and accepted as-is for v1; #50 tracks a possible future serverNow() adapter capability. Full repo suite: 462 tests pass, CI green.

@jodeleeuw
jodeleeuw merged commit 9fd498b into main Jul 21, 2026
4 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/plugin-multiplayer-countdown

From a fresh checkout of main:

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

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

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

2 participants