@adriansteffan/reactive 0.0.42 → 0.0.44
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/.claude/settings.local.json +11 -2
- package/README.md +106 -3
- package/dist/{mod-UqYdghJl.js → mod-D6DlS3ur.js} +6483 -6120
- package/dist/mod.d.ts +54 -2
- package/dist/reactive.es.js +42 -33
- package/dist/reactive.umd.js +38 -36
- package/dist/style.css +1 -1
- package/dist/{web-pL-YTTVv.js → web-D7VcCd-t.js} +1 -1
- package/dist/{web-eGzX65_f.js → web-o3I0sgwu.js} +1 -1
- package/package.json +1 -1
- package/src/components/canvasblock.tsx +112 -70
- package/src/components/checkdevice.tsx +15 -0
- package/src/components/enterfullscreen.tsx +5 -3
- package/src/components/exitfullscreen.tsx +4 -1
- package/src/components/experimentprovider.tsx +7 -2
- package/src/components/experimentrunner.tsx +66 -52
- package/src/components/microphonecheck.tsx +3 -0
- package/src/components/mobilefilepermission.tsx +3 -0
- package/src/components/plaininput.tsx +17 -0
- package/src/components/prolificending.tsx +3 -0
- package/src/components/quest.tsx +58 -8
- package/src/components/storeui.tsx +15 -11
- package/src/components/text.tsx +11 -0
- package/src/components/upload.tsx +56 -271
- package/src/mod.tsx +1 -0
- package/src/utils/bytecode.ts +50 -0
- package/src/utils/simulation.ts +268 -0
- package/src/utils/upload.ts +299 -0
- package/template/README.md +59 -0
- package/template/backend/src/backend.ts +1 -0
- package/template/package.json +2 -0
- package/template/simulate.ts +15 -0
- package/template/src/Experiment.tsx +58 -5
- package/template/src/main.tsx +1 -1
- package/template/tsconfig.json +2 -3
- package/tsconfig.json +1 -0
- package/vite.config.ts +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseComponentProps } from '../mod';
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { registerSimulation } from '../utils/simulation';
|
|
3
4
|
|
|
4
5
|
interface BaseFieldConfig {
|
|
5
6
|
type: 'string' | 'integer' | 'float' | 'boolean';
|
|
@@ -33,6 +34,19 @@ interface BooleanFieldConfig extends BaseFieldConfig {
|
|
|
33
34
|
|
|
34
35
|
type FieldConfig = StringFieldConfig | NumberFieldConfig | BooleanFieldConfig;
|
|
35
36
|
|
|
37
|
+
function buildFieldValues(fields: FieldConfig[], store?: Record<string, any>): Record<string, any> {
|
|
38
|
+
const values: Record<string, any> = {};
|
|
39
|
+
fields.forEach(field => {
|
|
40
|
+
values[field.storeKey] = store?.[field.storeKey] !== undefined ? store[field.storeKey] : field.defaultValue;
|
|
41
|
+
});
|
|
42
|
+
return values;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
registerSimulation('StoreUI', (trialProps, experimentState, _simulators, participant) => {
|
|
46
|
+
const values = buildFieldValues(trialProps.fields || [], experimentState.store);
|
|
47
|
+
return { responseData: values, participantState: participant, storeUpdates: values };
|
|
48
|
+
}, {});
|
|
49
|
+
|
|
36
50
|
interface StoreUIProps extends BaseComponentProps {
|
|
37
51
|
title?: string;
|
|
38
52
|
description?: string;
|
|
@@ -56,17 +70,7 @@ function StoreUI({
|
|
|
56
70
|
|
|
57
71
|
|
|
58
72
|
useEffect(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
fields.forEach(field => {
|
|
62
|
-
|
|
63
|
-
initialValues[field.storeKey] =
|
|
64
|
-
store?.[field.storeKey] !== undefined
|
|
65
|
-
? store[field.storeKey]
|
|
66
|
-
: field.defaultValue;
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
setValues(initialValues);
|
|
73
|
+
setValues(buildFieldValues(fields, store));
|
|
70
74
|
|
|
71
75
|
const initialTouched: Record<string, boolean> = {};
|
|
72
76
|
fields.forEach(field => {
|
package/src/components/text.tsx
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
2
|
import { BaseComponentProps, now } from '../mod';
|
|
3
|
+
import { registerSimulation } from '../utils/simulation';
|
|
4
|
+
|
|
5
|
+
registerSimulation('Text', (trialProps, _experimentState, simulators, participant) => {
|
|
6
|
+
const result = simulators.respond(trialProps, participant);
|
|
7
|
+
return { responseData: result.value, participantState: result.participantState, duration: result.value.reactionTime };
|
|
8
|
+
}, {
|
|
9
|
+
respond: (_input: any, participant: any) => ({
|
|
10
|
+
value: { key: 'button', reactionTime: 500 + Math.random() * 1500 },
|
|
11
|
+
participantState: participant,
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
3
14
|
|
|
4
15
|
function Text({
|
|
5
16
|
content,
|
|
@@ -13,8 +13,54 @@ import {
|
|
|
13
13
|
TrialData,
|
|
14
14
|
} from '../utils/common';
|
|
15
15
|
import { BlobWriter, TextReader, ZipWriter } from '@zip.js/zip.js';
|
|
16
|
-
|
|
17
16
|
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
|
17
|
+
import { buildUploadFiles, convertArrayOfObjectsToCSV, CSVBuilder } from '../utils/upload';
|
|
18
|
+
import { registerSimulation, getBackendUrl, getInitialParticipant } from '../utils/simulation';
|
|
19
|
+
|
|
20
|
+
registerSimulation('Upload', async (trialProps, experimentState, _simulators, participant) => {
|
|
21
|
+
const sessionID = trialProps.sessionID || `sim_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
22
|
+
const files = buildUploadFiles({
|
|
23
|
+
sessionID,
|
|
24
|
+
data: experimentState.data || [],
|
|
25
|
+
store: experimentState.store,
|
|
26
|
+
generateFiles: trialProps.generateFiles,
|
|
27
|
+
sessionCSVBuilder: trialProps.sessionCSVBuilder,
|
|
28
|
+
trialCSVBuilder: trialProps.trialCSVBuilder,
|
|
29
|
+
uploadRaw: trialProps.uploadRaw ?? true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const initialParticipant = getInitialParticipant();
|
|
33
|
+
if (initialParticipant) {
|
|
34
|
+
files.push({
|
|
35
|
+
filename: `${sessionID}_participant_initial.csv`,
|
|
36
|
+
content: convertArrayOfObjectsToCSV([initialParticipant]),
|
|
37
|
+
encoding: 'utf8',
|
|
38
|
+
});
|
|
39
|
+
files.push({
|
|
40
|
+
filename: `${sessionID}_participant_final.csv`,
|
|
41
|
+
content: convertArrayOfObjectsToCSV([participant]),
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const backendUrl = getBackendUrl();
|
|
47
|
+
if (backendUrl) {
|
|
48
|
+
const payload = {
|
|
49
|
+
sessionId: sessionID,
|
|
50
|
+
files: files.map((f) => ({ ...f, encoding: f.encoding ?? 'utf8' })),
|
|
51
|
+
};
|
|
52
|
+
const res = await fetch(`${backendUrl}/data`, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify(payload),
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
console.error(`Simulation upload failed: ${res.status} ${res.statusText}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return { responseData: { files }, participantState: participant };
|
|
63
|
+
}, {});
|
|
18
64
|
|
|
19
65
|
interface UploadPayload {
|
|
20
66
|
sessionId: string;
|
|
@@ -42,179 +88,6 @@ registerComponentParams('Upload', [
|
|
|
42
88
|
},
|
|
43
89
|
]);
|
|
44
90
|
|
|
45
|
-
type DataObject = {
|
|
46
|
-
[key: string]: string | number | boolean | null | undefined;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
function escapeCsvValue(value: any): string {
|
|
50
|
-
if (value === null || value === undefined) {
|
|
51
|
-
return '';
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const stringValue = String(value);
|
|
55
|
-
|
|
56
|
-
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
|
|
57
|
-
const escapedValue = stringValue.replace(/"/g, '""');
|
|
58
|
-
return `"${escapedValue}"`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return stringValue;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function convertArrayOfObjectsToCSV(data: DataObject[]): string {
|
|
65
|
-
if (!data || data.length === 0) {
|
|
66
|
-
return '';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const headerSet = new Set<string>();
|
|
70
|
-
data.forEach((obj) => {
|
|
71
|
-
Object.keys(obj).forEach((key) => {
|
|
72
|
-
headerSet.add(key);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
const headers = Array.from(headerSet);
|
|
76
|
-
|
|
77
|
-
const headerRow = headers.map((header) => escapeCsvValue(header)).join(',');
|
|
78
|
-
|
|
79
|
-
const dataRows = data.map((obj) => {
|
|
80
|
-
return headers
|
|
81
|
-
.map((header) => {
|
|
82
|
-
const value = obj[header];
|
|
83
|
-
return escapeCsvValue(value);
|
|
84
|
-
})
|
|
85
|
-
.join(',');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
return [headerRow, ...dataRows].join('\n');
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// TODO: for better cohesion move this into the components on registration
|
|
92
|
-
const defaultFlatteningFunctions = {
|
|
93
|
-
'CanvasBlock': (item: TrialData) => {
|
|
94
|
-
const responseData = item.responseData;
|
|
95
|
-
if (Array.isArray(responseData)) {
|
|
96
|
-
return responseData.map((i) => ({
|
|
97
|
-
block: item.name,
|
|
98
|
-
...i,
|
|
99
|
-
}));
|
|
100
|
-
}
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const transform = ({responseData, ...obj}: any) => ({ ...obj, ...Object.entries(responseData || {}).reduce((acc, [k, v]) => ({...acc, [`data_${k}`]: v}), {}) });
|
|
106
|
-
|
|
107
|
-
function combineTrialsToCsv(
|
|
108
|
-
data: any[],
|
|
109
|
-
filename: string,
|
|
110
|
-
names: string[],
|
|
111
|
-
flatteningFunctions: Record<string, (item: any) => any[] | Record<string, any[]>>,
|
|
112
|
-
fun?: (obj: any) => any,
|
|
113
|
-
): FileUpload | FileUpload[] {
|
|
114
|
-
|
|
115
|
-
// Collect all flattener results first, filtering out completely empty results
|
|
116
|
-
const allResults: (any[] | Record<string, any[]>)[] = names.flatMap((name) => {
|
|
117
|
-
const matchingItems = data.filter((d) => d.name === name);
|
|
118
|
-
|
|
119
|
-
return matchingItems.map((item) => {
|
|
120
|
-
const flattener = item.type && flatteningFunctions[item.type];
|
|
121
|
-
const result = flattener ? flattener(item) : [transform(item)];
|
|
122
|
-
|
|
123
|
-
// Filter out completely empty results
|
|
124
|
-
if (Array.isArray(result) && result.length === 0) {
|
|
125
|
-
return null; // Signal this trial should be completely skipped
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
129
|
-
// Check if all arrays in the object are empty
|
|
130
|
-
const hasAnyData = Object.values(result).some(val =>
|
|
131
|
-
Array.isArray(val) && val.length > 0
|
|
132
|
-
);
|
|
133
|
-
if (!hasAnyData) {
|
|
134
|
-
return null; // Signal this trial should be completely skipped
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
return result;
|
|
139
|
-
});
|
|
140
|
-
}).filter(result => result !== null);
|
|
141
|
-
|
|
142
|
-
// Check if any result is a multi-table object (has string keys with array values)
|
|
143
|
-
const hasMultiTable = allResults.some((result) =>
|
|
144
|
-
result &&
|
|
145
|
-
typeof result === 'object' &&
|
|
146
|
-
!Array.isArray(result) &&
|
|
147
|
-
Object.keys(result).some(key => Array.isArray(result[key]))
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
if (!hasMultiTable) {
|
|
151
|
-
// all results are arrays, combine them into one CSV
|
|
152
|
-
const processedData = allResults
|
|
153
|
-
.flatMap((result) => Array.isArray(result) ? result : [])
|
|
154
|
-
.map((x) => (fun ? fun(x) : x));
|
|
155
|
-
|
|
156
|
-
// Skip creating CSV if all flatteners returned empty arrays
|
|
157
|
-
if (processedData.length === 0) {
|
|
158
|
-
return [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return {
|
|
162
|
-
filename,
|
|
163
|
-
encoding: 'utf8' as const,
|
|
164
|
-
content: convertArrayOfObjectsToCSV(processedData),
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// handle multi-table results
|
|
169
|
-
// Collect all table keys from all results
|
|
170
|
-
const allTableKeys = new Set<string>();
|
|
171
|
-
allResults.forEach((result) => {
|
|
172
|
-
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
173
|
-
Object.keys(result).forEach(key => {
|
|
174
|
-
if (Array.isArray(result[key])) {
|
|
175
|
-
allTableKeys.add(key);
|
|
176
|
-
}
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Create separate CSV files for each table key
|
|
182
|
-
const files: FileUpload[] = [];
|
|
183
|
-
|
|
184
|
-
for (const tableKey of allTableKeys) {
|
|
185
|
-
const tableData = allResults.flatMap((result) => {
|
|
186
|
-
if (Array.isArray(result)) {
|
|
187
|
-
// If this result is a simple array, include it in all tables for backward compatibility
|
|
188
|
-
return result;
|
|
189
|
-
} else if (result && typeof result === 'object' && result[tableKey]) {
|
|
190
|
-
// If this result has data for this table key, include it
|
|
191
|
-
return result[tableKey];
|
|
192
|
-
}
|
|
193
|
-
return [];
|
|
194
|
-
}).map((x) => (fun ? fun(x) : x));
|
|
195
|
-
|
|
196
|
-
// Skip creating CSV if all flatteners returned empty arrays for this table
|
|
197
|
-
if (tableData.length === 0) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Remove file extension from filename and add table key
|
|
202
|
-
const baseFilename = filename.replace(/\.csv$/, '');
|
|
203
|
-
|
|
204
|
-
files.push({
|
|
205
|
-
filename: `${baseFilename}_${tableKey}.csv`,
|
|
206
|
-
encoding: 'utf8' as const,
|
|
207
|
-
content: convertArrayOfObjectsToCSV(tableData),
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// Return empty array if no files were created
|
|
212
|
-
if (files.length === 0) {
|
|
213
|
-
return [];
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return files.length === 1 ? files[0] : files;
|
|
217
|
-
}
|
|
218
91
|
|
|
219
92
|
interface FileBackend {
|
|
220
93
|
directoryExists(path: string): Promise<boolean>;
|
|
@@ -345,12 +218,6 @@ const getUniqueDirectoryName = async (
|
|
|
345
218
|
return uniqueSessionID;
|
|
346
219
|
};
|
|
347
220
|
|
|
348
|
-
type CSVBuilder = {
|
|
349
|
-
filename?: string;
|
|
350
|
-
trials?: string[];
|
|
351
|
-
fun?: (row: Record<string, any>) => Record<string, any>;
|
|
352
|
-
};
|
|
353
|
-
|
|
354
221
|
export default function Upload({
|
|
355
222
|
data,
|
|
356
223
|
next,
|
|
@@ -427,97 +294,15 @@ export default function Upload({
|
|
|
427
294
|
|
|
428
295
|
const sessionIDUpload = sessionID ?? uuidv4();
|
|
429
296
|
|
|
430
|
-
const files
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (sessionCSVBuilder) {
|
|
440
|
-
type ParamDetails = {
|
|
441
|
-
value?: any;
|
|
442
|
-
defaultValue: any;
|
|
443
|
-
};
|
|
444
|
-
let paramsDict: Record<string, any> = {};
|
|
445
|
-
const paramsSource: Record<string, ParamDetails | any> | undefined =
|
|
446
|
-
data?.[0]?.responseData?.params;
|
|
447
|
-
if (paramsSource && typeof paramsSource === 'object' && paramsSource !== null) {
|
|
448
|
-
paramsDict = Object.entries(paramsSource).reduce(
|
|
449
|
-
(
|
|
450
|
-
accumulator: Record<string, any>,
|
|
451
|
-
[paramName, paramDetails]: [string, ParamDetails | any],
|
|
452
|
-
) => {
|
|
453
|
-
if (
|
|
454
|
-
paramDetails &&
|
|
455
|
-
typeof paramDetails === 'object' &&
|
|
456
|
-
'defaultValue' in paramDetails
|
|
457
|
-
) {
|
|
458
|
-
const chosenValue = paramDetails.value ?? paramDetails.defaultValue;
|
|
459
|
-
accumulator[paramName] = chosenValue;
|
|
460
|
-
}
|
|
461
|
-
return accumulator;
|
|
462
|
-
},
|
|
463
|
-
{} as Record<string, any>,
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
let content = {
|
|
468
|
-
sessionID: sessionIDUpload,
|
|
469
|
-
userAgent: data[0].responseData.userAgent,
|
|
470
|
-
...paramsDict,
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
if (
|
|
474
|
-
sessionCSVBuilder.trials &&
|
|
475
|
-
Array.isArray(sessionCSVBuilder.trials) &&
|
|
476
|
-
sessionCSVBuilder.trials.length > 0
|
|
477
|
-
) {
|
|
478
|
-
for (const trialName of sessionCSVBuilder.trials) {
|
|
479
|
-
const matchingDataElement = data.find((element) => element.name === trialName);
|
|
480
|
-
|
|
481
|
-
if (matchingDataElement?.responseData) {
|
|
482
|
-
if (
|
|
483
|
-
typeof matchingDataElement.responseData === 'object' &&
|
|
484
|
-
matchingDataElement.responseData !== null
|
|
485
|
-
) {
|
|
486
|
-
content = { ...content, ...matchingDataElement.responseData };
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
files.push({
|
|
493
|
-
content: convertArrayOfObjectsToCSV([
|
|
494
|
-
sessionCSVBuilder.fun ? sessionCSVBuilder.fun(content) : content,
|
|
495
|
-
]),
|
|
496
|
-
filename: `${sessionIDUpload}${sessionCSVBuilder.filename}.csv`,
|
|
497
|
-
encoding: 'utf8' as const,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (trialCSVBuilder) {
|
|
502
|
-
for (const builder of trialCSVBuilder.builders) {
|
|
503
|
-
const result = combineTrialsToCsv(
|
|
504
|
-
data,
|
|
505
|
-
`${sessionIDUpload}${builder.filename}.csv`,
|
|
506
|
-
builder.trials ?? [],
|
|
507
|
-
{...defaultFlatteningFunctions, ...trialCSVBuilder.flatteners},
|
|
508
|
-
builder.fun,
|
|
509
|
-
);
|
|
510
|
-
|
|
511
|
-
if (Array.isArray(result)) {
|
|
512
|
-
// Only push files if the array is not empty
|
|
513
|
-
if (result.length > 0) {
|
|
514
|
-
files.push(...result);
|
|
515
|
-
}
|
|
516
|
-
} else {
|
|
517
|
-
files.push(result);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
297
|
+
const files = buildUploadFiles({
|
|
298
|
+
sessionID: sessionIDUpload,
|
|
299
|
+
data,
|
|
300
|
+
store,
|
|
301
|
+
generateFiles,
|
|
302
|
+
sessionCSVBuilder,
|
|
303
|
+
trialCSVBuilder,
|
|
304
|
+
uploadRaw,
|
|
305
|
+
});
|
|
521
306
|
|
|
522
307
|
try {
|
|
523
308
|
const payload: UploadPayload = {
|
package/src/mod.tsx
CHANGED
package/src/utils/bytecode.ts
CHANGED
|
@@ -207,3 +207,53 @@ export function compileTimeline(timeline: TimelineItem[]): {
|
|
|
207
207
|
|
|
208
208
|
return { instructions, markers };
|
|
209
209
|
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Walks bytecode from `fromPointer`, processing IfGoto and UpdateStore instructions,
|
|
213
|
+
* and returns the pointer of the next ExecuteContent (or past-end if none found).
|
|
214
|
+
*/
|
|
215
|
+
export function applyMetadata<T extends Record<string, any>>(
|
|
216
|
+
trialData: T,
|
|
217
|
+
content: { metadata?: any; nestMetadata?: boolean },
|
|
218
|
+
data: RefinedTrialData[],
|
|
219
|
+
store: Store,
|
|
220
|
+
): T {
|
|
221
|
+
const metadata = typeof content.metadata === 'function' ? content.metadata(data, store) : content.metadata;
|
|
222
|
+
if (content.nestMetadata) return { ...trialData, metadata };
|
|
223
|
+
if (metadata) return { ...metadata, ...trialData };
|
|
224
|
+
return trialData;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function advanceToNextContent(
|
|
228
|
+
bytecode: { instructions: UnifiedBytecodeInstruction[]; markers: Record<string, number> },
|
|
229
|
+
fromPointer: number,
|
|
230
|
+
getStore: () => Store,
|
|
231
|
+
getData: () => RefinedTrialData[],
|
|
232
|
+
onUpdateStore: (newStore: Store) => void,
|
|
233
|
+
): number {
|
|
234
|
+
let pointer = fromPointer;
|
|
235
|
+
while (pointer < bytecode.instructions.length) {
|
|
236
|
+
const instr = bytecode.instructions[pointer];
|
|
237
|
+
switch (instr.type) {
|
|
238
|
+
case 'ExecuteContent':
|
|
239
|
+
return pointer;
|
|
240
|
+
case 'IfGoto':
|
|
241
|
+
if (instr.cond(getStore(), getData())) {
|
|
242
|
+
const target = bytecode.markers[instr.marker];
|
|
243
|
+
if (target !== undefined) {
|
|
244
|
+
pointer = target;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
pointer++;
|
|
249
|
+
break;
|
|
250
|
+
case 'UpdateStore':
|
|
251
|
+
onUpdateStore(instr.fun(getStore(), getData()));
|
|
252
|
+
pointer++;
|
|
253
|
+
break;
|
|
254
|
+
default:
|
|
255
|
+
pointer++;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return pointer;
|
|
259
|
+
}
|