mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-23 23:58:02 -05:00
Merge branch 'feature/task-queue'
Conflicts: app/views/play/level/tome/spell.coffee
This commit is contained in:
commit
1a2fa68473
23 changed files with 849 additions and 24 deletions
|
@ -21,6 +21,7 @@ module.exports = class LevelBus extends Bus
|
|||
'thang-code-ran': 'onCodeRan'
|
||||
'level-show-victory': 'onVictory'
|
||||
'tome:spell-changed': 'onSpellChanged'
|
||||
'tome:spell-created': 'onSpellCreated'
|
||||
|
||||
constructor: ->
|
||||
super(arguments...)
|
||||
|
@ -91,12 +92,26 @@ module.exports = class LevelBus extends Bus
|
|||
code = @session.get('code')
|
||||
code ?= {}
|
||||
parts = e.spell.spellKey.split('/')
|
||||
|
||||
code[parts[0]] ?= {}
|
||||
code[parts[0]][parts[1]] = e.spell.getSource()
|
||||
@changedSessionProperties.code = true
|
||||
@session.set({'code': code})
|
||||
@saveSession()
|
||||
|
||||
onSpellCreated: (e) ->
|
||||
return unless @onPoint()
|
||||
spellTeam = e.spell.team
|
||||
@teamSpellMap[spellTeam] ?= []
|
||||
|
||||
unless e.spell.spellKey in @teamSpellMap[spellTeam]
|
||||
@teamSpellMap[spellTeam].push e.spell.spellKey
|
||||
@changedSessionProperties.teamSpells = true
|
||||
@session.set({'teamSpells': @teamSpellMap})
|
||||
@saveSession()
|
||||
|
||||
|
||||
|
||||
onScriptStateChanged: (e) ->
|
||||
return unless @onPoint()
|
||||
@fireScriptsRef?.update(e)
|
||||
|
@ -220,4 +235,12 @@ module.exports = class LevelBus extends Bus
|
|||
|
||||
destroy: ->
|
||||
super()
|
||||
@session.off 'change:multiplayer', @onMultiplayerChanged, @
|
||||
@session.off 'change:multiplayer', @onMultiplayerChanged, @
|
||||
|
||||
setTeamSpellMap: (spellMap) ->
|
||||
@teamSpellMap = spellMap
|
||||
console.log @teamSpellMap
|
||||
@changedSessionProperties.teamSpells = true
|
||||
@session.set({'teamSpells': @teamSpellMap})
|
||||
@saveSession()
|
||||
|
||||
|
|
|
@ -20,6 +20,9 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
'db/*path': 'routeToServer'
|
||||
'file/*path': 'routeToServer'
|
||||
|
||||
'play/level/:levelID/leaderboard/:teamID/:startRank/:endRank': 'getPaginatedLevelRank'
|
||||
'play/level/:levelID/player/:playerID': 'getPlayerLevelInfo'
|
||||
|
||||
# most go through here
|
||||
'*name': 'general'
|
||||
|
||||
|
@ -27,6 +30,13 @@ module.exports = class CocoRouter extends Backbone.Router
|
|||
general: (name) ->
|
||||
@openRoute(name)
|
||||
|
||||
getPaginatedLevelRank: (levelID,teamID,startRank,endRank) ->
|
||||
return
|
||||
|
||||
getPlayerLevelInfo: (levelID,playerID) ->
|
||||
return
|
||||
|
||||
|
||||
editorModelView: (modelName, slugOrId, subview) ->
|
||||
modulePrefix = "views/editor/#{modelName}/"
|
||||
suffix = subview or (if slugOrId then 'edit' else 'home')
|
||||
|
|
|
@ -135,7 +135,6 @@ module.exports = class GoalManager extends CocoClass
|
|||
status: null # should eventually be either 'success', 'failure', or 'incomplete'
|
||||
keyFrame: 0 # when it became a 'success' or 'failure'
|
||||
}
|
||||
|
||||
@initGoalState(state, [goal.killThangs, goal.saveThangs], 'killed')
|
||||
@initGoalState(state, [goal.getToLocations?.who, goal.keepFromLocations?.who], 'arrived')
|
||||
@initGoalState(state, [goal.leaveOffSides?.who, goal.keepFromLeavingOffSides?.who], 'left')
|
||||
|
|
|
@ -25,4 +25,6 @@ block content
|
|||
div.homepage_button
|
||||
a#beginner-campaign(href="/play/level/rescue-mission")
|
||||
canvas(width="125", height="150")
|
||||
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
|
||||
button(data-i18n="home.play").btn.btn-warning.btn-lg.highlight Play
|
||||
if me.isAdmin()
|
||||
button.btn.btn-warning.btn-lg.highlight#simulate-button SIMULATE
|
|
@ -2,6 +2,10 @@ View = require 'views/kinds/RootView'
|
|||
template = require 'templates/home'
|
||||
WizardSprite = require 'lib/surface/WizardSprite'
|
||||
ThangType = require 'models/ThangType'
|
||||
LevelLoader = require 'lib/LevelLoader'
|
||||
God = require 'lib/God'
|
||||
|
||||
GoalManager = require 'lib/world/GoalManager'
|
||||
|
||||
module.exports = class HomeView extends View
|
||||
id: 'home-view'
|
||||
|
@ -10,6 +14,7 @@ module.exports = class HomeView extends View
|
|||
events:
|
||||
'mouseover #beginner-campaign': 'onMouseOverButton'
|
||||
'mouseout #beginner-campaign': 'onMouseOutButton'
|
||||
'click #simulate-button': 'onSimulateButtonClick'
|
||||
|
||||
getRenderData: ->
|
||||
c = super()
|
||||
|
@ -97,4 +102,91 @@ module.exports = class HomeView extends View
|
|||
|
||||
destroy: ->
|
||||
super()
|
||||
@wizardSprite?.destroy()
|
||||
@wizardSprite?.destroy()
|
||||
|
||||
onSimulateButtonClick: (e) =>
|
||||
$.get "/queue/scoring", (data) =>
|
||||
levelName = data.sessions[0].levelID
|
||||
#TODO: Refactor. So much refactor.
|
||||
world = {}
|
||||
god = new God()
|
||||
levelLoader = new LevelLoader(levelName, @supermodel, data.sessions[0].sessionID)
|
||||
levelLoader.once 'loaded-all', =>
|
||||
world = levelLoader.world
|
||||
level = levelLoader.level
|
||||
levelLoader.destroy()
|
||||
god.level = level.serialize @supermodel
|
||||
god.worldClassMap = world.classMap
|
||||
god.goalManager = new GoalManager(world)
|
||||
#move goals in here
|
||||
goalsToAdd = god.goalManager.world.scripts[0].noteChain[0].goals.add
|
||||
god.goalManager.goals = goalsToAdd
|
||||
god.goalManager.goalStates =
|
||||
"destroy-humans":
|
||||
keyFrame: 0
|
||||
killed:
|
||||
"Human Base": false
|
||||
status: "incomplete"
|
||||
"destroy-ogres":
|
||||
keyFrame:0
|
||||
killed:
|
||||
"Ogre Base": false
|
||||
status: "incomplete"
|
||||
god.spells = @filterProgrammableComponents level.attributes.thangs, @generateSpellToSourceMap data.sessions
|
||||
god.createWorld()
|
||||
|
||||
Backbone.Mediator.subscribe 'god:new-world-created', @onWorldCreated, @
|
||||
|
||||
onWorldCreated: (data) ->
|
||||
console.log "GOAL STATES"
|
||||
console.log data
|
||||
|
||||
|
||||
filterProgrammableComponents: (thangs, spellToSourceMap) =>
|
||||
spells = {}
|
||||
for thang in thangs
|
||||
isTemplate = false
|
||||
for component in thang.components
|
||||
if component.config? and _.has component.config,'programmableMethods'
|
||||
for methodName, method of component.config.programmableMethods
|
||||
if typeof method is 'string'
|
||||
isTemplate = true
|
||||
break
|
||||
|
||||
pathComponents = [thang.id,methodName]
|
||||
pathComponents[0] = _.string.slugify pathComponents[0]
|
||||
spellKey = pathComponents.join '/'
|
||||
spells[spellKey] ?= {}
|
||||
spells[spellKey].thangs ?= {}
|
||||
spells[spellKey].name = methodName
|
||||
thangID = _.string.slugify thang.id
|
||||
spells[spellKey].thangs[thang.id] ?= {}
|
||||
spells[spellKey].thangs[thang.id].aether = @createAether methodName, method
|
||||
if spellToSourceMap[thangID]? then source = spellToSourceMap[thangID][methodName] else source = ""
|
||||
spells[spellKey].thangs[thang.id].aether.transpile source
|
||||
if isTemplate
|
||||
break
|
||||
|
||||
spells
|
||||
|
||||
createAether : (methodName, method) ->
|
||||
aetherOptions =
|
||||
functionName: methodName
|
||||
protectAPI: false
|
||||
includeFlow: false
|
||||
return new Aether aetherOptions
|
||||
|
||||
generateSpellToSourceMap: (sessions) ->
|
||||
spellKeyToSourceMap = {}
|
||||
spellSources = {}
|
||||
for session in sessions
|
||||
teamSpells = session.teamSpells[session.team]
|
||||
_.merge spellSources, _.pick(session.code, teamSpells)
|
||||
|
||||
#merge common ones, this overwrites until the last session
|
||||
commonSpells = session.teamSpells["common"]
|
||||
if commonSpells?
|
||||
_.merge spellSources, _.pick(session.code, commonSpells)
|
||||
|
||||
spellSources
|
||||
|
||||
|
|
|
@ -26,12 +26,16 @@ module.exports = class Spell
|
|||
@view.render() # Get it ready and code loaded in advance
|
||||
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel
|
||||
@tabView.render()
|
||||
@team = @permissions.readwrite[0] ? "common"
|
||||
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
||||
|
||||
|
||||
destroy: ->
|
||||
@view.destroy()
|
||||
@tabView.destroy()
|
||||
@thangs = null
|
||||
|
||||
|
||||
addThang: (thang) ->
|
||||
if @thangs[thang.id]
|
||||
@thangs[thang.id].thang = thang
|
||||
|
|
|
@ -62,6 +62,7 @@ module.exports = class TomeView extends View
|
|||
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel
|
||||
@thangList = @insertSubView new ThangListView spells: @spells, thangs: @options.thangs, supermodel: @supermodel
|
||||
@castButton = @insertSubView new CastButtonView spells: @spells
|
||||
@teamSpellMap = @generateTeamSpellMap(@spells)
|
||||
else
|
||||
@cast()
|
||||
console.warn "Warning: There are no Programmable Thangs in this level, which makes it unplayable."
|
||||
|
@ -74,6 +75,21 @@ module.exports = class TomeView extends View
|
|||
@thangList.adjustThangs @spells, thangs
|
||||
@spellList.adjustSpells @spells
|
||||
|
||||
generateTeamSpellMap: (spellObject) ->
|
||||
teamSpellMap = {}
|
||||
for spellName, spell of spellObject
|
||||
teamName = spell.team
|
||||
teamSpellMap[teamName] ?= []
|
||||
|
||||
spellNameElements = spellName.split '/'
|
||||
thangName = spellNameElements[0]
|
||||
spellName = spellNameElements[1]
|
||||
|
||||
teamSpellMap[teamName].push thangName if thangName not in teamSpellMap[teamName]
|
||||
|
||||
return teamSpellMap
|
||||
|
||||
|
||||
createSpells: (programmableThangs, world) ->
|
||||
pathPrefixComponents = ['play', 'level', @options.levelID, @options.session.id, 'code']
|
||||
@spells ?= {}
|
||||
|
|
|
@ -360,6 +360,7 @@ module.exports = class PlayLevelView extends View
|
|||
register: ->
|
||||
@bus = LevelBus.get(@levelID, @session.id)
|
||||
@bus.setSession(@session)
|
||||
@bus.setTeamSpellMap @tome.teamSpellMap
|
||||
@bus.connect() if @session.get('multiplayer')
|
||||
|
||||
onSessionWillSave: (e) ->
|
||||
|
@ -391,4 +392,4 @@ module.exports = class PlayLevelView extends View
|
|||
@bus?.destroy()
|
||||
#@instance.save() unless @instance.loading
|
||||
console.profileEnd?() if PROFILE_ME
|
||||
@session.off 'change:multiplayer', @onMultiplayerChanged, @
|
||||
@session.off 'change:multiplayer', @onMultiplayerChanged, @
|
||||
|
|
|
@ -59,7 +59,9 @@
|
|||
"express-useragent": "~0.0.9",
|
||||
"gridfs-stream": "0.4.x",
|
||||
"stream-buffers": "0.2.x",
|
||||
"sendwithus": "2.0.x"
|
||||
"sendwithus": "2.0.x",
|
||||
"aws-sdk":"~2.0.0",
|
||||
"bayesian-battle":"0.0.x"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jade": "0.33.x",
|
||||
|
|
|
@ -25,14 +25,3 @@ createAndConfigureApp = ->
|
|||
serverSetup.setupRoutes app
|
||||
app
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -7,18 +7,22 @@ testing = '--unittest' in process.argv
|
|||
|
||||
|
||||
module.exports.connect = () ->
|
||||
address = module.exports.generateMongoConnectionString()
|
||||
winston.info "Connecting to Mongo with connection string #{address}"
|
||||
|
||||
mongoose.connect address
|
||||
mongoose.connection.once 'open', -> Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
|
||||
|
||||
|
||||
module.exports.generateMongoConnectionString = ->
|
||||
if config.mongo.mongoose_replica_string
|
||||
address = config.mongo.mongoose_replica_string
|
||||
winston.info "Connecting to replica set: #{address}"
|
||||
else
|
||||
dbName = config.mongo.db
|
||||
dbName += '_unittest' if testing
|
||||
address = config.mongo.host + ":" + config.mongo.port
|
||||
if config.mongo.username and config.mongo.password
|
||||
address = config.mongo.username + ":" + config.mongo.password + "@" + address
|
||||
# address = config.mongo.username + "@" + address # if connecting to production server
|
||||
address = "mongodb://#{address}/#{dbName}"
|
||||
winston.info "Connecting to standalone server #{address}"
|
||||
mongoose.connect address
|
||||
mongoose.connection.once 'open', ->
|
||||
Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
|
||||
|
||||
return address
|
|
@ -33,3 +33,11 @@ module.exports.badInput = (res, message='Unprocessable Entity. Bad Input.') ->
|
|||
module.exports.serverError = (res, message='Internal Server Error') ->
|
||||
res.send 500, message
|
||||
res.end()
|
||||
|
||||
module.exports.gatewayTimeoutError = (res, message="Gateway timeout") ->
|
||||
res.send 504, message
|
||||
res.end()
|
||||
|
||||
module.exports.clientTimeout = (res, message="The server did not recieve the client response in a timely manner") ->
|
||||
res.send 408, message
|
||||
res.end()
|
|
@ -33,4 +33,5 @@ module.exports.routes =
|
|||
'routes/languages'
|
||||
'routes/mail'
|
||||
'routes/sprites'
|
||||
'routes/queue'
|
||||
]
|
||||
|
|
283
server/commons/queue.coffee
Normal file
283
server/commons/queue.coffee
Normal file
|
@ -0,0 +1,283 @@
|
|||
config = require '../../server_config'
|
||||
log = require 'winston'
|
||||
mongoose = require 'mongoose'
|
||||
async = require 'async'
|
||||
aws = require 'aws-sdk'
|
||||
db = require './database'
|
||||
mongoose = require 'mongoose'
|
||||
events = require 'events'
|
||||
crypto = require 'crypto'
|
||||
|
||||
module.exports.queueClient = undefined
|
||||
|
||||
defaultMessageVisibilityTimeoutInSeconds = 20
|
||||
defaultMessageReceiptTimeout = 10
|
||||
|
||||
|
||||
module.exports.initializeQueueClient = (cb) ->
|
||||
module.exports.queueClient = generateQueueClient() unless queueClient?
|
||||
|
||||
cb?()
|
||||
|
||||
|
||||
generateQueueClient = ->
|
||||
#if config.queue.accessKeyId
|
||||
if false #TODO: Change this in production
|
||||
queueClient = new SQSQueueClient()
|
||||
else
|
||||
queueClient = new MongoQueueClient()
|
||||
|
||||
|
||||
class SQSQueueClient
|
||||
registerQueue: (queueName, options, callback) ->
|
||||
queueCreationOptions =
|
||||
QueueName: queueName
|
||||
|
||||
@sqs.createQueue queueCreationOptions, (err,data) =>
|
||||
@_logAndThrowFatalException "There was an error creating a new SQS queue, reason: #{JSON.stringify err}" if err?
|
||||
|
||||
newQueue = new SQSQueue queueName, data.QueueUrl, @sqs
|
||||
|
||||
callback? err, newQueue
|
||||
|
||||
|
||||
constructor: ->
|
||||
@_configure()
|
||||
@sqs = @_generateSQSInstance()
|
||||
|
||||
|
||||
_configure: ->
|
||||
aws.config.update
|
||||
accessKeyId: config.queue.accessKeyId
|
||||
secretAccessKey: config.queue.secretAccessKey
|
||||
region: config.queue.region
|
||||
|
||||
|
||||
_generateSQSInstance: -> new aws.SQS()
|
||||
|
||||
|
||||
_logAndThrowFatalException: (errorMessage) ->
|
||||
log.error errorMessage
|
||||
throw new Error errorMessage
|
||||
|
||||
|
||||
|
||||
class SQSQueue extends events.EventEmitter
|
||||
constructor: (@queueName, @queueUrl, @sqs) ->
|
||||
|
||||
|
||||
subscribe: (eventName, callback) -> @on eventName, callback
|
||||
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
|
||||
|
||||
|
||||
receiveMessage: (callback) ->
|
||||
queueReceiveOptions =
|
||||
QueueUrl: @queueUrl
|
||||
WaitTimeSeconds: defaultMessageReceiptTimeout
|
||||
|
||||
@sqs.receiveMessage queueReceiveOptions, (err, data) =>
|
||||
if err?
|
||||
@emit 'error',err,originalData
|
||||
else
|
||||
originalData = data
|
||||
data = new SQSMessage originalData, this
|
||||
@emit 'message',err,data
|
||||
|
||||
callback? err,data
|
||||
|
||||
|
||||
deleteMessage: (receiptHandle, callback) ->
|
||||
queueDeletionOptions =
|
||||
QueueUrl: @queueUrl
|
||||
ReceiptHandle: receiptHandle
|
||||
|
||||
@sqs.deleteMessage queueDeletionOptions, (err, data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'message',err,data
|
||||
|
||||
callback? err,data
|
||||
|
||||
|
||||
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
|
||||
messageVisibilityTimeoutOptions =
|
||||
QueueUrl: @queueUrl
|
||||
ReceiptHandle: receiptHandle
|
||||
VisibilityTimeout: secondsFromNow
|
||||
|
||||
@sqs.changeMessageVisibility messageVisibilityTimeoutOptions, (err, data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'edited',err,data
|
||||
|
||||
callback? err,data
|
||||
|
||||
|
||||
sendMessage: (messageBody, delaySeconds, callback) ->
|
||||
queueSendingOptions =
|
||||
QueueUrl: @queueUrl
|
||||
MessageBody: messageBody
|
||||
DelaySeconds: delaySeconds
|
||||
|
||||
@sqs.sendMessage queueSendingOptions, (err, data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'sent',err, data
|
||||
|
||||
callback? err,data
|
||||
|
||||
|
||||
listenForever: => async.forever (asyncCallback) => @receiveMessage (err, data) -> asyncCallback(null)
|
||||
|
||||
|
||||
class SQSMessage
|
||||
constructor: (@originalMessage, @parentQueue) ->
|
||||
|
||||
isEmpty: -> not @originalMessage.Messages?[0]?
|
||||
|
||||
getBody: -> @originalMessage.Messages[0].Body
|
||||
|
||||
getID: -> @originalMessage.Messages[0].MessageId
|
||||
|
||||
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callback
|
||||
|
||||
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
|
||||
|
||||
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
|
||||
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
|
||||
|
||||
getReceiptHandle: -> @originalMessage.Messages[0].ReceiptHandle
|
||||
|
||||
|
||||
|
||||
|
||||
class MongoQueueClient
|
||||
registerQueue: (queueName, options, callback) ->
|
||||
newQueue = new MongoQueue queueName,options,@messageModel
|
||||
callback(null, newQueue)
|
||||
|
||||
|
||||
constructor: ->
|
||||
@_configure()
|
||||
@_createMongoConnection()
|
||||
@messageModel = @_generateMessageModel()
|
||||
|
||||
|
||||
_configure: -> @databaseAddress = db.generateMongoConnectionString()
|
||||
|
||||
|
||||
_createMongoConnection: ->
|
||||
@mongooseConnection = mongoose.createConnection @databaseAddress
|
||||
@mongooseConnection.on 'error', -> log.error "There was an error connecting to the queue in MongoDB"
|
||||
@mongooseConnection.once 'open', -> log.info "Successfully connected to MongoDB queue!"
|
||||
|
||||
|
||||
_generateMessageModel: ->
|
||||
schema = new mongoose.Schema
|
||||
messageBody: Object,
|
||||
queue: {type: String, index:true}
|
||||
scheduledVisibilityTime: {type: Date, index: true}
|
||||
receiptHandle: {type: String, index: true}
|
||||
|
||||
@mongooseConnection.model 'messageQueue',schema
|
||||
|
||||
|
||||
class MongoQueue extends events.EventEmitter
|
||||
constructor: (queueName, options, messageModel) ->
|
||||
@Message = messageModel
|
||||
@queueName = queueName
|
||||
|
||||
|
||||
subscribe: (eventName, callback) -> @on eventName, callback
|
||||
unsubscribe: (eventName, callback) -> @removeListener eventName, callback
|
||||
|
||||
|
||||
receiveMessage: (callback) ->
|
||||
conditions =
|
||||
queue: @queueName
|
||||
scheduledVisibilityTime:
|
||||
$lt: new Date()
|
||||
|
||||
options =
|
||||
sort: 'scheduledVisibilityTime'
|
||||
|
||||
update =
|
||||
$set:
|
||||
receiptHandle: @_generateRandomReceiptHandle()
|
||||
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate()
|
||||
|
||||
@Message.findOneAndUpdate conditions, update, options, (err, data) =>
|
||||
return @emit 'error',err,data if err?
|
||||
|
||||
originalData = data
|
||||
data = new MongoMessage originalData, this
|
||||
@emit 'message',err,data
|
||||
callback? err,data
|
||||
|
||||
|
||||
deleteMessage: (receiptHandle, callback) ->
|
||||
conditions =
|
||||
queue: @queueName
|
||||
receiptHandle: receiptHandle
|
||||
scheduledVisibilityTime:
|
||||
$lt: new Date()
|
||||
|
||||
@Message.findOneAndRemove conditions, {}, (err, data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'delete',err,data
|
||||
|
||||
callback? err,data
|
||||
|
||||
|
||||
sendMessage: (messageBody, delaySeconds, callback) ->
|
||||
messageToSend = new @Message
|
||||
messageBody: messageBody
|
||||
queue: @queueName
|
||||
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate delaySeconds
|
||||
|
||||
messageToSend.save (err,data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'sent',err, data
|
||||
callback? err,data
|
||||
|
||||
changeMessageVisibilityTimeout: (secondsFromNow, receiptHandle, callback) ->
|
||||
conditions =
|
||||
queue: @queueName
|
||||
receiptHandle: receiptHandle
|
||||
scheduledVisibilityTime:
|
||||
$lt: new Date()
|
||||
|
||||
update =
|
||||
$set:
|
||||
scheduledVisibilityTime: @_constructDefaultVisibilityTimeoutDate secondsFromNow
|
||||
|
||||
@Message.findOneAndUpdate conditions, update, (err, data) =>
|
||||
if err? then @emit 'error',err,data else @emit 'update',err,data
|
||||
|
||||
callback? err, data
|
||||
|
||||
|
||||
listenForever: => async.forever (asyncCallback) => @recieveMessage (err, data) -> asyncCallback(null)
|
||||
|
||||
|
||||
_constructDefaultVisibilityTimeoutDate: (timeoutSeconds) ->
|
||||
timeoutSeconds ?= defaultMessageVisibilityTimeoutInSeconds
|
||||
newDate = new Date()
|
||||
newDate = new Date(newDate.getTime() + 1000 * timeoutSeconds)
|
||||
|
||||
newDate
|
||||
|
||||
|
||||
_generateRandomReceiptHandle: -> crypto.randomBytes(20).toString('hex')
|
||||
|
||||
|
||||
|
||||
class MongoMessage
|
||||
constructor: (@originalMessage, @parentQueue) ->
|
||||
|
||||
isEmpty: -> not @originalMessage
|
||||
|
||||
getBody: -> @originalMessage.messageBody
|
||||
|
||||
getID: -> @originalMesage._id
|
||||
|
||||
removeFromQueue: (callback) -> @parentQueue.deleteMessage @getReceiptHandle(), callbacks
|
||||
|
||||
requeue: (callback) -> @parentQueue.changeMessageVisibilityTimeout 0, @getReceiptHandle(), callback
|
||||
|
||||
changeMessageVisibilityTimeout: (secondsFromFunctionCall, callback) ->
|
||||
@parentQueue.changeMessageVisibilityTimeout secondsFromFunctionCall,@getReceiptHandle(), callback
|
||||
|
||||
getReceiptHandle: -> @originalMessage.receiptHandle
|
|
@ -7,7 +7,7 @@ class LevelSessionHandler extends Handler
|
|||
modelClass: LevelSession
|
||||
editableProperties: ['multiplayer', 'players', 'code', 'completed', 'state',
|
||||
'levelName', 'creatorName', 'levelID', 'screenshot',
|
||||
'chat']
|
||||
'chat', 'teamSpells']
|
||||
|
||||
getByRelationship: (req, res, args...) ->
|
||||
return @sendNotFoundError(res) unless args.length is 2 and args[1] is 'active'
|
||||
|
|
|
@ -55,9 +55,18 @@ _.extend LevelSessionSchema.properties,
|
|||
|
||||
# TODO: specify this more
|
||||
code: { type: 'object' }
|
||||
teamSpells:
|
||||
type: 'object'
|
||||
additionalProperties:
|
||||
type: 'array'
|
||||
|
||||
|
||||
players: { type: 'object' }
|
||||
chat: { type: 'array' }
|
||||
|
||||
meanStrength: {type: 'number', default: 25}
|
||||
standardDeviation: {type:'number', default:25/3, minimum: 0}
|
||||
totalScore: {type: 'number', default: 10}
|
||||
|
||||
|
||||
c.extendBasicProperties LevelSessionSchema, 'level.session'
|
||||
|
|
306
server/queues/scoring.coffee
Normal file
306
server/queues/scoring.coffee
Normal file
|
@ -0,0 +1,306 @@
|
|||
config = require '../../server_config'
|
||||
log = require 'winston'
|
||||
mongoose = require 'mongoose'
|
||||
async = require 'async'
|
||||
errors = require '../commons/errors'
|
||||
aws = require 'aws-sdk'
|
||||
db = require './../routes/db'
|
||||
mongoose = require 'mongoose'
|
||||
queues = require '../commons/queue'
|
||||
LevelSession = require '../levels/sessions/LevelSession'
|
||||
TaskLog = require './task/ScoringTask'
|
||||
bayes = new (require 'bayesian-battle')()
|
||||
|
||||
scoringTaskQueue = undefined
|
||||
scoringTaskTimeoutInSeconds = 400
|
||||
|
||||
|
||||
module.exports.setup = (app) -> connectToScoringQueue()
|
||||
|
||||
connectToScoringQueue = ->
|
||||
queues.initializeQueueClient ->
|
||||
queues.queueClient.registerQueue "scoring", {}, (err,data) ->
|
||||
throwScoringQueueRegistrationError(err) if err?
|
||||
scoringTaskQueue = data
|
||||
log.info "Connected to scoring task queue!"
|
||||
|
||||
throwScoringQueueRegistrationError = (error) ->
|
||||
log.error "There was an error registering the scoring queue: #{error}"
|
||||
throw new Error "There was an error registering the scoring queue."
|
||||
|
||||
module.exports.createNewTask = (req, res) ->
|
||||
scoringTaskQueue.sendMessage req.body, 0, (err, data) ->
|
||||
return errors.badInput res, "There was an error creating the message, reason: #{err}" if err?
|
||||
|
||||
res.send data
|
||||
res.end()
|
||||
|
||||
|
||||
module.exports.dispatchTaskToConsumer = (req, res) ->
|
||||
userID = getUserIDFromRequest req,res
|
||||
return errors.forbidden res, "You need to be logged in to simulate games" if isUserAnonymous req
|
||||
|
||||
scoringTaskQueue.receiveMessage (taskQueueReceiveError, message) ->
|
||||
if (not message?) or message.isEmpty() or taskQueueReceiveError?
|
||||
return errors.gatewayTimeoutError res, "No messages were receieved from the queue. Msg:#{taskQueueReceiveError}"
|
||||
|
||||
messageBody = parseTaskQueueMessage req, res, message
|
||||
return errors.serverError res, "There was an error parsing the queue message" unless messageBody?
|
||||
|
||||
constructTaskObject messageBody, (taskConstructionError, taskObject) ->
|
||||
return errors.serverError res, "There was an error constructing the scoring task" if taskConstructionError?
|
||||
|
||||
message.changeMessageVisibilityTimeout scoringTaskTimeoutInSeconds
|
||||
|
||||
constructTaskLogObject userID,message.getReceiptHandle(), (taskLogError, taskLogObject) ->
|
||||
return errors.serverError res, "There was an error creating the task log object." if taskLogError?
|
||||
|
||||
setTaskObjectTaskLogID taskObject, taskLogObject._id
|
||||
|
||||
sendResponseObject req, res, taskObject
|
||||
|
||||
|
||||
getUserIDFromRequest = (req) -> if req.user? then return req.user._id else return null
|
||||
|
||||
|
||||
isUserAnonymous = (req) -> if req.user? then return req.user.anonymous else return true
|
||||
|
||||
|
||||
parseTaskQueueMessage = (req, res, message) ->
|
||||
try
|
||||
if typeof message.getBody() is "object" then return message.getBody()
|
||||
|
||||
return messageBody = JSON.parse message.getBody()
|
||||
catch e
|
||||
sendResponseObject req, res, {"error":"There was an error parsing the task.Error: #{e}" }
|
||||
return null
|
||||
|
||||
constructTaskObject = (taskMessageBody, callback) ->
|
||||
async.map taskMessageBody.sessions, getSessionInformation, (err, sessions) ->
|
||||
return callback err, data if err?
|
||||
|
||||
taskObject =
|
||||
"messageGenerated": Date.now()
|
||||
"sessions": []
|
||||
|
||||
for session in sessions
|
||||
sessionInformation =
|
||||
"sessionID": session.sessionID
|
||||
"sessionChangedTime": session.changed
|
||||
"team": session.team ? "No team"
|
||||
"code": session.code
|
||||
"teamSpells": session.teamSpells ? {}
|
||||
"levelID": session.levelID
|
||||
|
||||
taskObject.sessions.push sessionInformation
|
||||
callback err, taskObject
|
||||
|
||||
|
||||
getSessionInformation = (sessionIDString, callback) ->
|
||||
LevelSession.findOne {"_id": sessionIDString }, (err, session) ->
|
||||
return callback err, {"error":"There was an error retrieving the session."} if err?
|
||||
|
||||
session = session.toObject()
|
||||
sessionInformation =
|
||||
"sessionID": session._id
|
||||
"code": _.cloneDeep session.code
|
||||
"changed": session.changed
|
||||
"creator": session.creator
|
||||
"team": session.team
|
||||
"teamSpells": session.teamSpells
|
||||
"levelID": session.levelID
|
||||
|
||||
callback err, sessionInformation
|
||||
|
||||
|
||||
constructTaskLogObject = (calculatorUserID, messageIdentifierString, callback) ->
|
||||
taskLogObject = new TaskLog
|
||||
"createdAt": new Date()
|
||||
"calculator":calculatorUserID
|
||||
"sentDate": Date.now()
|
||||
"messageIdentifierString":messageIdentifierString
|
||||
|
||||
taskLogObject.save callback
|
||||
|
||||
|
||||
setTaskObjectTaskLogID = (taskObject, taskLogObjectID) -> taskObject.taskID = taskLogObjectID
|
||||
|
||||
|
||||
sendResponseObject = (req,res,object) ->
|
||||
res.setHeader('Content-Type', 'application/json')
|
||||
res.send(object)
|
||||
res.end()
|
||||
|
||||
module.exports.processTaskResult = (req, res) ->
|
||||
clientResponseObject = verifyClientResponse req.body, res
|
||||
|
||||
if clientResponseObject?
|
||||
TaskLog.findOne {"_id": clientResponseObject.taskID}, (err, taskLog) ->
|
||||
return errors.serverError res, "There was an error retrieiving the task log object" if err?
|
||||
|
||||
taskLogJSON = taskLog.toObject()
|
||||
|
||||
return errors.badInput res, "That computational task has already been performed" if taskLogJSON.calculationTimeMS
|
||||
return handleTimedOutTask req, res, clientResponseObject if hasTaskTimedOut taskLogJSON.sentDate
|
||||
|
||||
logTaskComputation clientResponseObject, taskLog, (loggingError) ->
|
||||
if loggingError?
|
||||
return errors.serverError res, "There as a problem logging the task computation: #{loggingError}"
|
||||
|
||||
updateScores clientResponseObject, (updatingScoresError, newScores) ->
|
||||
if updatingScoresError?
|
||||
return errors.serverError res, "There was an error updating the scores.#{updatingScoresError}"
|
||||
|
||||
sendResponseObject req, res, {"message":"The scores were updated successfully!"}
|
||||
|
||||
|
||||
|
||||
|
||||
hasTaskTimedOut = (taskSentTimestamp) -> taskSentTimestamp + scoringTaskTimeoutInSeconds * 1000 < Date.now()
|
||||
|
||||
|
||||
handleTimedOutTask = (req, res, taskBody) -> errors.clientTimeout res, "The results weren't provided within the timeout"
|
||||
|
||||
|
||||
verifyClientResponse = (responseObject, res) ->
|
||||
unless typeof responseObject is "object"
|
||||
errors.badInput res, "The response to that query is required to be a JSON object."
|
||||
null
|
||||
else
|
||||
responseObject
|
||||
|
||||
|
||||
logTaskComputation = (taskObject,taskLogObject, callback) ->
|
||||
taskLogObject.calculationTimeMS = taskObject.calculationTimeMS
|
||||
taskLogObject.sessions = taskObject.sessions
|
||||
taskLogObject.save callback
|
||||
|
||||
|
||||
updateScores = (taskObject,callback) ->
|
||||
sessionIDs = _.pluck taskObject.sessions, 'sessionID'
|
||||
|
||||
async.map sessionIDs, retrieveOldScoreMetrics, (err, oldScores) ->
|
||||
callback err, {"error": "There was an error retrieving the old scores"} if err?
|
||||
|
||||
oldScoreArray = _.toArray putRankingFromMetricsIntoScoreObject taskObject, oldScores
|
||||
|
||||
newScoreArray = bayes.updatePlayerSkills oldScoreArray
|
||||
|
||||
saveNewScoresToDatabase newScoreArray, callback
|
||||
|
||||
|
||||
saveNewScoresToDatabase = (newScoreArray, callback) ->
|
||||
async.eachSeries newScoreArray, updateScoreInSession, (err) ->
|
||||
if err? then callback err, null else callback err, {"message":"All scores were saved successfully."}
|
||||
|
||||
|
||||
updateScoreInSession = (scoreObject,callback) ->
|
||||
sessionObjectQuery =
|
||||
"_id": scoreObject.id
|
||||
|
||||
LevelSession.findOne sessionObjectQuery, (err, session) ->
|
||||
return callback err, null if err?
|
||||
|
||||
session.meanStrength = scoreObject.meanStrength
|
||||
session.standardDeviation = scoreObject.standardDeviation
|
||||
session.totalScore = scoreObject.meanStrength - 1.8 * scoreObject.standardDeviation
|
||||
|
||||
log.info "Saving session #{session._id}!"
|
||||
|
||||
session.save callback
|
||||
|
||||
|
||||
putRankingFromMetricsIntoScoreObject = (taskObject,scoreObject) ->
|
||||
scoreObject = _.indexBy scoreObject, 'id'
|
||||
|
||||
for session in taskObject.sessions
|
||||
scoreObject[session.sessionID].gameRanking = session.metrics.rank
|
||||
|
||||
scoreObject
|
||||
|
||||
retrieveOldScoreMetrics = (sessionID, callback) ->
|
||||
sessionQuery =
|
||||
"_id":sessionID
|
||||
|
||||
LevelSession.findOne sessionQuery, (err, session) ->
|
||||
return callback err, {"error":"There was an error retrieving the session."} if err?
|
||||
|
||||
defaultScore = (25 - 1.8*(25/3))
|
||||
defaultStandardDeviation = 25/3
|
||||
|
||||
oldScoreObject =
|
||||
"standardDeviation":session.standardDeviation ? defaultStandardDeviation
|
||||
"meanStrength":session.meanStrength ? 25
|
||||
"totalScore":session.totalScore ? defaultScore
|
||||
"id": sessionID
|
||||
|
||||
callback err, oldScoreObject
|
||||
|
||||
|
||||
|
||||
|
||||
###Sample Messages
|
||||
sampleQueueMessage =
|
||||
{
|
||||
"sessions": ["52dea9b77e486eeb97000001","52d981a73cf02dcf260003cb"]
|
||||
}
|
||||
|
||||
sampleUndoneTaskObject =
|
||||
"taskID": "507f191e810c19729de860ea"
|
||||
"sessions" : [
|
||||
{
|
||||
"ID":"52dfeb17c8b5f435c7000025"
|
||||
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
|
||||
"team":"humans"
|
||||
"code": "code goes here"
|
||||
},
|
||||
{
|
||||
"ID":"51eb2714fa058cb20d00fedg"
|
||||
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
|
||||
"team":"ogres"
|
||||
"code": "code goes here"
|
||||
}
|
||||
]
|
||||
sampleResponseObject =
|
||||
"taskID": "507f191e810c19729de860ea"
|
||||
"calculationTime":3201
|
||||
"sessions": [
|
||||
{
|
||||
"ID":"52dfeb17c8b5f435c7000025"
|
||||
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
|
||||
"metrics": {
|
||||
"rank":2
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID":"51eb2714fa058cb20d00fedg"
|
||||
"sessionChangedTime": "2014-01-22T16:28:12.450Z"
|
||||
"metrics": {
|
||||
"rank":1
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
sampleTaskLogObject=
|
||||
{
|
||||
"_id":ObjectId("507f191e810c19729de860ea") #datestamp is built into objectId
|
||||
"calculatedBy":ObjectId("51eb2714fa058cb20d0006ef")
|
||||
"calculationTime":3201
|
||||
timedOut: false
|
||||
"sessions":[
|
||||
{
|
||||
"ID":ObjectId("52dfeb17c8b5f435c7000025")
|
||||
"metrics": {
|
||||
"rank":2
|
||||
}
|
||||
},
|
||||
{
|
||||
"ID":ObjectId("51eb2714fa058cb20d00feda")
|
||||
"metrics": {
|
||||
"rank":1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
###
|
0
server/queues/sendwithus.coffee
Normal file
0
server/queues/sendwithus.coffee
Normal file
12
server/queues/task/ScoringTask.coffee
Normal file
12
server/queues/task/ScoringTask.coffee
Normal file
|
@ -0,0 +1,12 @@
|
|||
mongoose = require('mongoose')
|
||||
|
||||
ScoringTaskSchema = new mongoose.Schema(
|
||||
createdAt: {type: Date, expires: 3600} #expire document 1 hour after they are created
|
||||
calculator: {type:mongoose.Schema.Types.ObjectId}
|
||||
sentDate: {type: Number}
|
||||
messageIdentifierString: {type: String}
|
||||
calculationTimeMS: {type: Number, default: 0}
|
||||
sessions: {type: Array, default: []}
|
||||
)
|
||||
|
||||
module.exports = mongoose.model('scoringTask', ScoringTaskSchema)
|
50
server/routes/queue.coffee
Normal file
50
server/routes/queue.coffee
Normal file
|
@ -0,0 +1,50 @@
|
|||
log = require 'winston'
|
||||
errors = require '../commons/errors'
|
||||
scoringQueue = require '../queues/scoring'
|
||||
|
||||
|
||||
module.exports.setup = (app) ->
|
||||
scoringQueue.setup()
|
||||
|
||||
app.all '/queue/*', (req, res) ->
|
||||
setResponseHeaderToJSONContentType res
|
||||
|
||||
queueName = getQueueNameFromPath req.path
|
||||
try
|
||||
handler = loadQueueHandler queueName
|
||||
if isHTTPMethodGet req
|
||||
handler.dispatchTaskToConsumer req,res
|
||||
else if isHTTPMethodPut req
|
||||
handler.processTaskResult req,res
|
||||
else if isHTTPMethodPost req
|
||||
handler.createNewTask req, res #TODO: do not use this in production
|
||||
else
|
||||
sendMethodNotSupportedError req, res
|
||||
catch error
|
||||
log.error error
|
||||
sendQueueError req, res, error
|
||||
|
||||
setResponseHeaderToJSONContentType = (res) -> res.setHeader('Content-Type', 'application/json')
|
||||
|
||||
getQueueNameFromPath = (path) ->
|
||||
pathPrefix = '/queue/'
|
||||
pathAfterPrefix = path[pathPrefix.length..]
|
||||
partsOfURL = pathAfterPrefix.split '/'
|
||||
queueName = partsOfURL[0]
|
||||
queueName
|
||||
|
||||
loadQueueHandler = (queueName) -> require ('../queues/' + queueName)
|
||||
|
||||
|
||||
isHTTPMethodGet = (req) -> return req.route.method is 'get'
|
||||
|
||||
isHTTPMethodPost = (req) -> return req.route.method is 'post'
|
||||
|
||||
isHTTPMethodPut = (req) -> return req.route.method is 'put'
|
||||
|
||||
|
||||
sendMethodNotSupportedError = (req, res) -> errors.badMethod(res,"Queues do not support the HTTP method used." )
|
||||
|
||||
sendQueueError = (req,res, error) -> errors.serverError(res, "Route #{req.path} had a problem: #{error}")
|
||||
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
config = require '../server_config'
|
||||
sendwithusAPI = require 'sendwithus'
|
||||
swuAPIKey = config.mail.sendwithusAPIKey
|
||||
queues = require './commons/queue'
|
||||
|
||||
module.exports.setupRoutes = (app) ->
|
||||
return
|
||||
|
||||
|
||||
options = { DEBUG: not config.isProduction }
|
||||
module.exports.api = new sendwithusAPI swuAPIKey, options
|
||||
|
|
|
@ -31,6 +31,14 @@ config.mail.mailchimpAPIKey = process.env.COCO_MAILCHIMP_API_KEY || '';
|
|||
config.mail.mailchimpWebhook = process.env.COCO_MAILCHIMP_WEBHOOK || '/mail/webhook';
|
||||
config.mail.sendwithusAPIKey = process.env.COCO_SENDWITHUS_API_KEY || '';
|
||||
|
||||
config.queue = {};
|
||||
config.queue.accessKeyId = process.env.COCO_AWS_ACCESS_KEY_ID || '';
|
||||
config.queue.secretAccessKey = process.env.COCO_AWS_SECRET_ACCESS_KEY || '';
|
||||
config.queue.region = 'us-east-1';
|
||||
config.queue.simulationQueueName = "simulationQueue";
|
||||
config.mongoQueue = {};
|
||||
config.mongoQueue.queueDatabaseName = "coco_queue";
|
||||
|
||||
config.salt = process.env.COCO_SALT || 'pepper';
|
||||
config.cookie_secret = process.env.COCO_COOKIE_SECRET || 'chips ahoy';
|
||||
|
||||
|
|
|
@ -80,6 +80,7 @@ setupFacebookCrossDomainCommunicationRoute = (app) ->
|
|||
|
||||
exports.setupRoutes = (app) ->
|
||||
app.use app.router
|
||||
|
||||
baseRoute.setup app
|
||||
setupFacebookCrossDomainCommunicationRoute app
|
||||
setupFallbackRouteToIndex app
|
||||
|
|
Loading…
Reference in a new issue