feat: resume on page reload + first-class jsPsych.state (9.0) - #3713
Draft
jodeleeuw wants to merge 8 commits into
Draft
feat: resume on page reload + first-class jsPsych.state (9.0)#3713jodeleeuw wants to merge 8 commits into
jodeleeuw wants to merge 8 commits into
Conversation
Adds an opt-in `resume` option to initJsPsych() that persists a session log (timeline variable orders, conditional/loop function outcomes, and trial results) plus a user state object to localStorage after every trial. On reload, the timeline replays the log — skipping plugin execution and callbacks — to rebuild data and position, then continues live at the interruption point. Includes jsPsych.resume.state/clear(), an on_resume callback, and a run_on_resume universal trial parameter for environment-establishing trials. Resolves #3573 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 8e643ea The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
Contributor
📦 Preview build readyBuilt from PR head Changed packages: Quick-start HTML: <script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@9898ada5c0e174f86ea3b7fd4482d75343cd64ed/packages/jspsych/dist/index.browser.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@9898ada5c0e174f86ea3b7fd4482d75343cd64ed/packages/jspsych/css/jspsych.css">
<script src="https://cdn.jsdelivr.net/gh/jspsych/jsPsych@9898ada5c0e174f86ea3b7fd4482d75343cd64ed/packages/plugin-html-keyboard-response/dist/index.browser.min.js"></script>All package URLs
Last updated 2026-08-08 01:14 UTC for PR head |
…operties, and progress integration Promotes the resume feature's state object to a first-class jsPsych.state property (jsPsych.resume now only has clear()). jsPsych seeds Math.random at construction and stores the seed in state.rng_seed, making build-time randomization reproducible across reloads so resumed sessions reconstruct identical timelines. addProperties() values and manually set progress bar positions are persisted via the reserved state keys data_properties and progress, and restored on resume. Targets 9.0 (changeset bumped to major). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Renames the internally written state keys to _rng_seed, _data_properties, and _progress to keep them out of the way of user-defined keys while leaving them accessible. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sion() With state promoted to jsPsych.state, the resume namespace held only clear(). A top-level clearSavedSession() method follows the existing verb-style instance methods and removes the confusing adjacency with jsPsych.resumeExperiment(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Member
Author
|
@jadeddelta and @becky-gilbert I'd love to get your thoughts on the API design here. Don't worry about a technical review. |
Adds a savedAt timestamp to the persisted session and a max_age option
that discards expired partial sessions. Adds return-policy options:
incomplete_session ("resume" | "restart" | "block") and
completed_session ("restart" | "block"), with a configurable
block_message shown when a policy refuses to run the experiment.
Blocking a completed session writes a minimal completion marker with
the log and state dropped; blocking an incomplete session leaves the
stored record untouched so the lockout persists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each successful resume appends {trial_index, time_away, resumed_at} to
jsPsych.state._resumes. time_away captures the wall-clock break
duration, which is otherwise invisible because the experiment clock
runs continuously across the interruption.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resetSession() discards the saved session and restarts the experiment without a page reload, callable from any phase: before run() it only clears, mid-run it aborts the timeline and re-runs it from the top, and on a blocked or finished page it runs the experiment again. The restart resets everything jsPsych controls (data, state, clock, progress bar) while keeping _rng_seed (the built timeline came from its draws) and addProperties values. The in-flight trial's pending persist is suppressed, closing the clear-inside-on_finish footgun. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fixes an inaccurate count description for _resumes, clarifies the build-time vs run-time randomization distinction, makes the policy table cells parallel, restructures the dense resume option reference row into per-field sentences, and renames the seed data property in examples so the underscore reserved-key convention is not implied to apply to trial data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #3573
What this does
Two related additions, targeted at 9.0 (the changeset is a major bump):
localStorage(or a custom storage adapter) after every trial. On reload, the timeline replays the log to rebuild experiment state and continues live from the interruption point.jsPsych.state: a first-class, JSON-serializable state store on the jsPsych instance. It works in-memory for every experiment, and becomes durable across reloads when resume is enabled. jsPsych itself uses it for the RNG seed,addProperties()values, and the manual progress-bar position (reserved keys_rng_seed,_data_properties,_progress,_resumes).Design: record–replay instead of a serialized cursor
The v8 timeline's position lives in the async call stack of
Timeline.run(), so there is no serializable cursor. Instead, the session log records the outcome of every nondeterministic decision — timeline-variable orders fromgenerateTimelineVariableOrder(),conditional_function/loop_functionresults, and trial results captured afteron_finish/extension mutations. On reload, the timeline runs from the top in a replay mode that consumes the log: trials short-circuit (no plugin execution, no callbacks), their logged results flow through the normaldata.write()path so all internal structures rebuild naturally, and execution transitions seamlessly to live when the log is exhausted. This forks at the leaf exactly like simulation mode; the control-flow engine is untouched.Key behaviors:
time_elapsedandtrial_index; the experiment clock continues from the persisted elapsed time.run_on_resume: true(defaultfalse) re-executes a trial live during replay — for environment-establishing trials (fullscreen, initialize-camera, connections). Its fresh result replaces the logged one.jsPsych.stateand theon_resumehook.RNG auto-seeding (behavior change for all experiments)
initJsPsych()now always seedsMath.random(alea via seedrandom) and stores the seed injsPsych.state._rng_seed; a saved session restores its seed. This patches the page-globalMath.randomfor every jsPsych experiment, whether or not resume is used — called out prominently in the changeset and docs.Seeding happens at construction, before user code builds the timeline, deliberately: it makes build-time randomization (shuffling a stimulus list while constructing the timeline) reproducible, so a reloaded page reconstructs an identical timeline and replay stays aligned. Without this, any build-time shuffle would derail resume at the first mismatch. Precedence: a user-set seed (via
setSeed()) > seed restored from a saved session > freshly generated. NewjsPsych.randomization.getSeed()accessor.Core state now persisted via
jsPsych.statejsPsych.data.addProperties()values are stored understate._data_propertiesand restored on resume, so they apply to live trials after the resume point (previously they'd be silently lost).jsPsych.progressBar.progressis stored understate._progressand restored on resume (auto-update mode recomputes from the timeline and is unaffected).Return policies and expiry
The
resumeoption controls what happens when a participant comes back:incomplete_sessiondecides the fate of an unfinished session ("resume" replays it, "restart" discards it, "block" refuses to run — enforcing single-sitting completion), andcompleted_sessiondecides whether finishing once locks the experiment ("block" keeps a minimal completion marker with the log and participant data dropped; "restart" clears storage as before). Blocked runs displayblock_messageand fire no callbacks.max_ageexpires stale partial sessions on the "resume" path only — a blocking record deliberately never expires. The docs state plainly that lockout via browser storage is a deterrent, not a security measure. Every successful resume also appends {trial_index, time_away, resumed_at} to the reserved_resumesstate key, so researchers can see that an interruption happened and how long the wall-clock break was — information the continuous experiment clock otherwise erases.Testing
packages/jspsych/tests/core/resume.test.tsuses shared in-memory storage across two JsPsych instances to simulate reload: round-trip resume, randomization reproduced without re-sampling,conditional_function/loop_functionnot re-invoked,run_on_resume, state restoration andon_resumefiring exactly once,record_data: falsereplay, session cleared on completion/abort, max_age expiry on both sides of the boundary, the full incomplete_session/completed_session policy matrix (blocked runs fire no callbacks and leave blocking records in storage; resetSession() unblocks both record kinds and restarts in place; config-changed markers discarded), key/format-version mismatches, corrupted-session degradation, storage unavailable, feature-off no-op, seed persistence with build-time shuffles, user-seed precedence,_data_propertiesrestoration, and manual progress restoration. packages/jspsych suite: 35 suites / 442 tests passing;tscclean.Review notes / open questions
clearSavedSession() inside on_finish footgun— resolved:resetSession()(which replaced it) suppresses the in-flight trial's persist and restarts the experiment in place, making it safe to call from any callback. It also restarts blocked and completed pages without a reload. Note: an in-place restart reuses the timeline arrays already built at page load (same_rng_seed), so build-time randomization repeats until a real reload — documented.resize/virtual-chinrestscale transform, camera/mic deviceId persistence for smootherrun_on_resumere-runs, a way to detect a resumed session outsideon_resume.🤖 Generated with Claude Code