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.
- package/README.md +1 -1
- package/css/jspsych.css +18 -8
- package/dist/index.browser.js +3080 -4286
- package/dist/index.browser.js.map +1 -1
- package/dist/index.browser.min.js +6 -2
- package/dist/index.browser.min.js.map +1 -1
- package/dist/index.cjs +2314 -4066
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +984 -6
- package/dist/index.js +2313 -4066
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
- package/src/ExtensionManager.spec.ts +123 -0
- package/src/ExtensionManager.ts +81 -0
- package/src/JsPsych.ts +195 -690
- package/src/ProgressBar.spec.ts +60 -0
- package/src/ProgressBar.ts +60 -0
- package/src/index.scss +29 -8
- package/src/index.ts +4 -9
- package/src/modules/data/DataCollection.ts +1 -1
- package/src/modules/data/DataColumn.ts +12 -1
- package/src/modules/data/index.ts +92 -103
- package/src/modules/extensions.ts +4 -0
- package/src/modules/plugin-api/AudioPlayer.ts +101 -0
- package/src/modules/plugin-api/KeyboardListenerAPI.ts +1 -1
- package/src/modules/plugin-api/MediaAPI.ts +48 -106
- package/src/modules/plugin-api/__mocks__/AudioPlayer.ts +38 -0
- package/src/modules/plugin-api/index.ts +11 -14
- package/src/modules/plugins.ts +26 -27
- package/src/modules/randomization.ts +1 -1
- package/src/timeline/Timeline.spec.ts +921 -0
- package/src/timeline/Timeline.ts +342 -0
- package/src/timeline/TimelineNode.ts +174 -0
- package/src/timeline/Trial.spec.ts +897 -0
- package/src/timeline/Trial.ts +419 -0
- package/src/timeline/index.ts +232 -0
- package/src/timeline/util.spec.ts +124 -0
- package/src/timeline/util.ts +146 -0
- package/dist/JsPsych.d.ts +0 -112
- package/dist/TimelineNode.d.ts +0 -34
- package/dist/migration.d.ts +0 -3
- package/dist/modules/data/DataCollection.d.ts +0 -46
- package/dist/modules/data/DataColumn.d.ts +0 -15
- package/dist/modules/data/index.d.ts +0 -25
- package/dist/modules/data/utils.d.ts +0 -3
- package/dist/modules/extensions.d.ts +0 -22
- package/dist/modules/plugin-api/HardwareAPI.d.ts +0 -15
- package/dist/modules/plugin-api/KeyboardListenerAPI.d.ts +0 -34
- package/dist/modules/plugin-api/MediaAPI.d.ts +0 -32
- package/dist/modules/plugin-api/SimulationAPI.d.ts +0 -44
- package/dist/modules/plugin-api/TimeoutAPI.d.ts +0 -17
- package/dist/modules/plugin-api/index.d.ts +0 -8
- package/dist/modules/plugins.d.ts +0 -136
- package/dist/modules/randomization.d.ts +0 -42
- package/dist/modules/turk.d.ts +0 -40
- package/dist/modules/utils.d.ts +0 -13
- package/src/TimelineNode.ts +0 -544
- package/src/modules/plugin-api/HardwareAPI.ts +0 -32
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { DataCollection } from "../modules/data/DataCollection";
|
|
2
|
+
import {
|
|
3
|
+
repeat,
|
|
4
|
+
sampleWithReplacement,
|
|
5
|
+
sampleWithoutReplacement,
|
|
6
|
+
shuffle,
|
|
7
|
+
shuffleAlternateGroups,
|
|
8
|
+
} from "../modules/randomization";
|
|
9
|
+
import { TimelineNode } from "./TimelineNode";
|
|
10
|
+
import { Trial } from "./Trial";
|
|
11
|
+
import { PromiseWrapper } from "./util";
|
|
12
|
+
import {
|
|
13
|
+
TimelineArray,
|
|
14
|
+
TimelineDescription,
|
|
15
|
+
TimelineNodeDependencies,
|
|
16
|
+
TimelineNodeStatus,
|
|
17
|
+
TimelineVariable,
|
|
18
|
+
TrialDescription,
|
|
19
|
+
TrialResult,
|
|
20
|
+
isTimelineDescription,
|
|
21
|
+
isTrialDescription,
|
|
22
|
+
} from ".";
|
|
23
|
+
|
|
24
|
+
export class Timeline extends TimelineNode {
|
|
25
|
+
public readonly children: TimelineNode[] = [];
|
|
26
|
+
public readonly description: TimelineDescription;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
dependencies: TimelineNodeDependencies,
|
|
30
|
+
description: TimelineDescription | TimelineArray,
|
|
31
|
+
public readonly parent?: Timeline
|
|
32
|
+
) {
|
|
33
|
+
super(dependencies);
|
|
34
|
+
this.description = Array.isArray(description) ? { timeline: description } : description;
|
|
35
|
+
this.initializeParameterValueCache();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private currentChild?: TimelineNode;
|
|
39
|
+
private shouldAbort = false;
|
|
40
|
+
|
|
41
|
+
public async run() {
|
|
42
|
+
if (typeof this.index === "undefined") {
|
|
43
|
+
// We're the first timeline node to run. Otherwise, another node would have set our index
|
|
44
|
+
// right before running us.
|
|
45
|
+
this.index = 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.status = TimelineNodeStatus.RUNNING;
|
|
49
|
+
|
|
50
|
+
const { conditional_function, loop_function, repetitions = 1 } = this.description;
|
|
51
|
+
|
|
52
|
+
// Generate initial timeline variable order so the first set of timeline variables is already
|
|
53
|
+
// available to the `on_timeline_start` and `conditional_function` callbacks
|
|
54
|
+
let timelineVariableOrder = this.generateTimelineVariableOrder();
|
|
55
|
+
this.setCurrentTimelineVariablesByIndex(timelineVariableOrder[0]);
|
|
56
|
+
let isInitialTimelineVariableOrder = true; // So we don't regenerate the order in the first iteration
|
|
57
|
+
|
|
58
|
+
let currentLoopIterationResults: TrialResult[];
|
|
59
|
+
|
|
60
|
+
if (!conditional_function || conditional_function()) {
|
|
61
|
+
this.onStart();
|
|
62
|
+
|
|
63
|
+
for (let repetition = 0; repetition < repetitions; repetition++) {
|
|
64
|
+
do {
|
|
65
|
+
currentLoopIterationResults = [];
|
|
66
|
+
|
|
67
|
+
// Generate a new timeline variable order in each iteration except for the first one where
|
|
68
|
+
// it has been done before
|
|
69
|
+
if (isInitialTimelineVariableOrder) {
|
|
70
|
+
isInitialTimelineVariableOrder = false;
|
|
71
|
+
} else {
|
|
72
|
+
timelineVariableOrder = this.generateTimelineVariableOrder();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const timelineVariableIndex of timelineVariableOrder) {
|
|
76
|
+
this.setCurrentTimelineVariablesByIndex(timelineVariableIndex);
|
|
77
|
+
|
|
78
|
+
for (const childNode of this.instantiateChildNodes()) {
|
|
79
|
+
const previousChild = this.currentChild;
|
|
80
|
+
this.currentChild = childNode;
|
|
81
|
+
childNode.index = previousChild
|
|
82
|
+
? previousChild.getLatestNode().index + 1
|
|
83
|
+
: this.index;
|
|
84
|
+
|
|
85
|
+
await childNode.run();
|
|
86
|
+
// @ts-expect-error TS thinks `this.status` must be `RUNNING` now, but it might have
|
|
87
|
+
// changed while `await`ing
|
|
88
|
+
if (this.status === TimelineNodeStatus.PAUSED) {
|
|
89
|
+
await this.resumePromise.get();
|
|
90
|
+
}
|
|
91
|
+
if (this.shouldAbort) {
|
|
92
|
+
this.status = TimelineNodeStatus.ABORTED;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
currentLoopIterationResults.push(...this.currentChild.getResults());
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
} while (loop_function && loop_function(new DataCollection(currentLoopIterationResults)));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
this.onFinish();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
this.status = TimelineNodeStatus.COMPLETED;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private onStart() {
|
|
109
|
+
if (this.description.on_timeline_start) {
|
|
110
|
+
this.description.on_timeline_start();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private onFinish() {
|
|
115
|
+
if (this.description.on_timeline_finish) {
|
|
116
|
+
this.description.on_timeline_finish();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pause() {
|
|
121
|
+
if (this.currentChild instanceof Timeline) {
|
|
122
|
+
this.currentChild.pause();
|
|
123
|
+
}
|
|
124
|
+
this.status = TimelineNodeStatus.PAUSED;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private resumePromise = new PromiseWrapper();
|
|
128
|
+
resume() {
|
|
129
|
+
if (this.status == TimelineNodeStatus.PAUSED) {
|
|
130
|
+
if (this.currentChild instanceof Timeline) {
|
|
131
|
+
this.currentChild.resume();
|
|
132
|
+
}
|
|
133
|
+
this.status = TimelineNodeStatus.RUNNING;
|
|
134
|
+
this.resumePromise.resolve();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* If the timeline is running or paused, aborts the timeline after the current trial has completed
|
|
140
|
+
*/
|
|
141
|
+
abort() {
|
|
142
|
+
if (this.status === TimelineNodeStatus.RUNNING || this.status === TimelineNodeStatus.PAUSED) {
|
|
143
|
+
if (this.currentChild instanceof Timeline) {
|
|
144
|
+
this.currentChild.abort();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.shouldAbort = true;
|
|
148
|
+
if (this.status === TimelineNodeStatus.PAUSED) {
|
|
149
|
+
this.resume();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private instantiateChildNodes() {
|
|
155
|
+
const newChildNodes = this.description.timeline.map((childDescription) => {
|
|
156
|
+
return isTimelineDescription(childDescription)
|
|
157
|
+
? new Timeline(this.dependencies, childDescription, this)
|
|
158
|
+
: new Trial(this.dependencies, childDescription, this);
|
|
159
|
+
});
|
|
160
|
+
this.children.push(...newChildNodes);
|
|
161
|
+
return newChildNodes;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private currentTimelineVariables: Record<string, any>;
|
|
165
|
+
private setCurrentTimelineVariablesByIndex(index: number | null) {
|
|
166
|
+
this.currentTimelineVariables = {
|
|
167
|
+
...this.parent?.getAllTimelineVariables(),
|
|
168
|
+
...(index === null ? undefined : this.description.timeline_variables[index]),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* If the timeline has timeline variables, returns the order of `timeline_variables` array indices
|
|
174
|
+
* to be used, according to the timeline's `sample` setting. If the timeline has no timeline
|
|
175
|
+
* variables, returns `[null]`.
|
|
176
|
+
*/
|
|
177
|
+
private generateTimelineVariableOrder() {
|
|
178
|
+
const timelineVariableLength = this.description.timeline_variables?.length;
|
|
179
|
+
if (!timelineVariableLength) {
|
|
180
|
+
return [null];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let order = [...Array(timelineVariableLength).keys()];
|
|
184
|
+
|
|
185
|
+
const sample = this.description.sample;
|
|
186
|
+
|
|
187
|
+
if (sample) {
|
|
188
|
+
switch (sample.type) {
|
|
189
|
+
case "custom":
|
|
190
|
+
order = sample.fn(order);
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case "with-replacement":
|
|
194
|
+
order = sampleWithReplacement(order, sample.size, sample.weights);
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case "without-replacement":
|
|
198
|
+
order = sampleWithoutReplacement(order, sample.size);
|
|
199
|
+
break;
|
|
200
|
+
|
|
201
|
+
case "fixed-repetitions":
|
|
202
|
+
order = repeat(order, sample.size);
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
case "alternate-groups":
|
|
206
|
+
order = shuffleAlternateGroups(sample.groups, sample.randomize_group_order);
|
|
207
|
+
break;
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Invalid type "${
|
|
212
|
+
// @ts-expect-error TS doesn't have a type for `sample` in this case
|
|
213
|
+
sample.type
|
|
214
|
+
}" in timeline sample parameters. Valid options for type are "custom", "with-replacement", "without-replacement", "fixed-repetitions", and "alternate-groups"`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (this.description.randomize_order) {
|
|
220
|
+
order = shuffle(order);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return order;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Returns the current values of all timeline variables, including those from parent timelines
|
|
228
|
+
*/
|
|
229
|
+
public getAllTimelineVariables() {
|
|
230
|
+
return this.currentTimelineVariables;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
public evaluateTimelineVariable(variable: TimelineVariable) {
|
|
234
|
+
if (this.currentTimelineVariables?.hasOwnProperty(variable.name)) {
|
|
235
|
+
return this.currentTimelineVariables[variable.name];
|
|
236
|
+
}
|
|
237
|
+
throw new Error(`Timeline variable ${variable.name} not found.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
public getResults() {
|
|
241
|
+
const results: TrialResult[] = [];
|
|
242
|
+
for (const child of this.children) {
|
|
243
|
+
if (child instanceof Trial) {
|
|
244
|
+
const childResult = child.getResult();
|
|
245
|
+
if (childResult) {
|
|
246
|
+
results.push(childResult);
|
|
247
|
+
}
|
|
248
|
+
} else if (child instanceof Timeline) {
|
|
249
|
+
results.push(...child.getResults());
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return results;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Returns the naive progress of the timeline (as a fraction), without considering conditional or
|
|
258
|
+
* loop functions.
|
|
259
|
+
*/
|
|
260
|
+
public getNaiveProgress() {
|
|
261
|
+
if (this.status === TimelineNodeStatus.PENDING) {
|
|
262
|
+
return 0;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const activeNode = this.getLatestNode();
|
|
266
|
+
if (!activeNode) {
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let completedTrials = activeNode.index;
|
|
271
|
+
if (activeNode.getStatus() === TimelineNodeStatus.COMPLETED) {
|
|
272
|
+
completedTrials++;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Math.min(completedTrials / this.getNaiveTrialCount(), 1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Recursively computes the naive number of trials in the timeline, without considering
|
|
280
|
+
* conditional or loop functions.
|
|
281
|
+
*/
|
|
282
|
+
public getNaiveTrialCount() {
|
|
283
|
+
// Since child timeline nodes are instantiated lazily, we cannot rely on them but instead have
|
|
284
|
+
// to recurse the description programmatically.
|
|
285
|
+
|
|
286
|
+
const getTrialCount = (description: TimelineArray | TimelineDescription | TrialDescription) => {
|
|
287
|
+
const getTimelineArrayTrialCount = (description: TimelineArray) =>
|
|
288
|
+
description
|
|
289
|
+
.map((childDescription) => getTrialCount(childDescription))
|
|
290
|
+
.reduce((a, b) => a + b);
|
|
291
|
+
|
|
292
|
+
if (Array.isArray(description)) {
|
|
293
|
+
return getTimelineArrayTrialCount(description);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (isTrialDescription(description)) {
|
|
297
|
+
return 1;
|
|
298
|
+
}
|
|
299
|
+
if (isTimelineDescription(description)) {
|
|
300
|
+
let conditionCount = description.timeline_variables?.length || 1;
|
|
301
|
+
|
|
302
|
+
switch (description.sample?.type) {
|
|
303
|
+
case "with-replacement":
|
|
304
|
+
case "without-replacement":
|
|
305
|
+
conditionCount = description.sample.size;
|
|
306
|
+
break;
|
|
307
|
+
|
|
308
|
+
case "fixed-repetitions":
|
|
309
|
+
conditionCount *= description.sample.size;
|
|
310
|
+
break;
|
|
311
|
+
|
|
312
|
+
case "alternate-groups":
|
|
313
|
+
conditionCount = description.sample.groups
|
|
314
|
+
.map((group) => group.length)
|
|
315
|
+
.reduce((a, b) => a + b, 0);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return (
|
|
320
|
+
getTimelineArrayTrialCount(description.timeline) *
|
|
321
|
+
(description.repetitions ?? 1) *
|
|
322
|
+
conditionCount
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
return 0;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
return getTrialCount(this.description);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public getLatestNode() {
|
|
332
|
+
return this.currentChild?.getLatestNode() ?? this;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public getActiveTimelineByName(name: string) {
|
|
336
|
+
if (this.description.name === name) {
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return this.currentChild?.getActiveTimelineByName(name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Timeline } from "./Timeline";
|
|
2
|
+
import { ParameterObjectPathCache } from "./util";
|
|
3
|
+
import {
|
|
4
|
+
TimelineArray,
|
|
5
|
+
TimelineDescription,
|
|
6
|
+
TimelineNodeDependencies,
|
|
7
|
+
TimelineNodeStatus,
|
|
8
|
+
TimelineVariable,
|
|
9
|
+
TrialDescription,
|
|
10
|
+
TrialResult,
|
|
11
|
+
} from ".";
|
|
12
|
+
|
|
13
|
+
export type GetParameterValueOptions = {
|
|
14
|
+
/**
|
|
15
|
+
* If true, and the retrieved parameter value is a function, invoke the function and return its
|
|
16
|
+
* return value (defaults to `true`)
|
|
17
|
+
*/
|
|
18
|
+
evaluateFunctions?: boolean;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether to fall back to parent timeline node parameters (defaults to `true`)
|
|
22
|
+
*/
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether the timeline node should cache the parameter lookup result for successive lookups,
|
|
27
|
+
* including those of nested properties or array elements (defaults to `true`)
|
|
28
|
+
*/
|
|
29
|
+
cacheResult?: boolean;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A function that will be invoked with the original result of the parameter value lookup.
|
|
33
|
+
* Whatever it returns will subsequently be used instead of the original result. This allows to
|
|
34
|
+
* modify results before they are cached.
|
|
35
|
+
*/
|
|
36
|
+
replaceResult?: (originalResult: any) => any;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export abstract class TimelineNode {
|
|
40
|
+
public abstract readonly description: TimelineDescription | TrialDescription | TimelineArray;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The globally unique trial index of this node. It is set when the node is run. Timeline nodes
|
|
44
|
+
* have the same trial index as their first trial.
|
|
45
|
+
*/
|
|
46
|
+
public index?: number;
|
|
47
|
+
|
|
48
|
+
public abstract readonly parent?: Timeline;
|
|
49
|
+
|
|
50
|
+
abstract run(): Promise<void>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Returns a flat array of all currently available results of this node
|
|
54
|
+
*/
|
|
55
|
+
abstract getResults(): TrialResult[];
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Recursively evaluates the given timeline variable, starting at the current timeline node.
|
|
59
|
+
* Returns the result, or `undefined` if the variable is neither specified in the timeline
|
|
60
|
+
* description of this node, nor in the description of any parent node.
|
|
61
|
+
*/
|
|
62
|
+
abstract evaluateTimelineVariable(variable: TimelineVariable): any;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns the most recent (child) TimelineNode. For trial nodes, this is always the trial node
|
|
66
|
+
* itself since trial nodes do not have child nodes. For timeline nodes, the return value is a
|
|
67
|
+
* Trial object most of the time, but it may also be a Timeline object when a timeline hasn't yet
|
|
68
|
+
* instantiated its children (e.g. during initial timeline callback functions).
|
|
69
|
+
*/
|
|
70
|
+
abstract getLatestNode(): TimelineNode;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Returns an active child timeline (or itself) that matches the given name, or `undefined` if no such child exists.
|
|
74
|
+
*/
|
|
75
|
+
abstract getActiveTimelineByName(name: string): Timeline | undefined;
|
|
76
|
+
|
|
77
|
+
protected status = TimelineNodeStatus.PENDING;
|
|
78
|
+
|
|
79
|
+
constructor(protected readonly dependencies: TimelineNodeDependencies) {}
|
|
80
|
+
|
|
81
|
+
getStatus() {
|
|
82
|
+
return this.status;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private parameterValueCache = new ParameterObjectPathCache();
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Initializes the parameter value cache with `this.description`. To be called by subclass
|
|
89
|
+
* constructors after setting `this.description`.
|
|
90
|
+
*/
|
|
91
|
+
protected initializeParameterValueCache() {
|
|
92
|
+
this.parameterValueCache.initialize(this.description);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Resets all cached parameter values in this timeline node and all of its parents. This is
|
|
97
|
+
* necessary to re-evaluate function parameters and timeline variables at each new trial.
|
|
98
|
+
*/
|
|
99
|
+
protected resetParameterValueCache() {
|
|
100
|
+
this.parameterValueCache.reset();
|
|
101
|
+
this.parent?.resetParameterValueCache();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Retrieves a parameter value from the description of this timeline node, recursively falling
|
|
106
|
+
* back to the description of each parent timeline node unless `recursive` is set to `false`. If
|
|
107
|
+
* the parameter...
|
|
108
|
+
*
|
|
109
|
+
* * is a timeline variable, evaluates the variable and returns the result.
|
|
110
|
+
* * is not specified, returns `undefined`.
|
|
111
|
+
* * is a function and `evaluateFunctions` is not set to `false`, invokes the function and returns
|
|
112
|
+
* its return value
|
|
113
|
+
* * has previously been looked up, return the cached result of the previous lookup
|
|
114
|
+
*
|
|
115
|
+
* @param parameterPath The path of the respective parameter in the timeline node description. If
|
|
116
|
+
* the path is an array, nested object properties or array items will be looked up.
|
|
117
|
+
* @param options See {@link GetParameterValueOptions}
|
|
118
|
+
*/
|
|
119
|
+
public getParameterValue(
|
|
120
|
+
parameterPath: string | string[],
|
|
121
|
+
options: GetParameterValueOptions = {}
|
|
122
|
+
): any {
|
|
123
|
+
const {
|
|
124
|
+
evaluateFunctions = true,
|
|
125
|
+
recursive = true,
|
|
126
|
+
cacheResult = true,
|
|
127
|
+
replaceResult,
|
|
128
|
+
} = options;
|
|
129
|
+
|
|
130
|
+
if (typeof parameterPath === "string") {
|
|
131
|
+
parameterPath = [parameterPath];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let { doesPathExist, value: result } = this.parameterValueCache.lookup(parameterPath);
|
|
135
|
+
if (!doesPathExist && recursive && this.parent) {
|
|
136
|
+
result = this.parent.getParameterValue(parameterPath, options);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof result === "function" && evaluateFunctions) {
|
|
140
|
+
result = result();
|
|
141
|
+
}
|
|
142
|
+
if (result instanceof TimelineVariable) {
|
|
143
|
+
result = this.evaluateTimelineVariable(result);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (typeof replaceResult === "function") {
|
|
147
|
+
result = replaceResult(result);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (cacheResult) {
|
|
151
|
+
this.parameterValueCache.set(parameterPath, result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Retrieves and evaluates the `data` parameter. It is different from other parameters in that
|
|
159
|
+
* it's properties may be functions that have to be evaluated, and parent nodes' data parameter
|
|
160
|
+
* properties are merged into the result.
|
|
161
|
+
*/
|
|
162
|
+
public getDataParameter(): Record<string, any> | undefined {
|
|
163
|
+
const data = this.getParameterValue("data", { recursive: false });
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...Object.fromEntries(
|
|
167
|
+
typeof data === "object"
|
|
168
|
+
? Object.keys(data).map((key) => [key, this.getParameterValue(["data", key])])
|
|
169
|
+
: []
|
|
170
|
+
),
|
|
171
|
+
...this.parent?.getDataParameter(),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|