npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@jspsych-multiplayer/plugin-multiplayer-sync

v0.1.0

Published

jsPsych plugin: synchronization barrier (push → wait) for the multiplayer API

Readme

@jspsych-multiplayer/plugin-multiplayer-sync

A synchronization-barrier plugin for multiplayer jsPsych experiments, built on the multiplayer plugin API. It packages the common push → wait pattern into a single declarative trial: optionally push this participant's data into the shared group session, show a waiting message, and end the trial once a condition over the group session is met (or an optional timeout elapses).

This replaces the awkward idioms previously needed for synchronization points — a call-function trial with async/done, or an html-keyboard-response trial with choices: "NO_KEYS", an on_start that awaits pluginAPI.wait(), and a manual jsPsych.finishTrial().

Status: built against the jsPsych multiplayer API from jsPsych#3694, which is not yet released. The plugin codes against a local interface mirroring that API (src/multiplayer-api.ts) and reaches the real object with one cast — the single seam to re-verify once #3694 lands. Tests run against an in-memory mock, so no live group session is needed.

Prerequisites

Requires a connected multiplayer adapter (e.g. @jspsych-multiplayer/adapter-multiplayer-jatos). Connect it before jsPsych.run():

const jsPsych = initJsPsych();
await jsPsych.pluginAPI.connect(new jsPsychAdapterMultiplayerJatos());
await jsPsych.run(timeline);

Parameters

| Parameter | Type | Default | Description | | -------------- | ----------------------- | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | wait_for | function | undefined (required) | Predicate (group) => boolean evaluated against the full group session on every update. The trial ends when it returns true. Same condition you would pass to pluginAPI.wait(). | | push_data | object | function | null | Data pushed into the group session when the trial starts, before waiting. null waits without pushing. May be a function returning the object, e.g. () => ({ offer }). | | message | HTML string | function | "<p>Waiting for other players…</p>" | Shown while waiting. | | timeout | integer | null | Max time to wait, in ms. On elapse the trial ends with timed_out: true and on_timeout is called. null waits indefinitely. | | on_timeout | function | null | Called if timeout elapses before wait_for is satisfied. | | minimum_wait | integer | 0 | Minimum time, in ms, to keep the message on screen so it doesn't flash by — applies whether the trial ends because the condition is met or because the timeout elapses. |

Data Generated

| Name | Type | Description | | ----------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | group | object | The full group session snapshot at the moment the condition was met (or the timeout fired). Read peers / assign roles from here in on_finish. | | wait_time | integer | Time spent waiting, in ms, from trial start until the trial ended. | | timed_out | boolean | True if the trial ended because timeout elapsed rather than because wait_for was met. | | wait_error | string | null | Message from the wait() rejection when the trial ended without wait_for being met (on a genuine timeout, the timeout message); null when the condition was satisfied. |

Writing robust wait_for predicates

push uses overwrite-per-participant semantics: each participant has a single entry in the group session, and every push replaces it. A fast peer that clears a barrier and pushes again in a later trial can therefore overwrite the very entry your wait_for predicate is still checking — and the condition you were waiting for may never (re)appear.

Prefer predicates that are monotone: once true, they stay true under any later push. For example:

// Fragile: p2's next push may not include `status`, so this can flicker back to false
wait_for: (group) => group["p2"]?.status === "ready",

// Robust: presence in the session is monotone — participants don't disappear
wait_for: (group) => Object.keys(group).length >= 2,

// Robust: carry a monotone counter/phase forward in every push and compare with >=
wait_for: (group) => Object.values(group).every((p) => p.trial_index >= 5),

If a barrier must key on transient fields, include them in every subsequent push (so they are never overwritten away), or advance a phase/counter field that only increases.

Example: a lobby that waits for two players

const lobby = {
  type: jsPsychMultiplayerSync,
  push_data: { status: "ready" },
  wait_for: (group) => Object.keys(group).length >= 2,
  message: "<p>Waiting for another player to join…</p>",
  on_finish: (data) => {
    // Role assignment stays experiment-specific — do it here off data.group, or hand the snapshot
    // to @jspsych-multiplayer/plugin-multiplayer-role for deterministic consensus.
    const [proposerId, responderId] = Object.keys(data.group).sort();
    myRole = jsPsych.pluginAPI.participantId === proposerId ? "proposer" : "responder";
  },
};

Scope

A jsPsych plugin is a trial, so this plugin covers synchronization points that are their own timeline step (barriers, lobbies, send-then-wait handoffs). For communication in the middle of another interactive trial, use the raw jsPsych.pluginAPI (push, wait, get, getAll, subscribe, communicate) directly.