Skip to content

Add a DataPipe extension, so an experiment needs no DataPipe code - #3718

Merged
jodeleeuw merged 12 commits into
mainfrom
feat/extension-pipe
Sep 16, 2026
Merged

jodeleeuw merged 12 commits into
mainfrom
feat/extension-pipe

Conversation

@jodeleeuw

Copy link
Copy Markdown
Member

Draft: blocked on datapipe-client being published. See "Why this is a draft" at the bottom — CI cannot pass until then.

A new extension for sending data to DataPipe. Registering it is the whole integration:

const jsPsych = initJsPsych({
  extensions: [{
    type: jsPsychExtensionPipe,
    params: { experiment_id: "...", filename: () => `${subject_id}.csv` }
  }]
});

const subject_id = jsPsych.randomization.randomID(10);
jsPsych.run(timeline);

No save trial, no await, no session variable threaded through the timeline. Each trial is staged as it finishes, so a participant who closes the tab at trial 199 of 200 does not take all 199 with them, and the complete dataset is submitted when the experiment ends.

It does not use the per-trial extension callbacks

This is the part most worth reviewing, because it is a deliberate departure from how every other extension works.

on_start/on_load/on_finish fire only for trials whose own extensions parameter names the extension — which is a different thing from the array passed to initJsPsych. TimelineNode.getParameterValue returns the nearest value walking up, and only data gets special merge treatment via getDataParameter(), so a nested extensions shadows rather than merges. A researcher who declares this extension on the root timeline and then writes extensions: [{type: jsPsychExtensionMouseTracking}] on one inner trial would silently stop staging that trial. Losing data quietly is the exact failure this feature exists to prevent.

There is a second problem: ExtensionManager.onFinish runs every extension under one Promise.all and merges afterwards, so on_finish cannot see a complete trial record anyway — not even its own contribution, let alone another extension's.

So the extension wraps jsPsych's two global callbacks through the public getInitSettings():

  • on_data_update fires for every trial after its data is final.
  • on_finish is awaited by run(), which is what lets the final upload block the end of the experiment.

Both are wrapped, never replaced, and the save runs before the researcher's own on_finish — that is where a redirect to Prolific or MTurk usually lives, and running after it would race a page navigation against the upload.

The consequence for researchers is that they add extensions to initJsPsych and to nothing else.

It survives abortExperiment()

abortExperiment() unwinds the timeline and falls through to on_finish, so an extension-owned save still runs. A save trial is never reached, which means a participant failed out by an attention check currently loses everything. There is a test for this.

Two statics

getCondition and saveBase64Data. Neither fits the extension's lifecycle — a condition usually decides which timeline to build, so it must be known before initJsPsych runs, and a media file is saved from a trial's on_finish. They are here only so an experiment needs one script tag rather than two.

getCondition throws. Everything about staging fails quietly on purpose, because the data is submitted again at the end; a condition has no safe fallback value, and a participant sent down the wrong branch looks like a successful run until someone reads the data.

Verification

25 tests. Verified against the real datapipe-client (linked into the monorepo, not the mock): tsc --noEmit clean and the full rollup build passes. Browser bundle 52.5 KB gzipped, essentially all of it the Firebase SDK.

Docs page at docs/extensions/pipe.md, wired into mkdocs.yml and the extension list.

Why this is a draft

packages/extension-pipe depends on datapipe-client, which is not yet published to npm. The root package-lock.json therefore has no entry for it, and CI runs npm ci, which fails when package.json and the lockfile disagree. The lockfile cannot be regenerated until the package exists on the registry.

To unblock: publish datapipe-client (from jspsych/datapipe#234, already merged to test), then npm install at this repo's root to update the lockfile, and mark this ready for review.

The tests mock datapipe-client as a virtual module, so jest passes today regardless — it is only npm ci that is blocked.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX

jodeleeuw and others added 4 commits September 15, 2026 08:50
Registering the extension in initJsPsych is the whole integration. It
stages each trial as it finishes, so a participant who closes the tab
partway through does not take their data with them, and it submits the
complete dataset when the experiment ends. No save trial, no await, and
no session variable threaded through the timeline.

It takes jsPsych's two global callbacks through the public
getInitSettings(), wrapping rather than replacing them, instead of using
the per-trial extension callbacks. Those fire only for trials whose own
`extensions` parameter names the extension -- a different thing from the
array passed to initJsPsych, and one a nested timeline shadows without
merging -- so an extension relying on them would silently miss trials.
on_finish is also awaited by run(), which is what lets the final upload
block the end of the experiment, and what makes the save survive
abortExperiment(): that unwinds the timeline and falls through, where a
save trial is simply never reached.

The staging protocol itself lives in datapipe-client, which is published
from the DataPipe repository. It is mocked in these tests as a virtual
module, since it is not installed in this monorepo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
Covers the one deviation from the usual extension shape: there are no
trial parameters and it is never added to a trial or timeline, because
saving an experiment's data should not depend on where else a researcher
happened to use an extension.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
createSession() returns synchronously and the /api/session round trip
finishes later, so sessionId is empty until it does. A short experiment
can reach on_finish inside that window, and the submission would then go
without the id -- leaving DataPipe unable to match the file to the staged
copy, which it would recover a second time as a spurious .partial.json.

flush() already waits for the start request to settle, so flushing before
reading sessionId fixes it, and restores the flush-before-submit ordering
the plugin had: it also makes the staged copy as complete as it can be if
the submission fails.

Verified against the real datapipe-client types rather than the test's
mock: tsc and the rollup build both pass with the package linked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
Both are static, because neither fits the extension's own lifecycle: a
condition usually decides which timeline to build, so it has to be known
before initJsPsych() runs, and a media file is saved from a trial's
on_finish. They are here only so an experiment needs one script tag
rather than two -- without them the docs would recommend the extension
for saving data and then send researchers back to the plugin for the
other two things DataPipe does.

getCondition throws, and the tests pin that it propagates rather than
returning a fallback. A participant sent down the wrong branch, or an
empty one, looks like a successful run until someone reads the data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
@changeset-bot

changeset-bot Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1f44c89

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

This PR includes changesets to release 1 package
Name Type
@jspsych/extension-pipe 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

Review catch on #3718. The option let a jsPsychPipe save trial own the
submission instead of the extension, and that combination cannot work.

The plugin has no session, so it cannot send a sessionId. DataPipe
therefore has no way to tell that the submission completes the staged
copy, and nothing discards the staging node. Meanwhile the extension saw
the trial's success and cancelled the abandonment stamp, so the sweep's
`abandoned` branch never fired either. The node sat there until the
24-hour expiry backstop, which recovered the same trials a second time as
a .partial.json -- a duplicate of data the researcher already had, a day
late, with a phantom "session in progress" row on the dashboard until
then.

Making it work would mean giving the plugin the session back, which is
exactly the coupling that splitting the client out removed. It is not
worth that for an option whose only purpose was keeping a save trial in
the timeline, and the extension already saves at the same point.

Both docs now say plainly not to keep a save trial, and why: the second
submission is refused as a duplicate filename, which the extension reads
as a failed save and responds to by marking the session abandoned. A
saveBase64 trial is unaffected.

24 tests, tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
@jodeleeuw

Copy link
Copy Markdown
Member Author

You were right to be suspicious — it was the duplicate problem, not just a strange one. Removed save_at_end in 1428778.

The sequence, once I traced it:

  1. The plugin's save trial submits the complete file without a sessionId — the plugin has no session, and I removed that parameter from it entirely when streaming moved out.
  2. DataPipe therefore cannot tell that the submission completes the staged copy, so nothing calls discardStaging and the staging node survives.
  3. The extension saw the trial's success: true and called close({submitted: true}), which cancels the abandonment stamp — so the sweep's abandoned branch never fired either.
  4. The node sat until now >= session.expiresAt, the 24-hour backstop in scheduled-staging-sweep.ts, and recoverSession wrote those same trials out again as a .partial.json.

So the researcher got their complete file and then, a day later, a partial duplicating it — plus a phantom "1 session in progress" row on the dashboard until the sweep cleaned up, since the mirror doc is only removed by discardSession.

It can't be fixed without undoing the split. Linking a submission to a session means sessionId on /api/data, which means the plugin needs the session back — the exact coupling removing the streaming code got rid of. Not worth it for an option whose only purpose was keeping a save trial in the timeline, when the extension already saves at the same point.

Both the README and the docs page now say plainly not to keep a save trial, and why it bites even without the option: two submissions, the second refused as a duplicate filename, which the extension reads as a failed save and responds to by marking the session abandoned. A saveBase64 trial for media is unaffected — it's only the save action that collides.

There's a test pinning it now: a stray pipe save trial in the timeline no longer diverts anything, and the session is closed once, on the extension's own result.

jsPsych.simulate() reaches this extension exactly like a real run:
simulate() sets simulationMode and then calls run(), so extensions
initialize identically. simulationMode is private and getSimulationMode
lives only on the internal dependencies object, so the extension has no
way to notice.

Left on, simulating an experiment consumes one of its sessions and writes
a real file of fake data into the researcher's dataset -- silently, and
in the one situation where they are least expecting real side effects.

`enabled: false` turns everything off: no session, no staging, no
submission, and the researcher's own callbacks untouched. Documented
against simulate() specifically rather than as a generic flag, since that
is the case nobody would think to look for.

Also guards double registration. initialize() is called once per entry in
the extensions array and the same instance answers each time, so listing
the extension twice would wrap the global callbacks twice and submit the
data twice.

27 tests, tsc clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
The blocker on this branch: package.json declared datapipe-client but the
lockfile had no entry, and CI runs npm ci, which fails when the two
disagree. The lockfile could not be regenerated until the package existed
on the registry. It does now (0.1.0), so this is the install.

Adds 932 lines, nearly all of it firebase's tree. That is the cost of the
split and it is paid here rather than by every plugin-pipe user, which
was the point.

The jest mock is no longer `virtual`. It had to be while the module did
not exist; now that it does, letting it resolve for real means a mock
that drifts from the package it stands in for fails here instead of
passing against a module that is not there.

Verified: npm ci exits 0, 27 tests, tsc clean, and the rollup build
resolves the published package rather than the local symlink used during
development.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgDfss2zhjwMky2mBKbcBX
@jodeleeuw
jodeleeuw marked this pull request as ready for review September 15, 2026 18:51
@github-actions

github-actions Bot commented Sep 15, 2026 •

Copy link
Copy Markdown
Contributor

📦 Preview build ready

Built from PR head 1193e78 and published at 2f7d1ef on branch preview/pr-3718.
URLs below are pinned to an immutable commit SHA, so they are safe to share and are cached permanently by jsDelivr.

Changed packages: extension-pipe

Quick-start HTML:

<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/jspsych/dist/index.browser.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/jspsych/css/jspsych.css">
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-keyboard-response/dist/index.browser.min.js"></script>
All package URLs
  • @jspsych/extension-mouse-tracking → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/extension-mouse-tracking/dist/index.browser.min.js
  • @jspsych/extension-pipe → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/extension-pipe/dist/index.browser.min.js
  • @jspsych/extension-record-video → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/extension-record-video/dist/index.browser.min.js
  • @jspsych/extension-webgazer → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/extension-webgazer/dist/index.browser.min.js
  • jspsych → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/jspsych/dist/index.browser.min.js
  • @jspsych/plugin-animation → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-animation/dist/index.browser.min.js
  • @jspsych/plugin-audio-button-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-audio-button-response/dist/index.browser.min.js
  • @jspsych/plugin-audio-keyboard-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-audio-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-audio-slider-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-audio-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-browser-check → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-browser-check/dist/index.browser.min.js
  • @jspsych/plugin-call-function → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-call-function/dist/index.browser.min.js
  • @jspsych/plugin-canvas-button-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-canvas-button-response/dist/index.browser.min.js
  • @jspsych/plugin-canvas-keyboard-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-canvas-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-canvas-slider-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-canvas-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-categorize-animation → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-categorize-animation/dist/index.browser.min.js
  • @jspsych/plugin-categorize-html → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-categorize-html/dist/index.browser.min.js
  • @jspsych/plugin-categorize-image → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-categorize-image/dist/index.browser.min.js
  • @jspsych/plugin-cloze → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-cloze/dist/index.browser.min.js
  • @jspsych/plugin-external-html → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-external-html/dist/index.browser.min.js
  • @jspsych/plugin-free-sort → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-free-sort/dist/index.browser.min.js
  • @jspsych/plugin-fullscreen → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-fullscreen/dist/index.browser.min.js
  • @jspsych/plugin-html-audio-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-audio-response/dist/index.browser.min.js
  • @jspsych/plugin-html-button-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-button-response/dist/index.browser.min.js
  • @jspsych/plugin-html-keyboard-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-html-slider-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-html-video-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-html-video-response/dist/index.browser.min.js
  • @jspsych/plugin-iat-html → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-iat-html/dist/index.browser.min.js
  • @jspsych/plugin-iat-image → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-iat-image/dist/index.browser.min.js
  • @jspsych/plugin-image-button-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-image-button-response/dist/index.browser.min.js
  • @jspsych/plugin-image-keyboard-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-image-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-image-slider-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-image-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-initialize-camera → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-initialize-camera/dist/index.browser.min.js
  • @jspsych/plugin-initialize-microphone → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-initialize-microphone/dist/index.browser.min.js
  • @jspsych/plugin-instructions → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-instructions/dist/index.browser.min.js
  • @jspsych/plugin-maxdiff → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-maxdiff/dist/index.browser.min.js
  • @jspsych/plugin-mirror-camera → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-mirror-camera/dist/index.browser.min.js
  • @jspsych/plugin-preload → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-preload/dist/index.browser.min.js
  • @jspsych/plugin-reconstruction → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-reconstruction/dist/index.browser.min.js
  • @jspsych/plugin-resize → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-resize/dist/index.browser.min.js
  • @jspsych/plugin-same-different-html → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-same-different-html/dist/index.browser.min.js
  • @jspsych/plugin-same-different-image → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-same-different-image/dist/index.browser.min.js
  • @jspsych/plugin-serial-reaction-time-mouse → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-serial-reaction-time-mouse/dist/index.browser.min.js
  • @jspsych/plugin-serial-reaction-time → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-serial-reaction-time/dist/index.browser.min.js
  • @jspsych/plugin-sketchpad → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-sketchpad/dist/index.browser.min.js
  • @jspsych/plugin-survey-html-form → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey-html-form/dist/index.browser.min.js
  • @jspsych/plugin-survey-likert → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey-likert/dist/index.browser.min.js
  • @jspsych/plugin-survey-multi-choice → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey-multi-choice/dist/index.browser.min.js
  • @jspsych/plugin-survey-multi-select → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey-multi-select/dist/index.browser.min.js
  • @jspsych/plugin-survey-text → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey-text/dist/index.browser.min.js
  • @jspsych/plugin-survey → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-survey/dist/index.browser.min.js
  • @jspsych/plugin-video-button-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-video-button-response/dist/index.browser.min.js
  • @jspsych/plugin-video-keyboard-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-video-keyboard-response/dist/index.browser.min.js
  • @jspsych/plugin-video-slider-response → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-video-slider-response/dist/index.browser.min.js
  • @jspsych/plugin-virtual-chinrest → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-virtual-chinrest/dist/index.browser.min.js
  • @jspsych/plugin-visual-search-circle → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-visual-search-circle/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-calibrate → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-webgazer-calibrate/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-init-camera → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-webgazer-init-camera/dist/index.browser.min.js
  • @jspsych/plugin-webgazer-validate → https://cdn.jsdelivr.net/gh/jspsych/jsPsych@2f7d1ef53fab245b47acad286077b3c3c7d6962b/packages/plugin-webgazer-validate/dist/index.browser.min.js

Last updated 2026-09-16 14:38 UTC for PR head 1193e78.

@jodeleeuw jodeleeuw mentioned this pull request Sep 15, 2026
github-actions Bot pushed a commit that referenced this pull request Sep 15, 2026
jodeleeuw and others added 2 commits September 16, 2026 10:24
The wait message told participants not to close the page and was never
removed, so an experiment without its own ending screen left it up
forever. Once the upload settles, and after the researcher's on_finish,
the extension now shows done_message -- unless something else has taken
over the display. Both messages are configurable for translation.

Documents that a redirect in on_finish should set done_message, since
the done message stays visible while the destination loads.

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

A filename function that threw in initialize() stopped the hooks from
being installed, and since jsPsych does not handle that promise, the
experiment ran with no saving at all. The hooks are now installed first,
and a failed filename is retried at the end; if it fails again the data
is saved under a random name rather than not at all.

Registering the extension twice let the second entry overwrite params
after the first had already set up the session and filename. The
duplicate check now runs before params are stored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Sep 16, 2026
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Comment thread docs/extensions/pipe.md Outdated
Co-authored-by: jodeleeuw <595524+jodeleeuw@users.noreply.github.com>
Comment thread packages/extension-pipe/README.md
Co-authored-by: jodeleeuw <595524+jodeleeuw@users.noreply.github.com>
@jodeleeuw
jodeleeuw merged commit 1895e34 into main Sep 16, 2026
7 checks passed
@jodeleeuw
jodeleeuw deleted the feat/extension-pipe branch September 16, 2026 15:01
@github-actions github-actions Bot mentioned this pull request Sep 16, 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