jspsych 7.3.4 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +1 -1
  2. package/css/jspsych.css +18 -8
  3. package/dist/index.browser.js +3080 -4286
  4. package/dist/index.browser.js.map +1 -1
  5. package/dist/index.browser.min.js +6 -2
  6. package/dist/index.browser.min.js.map +1 -1
  7. package/dist/index.cjs +2314 -4066
  8. package/dist/index.cjs.map +1 -1
  9. package/dist/index.d.ts +984 -6
  10. package/dist/index.js +2313 -4066
  11. package/dist/index.js.map +1 -1
  12. package/package.json +6 -5
  13. package/src/ExtensionManager.spec.ts +123 -0
  14. package/src/ExtensionManager.ts +81 -0
  15. package/src/JsPsych.ts +195 -690
  16. package/src/ProgressBar.spec.ts +60 -0
  17. package/src/ProgressBar.ts +60 -0
  18. package/src/index.scss +29 -8
  19. package/src/index.ts +4 -9
  20. package/src/modules/data/DataCollection.ts +1 -1
  21. package/src/modules/data/DataColumn.ts +12 -1
  22. package/src/modules/data/index.ts +92 -103
  23. package/src/modules/extensions.ts +4 -0
  24. package/src/modules/plugin-api/AudioPlayer.ts +101 -0
  25. package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
  26. package/src/modules/plugin-api/MediaAPI.ts +48 -106
  27. package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
  28. package/src/modules/plugin-api/index.ts +11 -14
  29. package/src/modules/plugins.ts +26 -27
  30. package/src/modules/randomization.ts +1 -1
  31. package/src/timeline/Timeline.spec.ts +921 -0
  32. package/src/timeline/Timeline.ts +342 -0
  33. package/src/timeline/TimelineNode.ts +174 -0
  34. package/src/timeline/Trial.spec.ts +897 -0
  35. package/src/timeline/Trial.ts +419 -0
  36. package/src/timeline/index.ts +232 -0
  37. package/src/timeline/util.spec.ts +124 -0
  38. package/src/timeline/util.ts +146 -0
  39. package/dist/JsPsych.d.ts +0 -112
  40. package/dist/TimelineNode.d.ts +0 -34
  41. package/dist/migration.d.ts +0 -3
  42. package/dist/modules/data/DataCollection.d.ts +0 -46
  43. package/dist/modules/data/DataColumn.d.ts +0 -15
  44. package/dist/modules/data/index.d.ts +0 -25
  45. package/dist/modules/data/utils.d.ts +0 -3
  46. package/dist/modules/extensions.d.ts +0 -22
  47. package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
  48. package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
  49. package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
  50. package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
  51. package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
  52. package/dist/modules/plugin-api/index.d.ts +0 -8
  53. package/dist/modules/plugins.d.ts +0 -136
  54. package/dist/modules/randomization.d.ts +0 -42
  55. package/dist/modules/turk.d.ts +0 -40
  56. package/dist/modules/utils.d.ts +0 -13
  57. package/src/TimelineNode.ts +0 -544
  58. package/src/modules/plugin-api/HardwareAPI.ts +0 -32
@@ -0,0 +1,419 @@
1
+ import { Class } from "type-fest";
2
+
3
+ import { ParameterInfos } from "../modules/plugins";
4
+ import { JsPsychPlugin, ParameterType, PluginInfo } from "../modules/plugins";
5
+ import { deepCopy, deepMerge } from "../modules/utils";
6
+ import { Timeline } from "./Timeline";
7
+ import { GetParameterValueOptions, TimelineNode } from "./TimelineNode";
8
+ import { delay, isPromise, parameterPathArrayToString } from "./util";
9
+ import {
10
+ SimulationOptions,
11
+ TimelineNodeDependencies,
12
+ TimelineNodeStatus,
13
+ TimelineVariable,
14
+ TrialDescription,
15
+ TrialResult,
16
+ timelineDescriptionKeys,
17
+ } from ".";
18
+
19
+ export class Trial extends TimelineNode {
20
+ public readonly pluginClass: Class<JsPsychPlugin<any>>;
21
+ public pluginInstance: JsPsychPlugin<any>;
22
+ public trialObject?: TrialDescription;
23
+ public index?: number;
24
+
25
+ private result: TrialResult;
26
+ private readonly pluginInfo: PluginInfo;
27
+
28
+ constructor(
29
+ dependencies: TimelineNodeDependencies,
30
+ public readonly description: TrialDescription,
31
+ public readonly parent: Timeline
32
+ ) {
33
+ super(dependencies);
34
+ this.initializeParameterValueCache();
35
+
36
+ this.trialObject = deepCopy(description);
37
+ this.pluginClass = this.getParameterValue("type", { evaluateFunctions: false });
38
+ this.pluginInfo = this.pluginClass["info"];
39
+
40
+ if (!("version" in this.pluginInfo) && !("data" in this.pluginInfo)) {
41
+ console.warn(
42
+ this.pluginInfo["name"],
43
+ "is missing the 'version' and 'data' fields. Please update plugin as 'version' and 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
44
+ );
45
+ } else if (!("version" in this.pluginInfo)) {
46
+ console.warn(
47
+ this.pluginInfo["name"],
48
+ "is missing the 'version' field. Please update plugin as 'version' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
49
+ );
50
+ } else if (!("data" in this.pluginInfo)) {
51
+ console.warn(
52
+ this.pluginInfo["name"],
53
+ "is missing the 'data' field. Please update plugin as 'data' will be required in v9. See https://www.jspsych.org/latest/developers/plugin-development/ for more details."
54
+ );
55
+ }
56
+ }
57
+
58
+ public async run() {
59
+ this.status = TimelineNodeStatus.RUNNING;
60
+ this.processParameters();
61
+
62
+ this.onStart();
63
+ this.addCssClasses();
64
+
65
+ this.pluginInstance = this.dependencies.instantiatePlugin(this.pluginClass);
66
+
67
+ this.result = this.processResult(await this.executeTrial());
68
+
69
+ this.dependencies.onTrialResultAvailable(this);
70
+
71
+ this.status = TimelineNodeStatus.COMPLETED;
72
+
73
+ await this.onFinish();
74
+ this.removeCssClasses();
75
+
76
+ const gap = this.getParameterValue("post_trial_gap") ?? this.dependencies.getDefaultIti();
77
+ if (gap !== 0 && this.dependencies.getSimulationMode() !== "data-only") {
78
+ await delay(gap);
79
+ }
80
+
81
+ this.resetParameterValueCache();
82
+ }
83
+
84
+ private async executeTrial() {
85
+ const trialPromise = this.dependencies.finishTrialPromise.get();
86
+
87
+ /** Used as a way to figure out if `finishTrial()` has ben called without awaiting `trialPromise` */
88
+ let hasTrialPromiseBeenResolved = false;
89
+ trialPromise.then(() => {
90
+ hasTrialPromiseBeenResolved = true;
91
+ });
92
+
93
+ const { trialReturnValue, hasTrialBeenSimulated } = this.invokeTrialMethod();
94
+
95
+ // Wait until the trial has completed and grab result data
96
+ let result: TrialResult | void;
97
+ if (isPromise(trialReturnValue)) {
98
+ result = await Promise.race([trialReturnValue, trialPromise]);
99
+
100
+ // If `finishTrial()` was called, use the result provided to it. This may happen although
101
+ // `trialReturnValue` won the race ("run-to-completion").
102
+ if (hasTrialPromiseBeenResolved) {
103
+ result = await trialPromise;
104
+ }
105
+ } else {
106
+ // The `simulate` method always invokes `onLoad()`, so we don't call `onLoad()` when the trial
107
+ // has been simulated
108
+ if (!hasTrialBeenSimulated) {
109
+ this.onLoad();
110
+ }
111
+
112
+ result = await trialPromise;
113
+ }
114
+
115
+ // The trial has finished, time to clean up.
116
+ this.cleanupTrial();
117
+
118
+ return result;
119
+ }
120
+
121
+ private invokeTrialMethod(): {
122
+ trialReturnValue: void | Promise<void | TrialResult>;
123
+ hasTrialBeenSimulated: boolean;
124
+ } {
125
+ const globalSimulationMode = this.dependencies.getSimulationMode();
126
+
127
+ if (globalSimulationMode && typeof this.pluginInstance.simulate === "function") {
128
+ const simulationOptions = this.getSimulationOptions();
129
+
130
+ if (simulationOptions.simulate !== false) {
131
+ return {
132
+ hasTrialBeenSimulated: true,
133
+ trialReturnValue: this.pluginInstance.simulate(
134
+ this.trialObject,
135
+ simulationOptions.mode ?? globalSimulationMode,
136
+ simulationOptions,
137
+ this.onLoad
138
+ ),
139
+ };
140
+ }
141
+ }
142
+
143
+ return {
144
+ hasTrialBeenSimulated: false,
145
+ trialReturnValue: this.pluginInstance.trial(
146
+ this.dependencies.getDisplayElement(),
147
+ this.trialObject,
148
+ this.onLoad
149
+ ),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Cleanup the trial by removing the display element and removing event listeners
155
+ */
156
+ private cleanupTrial() {
157
+ this.dependencies.clearAllTimeouts();
158
+ this.dependencies.getDisplayElement().innerHTML = "";
159
+ }
160
+
161
+ /**
162
+ * Add the CSS classes from the `css_classes` parameter to the display element
163
+ */
164
+ private addCssClasses() {
165
+ const classes: string | string[] = this.getParameterValue("css_classes");
166
+ const classList = this.dependencies.getDisplayElement().classList;
167
+ if (typeof classes === "string") {
168
+ classList.add(classes);
169
+ } else if (Array.isArray(classes)) {
170
+ classList.add(...classes);
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Removes the provided css classes from the display element
176
+ */
177
+ private removeCssClasses() {
178
+ const classes = this.getParameterValue("css_classes");
179
+ if (classes) {
180
+ this.dependencies
181
+ .getDisplayElement()
182
+ .classList.remove(...(typeof classes === "string" ? [classes] : classes));
183
+ }
184
+ }
185
+
186
+ private processResult(result: TrialResult | void) {
187
+ if (!result) {
188
+ result = {};
189
+ }
190
+
191
+ for (const [parameterName, shouldParameterBeIncluded] of Object.entries(
192
+ this.getParameterValue("save_trial_parameters") ?? {}
193
+ )) {
194
+ if (this.pluginInfo.parameters[parameterName]) {
195
+ if (shouldParameterBeIncluded && !Object.hasOwn(result, parameterName)) {
196
+ let parameterValue = this.trialObject[parameterName];
197
+ if (typeof parameterValue === "function") {
198
+ parameterValue = parameterValue.toString();
199
+ }
200
+ result[parameterName] = parameterValue;
201
+ } else if (!shouldParameterBeIncluded && Object.hasOwn(result, parameterName)) {
202
+ delete result[parameterName];
203
+ }
204
+ } else {
205
+ console.warn(
206
+ `Non-existent parameter "${parameterName}" specified in save_trial_parameters.`
207
+ );
208
+ }
209
+ }
210
+
211
+ result = {
212
+ ...this.getDataParameter(),
213
+ ...result,
214
+ trial_type: this.pluginInfo.name,
215
+ trial_index: this.index,
216
+ plugin_version: this.pluginInfo["version"] ? this.pluginInfo["version"] : null,
217
+ };
218
+
219
+ // Add timeline variables to the result according to the `save_timeline_variables` parameter
220
+ const saveTimelineVariables = this.getParameterValue("save_timeline_variables");
221
+ if (saveTimelineVariables === true) {
222
+ result.timeline_variables = { ...this.parent.getAllTimelineVariables() };
223
+ } else if (Array.isArray(saveTimelineVariables)) {
224
+ result.timeline_variables = Object.fromEntries(
225
+ Object.entries(this.parent.getAllTimelineVariables()).filter(([key, _]) =>
226
+ saveTimelineVariables.includes(key)
227
+ )
228
+ );
229
+ }
230
+
231
+ return result;
232
+ }
233
+
234
+ /**
235
+ * Runs a callback function retrieved from a parameter value and returns its result.
236
+ *
237
+ * @param parameterName The name of the parameter to retrieve the callback function from.
238
+ * @param callbackParameters The parameters (if any) to be passed to the callback function
239
+ */
240
+ private runParameterCallback(parameterName: string, ...callbackParameters: unknown[]) {
241
+ const callback = this.getParameterValue(parameterName, { evaluateFunctions: false });
242
+ if (callback) {
243
+ return callback(...callbackParameters);
244
+ }
245
+ }
246
+
247
+ private onStart() {
248
+ this.dependencies.onTrialStart(this);
249
+ this.runParameterCallback("on_start", this.trialObject);
250
+ this.dependencies.runOnStartExtensionCallbacks(this.getParameterValue("extensions"));
251
+ }
252
+
253
+ private onLoad = () => {
254
+ this.runParameterCallback("on_load");
255
+ this.dependencies.runOnLoadExtensionCallbacks(this.getParameterValue("extensions"));
256
+ };
257
+
258
+ private async onFinish() {
259
+ const extensionResults = await this.dependencies.runOnFinishExtensionCallbacks(
260
+ this.getParameterValue("extensions")
261
+ );
262
+ Object.assign(this.result, extensionResults);
263
+
264
+ await Promise.resolve(this.runParameterCallback("on_finish", this.getResult()));
265
+
266
+ this.dependencies.onTrialFinished(this);
267
+ }
268
+
269
+ public evaluateTimelineVariable(variable: TimelineVariable) {
270
+ // Timeline variable values are specified at the timeline level, not at the trial level, hence
271
+ // deferring to the parent timeline here
272
+ return this.parent?.evaluateTimelineVariable(variable);
273
+ }
274
+
275
+ public getParameterValue(
276
+ parameterPath: string | string[],
277
+ options: GetParameterValueOptions = {}
278
+ ) {
279
+ // Disable recursion for timeline description keys
280
+ if (
281
+ timelineDescriptionKeys.includes(
282
+ typeof parameterPath === "string" ? parameterPath : parameterPath[0]
283
+ )
284
+ ) {
285
+ options.recursive = false;
286
+ }
287
+ return super.getParameterValue(parameterPath, options);
288
+ }
289
+
290
+ /**
291
+ * Retrieves and evaluates the `simulation_options` parameter, considering nested properties and
292
+ * global simulation options.
293
+ */
294
+ public getSimulationOptions() {
295
+ const simulationOptions: SimulationOptions = this.getParameterValue("simulation_options", {
296
+ replaceResult: (result = {}) => {
297
+ if (typeof result === "string") {
298
+ // Look up the global simulation options by their key
299
+ const globalSimulationOptions = this.dependencies.getGlobalSimulationOptions();
300
+ result = globalSimulationOptions[result] ?? globalSimulationOptions["default"] ?? {};
301
+ }
302
+
303
+ return deepMerge(
304
+ deepCopy(this.dependencies.getGlobalSimulationOptions().default),
305
+ deepCopy(result)
306
+ );
307
+ },
308
+ });
309
+
310
+ if (typeof simulationOptions === "undefined") {
311
+ return {};
312
+ }
313
+
314
+ simulationOptions.mode = this.getParameterValue(["simulation_options", "mode"]);
315
+ simulationOptions.simulate = this.getParameterValue(["simulation_options", "simulate"]);
316
+ simulationOptions.data = this.getParameterValue(["simulation_options", "data"]);
317
+
318
+ if (typeof simulationOptions.data === "object") {
319
+ simulationOptions.data = Object.fromEntries(
320
+ Object.keys(simulationOptions.data).map((key) => [
321
+ key,
322
+ this.getParameterValue(["simulation_options", "data", key]),
323
+ ])
324
+ );
325
+ }
326
+
327
+ return simulationOptions;
328
+ }
329
+
330
+ /**
331
+ * Returns the result object of this trial or `undefined` if the result is not yet known or the
332
+ * `record_data` trial parameter is `false`.
333
+ */
334
+ public getResult() {
335
+ return this.getParameterValue("record_data") === false ? undefined : this.result;
336
+ }
337
+
338
+ public getResults() {
339
+ const result = this.getResult();
340
+ return result ? [result] : [];
341
+ }
342
+
343
+ /**
344
+ * Checks that the parameters provided in the trial description align with the plugin's info
345
+ * object, resolves missing parameter values from the parent timeline, resolves timeline variable
346
+ * parameters, evaluates parameter functions if the expected parameter type is not `FUNCTION`, and
347
+ * sets default values for optional parameters.
348
+ */
349
+ private processParameters() {
350
+ const assignParameterValues = (
351
+ parameterObject: Record<string, any>,
352
+ parameterInfos: ParameterInfos,
353
+ parentParameterPath: string[] = []
354
+ ) => {
355
+ for (const [parameterName, parameterConfig] of Object.entries(parameterInfos)) {
356
+ const parameterPath = [...parentParameterPath, parameterName];
357
+
358
+ let parameterValue = this.getParameterValue(parameterPath, {
359
+ evaluateFunctions: parameterConfig.type !== ParameterType.FUNCTION,
360
+ replaceResult: (originalResult) => {
361
+ if (typeof originalResult === "undefined") {
362
+ if (typeof parameterConfig.default === "undefined") {
363
+ throw new Error(
364
+ `You must specify a value for the "${parameterPathArrayToString(
365
+ parameterPath
366
+ )}" parameter in the "${this.pluginInfo.name}" plugin.`
367
+ );
368
+ } else {
369
+ return parameterConfig.default;
370
+ }
371
+ } else {
372
+ return originalResult;
373
+ }
374
+ },
375
+ });
376
+
377
+ if (parameterConfig.array && !Array.isArray(parameterValue)) {
378
+ const parameterPathString = parameterPathArrayToString(parameterPath);
379
+ throw new Error(
380
+ `A non-array value (\`${parameterValue}\`) was provided for the array parameter "${parameterPathString}" in the "${this.pluginInfo.name}" plugin. Please make sure that "${parameterPathString}" is an array.`
381
+ );
382
+ }
383
+
384
+ if (parameterConfig.type === ParameterType.COMPLEX && parameterConfig.nested) {
385
+ // Assign parameter values according to the `nested` schema
386
+ if (parameterConfig.array) {
387
+ // ...for each nested array element
388
+ parameterValue = parameterValue.map((_, arrayIndex) => {
389
+ const arrayElementPath = [...parameterPath, arrayIndex.toString()];
390
+ const arrayElementValue = this.getParameterValue(arrayElementPath);
391
+ assignParameterValues(arrayElementValue, parameterConfig.nested, arrayElementPath);
392
+ return arrayElementValue;
393
+ });
394
+ } else {
395
+ // ...for the nested object
396
+ assignParameterValues(parameterValue, parameterConfig.nested, parameterPath);
397
+ }
398
+ }
399
+
400
+ parameterObject[parameterName] = parameterValue;
401
+ }
402
+ };
403
+
404
+ const trialObject = deepCopy(this.description);
405
+ assignParameterValues(trialObject, this.pluginInfo.parameters);
406
+ this.trialObject = trialObject;
407
+ }
408
+
409
+ public getLatestNode() {
410
+ return this;
411
+ }
412
+
413
+ public getActiveTimelineByName(name: string): Timeline | undefined {
414
+ // This returns undefined because the function is looking
415
+ // for a timeline. If we get to this point, then none
416
+ // of the parent nodes match the name.
417
+ return undefined;
418
+ }
419
+ }
@@ -0,0 +1,232 @@
1
+ import { Class } from "type-fest";
2
+
3
+ import { JsPsychExtension } from "../modules/extensions";
4
+ import { JsPsychPlugin, PluginInfo } from "../modules/plugins";
5
+ import { Trial } from "./Trial";
6
+ import { PromiseWrapper } from "./util";
7
+
8
+ export class TimelineVariable {
9
+ constructor(public readonly name: string) {}
10
+ }
11
+
12
+ export type Parameter<T> = T | (() => T) | TimelineVariable;
13
+
14
+ export type TrialExtensionsConfiguration = Array<{
15
+ type: Class<JsPsychExtension>;
16
+ params?: Record<string, any>;
17
+ }>;
18
+
19
+ export type SimulationMode = "visual" | "data-only";
20
+
21
+ export type SimulationOptions = {
22
+ data?: Record<string, any>;
23
+ mode?: SimulationMode;
24
+ simulate?: boolean;
25
+ };
26
+
27
+ export type SimulationOptionsParameter = Parameter<{
28
+ data?: Parameter<Record<string, Parameter<any>>>;
29
+ mode?: Parameter<SimulationMode>;
30
+ simulate?: Parameter<boolean>;
31
+ }>;
32
+
33
+ export interface TrialDescription extends Record<string, any> {
34
+ type: Parameter<Class<JsPsychPlugin<any>>>;
35
+
36
+ /** https://www.jspsych.org/latest/overview/plugins/#the-post_trial_gap-iti-parameter */
37
+ post_trial_gap?: Parameter<number>;
38
+
39
+ /** https://www.jspsych.org/latest/overview/plugins/#the-save_trial_parameters-parameter */
40
+ save_trial_parameters?: Parameter<Record<string, boolean>>;
41
+
42
+ /**
43
+ * Whether to include the values of timeline variables under a `timeline_variables` key. Can be
44
+ * `true` to save the values of all timeline variables, or an array of timeline variable names to
45
+ * only save specific timeline variables. Defaults to `false`.
46
+ */
47
+ save_timeline_variables?: Parameter<boolean | string[]>;
48
+
49
+ /** https://www.jspsych.org/latest/overview/style/#using-the-css_classes-trial-parameter */
50
+ css_classes?: Parameter<string | string[]>;
51
+
52
+ /** https://www.jspsych.org/latest/overview/simulation/#controlling-simulation-mode-with-simulation_options */
53
+ simulation_options?: SimulationOptionsParameter | string;
54
+
55
+ /** https://www.jspsych.org/latest/overview/extensions/ */
56
+ extensions?: Parameter<TrialExtensionsConfiguration>;
57
+
58
+ /**
59
+ * Whether to record the data of this trial. Defaults to `true`.
60
+ */
61
+ record_data?: Parameter<boolean>;
62
+
63
+ // Events
64
+
65
+ /** https://www.jspsych.org/latest/overview/events/#on_start-trial */
66
+ on_start?: (trial: any) => void;
67
+
68
+ /** https://www.jspsych.org/latest/overview/events/#on_load */
69
+ on_load?: () => void;
70
+
71
+ /** https://www.jspsych.org/latest/overview/events/#on_finish-trial */
72
+ on_finish?: (data: any) => void;
73
+ }
74
+
75
+ /** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
76
+ export type SampleOptions =
77
+ | { type: "with-replacement"; size: number; weights?: number[] }
78
+ | { type: "without-replacement"; size: number }
79
+ | { type: "fixed-repetitions"; size: number }
80
+ | { type: "alternate-groups"; groups: number[][]; randomize_group_order?: boolean }
81
+ | { type: "custom"; fn: (ids: number[]) => number[] };
82
+
83
+ export type TimelineArray = Array<TimelineDescription | TrialDescription | TimelineArray>;
84
+
85
+ export interface TimelineDescription extends Record<string, any> {
86
+ timeline: TimelineArray;
87
+ timeline_variables?: Record<string, any>[];
88
+
89
+ name?: string;
90
+
91
+ // Control flow
92
+
93
+ /** https://www.jspsych.org/latest/overview/timeline/#repeating-a-set-of-trials */
94
+ repetitions?: number;
95
+
96
+ /** https://www.jspsych.org/latest/overview/timeline/#looping-timelines */
97
+ loop_function?: (data: any) => boolean;
98
+
99
+ /** https://www.jspsych.org/latest/overview/timeline/#conditional-timelines */
100
+ conditional_function?: () => boolean;
101
+
102
+ // Randomization
103
+
104
+ /** https://www.jspsych.org/latest/overview/timeline/#random-orders-of-trials */
105
+ randomize_order?: boolean;
106
+
107
+ /** https://www.jspsych.org/latest/overview/timeline/#sampling-methods */
108
+ sample?: SampleOptions;
109
+
110
+ // Events
111
+
112
+ /** https://www.jspsych.org/latest/overview/events/#on_timeline_start */
113
+ on_timeline_start?: () => void;
114
+
115
+ /** https://www.jspsych.org/latest/overview/events/#on_timeline_finish */
116
+ on_timeline_finish?: () => void;
117
+ }
118
+
119
+ export const timelineDescriptionKeys = [
120
+ "timeline",
121
+ "timeline_variables",
122
+ "name",
123
+ "repetitions",
124
+ "loop_function",
125
+ "conditional_function",
126
+ "randomize_order",
127
+ "sample",
128
+ "on_timeline_start",
129
+ "on_timeline_finish",
130
+ ];
131
+
132
+ export function isTrialDescription(
133
+ description: TrialDescription | TimelineDescription
134
+ ): description is TrialDescription {
135
+ return !isTimelineDescription(description);
136
+ }
137
+
138
+ export function isTimelineDescription(
139
+ description: TrialDescription | TimelineDescription | TimelineArray
140
+ ): description is TimelineDescription | TimelineArray {
141
+ return Boolean((description as TimelineDescription).timeline) || Array.isArray(description);
142
+ }
143
+
144
+ export enum TimelineNodeStatus {
145
+ PENDING,
146
+ RUNNING,
147
+ PAUSED,
148
+ COMPLETED,
149
+ ABORTED,
150
+ }
151
+
152
+ /**
153
+ * Functions and options needed by `TimelineNode`s, provided by the `JsPsych` instance. This
154
+ * approach allows to keep the public `JsPsych` API slim and decouples the `JsPsych` and timeline
155
+ * node classes, simplifying unit testing.
156
+ */
157
+ export interface TimelineNodeDependencies {
158
+ /**
159
+ * Called at the start of a trial, prior to invoking the plugin's trial method.
160
+ */
161
+ onTrialStart: (trial: Trial) => void;
162
+
163
+ /**
164
+ * Called when a trial's result data is available, before invoking `onTrialFinished()`.
165
+ */
166
+ onTrialResultAvailable: (trial: Trial) => void;
167
+
168
+ /**
169
+ * Called after a trial has finished.
170
+ */
171
+ onTrialFinished: (trial: Trial) => void;
172
+
173
+ /**
174
+ * Invoke `on_start` extension callbacks according to `extensionsConfiguration`
175
+ */
176
+ runOnStartExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
177
+
178
+ /**
179
+ * Invoke `on_load` extension callbacks according to `extensionsConfiguration`
180
+ */
181
+ runOnLoadExtensionCallbacks(extensionsConfiguration: TrialExtensionsConfiguration): void;
182
+
183
+ /**
184
+ * Invoke `on_finish` extension callbacks according to `extensionsConfiguration` and return a
185
+ * joint extensions result object
186
+ */
187
+ runOnFinishExtensionCallbacks(
188
+ extensionsConfiguration: TrialExtensionsConfiguration
189
+ ): Promise<Record<string, any>>;
190
+
191
+ /**
192
+ * Returns the simulation mode or `undefined`, if the experiment is not running in simulation
193
+ * mode.
194
+ */
195
+ getSimulationMode(): SimulationMode | undefined;
196
+
197
+ /**
198
+ * Returns the global simulation options as passed to `jsPsych.simulate()`
199
+ */
200
+ getGlobalSimulationOptions(): Record<string, SimulationOptionsParameter>;
201
+
202
+ /**
203
+ * Given a plugin class, create a new instance of it and return it.
204
+ */
205
+ instantiatePlugin: <Info extends PluginInfo>(
206
+ pluginClass: Class<JsPsychPlugin<Info>>
207
+ ) => JsPsychPlugin<Info>;
208
+
209
+ /**
210
+ * Return JsPsych's display element so it can be provided to plugins
211
+ */
212
+ getDisplayElement: () => HTMLElement;
213
+
214
+ /**
215
+ * Return the default inter-trial interval as provided to `initJsPsych()`
216
+ */
217
+ getDefaultIti: () => number;
218
+
219
+ /**
220
+ * A `PromiseWrapper` whose promise is resolved with result data whenever `jsPsych.finishTrial()`
221
+ * is called.
222
+ */
223
+ finishTrialPromise: PromiseWrapper<TrialResult | void>;
224
+
225
+ /**
226
+ * Clear all of the timeouts
227
+ */
228
+ clearAllTimeouts: () => void;
229
+ }
230
+
231
+ export type TrialResult = Record<string, any>;
232
+ export type TrialResults = Array<Record<string, any>>;