@lookit/lookit-initjspsych
Version:
This package overloads jsPsych's init function.
291 lines (283 loc) • 9.69 kB
JavaScript
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