UNPKG

@lookit/lookit-initjspsych

Version:

This package overloads jsPsych's init function.

291 lines (283 loc) 9.69 kB
var chsInitJsPsych = (function (jspsychModule, Api, chsTemplates) { 'use strict'; function _interopNamespaceDefault(e) { var n = Object.create(null); if (e) { Object.keys(e).forEach(function (k) { if (k !== 'default') { var d = Object.getOwnPropertyDescriptor(e, k); Object.defineProperty(n, k, d.get ? d : { enumerable: true, get: function () { return e[k]; } }); } }); } n.default = e; return Object.freeze(n); } var jspsychModule__namespace = /*#__PURE__*/_interopNamespaceDefault(jspsychModule); class UndefinedTypeError extends Error { constructor(object) { super( `A trial object in the timeline has an undefined type. Maybe the type name is misspelled, or the plugin you want to use is not supported. Object: ${JSON.stringify(object)}.` ); } } class UndefinedTimelineError extends Error { constructor(el) { super( `An element in the timeline is not structured correctly or is missing required information. It may be a timeline node with a timeline array that is the wrong type or missing/undefined, or a trial object with a missing type. Element: ${JSON.stringify(el)}` ); } } class NoJsPsychInstanceError extends Error { constructor() { super("No jsPsych instance available for on_data_update."); this.name = "NoJsPsychInstanceError"; } } const get_session_recording_data = () => { const sessionRecorder = window.chs.sessionRecorder; return sessionRecorder ? sessionRecorder.getSessionTrialRecordingData() : null; }; const add_session_recording_data = (data, recordingData) => { if (recordingData && !data.chs_recording) { data.chs_recording = recordingData; } }; const retryWithBackoff = async (fn, { retries = 3, baseDelayMs = 1e3 } = {}) => { for (let attempt = 0; ; attempt++) { try { return await fn(); } catch (err) { if (attempt >= retries) { throw err; } await new Promise( (resolve) => setTimeout(resolve, baseDelayMs * 2 ** attempt) ); } } }; const on_data_update = (jsPsychInstance, responseUuid, userFunc) => { return async function(data) { if (!jsPsychInstance || !jsPsychInstance.data) { throw new NoJsPsychInstanceError(); } await Api.updateResponse(responseUuid, { exp_data: jsPsychInstance.data.get().values() }); await Api.finish(); if (typeof userFunc === "function") { userFunc(data); } }; }; const on_finish = (jsPsychInstance, responseUuid, userFunc) => { return async function(data) { if (!jsPsychInstance || !jsPsychInstance.getDisplayElement) { throw new NoJsPsychInstanceError(); } jsPsychInstance.getDisplayElement().innerHTML = chsTemplates.loadingAnimation(); const exp_data = data.values(); const { exit_url } = window.chs.study.attributes; if (typeof userFunc === "function") { userFunc(data); } try { await retryWithBackoff( () => Api.updateResponse(responseUuid, { exp_data, completed: true }) ); await Api.finish(); } catch (err) { console.error( "Error while saving final response data after retries: ", err ); } try { if (window.chs.pendingUploads) { const uploads = window.chs.pendingUploads; const results = await Promise.allSettled(uploads.map((u) => u.promise)); results.forEach((result, i) => { if (result.status === "rejected") { console.error( `Pending upload failed for "${uploads[i].file}": `, result.reason ); } }); let annotatedAny = false; uploads.forEach((upload) => { const trials = exp_data.filter( (trial) => trial.chs_recording !== void 0 && trial.chs_recording.filename === upload.file ); if (trials.length === 0) { console.warn( `No trial data found for uploaded recording "${upload.file}"; its upload status was not recorded in the data.` ); } trials.forEach((trial) => { trial.chs_recording.upload_status = upload.status; if (upload.error_message) { trial.chs_recording.upload_error = upload.error_message; } annotatedAny = true; }); }); if (annotatedAny) { try { await Api.updateResponse(responseUuid, { exp_data, completed: true }); await Api.finish(); } catch (err) { console.error( "Error while saving recording upload statuses: ", err ); } } } } catch (err) { console.error("Error while waiting for pending uploads: ", err); } if (exit_url) { let url; try { url = new URL(exit_url); } catch { try { url = new URL(`https://${exit_url}`); } catch { url = new URL(window.location.origin); } } const hash_child_id = window.chs.response.attributes.hash_child_id; if (hash_child_id) url.searchParams.set("child", hash_child_id); url.searchParams.set("response", window.chs.response.id); window.location.replace(url.toString()); } }; }; const isTimelineNodeArray = (description) => { return (Boolean(description.timeline) || Array.isArray(description)) && !description.type; }; const isTrialWithType = (description) => { return typeof description === "object" && !isTimelineNodeArray( description ); }; const lookitInitJsPsych = (responseUuid) => { return function(opts) { const { on_data_update: userOnDataUpdate, on_finish: userOnFinish, on_trial_start: userOnTrialStart, on_trial_finish: userOnTrialFinish, ...otherOpts } = opts || {}; let jsPsychInstance = null; let sessionTrialRecordingData = null; const onTrialStart = (trial) => { sessionTrialRecordingData = get_session_recording_data(); userOnTrialStart?.(trial); }; const onTrialFinish = (data) => { add_session_recording_data(data, sessionTrialRecordingData); sessionTrialRecordingData = null; userOnTrialFinish?.(data); }; const onDataUpdate = (...args) => { return on_data_update( jsPsychInstance, responseUuid, userOnDataUpdate )(...args); }; const onFinish = (...args) => { return on_finish(jsPsychInstance, responseUuid, userOnFinish)(...args); }; const jsPsych = jspsychModule__namespace.initJsPsych({ ...otherOpts, on_data_update: onDataUpdate, on_finish: onFinish, on_trial_start: onTrialStart, on_trial_finish: onTrialFinish }); jsPsychInstance = jsPsych; const origJsPsychRun = jsPsych.run; const lookitJsPsych = jsPsych; lookitJsPsych.run = async function(timeline) { const handleTrialTypes = (timeline2, callback) => { return timeline2.map( (el) => { if (isTimelineNodeArray( el )) { if (Array.isArray(el)) { return handleTrialTypes(el, callback); } else if ("timeline" in el && Array.isArray(el.timeline)) { const chsTimelineDescription = { ...el, timeline: handleTrialTypes( el.timeline, callback ) }; return chsTimelineDescription; } else { throw new UndefinedTimelineError(el); } } else if (isTrialWithType( el )) { if (el !== null && "type" in el && el.type !== null && el.type !== void 0) { const chsTrialDescription = el; callback(chsTrialDescription); return chsTrialDescription; } else { throw new UndefinedTypeError(el); } } else { throw new UndefinedTimelineError(el); } } ); }; const modifiedTimeline = handleTrialTypes( timeline, (trial) => { if ("type" in trial) { if (trial.type?.chsData) { trial.data = { ...trial.data, ...trial.type.chsData() }; } if (trial.type?.info?.name === "assent-video") { const originalOnFinish = trial.on_finish; trial.on_finish = (data) => { originalOnFinish?.(data); if (data["response"] === false) { jsPsych.abortExperiment(); } }; } } } ); return await origJsPsychRun(modifiedTimeline); }; return lookitJsPsych; }; }; return lookitInitJsPsych; })(jsPsychModule, chsData, chsTemplates); //# sourceMappingURL=https://unpkg.com/@lookit/lookit-initjspsych@4.0.0/dist/index.browser.js.map