diff --git a/.gitignore b/.gitignore
index 86c151a..8dba10d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,8 @@
.idea
+.meteor
.npm
+node_modules
packages/*
-smart.lock
-versions.json
docs/client/data
diff --git a/.travis.yml b/.travis.yml
index 3c72d80..dc4c680 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,9 @@ before_install:
# Later stuff for Meteor 1.4: see https://github.com/arunoda/travis-ci-meteor-packages/pull/45
# Make sure this matches what is in package.json.
env:
- - CXX=g++-4.8 METEOR_RELEASE=1.4.4.6
+ # barbatus:typescript requires ecmascript and modules from 1.6.x, so we just
+ # use that to run the test
+ - CXX=g++-4.8 METEOR_RELEASE=1.6.1.4
addons:
apt:
sources:
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 0000000..44cc61a
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,8 @@
+{
+ "editor.formatOnSave": true,
+ "files.exclude": {
+ "**/.npm": true, // this excludes all folders from the explore tree
+ "**/node_modules": true,
+ "packages": true // this excludes the folder only from the root
+ }
+}
diff --git a/Contributing.md b/Contributing.md
index 9aabbaf..7f9c7f8 100644
--- a/Contributing.md
+++ b/Contributing.md
@@ -1,14 +1,17 @@
## Code
-This repo is written in ES6 with a `.prettierrc` to auto-format code.
+This repo is written in ES6/TypeScript with a `.prettierrc` to auto-format code.
Initial parts of this codebase were written in Coffeescript. However, ES6
implements many useful functions from Coffeescript while allowing more people to
-read the code and contribute. We [decaffeinated] the repo and in the future may
-convert fully to TypeScript.
+read the code and contribute. We [decaffeinated] the repo then converted to TypeScript.
[decaffeinated]: https://github.com/TurkServer/turkserver-meteor/pull/99
+## Development
+
+`yarn install` will grab `@types/meteor` for developing the package.
+
TODO: set up format or lint hooks with something like AirBnb's [Javascript style
guide](https://github.com/airbnb/javascript).
diff --git a/admin/admin.js b/admin/admin.ts
similarity index 86%
rename from admin/admin.js
rename to admin/admin.ts
index e46da95..9db1a21 100644
--- a/admin/admin.js
+++ b/admin/admin.ts
@@ -8,8 +8,37 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
-// Server admin code
-const isAdmin = userId => userId != null && __guard__(Meteor.users.findOne(userId), x => x.admin);
+
+// Server admin code.
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+import { Accounts } from "meteor/accounts-base";
+
+import {
+ Batches,
+ Experiments,
+ Treatments,
+ Logs,
+ Qualifications,
+ HITTypes,
+ HITs,
+ Workers,
+ Assignments,
+ WorkerEmails,
+ checkAdmin
+} from "../lib/common";
+
+import { Assignment } from "../server/assignment";
+import { Instance } from "../server/instance";
+import { Batch } from "../server/batches";
+import { mturk } from "../server/mturk";
+import { config } from "../server/config";
+
+function isAdmin(userId: string): boolean {
+ if (userId == null) return false;
+ const user = Meteor.users.findOne(userId);
+ return (user && user.admin) || false;
+}
// Only admin gets server facts
Facts.setUserIdFilter(isAdmin);
@@ -211,9 +240,9 @@ const getAndCheckHitType = function(hitTypeId) {
Meteor.methods({
"ts-admin-account-balance"() {
- TurkServer.checkAdmin();
+ checkAdmin();
try {
- return TurkServer.mturk("GetAccountBalance", {});
+ return mturk("GetAccountBalance", {});
} catch (e) {
throw new Meteor.Error(403, e.toString());
}
@@ -221,7 +250,7 @@ Meteor.methods({
// This is the only method that uses the _id field of HITType instead of HITTypeId.
"ts-admin-register-hittype"(hitType_id) {
- TurkServer.checkAdmin();
+ checkAdmin();
// Build up the params to register the HIT Type
const params = HITTypes.findOne(hitType_id);
delete params._id;
@@ -256,7 +285,7 @@ Meteor.methods({
let hitTypeId = null;
try {
- hitTypeId = TurkServer.mturk("RegisterHITType", params);
+ hitTypeId = mturk("RegisterHITType", params);
} catch (e) {
throw new Meteor.Error(500, e.toString());
}
@@ -265,20 +294,20 @@ Meteor.methods({
},
"ts-admin-create-hit"(hitTypeId, params) {
- TurkServer.checkAdmin();
+ checkAdmin();
const hitType = getAndCheckHitType(hitTypeId);
params.HITTypeId = hitType.HITTypeId;
params.Question = `
- ${TurkServer.config.mturk.externalUrl}?batchId=${hitType.batchId}
- ${TurkServer.config.mturk.frameHeight}
+ ${config.mturk.externalUrl}?batchId=${hitType.batchId}
+ ${config.mturk.frameHeight}
\
`;
let hitId = null;
try {
- hitId = TurkServer.mturk("CreateHIT", params);
+ hitId = mturk("CreateHIT", params);
} catch (e) {
throw new Meteor.Error(500, e.toString());
}
@@ -294,12 +323,12 @@ Meteor.methods({
},
"ts-admin-refresh-hit"(HITId) {
- TurkServer.checkAdmin();
+ checkAdmin();
if (!HITId) {
throw new Meteor.Error(400, "HIT ID not specified");
}
try {
- const hitData = TurkServer.mturk("GetHIT", { HITId });
+ const hitData = mturk("GetHIT", { HITId });
HITs.update({ HITId }, { $set: hitData });
} catch (e) {
throw new Meteor.Error(500, e.toString());
@@ -307,12 +336,12 @@ Meteor.methods({
},
"ts-admin-expire-hit"(HITId) {
- TurkServer.checkAdmin();
+ checkAdmin();
if (!HITId) {
throw new Meteor.Error(400, "HIT ID not specified");
}
try {
- const hitData = TurkServer.mturk("ForceExpireHIT", { HITId });
+ const hitData = mturk("ForceExpireHIT", { HITId });
this.unblock(); // If successful, refresh the HIT
Meteor.call("ts-admin-refresh-hit", HITId);
@@ -322,13 +351,13 @@ Meteor.methods({
},
"ts-admin-change-hittype"(params) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(params.HITId, String);
check(params.HITTypeId, String);
// TODO: don't allow change if the old HIT Type has a different batchId from the new one
try {
- TurkServer.mturk("ChangeHITTypeOfHIT", params);
+ mturk("ChangeHITTypeOfHIT", params);
this.unblock(); // If successful, refresh the HIT
Meteor.call("ts-admin-refresh-hit", params.HITId);
} catch (e) {
@@ -337,7 +366,7 @@ Meteor.methods({
},
"ts-admin-extend-hit"(params) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(params.HITId, String);
const hit = HITs.findOne({ HITId: params.HITId });
@@ -345,7 +374,7 @@ Meteor.methods({
getAndCheckHitType(hit.HITTypeId);
try {
- TurkServer.mturk("ExtendHIT", params);
+ mturk("ExtendHIT", params);
this.unblock(); // If successful, refresh the HIT
Meteor.call("ts-admin-refresh-hit", params.HITId);
@@ -355,10 +384,10 @@ Meteor.methods({
},
"ts-admin-lobby-event"(batchId, event) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
- const batch = TurkServer.Batch.getBatch(batchId);
+ const batch = Batch.getBatch(batchId);
if (batch == null) {
throw new Meteor.Error(500, `Batch ${batchId} does not exist`);
}
@@ -368,7 +397,7 @@ Meteor.methods({
"ts-admin-create-message"(subject, message, copyFromId) {
let recipients;
- TurkServer.checkAdmin();
+ checkAdmin();
check(subject, String);
check(message, String);
@@ -384,12 +413,12 @@ Meteor.methods({
},
"ts-admin-send-message"(emailId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(emailId, String);
const email = WorkerEmails.findOne(emailId);
if (email.sentTime != null) {
- throw new Error(403, "Message already sent");
+ throw new Meteor.Error(403, "Message already sent");
}
const { recipients } = email;
@@ -399,7 +428,7 @@ Meteor.methods({
check(recipients, Array);
if (recipients.length === 0) {
- throw new Error(403, "No recipients on e-mail");
+ throw new Meteor.Error(403, "No recipients on e-mail");
}
let count = 0;
@@ -415,7 +444,7 @@ Meteor.methods({
};
try {
- TurkServer.mturk("NotifyWorkers", params);
+ mturk("NotifyWorkers", params);
} catch (e) {
throw new Meteor.Error(500, e.toString());
}
@@ -441,14 +470,14 @@ Meteor.methods({
// TODO implement this
"ts-admin-resend-message"(emailId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(emailId, String);
throw new Meteor.Error(500, "Not implemented");
},
"ts-admin-copy-message"(emailId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(emailId, String);
const email = WorkerEmails.findOne(emailId);
@@ -460,7 +489,7 @@ Meteor.methods({
},
"ts-admin-delete-message"(emailId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(emailId, String);
const email = WorkerEmails.findOne(emailId);
@@ -472,11 +501,11 @@ Meteor.methods({
},
"ts-admin-cleanup-user-state"() {
- TurkServer.checkAdmin();
+ checkAdmin();
// Find all users that are state: experiment but don't have an active assignment
// This shouldn't have to be used in most cases
Meteor.users.find({ "turkserver.state": "experiment" }).map(function(user) {
- if (TurkServer.Assignment.getCurrentUserAssignment(user._id) != null) {
+ if (Assignment.getCurrentUserAssignment(user._id) != null) {
return;
}
return Meteor.users.update(user._id, {
@@ -486,7 +515,7 @@ Meteor.methods({
},
"ts-admin-cancel-assignments"(batchId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
let count = 0;
@@ -496,7 +525,7 @@ Meteor.methods({
if (user.status != null ? user.status.online : undefined) {
return;
}
- const tsAsst = TurkServer.Assignment.getAssignment(asst._id);
+ const tsAsst = Assignment.getAssignment(asst._id);
tsAsst.setReturned();
// if they were disconnected in the middle of an experiment,
@@ -515,7 +544,7 @@ Meteor.methods({
// Refresh all assignments in a batch that are either unknown or submitted
"ts-admin-refresh-assignments"(batchId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
// We may encounter more than one error when refreshing a bunch of
@@ -528,7 +557,7 @@ Meteor.methods({
status: "completed",
mturkStatus: { $in: [null, "Submitted"] }
}).forEach(function(a) {
- const asst = TurkServer.Assignment.getAssignment(a._id);
+ const asst = Assignment.getAssignment(a._id);
// Refresh submitted assignments as they may have been auto-approved
try {
return asst.refreshStatus();
@@ -543,29 +572,29 @@ Meteor.methods({
},
"ts-admin-refresh-assignment"(asstId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(asstId, String);
- TurkServer.Assignment.getAssignment(asstId).refreshStatus();
+ Assignment.getAssignment(asstId).refreshStatus();
},
"ts-admin-approve-assignment"(asstId, msg) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(asstId, String);
- TurkServer.Assignment.getAssignment(asstId).approve(msg);
+ Assignment.getAssignment(asstId).approve(msg);
},
"ts-admin-reject-assignment"(asstId, msg) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(asstId, String);
- TurkServer.Assignment.getAssignment(asstId).reject(msg);
+ Assignment.getAssignment(asstId).reject(msg);
},
// Count number of submitted assignments in a batch
"ts-admin-count-submitted"(batchId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
// First refresh everything
@@ -579,18 +608,18 @@ Meteor.methods({
// Approve all submitted assignments in a batch
"ts-admin-approve-all"(batchId, msg) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
return Assignments.find({
batchId,
mturkStatus: "Submitted"
- }).forEach(asst => TurkServer.Assignment.getAssignment(asst._id).approve(msg));
+ }).forEach(asst => Assignment.getAssignment(asst._id).approve(msg));
},
// Count number of unpaid bonuses in a batch
"ts-admin-count-unpaid-bonuses"(batchId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
// First refresh everything
@@ -616,7 +645,7 @@ Meteor.methods({
// Pay all unpaid bonuses in a batch
"ts-admin-pay-bonuses"(batchId, msg) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
Assignments.find({
@@ -624,18 +653,18 @@ Meteor.methods({
mturkStatus: "Approved",
bonusPayment: { $gt: 0 },
bonusPaid: { $exists: false }
- }).forEach(asst => TurkServer.Assignment.getAssignment(asst._id).payBonus(msg));
+ }).forEach(asst => Assignment.getAssignment(asst._id).payBonus(msg));
},
"ts-admin-unset-bonus"(asstId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(asstId, String);
- return TurkServer.Assignment.getAssignment(asstId).setPayment(null);
+ return Assignment.getAssignment(asstId).setPayment(null);
},
"ts-admin-pay-bonus"(asstId, amount, reason) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(asstId, String);
check(amount, Number);
check(reason, String);
@@ -645,7 +674,7 @@ Meteor.methods({
throw new Meteor.Error(403, `You probably didn't mean to pay ${amount}`);
}
- const asst = TurkServer.Assignment.getAssignment(asstId);
+ const asst = Assignment.getAssignment(asstId);
try {
asst.setPayment(amount);
asst.payBonus(reason);
@@ -655,19 +684,19 @@ Meteor.methods({
},
"ts-admin-stop-experiment"(groupId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(groupId, String);
- TurkServer.Instance.getInstance(groupId).teardown();
+ Instance.getInstance(groupId).teardown();
},
"ts-admin-stop-all-experiments"(batchId) {
- TurkServer.checkAdmin();
+ checkAdmin();
check(batchId, String);
let count = 0;
Experiments.find({ batchId, endTime: { $exists: false } }).map(function(instance) {
- TurkServer.Instance.getInstance(instance._id).teardown();
+ Instance.getInstance(instance._id).teardown();
return count++;
});
@@ -677,7 +706,7 @@ Meteor.methods({
// Create and set up admin user (and password) if not existent
Meteor.startup(function() {
- const adminPw = TurkServer.config != null ? TurkServer.config.adminPassword : undefined;
+ const adminPw = config != null ? config.adminPassword : undefined;
if (adminPw == null) {
Meteor._debug("No admin password found for Turkserver. Please configure it in your settings.");
return;
diff --git a/client/index.ts b/client/index.ts
new file mode 100644
index 0000000..e69de29
diff --git a/lib/common.js b/lib/common.js
deleted file mode 100644
index 27527ad..0000000
--- a/lib/common.js
+++ /dev/null
@@ -1,124 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Sanity-check the conversion and remove this comment.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS103: Rewrite code to no longer use __guard__
- * DS207: Consider shorter variations of null checks
- * DS208: Avoid top-level this
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-// Clean up old index
-if (Meteor.isServer) {
- try {
- RoundTimers._dropIndex("_groupId_1_index_1");
- Meteor._debug("Dropped old non-unique index on RoundTimers");
- } catch (error) {}
-}
-
-TurkServer.partitionCollection(RoundTimers, {
- index: { index: 1 },
- indexOptions: {
- unique: 1,
- dropDups: true,
- name: "_groupId_1_index_1_unique"
- }
-});
-
-this.Workers = new Mongo.Collection("ts.workers");
-this.Assignments = new Mongo.Collection("ts.assignments");
-
-this.WorkerEmails = new Mongo.Collection("ts.workeremails");
-this.Qualifications = new Mongo.Collection("ts.qualifications");
-this.HITTypes = new Mongo.Collection("ts.hittypes");
-this.HITs = new Mongo.Collection("ts.hits");
-
-// Need a global here for export to test code, after updating to Meteor 1.4.
-ErrMsg = {
- // authentication
- unexpectedBatch: "This HIT is not recognized.",
- batchInactive: "This task is currently not accepting new assignments.",
- batchLimit:
- "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment.",
- simultaneousLimit:
- "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first.",
- alreadyCompleted: "You have already completed this HIT.",
- // operations
- authErr: "You are not logged in",
- stateErr: "You can't perform that operation right now",
- notAdminErr: "Not logged in as admin",
- adminErr: "Operation not permitted by admin",
- // Stuff
- usernameTaken: "Sorry, that username is taken.",
- userNotInLobbyErr: "User is not in lobby"
-};
-
-// TODO move this to a more appropriate location
-Meteor.methods({
- "ts-delete-treatment"(id) {
- TurkServer.checkAdmin();
- if (Batches.findOne({ treatmentIds: { $in: [id] } })) {
- throw new Meteor.Error(403, "can't delete treatments that are used by existing batches");
- }
-
- return Treatments.remove(id);
- }
-});
-
-// Helpful functions
-
-// Check if a particular user is an admin.
-// If no user is specified, attempts to check the current user.
-TurkServer.isAdmin = function(userId) {
- if (userId == null) {
- userId = Meteor.userId();
- }
- if (!userId) {
- return false;
- }
- return (
- __guard__(
- Meteor.users.findOne(
- {
- _id: userId,
- admin: { $exists: true }
- },
- {
- fields: {
- admin: 1
- }
- }
- ),
- x => x.admin
- ) || false
- );
-};
-
-TurkServer.checkNotAdmin = function() {
- if (Meteor.isClient) {
- // Don't register reactive dependencies on userId for a client check
- if (Deps.nonreactive(() => TurkServer.isAdmin())) {
- throw new Meteor.Error(403, ErrMsg.adminErr);
- }
- } else {
- if (TurkServer.isAdmin()) {
- throw new Meteor.Error(403, ErrMsg.adminErr);
- }
- }
-};
-
-TurkServer.checkAdmin = function() {
- if (Meteor.isClient) {
- if (!Deps.nonreactive(() => TurkServer.isAdmin())) {
- throw new Meteor.Error(403, ErrMsg.notAdminErr);
- }
- } else {
- if (!TurkServer.isAdmin()) {
- throw new Meteor.Error(403, ErrMsg.notAdminErr);
- }
- }
-};
-
-function __guard__(value, transform) {
- return typeof value !== "undefined" && value !== null ? transform(value) : undefined;
-}
diff --git a/lib/common.ts b/lib/common.ts
new file mode 100644
index 0000000..c19dd75
--- /dev/null
+++ b/lib/common.ts
@@ -0,0 +1,239 @@
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+import { Meteor } from "meteor/meteor";
+import { Mongo } from "meteor/mongo";
+import { Tracker } from "meteor/tracker";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+export interface Batch {
+ _id: string;
+ active: boolean;
+ treatments: string[];
+ // If a worker has returned an assignment, let them take another one
+ allowReturns: true;
+}
+export const Batches = new Mongo.Collection("ts.batches");
+
+/**
+ * @summary The collection of treatments that are available to tag to instances/worlds or user assignments.
+ *
+ * Treatments are objects of the following form:
+ * {
+ * name: "foo",
+ * key1:
+ * key2:
+ * }
+ *
+ * This allows "foo" to be used to assign a treatment to worlds or users, and the values of key1 and key2 are available in TurkServer.treatment() on the client side.
+ */
+export interface Treatment {
+ _id: string;
+ name: string;
+ [key: string]: any;
+}
+export const Treatments = new Mongo.Collection("ts.treatments");
+
+export interface Experiment {
+ _id: string;
+ batchId: string;
+ users: string[];
+ treatments: string[];
+ startTime?: Date;
+ endTime?: Date;
+}
+export const Experiments = new Mongo.Collection("ts.experiments");
+
+export interface ILobbyStatus {
+ _id: string;
+ asstId: string;
+ status: boolean; // Whether the user has clicked "ready"
+}
+export const LobbyStatus = new Mongo.Collection("ts.lobby");
+
+export interface LogEntry {
+ _id: string;
+}
+export const Logs = new Mongo.Collection("ts.logs");
+
+export interface RoundState {
+ _id: string;
+}
+export const RoundTimers = new Mongo.Collection("ts.rounds");
+
+export interface Worker {
+ _id: string;
+}
+export const Workers = new Mongo.Collection("ts.workers");
+
+export type MTurkStatus = "Submitted" | "Approved" | "Rejected";
+
+export type InstanceData = {
+ id: string;
+ joinTime: Date;
+ lastDisconnect?: Date;
+ disconnectedTime?: number;
+ lastIdle?: Date;
+ idleTime?: number;
+};
+
+export interface IAssignment {
+ _id: string;
+ batchId: string;
+ experimentId?: string;
+ hitId: string;
+ workerId: string;
+ assignmentId: string;
+ acceptTime?: Date;
+ instances?: InstanceData[];
+ treatments?: string[];
+ mturkStatus?: MTurkStatus;
+ status?: "assigned" | "completed" | "returned";
+ bonusPaid?: Date;
+ bonusPayment?: number;
+}
+export const Assignments = new Mongo.Collection("ts.assignments");
+
+export interface WorkerMemo {
+ subject: string;
+ message: string;
+ recipients: string[];
+ sentTime?: Date;
+}
+export const WorkerEmails = new Mongo.Collection("ts.workeremails");
+
+// TODO: check if we just get these from the AWS SDK.
+export interface Qualification {
+ _id: string;
+ name: string;
+ LocaleValue?: any;
+}
+export const Qualifications = new Mongo.Collection("ts.qualifications");
+
+export interface HITType {
+ _id: string;
+ batchId: string;
+ // MTurk fields
+ HITTypeId: string;
+ // TODO shim until we replace with aws-sdk
+ Reward: any;
+ QualificationRequirement: any[];
+}
+export const HITTypes = new Mongo.Collection("ts.hittypes");
+
+export interface HIT {
+ HITId: string;
+ HITTypeId: string;
+}
+export const HITs = new Mongo.Collection("ts.hits");
+
+// Need a global here for export to test code, after updating to Meteor 1.4.
+export const ErrMsg = {
+ // authentication
+ unexpectedBatch: "This HIT is not recognized.",
+ batchInactive: "This task is currently not accepting new assignments.",
+ batchLimit:
+ "You've attempted or completed the maximum number of HITs allowed in this group. Please return this assignment.",
+ simultaneousLimit:
+ "You are already connected through another HIT, or you previously returned a HIT from this group. If you still have the HIT open, please complete that one first.",
+ alreadyCompleted: "You have already completed this HIT.",
+ // operations
+ authErr: "You are not logged in",
+ stateErr: "You can't perform that operation right now",
+ notAdminErr: "Not logged in as admin",
+ adminErr: "Operation not permitted by admin",
+ // Stuff
+ usernameTaken: "Sorry, that username is taken.",
+ userNotInLobbyErr: "User is not in lobby"
+};
+
+// TODO move this to a more appropriate location
+Meteor.methods({
+ "ts-delete-treatment"(id) {
+ checkAdmin();
+ if (Batches.findOne({ treatmentIds: { $in: [id] } })) {
+ throw new Meteor.Error(403, "can't delete treatments that are used by existing batches");
+ }
+
+ return Treatments.remove(id);
+ }
+});
+
+// Helpful functions
+
+// Check if a particular user is an admin.
+// If no user is specified, attempts to check the current user.
+export function isAdmin(userId = Meteor.userId()): boolean {
+ if (userId == null) return false;
+
+ const user = Meteor.users.findOne(
+ { _id: userId, admin: { $exists: true } },
+ { fields: { admin: 1 } }
+ );
+ return (user && user.admin) || false;
+}
+
+export function checkNotAdmin() {
+ if (Meteor.isClient) {
+ // Don't register reactive dependencies on userId for a client check
+ if (Tracker.nonreactive(() => isAdmin())) {
+ throw new Meteor.Error(403, ErrMsg.adminErr);
+ }
+ } else {
+ if (isAdmin()) {
+ throw new Meteor.Error(403, ErrMsg.adminErr);
+ }
+ }
+}
+
+export function checkAdmin() {
+ if (Meteor.isClient) {
+ if (!Tracker.nonreactive(() => isAdmin())) {
+ throw new Meteor.Error(403, ErrMsg.notAdminErr);
+ }
+ } else {
+ if (!isAdmin()) {
+ throw new Meteor.Error(403, ErrMsg.notAdminErr);
+ }
+ }
+}
+
+/**
+ * @summary The global object containing all TurkServer functions.
+ * @namespace
+ */
+export const TurkServer = {
+ group: Partitioner.group,
+ partitionCollection: Partitioner.partitionCollection
+};
+
+/**
+ * @summary Get the current group (partition) of the environment.
+ * @locus Anywhere
+ * @function
+ * @returns {String} The group id.
+ */
+TurkServer.group = Partitioner.group;
+
+/**
+ * @summary Partition a collection for use across instances.
+ * @locus Server
+ * @param {Meteor.Collection} collection The collection to partition.
+ * @function
+ */
+TurkServer.partitionCollection = Partitioner.partitionCollection;
+
+TurkServer.partitionCollection(RoundTimers, {
+ index: { index: 1 },
+ indexOptions: {
+ unique: 1,
+ dropDups: true,
+ name: "_groupId_1_index_1_unique"
+ }
+});
diff --git a/lib/shared.js b/lib/shared.js
deleted file mode 100644
index 6a0a907..0000000
--- a/lib/shared.js
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * @summary The global object containing all TurkServer functions.
- * @namespace
- */
-TurkServer = TurkServer || {};
-
-TestUtils = TestUtils || {};
-
-Batches = new Mongo.Collection("ts.batches");
-
-/**
- * @summary The collection of treatments that are available to tag to instances/worlds or user assignments.
- *
- * Treatments are objects of the following form:
- * {
- * name: "foo",
- * key1:
- * key2:
- * }
- *
- * This allows "foo" to be used to assign a treatment to worlds or users, and the values of key1 and key2 are available in TurkServer.treatment() on the client side.
- */
-Treatments = new Mongo.Collection("ts.treatments");
-Experiments = new Mongo.Collection("ts.experiments");
-
-LobbyStatus = new Mongo.Collection("ts.lobby");
-Logs = new Mongo.Collection("ts.logs");
-
-RoundTimers = new Mongo.Collection("ts.rounds");
-
-/**
- * @summary Get the current group (partition) of the environment.
- * @locus Anywhere
- * @function
- * @returns {String} The group id.
- */
-TurkServer.group = Partitioner.group;
-
-/**
- * @summary Partition a collection for use across instances.
- * @locus Server
- * @param {Meteor.Collection} collection The collection to partition.
- * @function
- */
-TurkServer.partitionCollection = Partitioner.partitionCollection;
diff --git a/lib/util.js b/lib/util.ts
similarity index 85%
rename from lib/util.js
rename to lib/util.ts
index 3750df8..fca1f18 100644
--- a/lib/util.js
+++ b/lib/util.ts
@@ -9,12 +9,10 @@
/*
Server/client util files
*/
+import * as moment from "moment";
+import * as _ from "underscore";
-if (TurkServer.Util == null) {
- TurkServer.Util = {};
-}
-
-TurkServer.Util.formatMillis = function(millis) {
+export function formatMillis(millis) {
if (millis == null) {
return;
} // Can be 0 in which case we should render it
@@ -23,13 +21,13 @@ TurkServer.Util.formatMillis = function(millis) {
const time = diff.format("H:mm:ss");
const days = +diff.format("DDD") - 1;
return (negative ? "-" : "") + (days ? days + "d " : "") + time;
-};
+}
-TurkServer._mergeTreatments = function(arr) {
+export function _mergeTreatments(arr) {
const fields = { treatments: [] };
arr.forEach(function(treatment) {
fields.treatments.push(treatment.name);
return _.extend(fields, _.omit(treatment, "_id", "name"));
});
return fields;
-};
+}
diff --git a/package.js b/package.js
index 7c0779a..b8225aa 100644
--- a/package.js
+++ b/package.js
@@ -8,12 +8,23 @@ Package.describe({
Npm.depends({
"mturk-api": "1.3.2",
jspath: "0.3.2",
- deepmerge: "0.2.7" // For merging config parameters
+ deepmerge: "0.2.7", // For merging config parameters
+ // For TS/ES interpretation
+ "@babel/runtime": "7.7.2"
});
Package.onUse(function(api) {
api.versionsFrom("1.4.4.6");
+ // TypeScript support
+ // Modules: https://docs.meteor.com/v1.4/packages/modules.html
+ api.use("modules");
+ api.use("ecmascript");
+ // Should be replaced with straight up built-in 'typescript' in Meteor 1.8.2
+ // adornis:typescript from [1.4, 1.8)
+ // api.use("adornis:typescript@0.8.1");
+ api.use("barbatus:typescript@0.7.0");
+
// Client-only deps
api.use(["session", "ui", "templating", "reactive-var"], "client");
@@ -27,8 +38,7 @@ Package.onUse(function(api) {
"ejson",
"jquery",
"random",
- "underscore",
- "ecmascript",
+ "underscore", // TODO remove
"facts"
]);
@@ -57,25 +67,26 @@ Package.onUse(function(api) {
api.use("mizzao:user-status@0.6.5");
// Shared files
- api.addFiles(["lib/shared.js", "lib/common.js", "lib/util.js"]);
+ api.addFiles(["lib/common.ts", "lib/util.ts"]);
// Server files
api.addFiles(
[
- "server/config.js",
- "server/turkserver.js",
- "server/server_api.js",
- "server/mturk.js",
- "server/lobby_server.js",
- "server/batches.js",
- "server/instance.js",
- "server/logging.js",
- "server/assigners.js",
- "server/assigners_extra.js",
- "server/assignment.js",
- "server/connections.js",
- "server/timers_server.js",
- "server/accounts_mturk.js"
+ "server/config.ts",
+ "server/turkserver.ts",
+ "server/server_api.ts",
+ "server/mturk.ts",
+ "server/lobby_server.ts",
+ "server/batches.ts",
+ "server/instance.ts",
+ "server/logging.ts",
+ "server/assigners.ts",
+ "server/assigners_extra.ts",
+ "server/assignment.ts",
+ "server/connections.ts",
+ "server/timers_server.ts",
+ "server/accounts_mturk.ts",
+ "admin/admin.ts"
],
"server"
);
@@ -117,7 +128,8 @@ Package.onUse(function(api) {
"client"
);
- api.addFiles("admin/admin.js", "server");
+ api.mainModule("server/index.ts", "server");
+ api.mainModule("client/index.ts", "client");
api.export(["TurkServer"]);
@@ -131,12 +143,19 @@ Package.onUse(function(api) {
});
Package.onTest(function(api) {
+ // Need these specific versions for tests to agree to run
+ api.use("modules");
+ api.use("ecmascript");
+
+ // For compiling TS
+ api.use("barbatus:typescript");
+ // api.use("adornis:typescript");
+
api.use([
"accounts-base",
"accounts-password",
"check",
"deps",
- "ecmascript",
"mongo",
"random",
"ui",
@@ -155,16 +174,16 @@ Package.onTest(function(api) {
api.addFiles("tests/display_fix.css");
- api.addFiles("tests/utils.js"); // Deletes users so do it before insecure login
- api.addFiles("tests/insecure_login.js");
+ api.addFiles("tests/utils.ts"); // Deletes users so do it before insecure login
+ api.addFiles("tests/insecure_login.ts");
- api.addFiles("tests/lobby_tests.js");
- api.addFiles("tests/admin_tests.js", "server");
- api.addFiles("tests/auth_tests.js", "server");
- api.addFiles("tests/connection_tests.js", "server");
- api.addFiles("tests/experiment_tests.js", "server");
+ api.addFiles("tests/lobby_tests.ts");
+ api.addFiles("tests/admin_tests.ts", "server");
+ api.addFiles("tests/auth_tests.ts", "server");
+ api.addFiles("tests/connection_tests.ts", "server");
+ api.addFiles("tests/experiment_tests.ts", "server");
api.addFiles("tests/experiment_client_tests.js");
- api.addFiles("tests/timer_tests.js", "server");
+ api.addFiles("tests/timer_tests.ts", "server");
api.addFiles("tests/logging_tests.js");
// This goes after experiment tests, so we can be sure that assigning works
api.addFiles("tests/assigner_tests.js", "server");
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..1cef828
--- /dev/null
+++ b/package.json
@@ -0,0 +1,8 @@
+{
+ "devDependencies": {
+ "@types/meteor": "^1.4.36",
+ "@types/moment": "^2.13.0",
+ "@types/node": "^12.12.6",
+ "@types/underscore": "^1.9.3"
+ }
+}
diff --git a/server/accounts_mturk.js b/server/accounts_mturk.ts
similarity index 89%
rename from server/accounts_mturk.js
rename to server/accounts_mturk.ts
index 6c44fef..3c09c7f 100644
--- a/server/accounts_mturk.js
+++ b/server/accounts_mturk.ts
@@ -1,3 +1,10 @@
+import { Meteor } from "meteor/meteor";
+import { Accounts } from "meteor/accounts-base";
+
+import { Assignments, HITs, HITTypes, ErrMsg, Batches } from "../lib/common";
+import { config } from "./config";
+import { Assignment } from "./assignment";
+
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
@@ -41,11 +48,11 @@ Accounts.validateLoginAttempt(function(info) {
Authenticate a worker taking an assignment.
Returns an assignment object corresponding to the assignment.
*/
-const authenticateWorker = function(loginRequest) {
+export function authenticateWorker(loginRequest) {
const { batchId, hitId, assignmentId, workerId } = loginRequest;
// check if batchId is correct except for testing logins
- if (!loginRequest.test && !TurkServer.config.hits.acceptUnknownHits) {
+ if (!loginRequest.test && !config.hits.acceptUnknownHits) {
const hit = HITs.findOne({
HITId: hitId
});
@@ -79,7 +86,7 @@ const authenticateWorker = function(loginRequest) {
if (existing) {
// Was a different account in progress?
- const existingAsst = TurkServer.Assignment.getAssignment(existing._id);
+ const existingAsst = Assignment.getAssignment(existing._id);
if (workerId === existing.workerId) {
// Worker has already logged in to this HIT, no need to create record below
return existingAsst;
@@ -104,7 +111,7 @@ const authenticateWorker = function(loginRequest) {
Assignments.find({
workerId,
status: { $nin: ["completed", "returned"] }
- }).count() >= TurkServer.config.experiment.limit.simultaneous
+ }).count() >= config.experiment.limit.simultaneous
) {
throw new Meteor.Error(403, ErrMsg.simultaneousLimit);
}
@@ -119,13 +126,13 @@ const authenticateWorker = function(loginRequest) {
predicate.status = { $ne: "returned" };
}
- if (Assignments.find(predicate).count() >= TurkServer.config.experiment.limit.batch) {
+ if (Assignments.find(predicate).count() >= config.experiment.limit.batch) {
throw new Meteor.Error(403, ErrMsg.batchLimit);
}
// Either no one has this assignment before or this worker replaced someone;
// Create a new record for this worker on this assignment
- return TurkServer.Assignment.createAssignment({
+ return Assignment.createAssignment({
batchId,
hitId: loginRequest.hitId,
assignmentId: loginRequest.assignmentId,
@@ -133,7 +140,7 @@ const authenticateWorker = function(loginRequest) {
acceptTime: new Date(),
status: "assigned"
});
-};
+}
Accounts.registerLoginHandler("mturk", function(loginRequest) {
// Don't handle unless we have an mturk login
@@ -170,6 +177,3 @@ Accounts.registerLoginHandler("mturk", function(loginRequest) {
userId
};
});
-
-// Test exports
-TestUtils.authenticateWorker = authenticateWorker;
diff --git a/server/assigners.js b/server/assigners.ts
similarity index 83%
rename from server/assigners.js
rename to server/assigners.ts
index bf2410f..51fc689 100644
--- a/server/assigners.js
+++ b/server/assigners.ts
@@ -1,3 +1,11 @@
+import * as _ from "underscore";
+
+import { Batch } from "./batches";
+import { Lobby } from "./lobby_server";
+import { Experiments } from "../lib/common";
+import { Instance } from "./instance";
+import { Assignment } from "./assignment";
+
/**
* @summary Top-level class that determines flow of users in and out of the
* lobby. Overriding functions on this class controls how users are grouped
@@ -6,7 +14,10 @@
* @class
* @instancename assigner
*/
-class Assigner {
+export abstract class Assigner {
+ batch: Batch;
+ lobby: Lobby;
+
/**
* @summary Initialize this assigner for a particular batch. This should set up the assigner's internal state, including reconstructing state after a server restart.
* @param {String} batch The {@link TurkServer.Batch} object to initialize this assigner on.
@@ -42,7 +53,7 @@ class Assigner {
* @summary Function that is called when a user enters the lobby, either from the initial entry or after returning from a world.
* @param asst The user assignment {@link TurkServer.Assignment} (session) that just entered the lobby.
*/
- userJoined(asst) {}
+ userJoined?(asst: Assignment);
/**
* @summary Function that is called when the status of a user in the lobby changes (such as the user changing from not ready to ready.)
@@ -50,39 +61,31 @@ class Assigner {
* changed status.
* @param newStatus
*/
- userStatusChanged(asst, newStatus) {}
+ userStatusChanged?(asst: Assignment, newStatus: boolean);
/**
* @summary Function that is called when a user disconnects from the lobby. This is only triggered by users losing connectivity, not from being assigned to a new instance).
* @param asst The user assignment {@link TurkServer.Assignment} that departed.
*/
- userLeft(asst) {}
+ userLeft?(asst: Assignment);
}
-TurkServer.Assigner = Assigner;
-
-/**
- * @summary Pre-implemented assignment mechanisms.
- * @name Assigners
- */
-TurkServer.Assigners = {};
-
/**
* @summary Basic assigner that simulates a standalone app.
* It puts everyone who joins into a single group.
* Once the instance ends, puts users in exit survey.
* @class
- * @memberof Assigners
- * @alias TestAssigner
*/
-TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner {
+export class TestAssigner extends Assigner {
+ instance: Instance;
+
initialize(batch) {
super.initialize(batch);
const exp = Experiments.findOne({ batchId: this.batch.batchId });
// Take any experiment from this batch, creating it if it doesn't exist
if (exp != null) {
- this.instance = TurkServer.Instance.getInstance(exp._id);
+ this.instance = Instance.getInstance(exp._id);
} else {
// TODO: refactor once batch treatments are separated from instance
// treatments
@@ -91,7 +94,7 @@ TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner {
}
}
- userJoined(asst) {
+ userJoined(asst: Assignment) {
if (asst.getInstances().length > 0) {
this.lobby.pluckUsers([asst.userId]);
asst.showExitSurvey();
@@ -100,16 +103,14 @@ TurkServer.Assigners.TestAssigner = class extends TurkServer.Assigner {
this.lobby.pluckUsers([asst.userId]);
}
}
-};
+}
/**
* @summary Assigns everyone who joins in a separate group
* Anyone who is done with their instance goes into the exit survey
* @class
- * @memberof Assigners
- * @alias SimpleAssigner
*/
-TurkServer.Assigners.SimpleAssigner = class extends TurkServer.Assigner {
+export class SimpleAssigner extends Assigner {
userJoined(asst) {
if (asst.getInstances().length > 0) {
this.lobby.pluckUsers([asst.userId]);
@@ -119,7 +120,7 @@ TurkServer.Assigners.SimpleAssigner = class extends TurkServer.Assigner {
this.assignToNewInstance([asst], treatments);
}
}
-};
+}
/************************************************************************
* The assigners below are examples of different types of functionality.
@@ -128,7 +129,9 @@ TurkServer.Assigners.SimpleAssigner = class extends TurkServer.Assigner {
/*
Allows people to opt in after reaching a certain threshold.
*/
-TurkServer.Assigners.ThresholdAssigner = class extends TurkServer.Assigner {
+export class ThresholdAssigner extends Assigner {
+ readonly groupSize: number;
+
constructor(groupSize) {
super();
this.groupSize = groupSize;
@@ -146,23 +149,27 @@ TurkServer.Assigners.ThresholdAssigner = class extends TurkServer.Assigner {
const treatment = _.sample(this.batch.getTreatments());
this.assignToNewInstance(readyAssts, [treatment]);
}
-};
+}
/*
Assigns users to groups in a randomized, round-robin fashion
as soon as the join the lobby
*/
-TurkServer.Assigners.RoundRobinAssigner = class extends TurkServer.Assigner {
+export class RoundRobinAssigner extends Assigner {
+ readonly instanceIds: string[];
+ readonly instances: Instance[];
+
constructor(instanceIds) {
super();
this.instanceIds = instanceIds;
+ this.instances = [];
// Create instances if they don't exist
for (let instanceId of this.instanceIds) {
let instance;
try {
- instance = TurkServer.Instance.getInstance(instanceId);
+ instance = Instance.getInstance(instanceId);
} catch (err) {
// TODO pick treatments when creating instances
instance = this.batch.createInstance();
@@ -180,12 +187,15 @@ TurkServer.Assigners.RoundRobinAssigner = class extends TurkServer.Assigner {
this.lobby.pluckUsers([asst.userId]);
minUserInstance.addAssignment(asst);
}
-};
+}
/*
Assign users to fixed size experiments sequentially, as they arrive
*/
-TurkServer.Assigners.SequentialAssigner = class extends TurkServer.Assigner {
+export class SequentialAssigner extends Assigner {
+ readonly groupSize: number;
+ instance: Instance;
+
constructor(groupSize, instance) {
super();
this.groupSize = groupSize;
@@ -196,7 +206,7 @@ TurkServer.Assigners.SequentialAssigner = class extends TurkServer.Assigner {
userJoined(asst) {
if (this.instance.users().length >= this.groupSize) {
// Create a new instance, replacing the one we are holding
- const treatment = _.sample(this.batch.getTreatments());
+ const treatment: string = _.sample(this.batch.getTreatments());
this.instance = this.batch.createInstance([treatment]);
this.instance.setup();
}
@@ -204,4 +214,4 @@ TurkServer.Assigners.SequentialAssigner = class extends TurkServer.Assigner {
this.lobby.pluckUsers([asst.userId]);
this.instance.addAssignment(asst);
}
-};
+}
diff --git a/server/assigners_extra.js b/server/assigners_extra.ts
similarity index 96%
rename from server/assigners_extra.js
rename to server/assigners_extra.ts
index 3470015..0731e99 100644
--- a/server/assigners_extra.js
+++ b/server/assigners_extra.ts
@@ -1,3 +1,11 @@
+import * as _ from "underscore";
+
+import { Experiments } from "../lib/common";
+import { Assigner } from "./assigners";
+import { Assignment } from "./assignment";
+import { ensureTreatmentExists } from "./batches";
+import { Instance } from "./instance";
+
/*
This file contains more involved assigners used in actual experiments.
You can see the app code at https://github.com/TurkServer
@@ -8,7 +16,13 @@
* Assigns users first to a tutorial treatment, then to a single group.
* An event on the lobby is used to trigger the group.
*/
-TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner {
+export class TutorialGroupAssigner extends Assigner {
+ readonly tutorialTreatments: string[];
+ readonly groupTreatments: string[];
+ autoAssign: boolean;
+
+ instance: Instance;
+
constructor(tutorialTreatments, groupTreatments, autoAssign = false) {
super();
@@ -38,7 +52,7 @@ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner {
);
if (exp != null) {
- this.instance = TurkServer.Instance.getInstance(exp._id);
+ this.instance = Instance.getInstance(exp._id);
console.log("Auto-assigning to existing instance " + this.instance.groupId);
this.autoAssign = true;
}
@@ -88,11 +102,11 @@ TurkServer.Assigners.TutorialGroupAssigner = class extends TurkServer.Assigner {
this.instance.addAssignment(asst);
}
}
-};
+}
function ensureGroupTreatments(sizeArray) {
for (let size of _.uniq(sizeArray)) {
- TurkServer.ensureTreatmentExists({
+ ensureTreatmentExists({
name: "group_" + size,
groupSize: size
});
@@ -110,7 +124,11 @@ function ensureGroupTreatments(sizeArray) {
This was created for executing the crisis mapping experiment.
*/
-TurkServer.Assigners.TutorialRandomizedGroupAssigner = class TutorialRandomizedGroupAssigner extends TurkServer.Assigner {
+export class TutorialRandomizedGroupAssigner extends Assigner {
+ tutorialTreatments: string[];
+ groupTreatments: string[];
+ autoAssign: boolean;
+
static generateConfig(sizeArray, otherTreatments) {
ensureGroupTreatments(sizeArray);
@@ -386,7 +404,7 @@ TurkServer.Assigners.TutorialRandomizedGroupAssigner = class TutorialRandomizedG
this.lobby.pluckUsers([asst.userId]);
instance.addAssignment(asst);
}
-};
+}
/*
Assign people to a tutorial treatment and then sequentially to different sized
@@ -397,7 +415,7 @@ TurkServer.Assigners.TutorialRandomizedGroupAssigner = class TutorialRandomizedG
After the last group is filled, there is no more assignment.
*/
-TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssigner extends TurkServer.Assigner {
+export class TutorialMultiGroupAssigner extends Assigner {
static generateConfig(sizeArray, otherTreatments) {
ensureGroupTreatments(sizeArray);
@@ -601,4 +619,4 @@ TurkServer.Assigners.TutorialMultiGroupAssigner = class TutorialMultiGroupAssign
return instance;
}
-};
+}
diff --git a/server/assignment.js b/server/assignment.ts
similarity index 95%
rename from server/assignment.js
rename to server/assignment.ts
index 65fe291..a22ad94 100644
--- a/server/assignment.js
+++ b/server/assignment.ts
@@ -1,3 +1,10 @@
+import { Meteor } from "meteor/meteor";
+import { check, Match } from "meteor/check";
+
+import { Assignments, Experiments, ErrMsg, Workers } from "../lib/common";
+import { Batch } from "./batches";
+import { mturk } from "./mturk";
+
const _assignments = {};
const _userAssignments = {};
@@ -21,7 +28,16 @@ Assignments.find({ status: "assigned" }, { fields: { workerId: 1 } }).observe({
* @class
* @instancename assignment
*/
-class Assignment {
+export class Assignment {
+ userId: string;
+ asstId: string;
+ batchId: string;
+
+ // MTurk strings
+ hitId: string;
+ assignmentId: string;
+ workerId: string;
+
static createAssignment(data) {
const asstId = Assignments.insert(data);
return (_assignments[asstId] = new Assignment(asstId, data));
@@ -137,7 +153,7 @@ class Assignment {
* @returns {TurkServer.Batch} The assignment's batch.
*/
getBatch() {
- return TurkServer.Batch.getBatch(this.batchId);
+ return Batch.getBatch(this.batchId);
}
/**
@@ -152,7 +168,7 @@ class Assignment {
* @summary Add one or more treatments to a user's assignment. These treatments will be available on the client side through TurkServer.treatment()
* @param {String | String[]} String or list of strings corresponding to treatments to associate to the user
*/
- addTreatment(names) {
+ addTreatment(names: string | string[]): void {
check(names, Match.OneOf(String, [String]));
/*
@@ -162,7 +178,7 @@ class Assignment {
treatments: [ "foo, "bar" ]
}
*/
- if (_.isArray(names)) {
+ if (Array.isArray(names)) {
Assignments.update(this.asstId, {
$addToSet: { treatments: { $each: names } }
});
@@ -335,7 +351,7 @@ class Assignment {
let asstData;
try {
- asstData = TurkServer.mturk("GetAssignment", {
+ asstData = mturk("GetAssignment", {
AssignmentId: this.assignmentId
});
} catch (e) {
@@ -386,7 +402,7 @@ class Assignment {
check(message, String);
this._checkSubmittedStatus();
- TurkServer.mturk("ApproveAssignment", {
+ mturk("ApproveAssignment", {
AssignmentId: this.assignmentId,
RequesterFeedback: message
});
@@ -410,7 +426,7 @@ class Assignment {
check(message, String);
this._checkSubmittedStatus();
- TurkServer.mturk("RejectAssignment", {
+ mturk("RejectAssignment", {
AssignmentId: this.assignmentId,
RequesterFeedback: message
});
@@ -438,7 +454,7 @@ class Assignment {
throw new Error("Bonus already paid");
}
- TurkServer.mturk("GrantBonus", {
+ mturk("GrantBonus", {
WorkerId: data.workerId,
AssignmentId: data.assignmentId,
BonusAmount: {
@@ -579,7 +595,7 @@ class Assignment {
// Record a disconnect time if we are currently part of an instance
const now = new Date();
- updateObj = {
+ const updateObj = {
$set: {
"instances.$.lastDisconnect": now
}
@@ -656,7 +672,7 @@ class Assignment {
// TODO test that these are grabbing the right numbers
_getLastDisconnect(instanceId) {
const instances = this.getInstances();
- const instanceData = _.find(instances, inst => {
+ const instanceData = instances.find(inst => {
return inst.id === instanceId;
});
return instanceData && instanceData.lastDisconnect;
@@ -664,7 +680,7 @@ class Assignment {
_getLastIdle(instanceId) {
const instances = this.getInstances();
- const instanceData = _.find(instances, inst => {
+ const instanceData = instances.find(inst => {
return inst.id === instanceId;
});
return instanceData && instanceData.lastIdle;
@@ -696,5 +712,3 @@ function addResetIdleUpdateFields(obj, idleDurationMillis) {
obj.$unset["instances.$.lastIdle"] = null;
return obj;
}
-
-TurkServer.Assignment = Assignment;
diff --git a/server/batches.js b/server/batches.js
deleted file mode 100644
index 0b302ce..0000000
--- a/server/batches.js
+++ /dev/null
@@ -1,108 +0,0 @@
-// TODO: This file was created by bulk-decaffeinate.
-// Sanity-check the conversion and remove this comment.
-/*
- * decaffeinate suggestions:
- * DS102: Remove unnecessary code created because of implicit returns
- * DS206: Consider reworking classes to avoid initClass
- * DS207: Consider shorter variations of null checks
- * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
- */
-(function() {
- let _batches = undefined;
- const Cls = (TurkServer.Batch = class Batch {
- static initClass() {
- _batches = {};
- }
-
- static getBatch(batchId) {
- let batch;
- check(batchId, String);
- if ((batch = _batches[batchId]) != null) {
- return batch;
- } else {
- if (Batches.findOne(batchId) == null) {
- throw new Error("Batch does not exist");
- }
- // Return this if another Fiber created it while we yielded
- return _batches[batchId] != null
- ? _batches[batchId]
- : (_batches[batchId] = new Batch(batchId));
- }
- }
-
- static getBatchByName(batchName) {
- check(batchName, String);
- const batch = Batches.findOne({ name: batchName });
- if (!batch) {
- throw new Error("Batch does not exist");
- }
- return this.getBatch(batch._id);
- }
-
- static currentBatch() {
- let userId;
- if ((userId = Meteor.userId()) == null) {
- return;
- }
- return TurkServer.Assignment.getCurrentUserAssignment(userId).getBatch();
- }
-
- constructor(batchId) {
- this.batchId = batchId;
- if (_batches[this.batchId] != null) {
- throw new Error("Batch already exists; use getBatch");
- }
- this.lobby = new TurkServer.Lobby(this.batchId);
- }
-
- // Creating an instance does not set it up, or initialize the start time.
- createInstance(treatmentNames, fields) {
- fields = _.extend(fields || {}, {
- batchId: this.batchId,
- treatments: treatmentNames || []
- });
-
- const groupId = Experiments.insert(fields);
-
- // To prevent bugs if the instance is referenced before this returns, we
- // need to go through getInstance.
- const instance = TurkServer.Instance.getInstance(groupId);
-
- instance.bindOperation(() =>
- TurkServer.log({
- _meta: "created"
- })
- );
-
- return instance;
- }
-
- getTreatments() {
- return Batches.findOne(this.batchId).treatments;
- }
-
- setAssigner(assigner) {
- if (this.assigner != null) {
- throw new Error("Assigner already set for this batch");
- }
- this.assigner = assigner;
- return assigner.initialize(this);
- }
- });
- Cls.initClass();
- return Cls;
-})();
-
-TurkServer.ensureBatchExists = function(props) {
- if (props.name == null) {
- throw new Error("Batch must have a name");
- }
- return Batches.upsert({ name: props.name }, props);
-};
-
-TurkServer.ensureTreatmentExists = function(props) {
- if (props.name == null) {
- throw new Error("Treatment must have a name");
- }
- return Treatments.upsert({ name: props.name }, props);
-};
diff --git a/server/batches.ts b/server/batches.ts
new file mode 100644
index 0000000..f330ef8
--- /dev/null
+++ b/server/batches.ts
@@ -0,0 +1,120 @@
+import * as _ from "underscore";
+
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+
+import { Batches, Treatments, Experiments } from "../lib/common";
+import { Assignment } from "./assignment";
+import { Instance } from "./instance";
+
+import { Lobby } from "./lobby_server";
+import { Assigner } from "./assigners";
+import { log } from "./logging";
+
+// TODO: This file was created by bulk-decaffeinate.
+// Sanity-check the conversion and remove this comment.
+/*
+ * decaffeinate suggestions:
+ * DS102: Remove unnecessary code created because of implicit returns
+ * DS206: Consider reworking classes to avoid initClass
+ * DS207: Consider shorter variations of null checks
+ * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
+ */
+
+// I think this is just an in-memory map.
+const _batches = {};
+
+export class Batch {
+ batchId: string;
+ lobby: Lobby;
+ assigner: Assigner;
+
+ static getBatch(batchId) {
+ let batch;
+ check(batchId, String);
+ if ((batch = _batches[batchId]) != null) {
+ return batch;
+ } else {
+ if (Batches.findOne(batchId) == null) {
+ throw new Error("Batch does not exist");
+ }
+ // Return this if another Fiber created it while we yielded
+ return _batches[batchId] != null
+ ? _batches[batchId]
+ : (_batches[batchId] = new Batch(batchId));
+ }
+ }
+
+ static getBatchByName(batchName) {
+ check(batchName, String);
+ const batch = Batches.findOne({ name: batchName });
+ if (!batch) {
+ throw new Error("Batch does not exist");
+ }
+ return this.getBatch(batch._id);
+ }
+
+ static currentBatch() {
+ let userId;
+ if ((userId = Meteor.userId()) == null) {
+ return;
+ }
+ return Assignment.getCurrentUserAssignment(userId).getBatch();
+ }
+
+ constructor(batchId) {
+ this.batchId = batchId;
+ if (_batches[this.batchId] != null) {
+ throw new Error("Batch already exists; use getBatch");
+ }
+ this.lobby = new Lobby(this.batchId);
+ }
+
+ // Creating an instance does not set it up, or initialize the start time.
+ createInstance(treatmentNames: string[] = [], fields = {}) {
+ fields = _.extend(fields, {
+ batchId: this.batchId,
+ treatments: treatmentNames || []
+ });
+
+ const groupId = Experiments.insert(fields);
+
+ // To prevent bugs if the instance is referenced before this returns, we
+ // need to go through getInstance.
+ const instance = Instance.getInstance(groupId);
+
+ instance.bindOperation(() =>
+ log({
+ _meta: "created"
+ })
+ );
+
+ return instance;
+ }
+
+ getTreatments() {
+ return Batches.findOne(this.batchId).treatments;
+ }
+
+ setAssigner(assigner) {
+ if (this.assigner != null) {
+ throw new Error("Assigner already set for this batch");
+ }
+ this.assigner = assigner;
+ return assigner.initialize(this);
+ }
+}
+
+export function ensureBatchExists(props) {
+ if (props.name == null) {
+ throw new Error("Batch must have a name");
+ }
+ return Batches.upsert({ name: props.name }, props);
+}
+
+export function ensureTreatmentExists(props) {
+ if (props.name == null) {
+ throw new Error("Treatment must have a name");
+ }
+ return Treatments.upsert({ name: props.name }, props);
+}
diff --git a/server/config.js b/server/config.ts
similarity index 78%
rename from server/config.js
rename to server/config.ts
index 7d712c2..d89d0b9 100644
--- a/server/config.js
+++ b/server/config.ts
@@ -1,5 +1,7 @@
-const os = Npm.require("os");
-const merge = Npm.require("deepmerge");
+import * as os from "os";
+import * as merge from "deepmerge";
+
+import { Meteor } from "meteor/meteor";
// Client-side default settings, for reference
const defaultPublicSettings = {
@@ -30,4 +32,5 @@ const defaultSettings = {
// Read and merge settings on startup
let inputSettings = Meteor.settings && Meteor.settings.turkserver;
-TurkServer.config = merge(defaultSettings, inputSettings || {});
+
+export const config = merge(defaultSettings, inputSettings || {});
diff --git a/server/connections.js b/server/connections.ts
similarity index 84%
rename from server/connections.js
rename to server/connections.ts
index d66ede9..5560b03 100644
--- a/server/connections.js
+++ b/server/connections.ts
@@ -8,7 +8,17 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
-const attemptCallbacks = (callbacks, context, errMsg) =>
+import { Meteor } from "meteor/meteor";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+import { UserStatus } from "meteor/mizzao:user-status";
+
+import { ErrMsg } from "../lib/common";
+import { Assignment } from "./assignment";
+import { Instance } from "./instance";
+import { Accounts } from "meteor/accounts-base";
+
+const attemptCallbacks = (callbacks: Function[], context, errMsg) =>
Array.from(callbacks).map(cb =>
(() => {
try {
@@ -19,15 +29,23 @@ const attemptCallbacks = (callbacks, context, errMsg) =>
})()
);
-const connectCallbacks = [];
-const disconnectCallbacks = [];
-const idleCallbacks = [];
-const activeCallbacks = [];
-
-TurkServer.onConnect = func => connectCallbacks.push(func);
-TurkServer.onDisconnect = func => disconnectCallbacks.push(func);
-TurkServer.onIdle = func => idleCallbacks.push(func);
-TurkServer.onActive = func => activeCallbacks.push(func);
+const connectCallbacks: Function[] = [];
+const disconnectCallbacks: Function[] = [];
+const idleCallbacks: Function[] = [];
+const activeCallbacks: Function[] = [];
+
+export function onConnect(func: Function) {
+ connectCallbacks.push(func);
+}
+export function onDisconnect(func: Function) {
+ disconnectCallbacks.push(func);
+}
+export function onIdle(func: Function) {
+ idleCallbacks.push(func);
+}
+export function onActive(func: Function) {
+ activeCallbacks.push(func);
+}
// When getting user records in a session callback, we have to check if admin
const getUserNonAdmin = function(userId) {
@@ -49,7 +67,7 @@ const sessionReconnect = function(doc) {
return;
}
- const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId);
+ const asst = Assignment.getCurrentUserAssignment(doc.userId);
// TODO possible debug message, but probably caught below.
if (asst == null) {
@@ -68,7 +86,7 @@ const sessionReconnect = function(doc) {
const userReconnect = function(user) {
let groupId;
- const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id);
+ const asst = Assignment.getCurrentUserAssignment(user._id);
if (asst == null) {
Meteor._debug(`${user._id} reconnected but has no active assignment`);
@@ -90,7 +108,7 @@ const userReconnect = function(user) {
}
asst._reconnected(groupId);
- return TurkServer.Instance.getInstance(groupId).bindOperation(
+ return Instance.getInstance(groupId).bindOperation(
function() {
TurkServer.log({
_userId: user._id,
@@ -108,7 +126,7 @@ const userReconnect = function(user) {
const userDisconnect = function(user) {
let groupId;
- const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id);
+ const asst = Assignment.getCurrentUserAssignment(user._id);
// If they are disconnecting after completing an assignment, there will be no
// current assignment.
@@ -124,7 +142,7 @@ const userDisconnect = function(user) {
}
asst._disconnected(groupId);
- return TurkServer.Instance.getInstance(groupId).bindOperation(
+ return Instance.getInstance(groupId).bindOperation(
function() {
TurkServer.log({
_userId: user._id,
@@ -150,10 +168,10 @@ const userIdle = function(user) {
return;
}
- const asst = TurkServer.Assignment.getCurrentUserAssignment(user._id);
+ const asst = Assignment.getCurrentUserAssignment(user._id);
asst._isIdle(groupId, user.status.lastActivity);
- return TurkServer.Instance.getInstance(groupId).bindOperation(
+ return Instance.getInstance(groupId).bindOperation(
function() {
TurkServer.log({
_userId: user._id,
@@ -182,10 +200,10 @@ const sessionActive = function(doc) {
return;
}
- const asst = TurkServer.Assignment.getCurrentUserAssignment(doc.userId);
+ const asst = Assignment.getCurrentUserAssignment(doc.userId);
asst._isActive(groupId, doc.lastActivity);
- return TurkServer.Instance.getInstance(groupId).bindOperation(
+ return Instance.getInstance(groupId).bindOperation(
function() {
TurkServer.log({
_userId: doc.userId,
@@ -242,7 +260,7 @@ Meteor.startup(function() {
TODO: we might want to make these tests end-to-end so that they ensure all of
the user-status functionality is working as well.
*/
-TestUtils.connCallbacks = {
+export const connCallbacks = {
sessionReconnect(doc) {
sessionReconnect(doc);
return userReconnect(Meteor.users.findOne(doc.userId));
@@ -294,7 +312,7 @@ Meteor.methods({
}
// TODO what if this doesn't exist?
- const asst = TurkServer.Assignment.currentAssignment();
+ const asst = Assignment.currentAssignment();
// mark assignment as completed and save the data
asst.setCompleted(doc);
diff --git a/server/index.ts b/server/index.ts
new file mode 100644
index 0000000..c9ea2ef
--- /dev/null
+++ b/server/index.ts
@@ -0,0 +1,42 @@
+import { Assignment } from "./assignment";
+import { Assigner } from "./assigners";
+
+import { Batch, ensureBatchExists, ensureTreatmentExists } from "./batches";
+import { Instance, initialize } from "./instance";
+import { mturk } from "./mturk";
+import { startup } from "./turkserver";
+import { onConnect, onDisconnect, onIdle, onActive, connCallbacks } from "./connections";
+import { scheduleOutstandingRounds, clearRoundHandlers } from "./timers_server";
+import { authenticateWorker } from "./accounts_mturk";
+import { treatment } from "./server_api";
+
+import { formatMillis, _mergeTreatments } from "../lib/util";
+
+// import * as TurkServer from "meteor/mizzao:turkserver";
+export default {
+ Assignment,
+ Assigner,
+ Batch,
+ Instance,
+ // connections
+ onConnect,
+ onDisconnect,
+ onIdle,
+ onActive,
+ // etc
+ mturk,
+ ensureBatchExists,
+ ensureTreatmentExists,
+ formatMillis,
+ initialize,
+ startup,
+ treatment,
+ _mergeTreatments
+};
+
+export const TestUtils = {
+ authenticateWorker,
+ connCallbacks,
+ scheduleOutstandingRounds,
+ clearRoundHandlers
+};
diff --git a/server/instance.js b/server/instance.ts
similarity index 87%
rename from server/instance.js
rename to server/instance.ts
index 7ca7d64..31536a5 100644
--- a/server/instance.js
+++ b/server/instance.ts
@@ -1,3 +1,14 @@
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import { Experiments, Treatments } from "../lib/common";
+import { Assignment } from "./assignment";
+import { Batch } from "./batches";
+import { log } from "./logging";
+import { _mergeTreatments } from "../lib/util";
+
const init_queue = [];
/*
@@ -15,7 +26,9 @@ const _instances = new Map();
* @class
* @instancename instance
*/
-class Instance {
+export class Instance {
+ groupId: string;
+
/**
* @summary Get the instance by its id.
* @param {String} groupId
@@ -34,7 +47,7 @@ class Instance {
// A fiber may have created this at the same time; if so use that one
if ((inst = _instances.get(groupId) && inst != null)) return inst;
- inst = new TurkServer.Instance(groupId);
+ inst = new Instance(groupId);
_instances.set(groupId, inst);
return inst;
}
@@ -70,7 +83,7 @@ class Instance {
* @param {Function} func The function to execute.
* @param {Object} context Optional context to pass to the function.
*/
- bindOperation(func, context = {}) {
+ bindOperation(func, context: any = {}) {
context.instance = this;
Partitioner.bindGroup(this.groupId, func.bind(context));
}
@@ -81,7 +94,7 @@ class Instance {
setup() {
// Can't use fat arrow here.
this.bindOperation(function() {
- TurkServer.log({
+ log({
_meta: "initialized",
treatmentData: this.instance.treatment()
});
@@ -97,7 +110,7 @@ class Instance {
* @param {TurkServer.Assignment} asst The user assignment to add.
*/
addAssignment(asst) {
- check(asst, TurkServer.Assignment);
+ check(asst, Assignment);
if (this.isEnded()) {
throw new Error("Cannot add a user to an instance that has ended.");
@@ -140,7 +153,8 @@ class Instance {
* @returns {Array} the list of userIds
*/
users() {
- return Experiments.findOne(this.groupId).users || [];
+ const instance = Experiments.findOne(this.groupId);
+ return (instance && instance.users) || [];
}
/**
@@ -149,7 +163,7 @@ class Instance {
*/
batch() {
const instance = Experiments.findOne(this.groupId);
- return instance && TurkServer.Batch.getBatch(instance.batchId);
+ return instance && Batch.getBatch(instance.batchId);
}
/**
@@ -171,7 +185,7 @@ class Instance {
return (
instance &&
- TurkServer._mergeTreatments(
+ _mergeTreatments(
Treatments.find({
name: {
$in: instance.treatments
@@ -210,7 +224,7 @@ class Instance {
const now = new Date();
Partitioner.bindGroup(this.groupId, function() {
- return TurkServer.log({
+ return log({
_meta: "teardown",
_timestamp: now
});
@@ -228,7 +242,7 @@ class Instance {
const users = Experiments.findOne(this.groupId).users;
if (users == null) return;
- for (userId of users) {
+ for (let userId of users) {
this.sendUserToLobby(userId);
}
}
@@ -239,7 +253,7 @@ class Instance {
*/
sendUserToLobby(userId) {
Partitioner.clearUserGroup(userId);
- let asst = TurkServer.Assignment.getCurrentUserAssignment(userId);
+ let asst = Assignment.getCurrentUserAssignment(userId);
if (asst == null) return;
// If the user is still assigned, do final accounting and put them in lobby
@@ -250,7 +264,4 @@ class Instance {
}
}
-TurkServer.Instance = Instance;
-
-// XXX back-compat
-TurkServer.initialize = TurkServer.Instance.initialize;
+export const initialize = Instance.initialize;
diff --git a/server/lobby_server.js b/server/lobby_server.ts
similarity index 86%
rename from server/lobby_server.js
rename to server/lobby_server.ts
index 092247d..f22f326 100644
--- a/server/lobby_server.js
+++ b/server/lobby_server.ts
@@ -7,11 +7,22 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
-const { EventEmitter } = Npm.require("events");
+import { EventEmitter } from "events";
+import * as _ from "underscore";
+
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+
+import { ErrMsg, LobbyStatus, Batches } from "../lib/common";
+import { Assignment } from "./assignment";
+import { Batch } from "./batches";
// TODO add index on LobbyStatus if needed
-TurkServer.Lobby = class Lobby {
+export class Lobby {
+ batchId: string;
+ events: EventEmitter;
+
constructor(batchId) {
this.batchId = batchId;
check(this.batchId, String);
@@ -41,10 +52,10 @@ TurkServer.Lobby = class Lobby {
return Meteor.defer(() => this.events.emit("user-join", asst));
}
- getAssignments(selector) {
- selector = _.extend(selector || {}, { batchId: this.batchId });
+ getAssignments(selector = {}) {
+ selector = _.extend(selector, { batchId: this.batchId });
return Array.from(LobbyStatus.find(selector).fetch()).map(record =>
- TurkServer.Assignment.getAssignment(record.asstId)
+ Assignment.getAssignment(record.asstId)
);
}
@@ -57,7 +68,7 @@ TurkServer.Lobby = class Lobby {
const newStatus = !existing.status;
LobbyStatus.update(userId, { $set: { status: newStatus } });
- const asst = TurkServer.Assignment.getCurrentUserAssignment(userId);
+ const asst = Assignment.getCurrentUserAssignment(userId);
return Meteor.defer(() => this.events.emit("user-status", asst, newStatus));
}
@@ -72,7 +83,7 @@ TurkServer.Lobby = class Lobby {
return Meteor.defer(() => this.events.emit("user-leave", asst));
}
}
-};
+}
// Publish lobby contents for a particular batch, as well as users
// TODO can we simplify this by publishing users with turkserver.state = "lobby",
@@ -141,7 +152,7 @@ Meteor.methods({
throw new Meteor.Error(403, ErrMsg.userIdErr);
}
- TurkServer.Batch.currentBatch().lobby.toggleStatus(userId);
+ Batch.currentBatch().lobby.toggleStatus(userId);
return this.unblock();
}
});
diff --git a/server/logging.js b/server/logging.ts
similarity index 87%
rename from server/logging.js
rename to server/logging.ts
index 19128b7..cc86cde 100644
--- a/server/logging.js
+++ b/server/logging.ts
@@ -1,3 +1,8 @@
+import { Meteor } from "meteor/meteor";
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import { ErrMsg, Logs } from "../lib/common";
+
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
@@ -41,7 +46,9 @@ Logs.before.insert(function(userId, doc) {
return true;
});
-TurkServer.log = (doc, callback) => Logs.insert(doc, callback);
+export function log(doc, callback = null) {
+ Logs.insert(doc, callback);
+}
Meteor.methods({
"ts-log"(doc) {
diff --git a/server/mturk.js b/server/mturk.ts
similarity index 83%
rename from server/mturk.js
rename to server/mturk.ts
index 3248cd7..8dd7ce3 100644
--- a/server/mturk.js
+++ b/server/mturk.ts
@@ -1,26 +1,32 @@
-const mturk = Npm.require("mturk-api");
-const JSPath = Npm.require("jspath");
+import * as mturk_api from "mturk-api";
+import JSPath from "jspath";
+
+import { Meteor } from "meteor/meteor";
+import { check, Match } from "meteor/check";
+
+import { config } from "./config";
+import { Workers, Qualifications } from "../lib/common";
let api = undefined;
-if (!TurkServer.config.mturk.accessKeyId || !TurkServer.config.mturk.secretAccessKey) {
+if (!config.mturk.accessKeyId || !config.mturk.secretAccessKey) {
Meteor._debug("Missing Amazon API keys for connecting to MTurk. Please configure.");
} else {
- const config = {
- access: TurkServer.config.mturk.accessKeyId,
- secret: TurkServer.config.mturk.secretAccessKey,
- sandbox: TurkServer.config.mturk.sandbox
+ const mturkConfig = {
+ access: config.mturk.accessKeyId,
+ secret: config.mturk.secretAccessKey,
+ sandbox: config.mturk.sandbox
};
- const promise = mturk
- .connect(config)
+ const promise = mturk_api
+ .connect(mturkConfig)
.then(api => api)
.catch(console.error);
api = Promise.resolve(promise).await();
}
-TurkServer.mturk = function(op, params) {
- if (!api) {
+export function mturk(op, params) {
+ if (api == null) {
console.log("Ignoring operation " + op + " because MTurk is not configured.");
return;
}
@@ -29,7 +35,7 @@ TurkServer.mturk = function(op, params) {
const result = Promise.resolve(promise).await();
return transform(op, result);
-};
+}
/*
Translate results to be a little more similar to the original code:
@@ -65,9 +71,7 @@ function transform(op, result) {
return result;
}
-TurkServer.Util = TurkServer.Util || {};
-
-TurkServer.Util.assignQualification = function(workerId, qualId, value, notify = true) {
+export function assignQualification(workerId, qualId, value, notify = true) {
check(workerId, String);
check(qualId, String);
check(value, Match.Integer);
@@ -82,7 +86,7 @@ TurkServer.Util.assignQualification = function(workerId, qualId, value, notify =
"quals.id": qualId
}) != null
) {
- TurkServer.mturk("UpdateQualificationScore", {
+ mturk("UpdateQualificationScore", {
SubjectId: workerId,
QualificationTypeId: qualId,
IntegerValue: value
@@ -99,7 +103,7 @@ TurkServer.Util.assignQualification = function(workerId, qualId, value, notify =
}
);
} else {
- TurkServer.mturk("AssignQualification", {
+ mturk("AssignQualification", {
WorkerId: workerId,
QualificationTypeId: qualId,
IntegerValue: value,
@@ -114,7 +118,7 @@ TurkServer.Util.assignQualification = function(workerId, qualId, value, notify =
}
});
}
-};
+}
Meteor.startup(function() {
Qualifications.upsert(
diff --git a/server/server_api.js b/server/server_api.ts
similarity index 52%
rename from server/server_api.js
rename to server/server_api.ts
index a9d34a9..bce8e12 100644
--- a/server/server_api.js
+++ b/server/server_api.ts
@@ -1,21 +1,31 @@
+import { Treatments } from "../lib/common";
+import { _mergeTreatments } from "../lib/util";
+
+import { Assignment } from "./assignment";
+import { Instance } from "./instance";
+
+export interface TreatmentData {
+ [key: string]: any;
+}
+
/**
* @summary Access treatment data assigned to the current user (assignment)
* or the user's current world (instance).
* @locus Server
* @returns {Object} treatment key/value pairs
*/
-TurkServer.treatment = function() {
- const instance = TurkServer.Instance.currentInstance();
- const asst = TurkServer.Assignment.currentAssignment();
+export function treatment(): TreatmentData {
+ const instance = Instance.currentInstance();
+ const asst = Assignment.currentAssignment();
const instTreatments = (instance && instance.getTreatmentNames()) || [];
const asstTreatments = (asst && asst.getTreatmentNames()) || [];
- return TurkServer._mergeTreatments(
+ return _mergeTreatments(
Treatments.find({
name: {
$in: instTreatments.concat(asstTreatments)
}
})
);
-};
+}
diff --git a/server/timers_server.js b/server/timers_server.ts
similarity index 91%
rename from server/timers_server.js
rename to server/timers_server.ts
index f2b2b2e..d3dad9e 100644
--- a/server/timers_server.js
+++ b/server/timers_server.ts
@@ -1,3 +1,10 @@
+import { Meteor } from "meteor/meteor";
+import { check } from "meteor/check";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import { RoundTimers } from "../lib/common";
+
const _round_handlers = [];
/**
@@ -23,7 +30,12 @@ const ROUND_END_NEWROUND = "newstart";
* @summary Utilities for controlling round timers within instances.
* @namespace
*/
-class Timers {
+export class Timers {
+ // TODO: export the above properly or something.
+ static ROUND_END_TIMEOUT = ROUND_END_TIMEOUT;
+ static ROUND_END_MANUAL = ROUND_END_MANUAL;
+ static ROUND_END_NEWROUND = ROUND_END_NEWROUND;
+
/**
* @summary Starts a new round in the current instance.
* @function TurkServer.Timers.startNewRound
@@ -150,7 +162,7 @@ function tryEndingRound(roundId, endType, endTime = null) {
}
// When restarting server, re-schedule all un-ended rounds
-function scheduleOutstandingRounds() {
+export function scheduleOutstandingRounds() {
let scheduled = 0;
RoundTimers.direct.find({ ended: false }).forEach(round => {
@@ -165,20 +177,9 @@ function scheduleOutstandingRounds() {
Meteor.startup(scheduleOutstandingRounds);
-/*
- Exports
- */
-Timers.ROUND_END_TIMEOUT = ROUND_END_TIMEOUT;
-Timers.ROUND_END_MANUAL = ROUND_END_MANUAL;
-Timers.ROUND_END_NEWROUND = ROUND_END_NEWROUND;
-
-TurkServer.Timers = Timers;
-
/*
Testing functions
*/
-TestUtils.clearRoundHandlers = function() {
+export function clearRoundHandlers() {
_round_handlers.length = 0;
-};
-
-TestUtils.scheduleOutstandingRounds = scheduleOutstandingRounds;
+}
diff --git a/server/turkserver.js b/server/turkserver.ts
similarity index 85%
rename from server/turkserver.js
rename to server/turkserver.ts
index adb78b2..a2be27c 100644
--- a/server/turkserver.js
+++ b/server/turkserver.ts
@@ -1,3 +1,24 @@
+import { Meteor } from "meteor/meteor";
+import { Mongo } from "meteor/mongo";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import {
+ Batches,
+ Treatments,
+ LobbyStatus,
+ Experiments,
+ RoundTimers,
+ Logs,
+ Workers,
+ Assignments,
+ WorkerEmails,
+ Qualifications,
+ HITTypes,
+ HITs
+} from "../lib/common";
+import { check } from "meteor/check";
+
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
@@ -9,12 +30,16 @@
*/
// Collection modifiers, in case running on insecure
-TurkServer.isAdminRule = userId => Meteor.users.findOne(userId).admin === true;
+function isAdminRule(userId: string): boolean {
+ if (userId == null) return false;
+ const user = Meteor.users.findOne(userId);
+ return (user && user.admin) || false;
+}
const adminOnly = {
- insert: TurkServer.isAdminRule,
- update: TurkServer.isAdminRule,
- remove: TurkServer.isAdminRule
+ insert: isAdminRule,
+ update: isAdminRule,
+ remove: isAdminRule
};
const always = {
@@ -133,7 +158,9 @@ Meteor.publish(null, function() {
return null;
}
- const cursors = [Meteor.users.find(this.userId, { fields: { turkserver: 1 } })];
+ const cursors: Mongo.Cursor[] = [
+ Meteor.users.find(this.userId, { fields: { turkserver: 1 } })
+ ];
// Current user assignment data, including idle and disconnection time
// This won't be sent for the admin user
@@ -224,7 +251,9 @@ Meteor.publish(null, function() {
return sub.onStop(() => handle.stop());
});
-TurkServer.startup = func => Meteor.startup(() => Partitioner.directOperation(func));
+export function startup(func) {
+ Meteor.startup(() => Partitioner.directOperation(func));
+}
function __guard__(value, transform) {
return typeof value !== "undefined" && value !== null ? transform(value) : undefined;
diff --git a/tests/admin_tests.js b/tests/admin_tests.ts
similarity index 93%
rename from tests/admin_tests.js
rename to tests/admin_tests.ts
index a53eff5..66cea1f 100644
--- a/tests/admin_tests.js
+++ b/tests/admin_tests.ts
@@ -1,3 +1,12 @@
+import { Meteor } from "meteor/meteor";
+import { Random } from "meteor/random";
+import { Tinytest } from "meteor/tinytest";
+
+import { Batches, HITTypes, HITs, WorkerEmails, Workers } from "../lib/common";
+
+import TurkServer, { TestUtils } from "../server";
+import { assignQualification } from "../server/mturk";
+
// TODO: This file was created by bulk-decaffeinate.
// Sanity-check the conversion and remove this comment.
/*
@@ -14,6 +23,7 @@ Batches.upsert({ _id: batchId }, { _id: batchId });
HITTypes.upsert({ HITTypeId: hitTypeId }, { $set: { batchId } });
// Temporarily disable the admin check during these tests
+// TODO This won't work, so instead just make admin = true during tests.
const _checkAdmin = TurkServer.checkAdmin;
const withCleanup = TestUtils.getCleanupWrapper({
@@ -185,7 +195,7 @@ Tinytest.add(
return test.equal(params.SendNotification, false);
};
- TurkServer.Util.assignQualification(workerId, qual, value, false);
+ assignQualification(workerId, qual, value, false);
// Check that worker has been updated
const worker = Workers.findOne(workerId);
@@ -216,7 +226,7 @@ Tinytest.add(
return test.equal(params.IntegerValue, value);
};
- TurkServer.Util.assignQualification(workerId, qual, value, false);
+ assignQualification(workerId, qual, value, false);
// Check that worker has been updated
const worker = Workers.findOne(workerId);
diff --git a/tests/auth_tests.js b/tests/auth_tests.ts
similarity index 96%
rename from tests/auth_tests.js
rename to tests/auth_tests.ts
index eed30e2..5145db5 100644
--- a/tests/auth_tests.js
+++ b/tests/auth_tests.ts
@@ -6,6 +6,13 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
+import { Meteor } from "meteor/meteor";
+import { Tinytest } from "meteor/tinytest";
+
+import TurkServer, { TestUtils } from "../server";
+import { Batches, HITTypes, HITs, Assignments, ErrMsg } from "../lib/common";
+import { authenticateWorker } from "../server/accounts_mturk";
+
const hitType = "authHitType";
const hitId = "authHitId";
@@ -62,7 +69,7 @@ const withCleanup = TestUtils.getCleanupWrapper({
Tinytest.add(
"auth - with first time hit assignment",
withCleanup(function(test) {
- const asst = TestUtils.authenticateWorker({
+ const asst = authenticateWorker({
batchId: authBatchId,
hitId,
assignmentId,
@@ -92,7 +99,7 @@ Tinytest.add(
"auth - reject incorrect batch",
withCleanup(function(test) {
const testFunc = () =>
- TestUtils.authenticateWorker({
+ authenticateWorker({
batchId: otherBatchId,
hitId,
assignmentId,
@@ -110,7 +117,7 @@ Tinytest.add(
Batches.update(authBatchId, { $unset: { active: false } });
const testFunc = () =>
- TestUtils.authenticateWorker({
+ authenticateWorker({
batchId: authBatchId,
hitId,
assignmentId,
@@ -133,7 +140,7 @@ Tinytest.add(
});
// This needs to return an assignment
- const asst = TestUtils.authenticateWorker({
+ const asst = authenticateWorker({
batchId: authBatchId,
hitId,
assignmentId,
diff --git a/tests/connection_tests.js b/tests/connection_tests.ts
similarity index 96%
rename from tests/connection_tests.js
rename to tests/connection_tests.ts
index d833124..30e71b8 100644
--- a/tests/connection_tests.js
+++ b/tests/connection_tests.ts
@@ -6,6 +6,15 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
+import { Meteor } from "meteor/meteor";
+import { Tinytest } from "meteor/tinytest";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import TurkServer, { TestUtils } from "../server";
+import { Batches, Assignments, ErrMsg } from "../lib/common";
+import { Assignment } from "../server/assignment";
+
const batchId = "connectionBatch";
Batches.upsert({ _id: batchId }, { _id: batchId });
@@ -20,7 +29,7 @@ const userId = "connectionUserId";
Meteor.users.upsert(userId, { $set: { workerId } });
-let asst = null;
+let asst: Assignment = null;
const instanceId = "connectionInstance";
const instance = batch.createInstance();
diff --git a/tests/experiment_tests.js b/tests/experiment_tests.ts
similarity index 97%
rename from tests/experiment_tests.js
rename to tests/experiment_tests.ts
index f1a9397..5881d5b 100644
--- a/tests/experiment_tests.js
+++ b/tests/experiment_tests.ts
@@ -9,7 +9,19 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
-const Doobie = new Mongo.Collection("experiment_test");
+
+import { Meteor } from "meteor/meteor";
+import { Mongo } from "meteor/mongo";
+import { Accounts } from "meteor/accounts-base";
+import { Random } from "meteor/random";
+import { Tinytest } from "meteor/tinytest";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import TurkServer, { TestUtils } from "../server";
+import { Batches, Assignments, Experiments, Logs } from "../lib/common";
+
+const Doobie = new Mongo.Collection("experiment_test");
Partitioner.partitionCollection(Doobie);
diff --git a/tests/insecure_login.js b/tests/insecure_login.ts
similarity index 80%
rename from tests/insecure_login.js
rename to tests/insecure_login.ts
index 80e7de0..aac89fb 100644
--- a/tests/insecure_login.js
+++ b/tests/insecure_login.ts
@@ -1,7 +1,14 @@
-InsecureLogin = {
+import * as _ from "underscore";
+
+import { Meteor } from "meteor/meteor";
+import { Accounts } from "meteor/accounts-base";
+
+import { Batches } from "../lib/common";
+
+export const InsecureLogin = {
queue: [],
ran: false,
- ready: function(callback) {
+ ready: function(callback: Function) {
this.queue.push(callback);
if (this.ran) this.unwind();
},
diff --git a/tests/lobby_tests.js b/tests/lobby_tests.ts
similarity index 95%
rename from tests/lobby_tests.js
rename to tests/lobby_tests.ts
index 3634018..8b0b1e4 100644
--- a/tests/lobby_tests.js
+++ b/tests/lobby_tests.ts
@@ -6,6 +6,12 @@
* DS103: Rewrite code to no longer use __guard__
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
+import { Meteor } from "meteor/meteor";
+import { Tinytest } from "meteor/tinytest";
+
+import { Batches, Assignments, LobbyStatus } from "../lib/common";
+import TurkServer, { TestUtils } from "../server";
+
if (Meteor.isServer) {
// Create a batch to test the lobby on
const batchId = "lobbyBatchTest";
diff --git a/tests/timer_tests.js b/tests/timer_tests.ts
similarity index 75%
rename from tests/timer_tests.js
rename to tests/timer_tests.ts
index 8353cc1..897d39a 100644
--- a/tests/timer_tests.js
+++ b/tests/timer_tests.ts
@@ -7,6 +7,15 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
+import { Meteor } from "meteor/meteor";
+import { Tinytest } from "meteor/tinytest";
+
+import { Partitioner } from "meteor/mizzao:partitioner";
+
+import { TestUtils } from "../server";
+import { RoundTimers } from "../lib/common";
+import { Timers } from "../server/timers_server";
+
const testGroup = "timerTest";
// use a before here because tests are async
@@ -25,13 +34,13 @@ Tinytest.addAsync(
Partitioner.bindGroup(testGroup, function() {
const now = new Date();
- TurkServer.Timers.onRoundEnd(function(type) {
- test.equal(type, TurkServer.Timers.ROUND_END_TIMEOUT);
+ Timers.onRoundEnd(function(type) {
+ test.equal(type, Timers.ROUND_END_TIMEOUT);
// Cancel group binding
return Partitioner._currentGroup.withValue(null, next);
});
- return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
+ return Timers.startNewRound(now, new Date(now.getTime() + 100));
})
)
);
@@ -42,14 +51,14 @@ Tinytest.addAsync(
Partitioner.bindGroup(testGroup, function() {
const now = new Date();
- TurkServer.Timers.onRoundEnd(function(type) {
- test.equal(type, TurkServer.Timers.ROUND_END_NEWROUND);
+ Timers.onRoundEnd(function(type) {
+ test.equal(type, Timers.ROUND_END_NEWROUND);
return Partitioner._currentGroup.withValue(null, next);
});
- TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
- return TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
+ Timers.startNewRound(now, new Date(now.getTime() + 100));
+ return Timers.startNewRound(now, new Date(now.getTime() + 100));
})
)
);
@@ -58,14 +67,14 @@ Tinytest.addAsync(
"timers - end and start new rounds",
withCleanup((test, next) =>
Partitioner.bindGroup(testGroup, function() {
- TurkServer.Timers.onRoundEnd(type => test.equal(type, TurkServer.Timers.ROUND_END_MANUAL));
+ Timers.onRoundEnd(type => test.equal(type, Timers.ROUND_END_MANUAL));
const nRounds = 10;
for (let i = 1, end = nRounds, asc = 1 <= end; asc ? i <= end : i >= end; asc ? i++ : i--) {
const now = new Date();
- TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
- TurkServer.Timers.endCurrentRound();
+ Timers.startNewRound(now, new Date(now.getTime() + 100));
+ Timers.endCurrentRound();
}
// Make sure there are the right number of rounds
@@ -85,7 +94,7 @@ Tinytest.addAsync(
let count = 0;
const types = {};
- TurkServer.Timers.onRoundEnd(function(type) {
+ Timers.onRoundEnd(function(type) {
count++;
if (types[type] == null) {
types[type] = 0;
@@ -93,14 +102,14 @@ Tinytest.addAsync(
return types[type]++;
});
- TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
+ Timers.startNewRound(now, new Date(now.getTime() + 100));
- TurkServer.Timers.endCurrentRound();
+ Timers.endCurrentRound();
// round end callback should only have been called once
return Meteor.setTimeout(function() {
test.equal(count, 1);
- test.equal(types[TurkServer.Timers.ROUND_END_MANUAL], 1);
+ test.equal(types[Timers.ROUND_END_MANUAL], 1);
// Cancel group binding
return Partitioner._currentGroup.withValue(null, next);
}, 150);
@@ -119,7 +128,7 @@ Tinytest.addAsync(
const testFunc = function() {
try {
- return TurkServer.Timers.startNewRound(now, end);
+ return Timers.startNewRound(now, end);
} catch (e) {
// We should get at least one error here.
console.log(e);
@@ -149,13 +158,13 @@ Tinytest.addAsync(
Partitioner.bindGroup(testGroup, function() {
const now = new Date();
- TurkServer.Timers.onRoundEnd(function() {
+ Timers.onRoundEnd(function() {
test.ok();
// Cancel group binding
return Partitioner._currentGroup.withValue(null, next);
});
- TurkServer.Timers.startNewRound(now, new Date(now.getTime() + 100));
+ Timers.startNewRound(now, new Date(now.getTime() + 100));
// Prevent the normal timeout from being called
Meteor.clearTimeout(TestUtils.lastScheduledRound);
diff --git a/tests/utils.js b/tests/utils.ts
similarity index 93%
rename from tests/utils.js
rename to tests/utils.ts
index b6c099b..08c003a 100644
--- a/tests/utils.js
+++ b/tests/utils.ts
@@ -6,6 +6,11 @@
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
+import { Meteor } from "meteor/meteor";
+
+import { Assignments, Batches, Experiments, Treatments } from "../lib/common";
+import TurkServer, { TestUtils } from "../server";
+
if (Meteor.isClient) {
// Prevent router from complaining about missing path
Router.map(function() {
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..0601958
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "target": "es6",
+ "module": "commonjs",
+ "lib": ["es2015", "dom"],
+ "allowJs": true,
+ "checkJs": false,
+ "jsx": "preserve",
+ "noEmit": true,
+
+ "strict": false,
+ "noImplicitAny": false,
+ "strictNullChecks": false,
+
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": false,
+ "noFallthroughCasesInSwitch": false,
+
+ "baseUrl": ".",
+ "paths": {
+ "/*": ["*"]
+ },
+ "moduleResolution": "node",
+ "types": ["node"],
+ "typeRoots": ["types/", "node_modules/@types"]
+ }
+}
diff --git a/types/shims.d.ts b/types/shims.d.ts
new file mode 100644
index 0000000..84288ba
--- /dev/null
+++ b/types/shims.d.ts
@@ -0,0 +1,31 @@
+// Stand-ins for stuff that isn't typed yet
+
+// Untyped Meteor stuff
+declare var Facts: any;
+
+declare module "meteor/mongo" {
+ module Mongo {
+ interface Collection {
+ // For collection hooks
+ direct: any;
+ before: any;
+ }
+ }
+}
+
+// Override erroneous definition
+declare module "meteor/tracker" {
+ module Tracker {
+ // TODO: pass this type through
+ function nonreactive(func: Function): any;
+ }
+}
+
+// Partitioner
+declare module "meteor/mizzao:partitioner";
+declare module "meteor/mizzao:user-status";
+
+// Old MTurk stuff
+declare module "mturk-api";
+declare module "jspath";
+declare module "deepmerge";
diff --git a/types/tinytest.d.ts b/types/tinytest.d.ts
new file mode 100644
index 0000000..d4e31ec
--- /dev/null
+++ b/types/tinytest.d.ts
@@ -0,0 +1,40 @@
+// Not sure why the @types/meteor declares this as "tiny-test".
+
+declare module "meteor/tinytest" {
+ interface ILengthAble {
+ length: number;
+ }
+
+ interface ITinytestAssertions {
+ ok(doc: Object): void;
+ expect_fail(): void;
+ fail(doc: Object): void;
+ runId(): string;
+ equal(actual: T, expected: T, message?: string, not?: boolean): void;
+ notEqual(actual: T, expected: T, message?: string): void;
+ instanceOf(obj: Object, klass: Function, message?: string): void;
+ notInstanceOf(obj: Object, klass: Function, message?: string): void;
+ matches(actual: any, regexp: RegExp, message?: string): void;
+ notMatches(actual: any, regexp: RegExp, message?: string): void;
+ throws(f: Function, expected?: string | RegExp): void;
+ isTrue(v: boolean, msg?: string): void;
+ isFalse(v: boolean, msg?: string): void;
+ isNull(v: any, msg?: string): void;
+ isNotNull(v: any, msg?: string): void;
+ isUndefined(v: any, msg?: string): void;
+ isNotUndefined(v: any, msg?: string): void;
+ isNan(v: any, msg?: string): void;
+ isNotNan(v: any, msg?: string): void;
+ include(s: Array | Object | string, value: any, msg?: string, not?: boolean): void;
+
+ notInclude(s: Array | Object | string, value: any, msg?: string, not?: boolean): void;
+ length(obj: ILengthAble, expected_length: number, msg?: string): void;
+ _stringEqual(actual: string, expected: string, msg?: string): void;
+ }
+
+ module Tinytest {
+ function add(description: string, func: (test: ITinytestAssertions) => void): void;
+
+ function addAsync(description: string, func: (test: ITinytestAssertions) => void): void;
+ }
+}
diff --git a/types/user.d.ts b/types/user.d.ts
new file mode 100644
index 0000000..2cf6aff
--- /dev/null
+++ b/types/user.d.ts
@@ -0,0 +1,21 @@
+// TODO: move this to mizzao:user-status
+interface UserStatus {
+ online: boolean;
+ idle: boolean;
+}
+
+interface UserTurkServerState {
+ state: "exitsurvey";
+}
+
+// Define TurkServer-specific user data.
+declare module "meteor/meteor" {
+ module Meteor {
+ interface User {
+ admin: boolean;
+ turkserver: UserTurkServerState;
+ workerId: string;
+ status: UserStatus;
+ }
+ }
+}
diff --git a/yarn.lock b/yarn.lock
new file mode 100644
index 0000000..039e311
--- /dev/null
+++ b/yarn.lock
@@ -0,0 +1,64 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@types/connect@*":
+ version "3.4.32"
+ resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
+ integrity sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==
+ dependencies:
+ "@types/node" "*"
+
+"@types/meteor@^1.4.36":
+ version "1.4.36"
+ resolved "https://registry.yarnpkg.com/@types/meteor/-/meteor-1.4.36.tgz#60b9bc9e28fe0e57eefe91c4a0fc9a4341619068"
+ integrity sha512-FcWRi7hevQ7XsznwVx8uuTzOacT76MHIFvFQwgAU/XSOol97U8mNWYotguGa3HVqnaOqv2BGjsUaoYjdBrj5XQ==
+ dependencies:
+ "@types/connect" "*"
+ "@types/react" "*"
+ "@types/underscore" "*"
+
+"@types/moment@^2.13.0":
+ version "2.13.0"
+ resolved "https://registry.yarnpkg.com/@types/moment/-/moment-2.13.0.tgz#604ebd189bc3bc34a1548689404e61a2a4aac896"
+ integrity sha1-YE69GJvDvDShVIaJQE5hoqSqyJY=
+ dependencies:
+ moment "*"
+
+"@types/node@*":
+ version "12.12.5"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.5.tgz#66103d2eddc543d44a04394abb7be52506d7f290"
+ integrity sha512-KEjODidV4XYUlJBF3XdjSH5FWoMCtO0utnhtdLf1AgeuZLOrRbvmU/gaRCVg7ZaQDjVf3l84egiY0mRNe5xE4A==
+
+"@types/node@^12.12.6":
+ version "12.12.6"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.6.tgz#a47240c10d86a9a57bb0c633f0b2e0aea9ce9253"
+ integrity sha512-FjsYUPzEJdGXjwKqSpE0/9QEh6kzhTAeObA54rn6j3rR4C/mzpI9L0KNfoeASSPMMdxIsoJuCLDWcM/rVjIsSA==
+
+"@types/prop-types@*":
+ version "15.7.3"
+ resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7"
+ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==
+
+"@types/react@*":
+ version "16.9.11"
+ resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.11.tgz#70e0b7ad79058a7842f25ccf2999807076ada120"
+ integrity sha512-UBT4GZ3PokTXSWmdgC/GeCGEJXE5ofWyibCcecRLUVN2ZBpXQGVgQGtG2foS7CrTKFKlQVVswLvf7Js6XA/CVQ==
+ dependencies:
+ "@types/prop-types" "*"
+ csstype "^2.2.0"
+
+"@types/underscore@*", "@types/underscore@^1.9.3":
+ version "1.9.3"
+ resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.9.3.tgz#d7d9dc5a5ff76fa3d001b29bc7cc95ab0ccfe85e"
+ integrity sha512-SwbHKB2DPIDlvYqtK5O+0LFtZAyrUSw4c0q+HWwmH1Ve3KMQ0/5PlV3RX97+3dP7yMrnNQ8/bCWWvQpPl03Mug==
+
+csstype@^2.2.0:
+ version "2.6.7"
+ resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.7.tgz#20b0024c20b6718f4eda3853a1f5a1cce7f5e4a5"
+ integrity sha512-9Mcn9sFbGBAdmimWb2gLVDtFJzeKtDGIr76TUqmjZrw9LFXBMSU70lcs+C0/7fyCd6iBDqmksUcCOUIkisPHsQ==
+
+moment@*:
+ version "2.24.0"
+ resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
+ integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==