aiom
Version:
A Framework for interdependent (mcmc-like) behavioral experiments
150 lines (135 loc) • 6.67 kB
JavaScript
const express = require('express');
const path = require('path');
const fs = require('fs');
const fileUpload = require('express-fileupload');
const cors = require('cors');
const errorHandler = require('../middleware/errorHandler');
const { ExperimentConfig } = require('./config');
class Experiment {
constructor(options = {}) {
this.paint_AIOM_in_CLI();
this.experimentPath = options.experimentPath || process.cwd();
this.config = new ExperimentConfig(this.experimentPath);
this.experiment = this.config.get('experiment');
this.app = express();
this.setupMiddleware();
this.setupResources();
this.setupBasicRoutes();
this.setupExperimentRoutes();
this.setupOrderedExp();
}
paint_AIOM_in_CLI() {
console.log(`
█████╗ ██╗ ██████╗ ███╗ ███╗
██╔══██╗██║██╔═══██╗████╗ ████║
███████║██║██║ ██║██╔████╔██║
██╔══██║██║██║ ██║██║╚██╔╝██║
██║ ██║██║╚██████╔╝██║ ╚═╝ ██║
╚═╝ ╚═╝╚═╝ ╚═════╝ ╚═╝ ╚═╝
`);
}
setupMiddleware() {
this.app.use(cors());
this.app.use(express.json());
this.app.use(express.urlencoded({ extended: true }));
this.app.use(require('cookie-parser')());
this.app.use(fileUpload({
limits: { fileSize: 50 * 1024 * 1024 }, // 50MB limit
useTempFiles: false,
createParentPath: true
}));
this.app.use(errorHandler);
}
setupResources() {
// Serve package static files --- css styles, js scripts, etc.
const packageStatic = path.join(__dirname, '..', 'static');
this.app.use('/pkg-static', express.static(packageStatic));
// Serve experiment static files --- custom text, stimuli, etc.
const customTextDir = path.join(this.experimentPath, 'custom_text');
this.app.use('/exp-static', express.static(customTextDir));
this.app.use('/exp-production-example', express.static(path.join(this.experimentPath, 'static', 'stimuli', 'production_example')));
}
setupBasicRoutes() {
// Base routes that all experiments need
this.app.get('/', this.renderTemplate_prolific.bind(this, 'index'));
this.app.get('/consent', this.renderTemplate.bind(this, 'consent'));
this.app.get('/instruction', this.renderTemplate.bind(this, 'instruction'));
// this.app.get('/waitingroom', this.renderTemplate.bind(this, 'waitingroom'));
this.app.get("/early_stop", this.renderTemplate_prolific.bind(this, 'early_stop'));
this.app.get('/complete_redirect', this.handleCompleteRedirect.bind(this));
}
setupExperimentRoutes() {
const { api_router } = require('./api');
this.api_handler = new api_router(this.app, this.config, this.experimentPath);
}
setupOrderedExp() {
const task_order = this.config.getArray('task_order');
this.app.get("/experiment/:stage", (req, res) => {
const stage = req.params.stage;
if (stage === 'begin') {
this.sendTask(task_order[0], req, res);
} else if (task_order.includes(stage)) {
const currentIndex = task_order.indexOf(stage);
if (currentIndex < task_order.length - 1) {
this.sendTask(task_order[currentIndex + 1], req, res);
} else {
this.renderTemplate_prolific('thanks', req, res);
}
} else {
// Handle invalid stage
console.error(`Invalid stage requested: ${stage}`);
res.status(404).send('Stage not found');
}
});
}
sendTask(task, req, res) {
const templatePath = task === 'main' ?
this.getTemplatePath(this.experiment, 'experiment') :
this.getTemplatePath(task);
res.sendFile(templatePath);
}
handleCompleteRedirect(req, res) {
if (this.config.getBoolean('prolific')) {
require('../controllers/addon/prolific_Controller').complete_redirect(req, res);
} else {
res.status(200).json({ success: true });
}
}
renderTemplate(templateName, req, res) {
const templatePath = this.getTemplatePath(templateName);
res.sendFile(templatePath);
}
renderTemplate_prolific(templateName, req, res) {
const templatePath = this.config.getBoolean('prolific') ?
this.getTemplatePath(templateName + '-prolific') :
this.getTemplatePath(templateName);
res.sendFile(templatePath);
}
getTemplatePath(templateName, fileCat='base') {
// First check experiment directory
const expTemplate = path.join(this.experimentPath, 'public', fileCat, `${templateName}.html`);
// if not exist, Fall back to package template
return fs.existsSync(expTemplate) ? expTemplate :
path.join(__dirname, '..', 'templates', fileCat, `${templateName}.html`);
}
async start(port = 3000) {
try {
const { pool } = require('./database');
const result = await pool.query('SELECT NOW() as current_time, version() as pg_version');
console.log('✅ Database connected successfully');
console.log(`📅 Server time: ${result.rows[0].current_time}`);
console.log(`🐘 PostgreSQL version: ${result.rows[0].pg_version.split(' ')[0]}`);
} catch (error) {
console.error('❌ Database initialization failed:', error.message);
throw error;
}
return this.app.listen(port, () => {
console.log(`🧪 Experiment type: ${this.config.get('experiment')}`);
const test_url = this.config.getBoolean('prolific')
? `🌐 Server: http://localhost:${port}/?PROLIFIC_PID=test${Math.floor(Math.random()*10000)}&STUDY_ID=test${Math.floor(Math.random()*10000)}&SESSION_ID=test${Math.floor(Math.random()*10000)}`
: `🌐 Server: http://localhost:${port}`;
console.log(test_url);
});
}
}
module.exports = { Experiment };