jspsych 7.2.3 → 7.3.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.
@@ -18,5 +18,5 @@ export interface JsPsychExtension {
18
18
  * Called at the end of the trial.
19
19
  * @returns Data to append to the trial's data object.
20
20
  */
21
- on_finish(params?: Record<string, any>): Record<string, any>;
21
+ on_finish(params?: Record<string, any>): Record<string, any> | Promise<Record<string, any>>;
22
22
  }
@@ -3,7 +3,7 @@ export declare class MediaAPI {
3
3
  private webaudioContext?;
4
4
  constructor(useWebaudio: boolean, webaudioContext?: AudioContext);
5
5
  private video_buffers;
6
- getVideoBuffer(videoID: any): any;
6
+ getVideoBuffer(videoID: string): any;
7
7
  private context;
8
8
  private audio_buffers;
9
9
  initAudio(): void;
@@ -24,4 +24,9 @@ export declare class MediaAPI {
24
24
  private microphone_recorder;
25
25
  initializeMicrophoneRecorder(stream: MediaStream): void;
26
26
  getMicrophoneRecorder(): MediaRecorder;
27
+ private camera_stream;
28
+ private camera_recorder;
29
+ initializeCameraRecorder(stream: MediaStream, opts?: MediaRecorderOptions): void;
30
+ getCameraStream(): MediaStream;
31
+ getCameraRecorder(): MediaRecorder;
27
32
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jspsych",
3
- "version": "7.2.3",
3
+ "version": "7.3.0",
4
4
  "description": "Behavioral experiments in a browser",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
package/src/JsPsych.ts CHANGED
@@ -272,53 +272,71 @@ export class JsPsych {
272
272
  }
273
273
  }
274
274
  }
275
+
275
276
  // handle extension callbacks
276
- if (Array.isArray(current_trial.extensions)) {
277
- for (const extension of current_trial.extensions) {
278
- const ext_data_values = this.extensions[extension.type.info.name].on_finish(
279
- extension.params
280
- );
281
- Object.assign(trial_data_values, ext_data_values);
282
- }
283
- }
284
277
 
285
- // about to execute lots of callbacks, so switch context.
286
- this.internal.call_immediate = true;
278
+ const extensionCallbackResults = ((current_trial.extensions ?? []) as any[]).map((extension) =>
279
+ this.extensions[extension.type.info.name].on_finish(extension.params)
280
+ );
287
281
 
288
- // handle callback at plugin level
289
- if (typeof current_trial.on_finish === "function") {
290
- current_trial.on_finish(trial_data_values);
291
- }
282
+ const onExtensionCallbacksFinished = () => {
283
+ // about to execute lots of callbacks, so switch context.
284
+ this.internal.call_immediate = true;
292
285
 
293
- // handle callback at whole-experiment level
294
- this.opts.on_trial_finish(trial_data_values);
286
+ // handle callback at plugin level
287
+ if (typeof current_trial.on_finish === "function") {
288
+ current_trial.on_finish(trial_data_values);
289
+ }
295
290
 
296
- // after the above callbacks are complete, then the data should be finalized
297
- // for this trial. call the on_data_update handler, passing in the same
298
- // data object that just went through the trial's finish handlers.
299
- this.opts.on_data_update(trial_data_values);
291
+ // handle callback at whole-experiment level
292
+ this.opts.on_trial_finish(trial_data_values);
300
293
 
301
- // done with callbacks
302
- this.internal.call_immediate = false;
294
+ // after the above callbacks are complete, then the data should be finalized
295
+ // for this trial. call the on_data_update handler, passing in the same
296
+ // data object that just went through the trial's finish handlers.
297
+ this.opts.on_data_update(trial_data_values);
303
298
 
304
- // wait for iti
305
- if (this.simulation_mode === "data-only") {
306
- this.nextTrial();
307
- } else if (
308
- typeof current_trial.post_trial_gap === null ||
309
- typeof current_trial.post_trial_gap === "undefined"
310
- ) {
311
- if (this.opts.default_iti > 0) {
312
- setTimeout(this.nextTrial, this.opts.default_iti);
313
- } else {
299
+ // done with callbacks
300
+ this.internal.call_immediate = false;
301
+
302
+ // wait for iti
303
+ if (this.simulation_mode === "data-only") {
314
304
  this.nextTrial();
305
+ } else if (
306
+ typeof current_trial.post_trial_gap === null ||
307
+ typeof current_trial.post_trial_gap === "undefined"
308
+ ) {
309
+ if (this.opts.default_iti > 0) {
310
+ setTimeout(this.nextTrial, this.opts.default_iti);
311
+ } else {
312
+ this.nextTrial();
313
+ }
314
+ } else {
315
+ if (current_trial.post_trial_gap > 0) {
316
+ setTimeout(this.nextTrial, current_trial.post_trial_gap);
317
+ } else {
318
+ this.nextTrial();
319
+ }
315
320
  }
321
+ };
322
+
323
+ // Strictly using Promise.resolve to turn all values into promises would be cleaner here, but it
324
+ // would require user test code to make the event loop tick after every simulated key press even
325
+ // if there are no async `on_finish` methods. Hence, in order to avoid a breaking change, we
326
+ // only rely on the event loop if at least one `on_finish` method returns a promise.
327
+ if (extensionCallbackResults.some((result) => typeof result.then === "function")) {
328
+ Promise.all(
329
+ extensionCallbackResults.map((result) =>
330
+ Promise.resolve(result).then((ext_data_values) => {
331
+ Object.assign(trial_data_values, ext_data_values);
332
+ })
333
+ )
334
+ ).then(onExtensionCallbacksFinished);
316
335
  } else {
317
- if (current_trial.post_trial_gap > 0) {
318
- setTimeout(this.nextTrial, current_trial.post_trial_gap);
319
- } else {
320
- this.nextTrial();
336
+ for (const values of extensionCallbackResults) {
337
+ Object.assign(trial_data_values, values);
321
338
  }
339
+ onExtensionCallbacksFinished();
322
340
  }
323
341
  }
324
342
 
@@ -671,22 +689,20 @@ export class JsPsych {
671
689
 
672
690
  private evaluateTimelineVariables(trial) {
673
691
  for (const key of Object.keys(trial)) {
674
- if (key === "type") {
675
- // skip the `type` parameter as it contains a plugin
676
- //continue;
677
- }
678
- // timeline variables on the root level
679
692
  if (
680
693
  typeof trial[key] === "object" &&
681
694
  trial[key] !== null &&
682
695
  typeof trial[key].timelineVariablePlaceholder !== "undefined"
683
696
  ) {
684
- /*trial[key].toString().replace(/\s/g, "") ==
685
- "function(){returntimeline.timelineVariable(varname);}"
686
- )*/ trial[key] = trial[key].timelineVariableFunction();
697
+ trial[key] = trial[key].timelineVariableFunction();
687
698
  }
688
699
  // timeline variables that are nested in objects
689
- if (typeof trial[key] === "object" && trial[key] !== null) {
700
+ if (
701
+ typeof trial[key] === "object" &&
702
+ trial[key] !== null &&
703
+ key !== "timeline" &&
704
+ key !== "timeline_variables"
705
+ ) {
690
706
  this.evaluateTimelineVariables(trial[key]);
691
707
  }
692
708
  }
@@ -736,9 +752,11 @@ export class JsPsych {
736
752
  else if (typeof obj === "object") {
737
753
  if (info === null || !info.nested) {
738
754
  for (const key of Object.keys(obj)) {
739
- if (key === "type") {
755
+ if (key === "type" || key === "timeline" || key === "timeline_variables") {
740
756
  // Ignore the object's `type` field because it contains a plugin and we do not want to
741
- // call plugin functions
757
+ // call plugin functions. Also ignore `timeline` and `timeline_variables` because they
758
+ // are used in the `trials` parameter of the preload plugin and we don't want to actually
759
+ // evaluate those in that context.
742
760
  continue;
743
761
  }
744
762
  obj[key] = this.replaceFunctionsWithValues(obj[key], null);
@@ -357,7 +357,11 @@ export class TimelineNode {
357
357
  // recursive downward search for active trial to extract timeline variable
358
358
  timelineVariable(variable_name: string) {
359
359
  if (typeof this.timeline_parameters == "undefined") {
360
- return this.findTimelineVariable(variable_name);
360
+ const val = this.findTimelineVariable(variable_name);
361
+ if (typeof val === "undefined") {
362
+ console.warn("Timeline variable " + variable_name + " not found.");
363
+ }
364
+ return val;
361
365
  } else {
362
366
  // if progress.current_location is -1, then the timeline variable is being evaluated
363
367
  // in a function that runs prior to the trial starting, so we should treat that trial
@@ -371,7 +375,11 @@ export class TimelineNode {
371
375
  loc = loc - 1;
372
376
  }
373
377
  // now find the variable
374
- return this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
378
+ const val = this.timeline_parameters.timeline[loc].timelineVariable(variable_name);
379
+ if (typeof val === "undefined") {
380
+ console.warn("Timeline variable " + variable_name + " not found.");
381
+ }
382
+ return val;
375
383
  }
376
384
  }
377
385
 
@@ -19,5 +19,5 @@ export interface JsPsychExtension {
19
19
  * Called at the end of the trial.
20
20
  * @returns Data to append to the trial's data object.
21
21
  */
22
- on_finish(params?: Record<string, any>): Record<string, any>;
22
+ on_finish(params?: Record<string, any>): Record<string, any> | Promise<Record<string, any>>;
23
23
  }
@@ -13,7 +13,10 @@ export class MediaAPI {
13
13
 
14
14
  // video //
15
15
  private video_buffers = {};
16
- getVideoBuffer(videoID) {
16
+ getVideoBuffer(videoID: string) {
17
+ if (videoID.startsWith("blob:")) {
18
+ this.video_buffers[videoID] = videoID;
19
+ }
17
20
  return this.video_buffers[videoID];
18
21
  }
19
22
 
@@ -334,4 +337,21 @@ export class MediaAPI {
334
337
  getMicrophoneRecorder(): MediaRecorder {
335
338
  return this.microphone_recorder;
336
339
  }
340
+
341
+ private camera_stream: MediaStream = null;
342
+ private camera_recorder: MediaRecorder = null;
343
+
344
+ initializeCameraRecorder(stream: MediaStream, opts?: MediaRecorderOptions) {
345
+ this.camera_stream = stream;
346
+ const recorder = new MediaRecorder(stream, opts);
347
+ this.camera_recorder = recorder;
348
+ }
349
+
350
+ getCameraStream(): MediaStream {
351
+ return this.camera_stream;
352
+ }
353
+
354
+ getCameraRecorder(): MediaRecorder {
355
+ return this.camera_recorder;
356
+ }
337
357
  }