Added better live updater and various changes
authorBrendan Hansen <brendan.f.hansen@gmail.com>
Fri, 27 Sep 2019 04:33:40 +0000 (23:33 -0500)
committerBrendan Hansen <brendan.f.hansen@gmail.com>
Fri, 27 Sep 2019 04:33:40 +0000 (23:33 -0500)
33 files changed:
codebox/app/app.moon
codebox/config.moon
codebox/controllers/account/register.moon
codebox/controllers/controller.moon
codebox/controllers/executer/status_update.moon
codebox/controllers/leaderboard/update.moon [new file with mode: 0644]
codebox/controllers/problem/problem.moon
codebox/controllers/submission/status.moon
codebox/controllers/submission/view.moon
codebox/facades/updater.moon [new file with mode: 0644]
codebox/nginx.conf
codebox/services/scoring.moon
codebox/static/coffee/leaderboard_update.coffee [new file with mode: 0644]
codebox/static/coffee/submission_reloader.coffee
codebox/static/js/leaderboard_update.js [new file with mode: 0644]
codebox/static/js/leaderboard_update.js.map [new file with mode: 0644]
codebox/static/js/submission_reloader.js
codebox/static/js/submission_reloader.js.map
codebox/views/account/register.moon
codebox/views/leaderboard/view.moon
codebox/views/partials/layout.moon
codebox/views/problem/problem.moon
codebox/views/ssr/leaderboard.moon [new file with mode: 0644]
docker-compose.yaml
docker/updater/Dockerfile [new file with mode: 0644]
executer/app/routes.coffee
updater/.gitignore [new file with mode: 0644]
updater/Tupfile [new file with mode: 0644]
updater/Tuprules.tup [new file with mode: 0644]
updater/app/Tupfile [new file with mode: 0644]
updater/app/app.coffee [new file with mode: 0644]
updater/main.coffee [new file with mode: 0644]
updater/package.json [new file with mode: 0644]

index 39a38123f097aa34ea5203c8169054eddf45d180..dc1750761d5642499d1aaf62ae795e5fb98b2357 100644 (file)
@@ -3,6 +3,7 @@ console = require "lapis.console"
 
 bind = require "utils.binding"
 bind\bind_static 'executer', require 'facades.executer'
+bind\bind_static 'updater', require 'facades.updater'
 bind\bind_static 'crypto', require 'services.crypto'
 bind\bind_static 'uuidv4', require 'services.uuid'
 bind\bind_static 'queries', require 'services.queries'
@@ -29,6 +30,7 @@ class extends lapis.Application
                @navbar.selected = -1
 
                @scripts = {}
+               @raw_scripts = {}
 
        ['index':                "/"]: => redirect_to: @url_for 'problem'
 
@@ -38,6 +40,7 @@ class extends lapis.Application
        ['account.account':  "/account"]:  controller "account.account"
 
     ['leaderboard': '/leaderboard']: controller "leaderboard.view"
+    ['leaderboard.update': '/leaderboard/update']: controller "leaderboard.update"
 
        ['problem': '/problems']: controller "problem.problem"
        ['problem.description': '/problems/:problem_name']: controller "problem.problem"
index e622ccad78a8ab898b732fba05df9533826b3140..1a67555b5997e1e12f2a3f6947ffe0e2d9a3d73f 100644 (file)
@@ -7,6 +7,7 @@ config "development", ->
        req_secret (os.getenv 'REQ_SECRET')
 
        executer_addr 'http://192.168.0.4:8080'
+       updater_addr 'http://192.168.0.5:5000'
 
        postgres ->
                -- Have to use a fixed ip since the container name
index 4262eb843e6957381b423269ab3743f891aad56e..34fa80811682fc442a10e2df7bb40b5f903b11c0 100644 (file)
@@ -18,8 +18,8 @@ make_controller
                @flow 'csrf_validate'
 
                assert_valid @params, {
-                       { "username", exists: true, min_length: 2, matches_pattern: "^%w+$" }
-                       { "nickname", exists: true, min_length: 2 }
+                       { "username", exists: true, min_length: 1, matches_pattern: "^%w+$" }
+                       { "nickname", exists: true, min_length: 1 }
                        { "email", exists: true, min_length: 4, matches_pattern: "^%S+@%S+%.%S+$" }
                        { "password", exists: true, min_length: 2 }
                        { "password_confirmation", exists: true, min_length: 2, equals: @params.password, 'Passwords must be the same' }
index 3ab6bfeb5265226bd1da13db14f1bcddc230322f..8816f9b0e7d543bfd539a36fd5e9bb6848490a72 100644 (file)
@@ -12,6 +12,10 @@ import respond_to from require "lapis.application"
                                        for s in *routes.scripts
                                                table.insert @scripts, s
 
+                               if routes.raw_scripts
+                                       for s in *routes.raw_scripts
+                                               table.insert @raw_scripts, s
+
                                return if not routes.middleware
 
                                for middleware in *routes.middleware
index 9949072547825ff1e9e8f82dee196e2d72b2b842..665c4a81bd95d91013d5157d06fb2f8fbf60a57e 100644 (file)
@@ -7,6 +7,7 @@ import capture_errors, yield_error from require 'lapis.application'
 make_controller
     inject:
         scoring: 'scoring'
+               updater: 'updater'
 
        middleware: { 'internal_request' }
 
@@ -32,6 +33,8 @@ make_controller
             @scoring\score job.user_id, job.problem_id
             @scoring\place!
 
+               @updater\push_submission_update job.id
+
                json: { status: 'success' }
        ), =>
                json: { status: 'error', errors: @errors }
diff --git a/codebox/controllers/leaderboard/update.moon b/codebox/controllers/leaderboard/update.moon
new file mode 100644 (file)
index 0000000..59efe8d
--- /dev/null
@@ -0,0 +1,13 @@
+import make_controller from require "controllers.controller"
+Leaderboard = require 'views.ssr.leaderboard'
+
+make_controller
+    layout: false
+    middleware: { 'logged_in', 'competition_started' }
+
+    get: =>
+        @placements = @competition\get_leaderboard!
+
+        leaderboard_widget = Leaderboard @placements
+        leaderboard_widget\include_helper @
+        return { layout: false, status_code: 200, leaderboard_widget\render_to_string! }
index 66f9090dabc79a1c5ea825f24640b6e4e89caa93..087b9fa29012d8ebf85e14ee95aea431f654ccc0 100644 (file)
@@ -8,6 +8,10 @@ make_controller
 
        middleware: { 'logged_in', 'competition_started' }
        scripts: { "pie_chart" }
+       raw_scripts: {
+               "https://polyfill.io/v3/polyfill.min.js?features=es6"
+               "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"
+       }
 
     get: capture_errors_json =>
                @navbar.selected = 1
index 6b1ff30468f54559e56365cda157234450dfecfc..e641e689a2af1963790e6e975fb4625dbaa95138 100644 (file)
@@ -28,4 +28,4 @@ make_controller
 
         status_widget = JobResult @job
         status_widget\include_helper @
-        return { layout: false, status: status_code, status_widget\render_to_string! }
\ No newline at end of file
+        return { layout: false, status: status_code, status_widget\render_to_string! }
index f55f70a2c9949f0cc4047ec1ffe0430059cda2c4..9ca09d0264eac919763233ecfef3671911ebe341 100644 (file)
@@ -10,6 +10,7 @@ make_controller
 
        middleware: { 'logged_in' }
     scripts: { 'vendor/ace/ace', 'submission_reloader' }
+       raw_scripts: { '/socket.io/socket.io.js' }
 
     get: capture_errors_json =>
                @navbar.selected = 2
@@ -22,4 +23,4 @@ make_controller
         unless @job.user_id == @user.id
             yield_error "You are not allowed to view this submission!"
 
-       render: 'submission.view'
\ No newline at end of file
+       render: 'submission.view'
diff --git a/codebox/facades/updater.moon b/codebox/facades/updater.moon
new file mode 100644 (file)
index 0000000..5557dd3
--- /dev/null
@@ -0,0 +1,6 @@
+config = (require 'lapis.config').get!
+http = require 'lapis.nginx.http'
+
+class UpdaterFacade
+       push_submission_update: (job_id) =>
+               http.simple "#{config.updater_addr}/submission_update?submission_ida=#{job_id}"
index 6649d8f895dc687f6ac09ddb99c048aed648bb0a..fd664e7b0017de7b195b664ab20e2b9212572526 100644 (file)
@@ -60,5 +60,17 @@ http {
                proxy_http_version 1.1;
                proxy_pass $_url;
        }
+
+       location ~* \.io {
+               proxy_set_header Upgrade $http_upgrade;
+               proxy_set_header Connection "upgrade";
+               proxy_http_version 1.1;
+
+               proxy_set_header X-Real-IP $remote_addr;
+               proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+               proxy_set_header Host $host;
+               proxy_pass http://192.168.0.5:5000;
+               proxy_redirect off;
+       }
   }
 }
index 25eaf51c8c221cf3ed1f908d20842fea35b7a185..a7d705f15445ad45de1921283783ecae033ac634 100644 (file)
@@ -118,13 +118,11 @@ class Scoring extends Injectable
 
        score_user: (user_id) =>
         for p in *@comp_problems
-            problem = p\get_problem!
-            @score_problem_for_user user_id, problem
+            @score user_id, p.problem_id
 
-    score_problem: (problem_name) =>
-        problem = Problems\find short_name: problem_name
+    score_problem: (problem_id) =>
         for u in *@users
-            @score_problem_for_user u.id, problem
+            @score u.id, problem_id
 
     score_all: =>
         for p in *@comp_problems
diff --git a/codebox/static/coffee/leaderboard_update.coffee b/codebox/static/coffee/leaderboard_update.coffee
new file mode 100644 (file)
index 0000000..2156982
--- /dev/null
@@ -0,0 +1,2 @@
+$(document).ready ->
+    console.log "Hello!"
index 4515b2d9d073e7a138356cc975a66501f0c5de86..3b13791c565355d291c83d589b86bcba2392b4d6 100644 (file)
@@ -4,8 +4,9 @@ updateStatus = ->
     $.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->
         $('#status-container').html html
 
-        if data.status == 200
-            setTimeout updateStatus, 100
-
 $(document).ready ->
-    updateStatus()
\ No newline at end of file
+       socket = io()
+       socket.emit "request-submission-updates", submission_id
+
+       socket.on 'update', ->
+               updateStatus()
diff --git a/codebox/static/js/leaderboard_update.js b/codebox/static/js/leaderboard_update.js
new file mode 100644 (file)
index 0000000..748dde3
--- /dev/null
@@ -0,0 +1,9 @@
+// Generated by CoffeeScript 2.4.1
+(function() {
+  $(document).ready(function() {
+    return console.log("Hello!");
+  });
+
+}).call(this);
+
+//# sourceMappingURL=leaderboard_update.js.map
diff --git a/codebox/static/js/leaderboard_update.js.map b/codebox/static/js/leaderboard_update.js.map
new file mode 100644 (file)
index 0000000..b4ee4fa
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "version": 3,
+  "file": "leaderboard_update.js",
+  "sourceRoot": "..",
+  "sources": [
+    "coffee/leaderboard_update.coffee"
+  ],
+  "names": [],
+  "mappings": ";AAAA;EAAA,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;WACd,OAAO,CAAC,GAAR,CAAY,QAAZ;EADc,CAAlB;AAAA",
+  "sourcesContent": [
+    "$(document).ready ->\n    console.log \"Hello!\"\n"
+  ]
+}
\ No newline at end of file
index add5a3dc6f9c70fafc3ac46a0fb039ae1490ac98..ea248245847f928603c0934d8bc9608c4fe94957 100644 (file)
@@ -8,15 +8,17 @@
     return $.get('/submissions/status', {
       submission_id: submission_id
     }, function(html, _, data) {
-      $('#status-container').html(html);
-      if (data.status === 200) {
-        return setTimeout(updateStatus, 100);
-      }
+      return $('#status-container').html(html);
     });
   };
 
   $(document).ready(function() {
-    return updateStatus();
+    var socket;
+    socket = io();
+    socket.emit("request-submission-updates", submission_id);
+    return socket.on('update', function() {
+      return updateStatus();
+    });
   });
 
 }).call(this);
index 115737534f5963c2e648b4e80257f3f916ea9af0..a00c822b543901353a50ad590d93823fe00de967 100644 (file)
@@ -6,8 +6,8 @@
     "coffee/submission_reloader.coffee"
   ],
   "names": [],
-  "mappings": ";AAAA;AAAA,MAAA,aAAA,EAAA;;EAAA,aAAA,GAAgB,CAAC,IAAI,eAAJ,CAAoB,MAAM,CAAC,QAAQ,CAAC,MAApC,CAAD,CAA4C,CAAC,GAA7C,CAAiD,eAAjD;;EAEhB,YAAA,GAAe,QAAA,CAAA,CAAA;WACX,CAAC,CAAC,GAAF,CAAM,qBAAN,EAA6B;MAAE,aAAA,EAAe;IAAjB,CAA7B,EAA+D,QAAA,CAAC,IAAD,EAAO,CAAP,EAAU,IAAV,CAAA;MAC3D,CAAA,CAAE,mBAAF,CAAsB,CAAC,IAAvB,CAA4B,IAA5B;MAEA,IAAG,IAAI,CAAC,MAAL,KAAe,GAAlB;eACI,UAAA,CAAW,YAAX,EAAyB,GAAzB,EADJ;;IAH2D,CAA/D;EADW;;EAOf,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;WACd,YAAA,CAAA;EADc,CAAlB;AATA",
+  "mappings": ";AAAA;AAAA,MAAA,aAAA,EAAA;;EAAA,aAAA,GAAgB,CAAC,IAAI,eAAJ,CAAoB,MAAM,CAAC,QAAQ,CAAC,MAApC,CAAD,CAA4C,CAAC,GAA7C,CAAiD,eAAjD;;EAEhB,YAAA,GAAe,QAAA,CAAA,CAAA;WACX,CAAC,CAAC,GAAF,CAAM,qBAAN,EAA6B;MAAE,aAAA,EAAe;IAAjB,CAA7B,EAA+D,QAAA,CAAC,IAAD,EAAO,CAAP,EAAU,IAAV,CAAA;aAC3D,CAAA,CAAE,mBAAF,CAAsB,CAAC,IAAvB,CAA4B,IAA5B;IAD2D,CAA/D;EADW;;EAIf,CAAA,CAAE,QAAF,CAAW,CAAC,KAAZ,CAAkB,QAAA,CAAA,CAAA;AACjB,QAAA;IAAA,MAAA,GAAS,EAAA,CAAA;IACT,MAAM,CAAC,IAAP,CAAY,4BAAZ,EAA0C,aAA1C;WAEA,MAAM,CAAC,EAAP,CAAU,QAAV,EAAoB,QAAA,CAAA,CAAA;aACnB,YAAA,CAAA;IADmB,CAApB;EAJiB,CAAlB;AANA",
   "sourcesContent": [
-    "submission_id = (new URLSearchParams window.location.search).get 'submission_id'\n\nupdateStatus = ->\n    $.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->\n        $('#status-container').html html\n\n        if data.status == 200\n            setTimeout updateStatus, 100\n\n$(document).ready ->\n    updateStatus()"
+    "submission_id = (new URLSearchParams window.location.search).get 'submission_id'\n\nupdateStatus = ->\n    $.get '/submissions/status', { submission_id: submission_id }, (html, _, data) ->\n        $('#status-container').html html\n\n$(document).ready ->\n\tsocket = io()\n\tsocket.emit \"request-submission-updates\", submission_id\n\n\tsocket.on 'update', ->\n\t\tupdateStatus()\n"
   ]
 }
\ No newline at end of file
index 1061516e96f32e4d4c06ae820162de18c6e62e61..d2790c99024eb79d4b463c88122c882b1fc13afc 100644 (file)
@@ -9,15 +9,18 @@ class Register extends html.Widget
                        div class: 'split-1-1', ->
                                form method: 'POST', ->
                                        input type: 'hidden', name: 'csrf_token', value: @csrf_token
-                                       
+
                                        label for: 'username', 'Username'
                                        p -> input type: 'text', placeholder: 'Username', name: 'username', required: true, ''
 
-                                       label for: 'password', 'Password'
-                                       p -> input type: 'password', placeholder: 'Password', name: 'password', required: true, ''
+                    div class: 'split-2', ->
+                        div ->
+                            label for: 'password', 'Password'
+                            p -> input type: 'password', placeholder: 'Password', name: 'password', required: true, ''
 
-                                       label for: 'password_confirmation', 'Confirm Password'
-                                       p -> input type: 'password', placeholder: 'Confirm Password', name: 'password_confirmation', required: true, ''
+                        div ->
+                            label for: 'password_confirmation', 'Confirm Password'
+                            p -> input type: 'password', placeholder: 'Confirm Password', name: 'password_confirmation', required: true, ''
 
                                        label for: 'email', 'Email'
                                        p -> input type: 'text', placeholder: 'Email', name: 'email', required: true, ''
index 6a9a9b71df33fff2f24ce5f0df2420628df0f5fa..4cf3ff50539b5a3d078546dc6bf723200dec7df1 100644 (file)
@@ -1,50 +1,10 @@
 html = require 'lapis.html'
 import CompetitionProblems, LeaderboardProblems from require 'models'
+Leaderboard = require 'views.ssr.leaderboard'
 
-class Leaderboard extends html.Widget
+class LeaderboardView extends html.Widget
     content: =>
         h1 "#{@competition.name} - Leaderboard"
 
         div class: 'content', ->
-            div class: 'leaderboard', ->
-                drawn_labels = false
-                for place in *@placements
-                    @problems = place\get_problems!
-                    CompetitionProblems\include_in @problems, "problem_id",
-                        as: 'cp'
-                        flip: true
-                        local_key: 'problem_id'
-                        where: { competition_id: @competition.id }
-
-                    -- Sort the problems by letter
-                    prob.lnum = (prob.cp.letter\byte 1) for prob in *@problems
-                    table.sort @problems, (a, b) ->
-                        a.lnum < b.lnum
-
-                    unless drawn_labels
-                        div class: 'placement-labels', ->
-                            div "Place"
-                            div "Name"
-                            div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
-                                for prob in *@problems
-                                    div "#{prob.cp.letter}"
-                            div "Score"
-                        drawn_labels = true
-
-                    div class: 'placement', ->
-                        div "#{place.place}"
-                        div "#{place\get_user!.nickname}"
-
-                        div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
-                            for prob in *@problems
-                                prob_status = switch prob.status
-                                    when LeaderboardProblems.statuses.correct then "correct"
-                                    when LeaderboardProblems.statuses.wrong then "wrong"
-                                    when LeaderboardProblems.statuses.attempted then "attempted"
-
-                                div class: "#{prob_status}", ->
-                                    div "#{prob.points}"
-                                    div "#{prob.attempts}"
-
-                        div "#{place.score}"
-
+            widget (Leaderboard @placements)
index 67f288a3ab05028447f7f390936cdc891e49ca1c..17c18ac58fa924b0d4985a3cb347574329896f6f 100644 (file)
@@ -14,6 +14,9 @@ class DefaultLayout extends html.Widget
                                for s in *@scripts
                                        script type: "text/javascript", src: "/static/js/#{s}.js"
 
+                               for s in *@raw_scripts
+                                       script type: "text/javascript", src: s
+
                        body ->
                                widget Navbar
                                widget ErrorList
index 58eb4659e3dfec3d9e2c3f0ca992f504d45e0343..6f9a9a4b5531b47b0e3b75b2026aa27a8a78b26f 100644 (file)
@@ -4,9 +4,6 @@ import Problems from require 'models'
 
 class ProblemsView extends html.Widget
        content: =>
-               raw '<script src="https://polyfill.io/v3/polyfill.min.js?features=es6"></script>
-                       <script id="MathJax-script" async src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>'
-
                div class: 'sidebar-page-container', ->
                        div class: 'sidebar-problem-list', ->
                                widget (require 'views.partials.problem_sidebar')
diff --git a/codebox/views/ssr/leaderboard.moon b/codebox/views/ssr/leaderboard.moon
new file mode 100644 (file)
index 0000000..2c5752f
--- /dev/null
@@ -0,0 +1,48 @@
+html = require 'lapis.html'
+import CompetitionProblems, LeaderboardProblems from require 'models'
+
+class Leaderboard extends html.Widget
+    new: (@placements) =>
+
+    content: =>
+        div class: 'leaderboard', ->
+            drawn_labels = false
+            for place in *@placements
+                @problems = place\get_problems!
+                CompetitionProblems\include_in @problems, "problem_id",
+                    as: 'cp'
+                    flip: true
+                    local_key: 'problem_id'
+                    where: { competition_id: @competition.id }
+                 -- Sort the problems by letter
+                prob.lnum = (prob.cp.letter\byte 1) for prob in *@problems
+
+                table.sort @problems, (a, b) ->
+                    a.lnum < b.lnum
+
+                unless drawn_labels
+                    div class: 'placement-labels', ->
+                        div "Place"
+                        div "Name"
+                        div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
+                            for prob in *@problems
+                                div "#{prob.cp.letter}"
+                        div "Score"
+                    drawn_labels = true
+
+                div class: 'placement', ->
+                    div "#{place.place}"
+                    div "#{place\get_user!.nickname}"
+
+                    div class: 'problem', style: "grid-template-columns: repeat(#{#@problems}, 1fr)", ->
+                        for prob in *@problems
+                            prob_status = switch prob.status
+                                when LeaderboardProblems.statuses.correct then "correct"
+                                when LeaderboardProblems.statuses.wrong then "wrong"
+                                when LeaderboardProblems.statuses.attempted then "attempted"
+
+                            div class: "#{prob_status}", ->
+                                div "#{prob.points}"
+                                div "#{prob.attempts}"
+
+                    div "#{place.score}"
index 92278a6e3e605d89648db0f1c2cc77a52b66a0e4..6de52eddf9e1117c96217564728dba6e2a580fd8 100644 (file)
@@ -38,6 +38,19 @@ services:
             appnet:
                 ipv4_address: 192.168.0.4
 
+    updater:
+        env_file:
+            - config.env
+        build:
+            context: .
+            dockerfile: ./docker/updater/Dockerfile
+        volumes:
+            - ./updater/app:/app/app
+        command: node main.js
+        networks:
+            appnet:
+                ipv4_address: 192.168.0.5
+
     postgres:
         env_file:
             - config.env
diff --git a/docker/updater/Dockerfile b/docker/updater/Dockerfile
new file mode 100644 (file)
index 0000000..b11ba78
--- /dev/null
@@ -0,0 +1,11 @@
+FROM node:12.9.1
+
+RUN yarn global add coffeescript
+
+COPY ./updater/package.json /app/package.json
+WORKDIR /app
+RUN yarn
+
+COPY ./updater/main.js /app/main.js
+
+ENV PATH $(yarn global bin):$PATH
index c7f97a35c25bd8d226b59dfc97f80f4800d8116f..4df44b848e413c3841816748b34f061a4a6f86af 100644 (file)
@@ -24,7 +24,6 @@ async function handle_job(job_id, lang, code, cases, time_limit) {
                                                rej(-1);
                                        }
 
-                                       console.log("Updated job: ", job_id, status.status)
                                        resolve(1);
                                }
                        )
diff --git a/updater/.gitignore b/updater/.gitignore
new file mode 100644 (file)
index 0000000..c2f7987
--- /dev/null
@@ -0,0 +1,4 @@
+node_modules/
+.tup/
+yarn.lock
+*.js
diff --git a/updater/Tupfile b/updater/Tupfile
new file mode 100644 (file)
index 0000000..f0fe651
--- /dev/null
@@ -0,0 +1 @@
+include_rules
diff --git a/updater/Tuprules.tup b/updater/Tuprules.tup
new file mode 100644 (file)
index 0000000..d3dceca
--- /dev/null
@@ -0,0 +1 @@
+: foreach *.coffee |> coffee -c -o %B.js %f |> %B.js
\ No newline at end of file
diff --git a/updater/app/Tupfile b/updater/app/Tupfile
new file mode 100644 (file)
index 0000000..f0fe651
--- /dev/null
@@ -0,0 +1 @@
+include_rules
diff --git a/updater/app/app.coffee b/updater/app/app.coffee
new file mode 100644 (file)
index 0000000..71d9ff2
--- /dev/null
@@ -0,0 +1,67 @@
+express = require 'express'
+app = express()
+server = require('http').createServer(app)
+io = require('socket.io')(server)
+
+class UpdateForwarder
+       constructor: ->
+               @channels = new Map()
+
+       add_channel: (channel_name) ->
+               unless @channels.has channel_name
+                       @channels.set channel_name, []
+
+       join_channel: (channel_name, socket) ->
+               return unless @channels.has channel_name
+               @channels.get(channel_name).push(socket)
+
+       leave_channel: (channel_name, socket) ->
+               return unless @channels.has channel_name
+
+               sockets = @channels.get channel_name
+               idx = sockets.indexOf socket
+               sockets.splice idx, 1
+
+       leave: (socket) ->
+               for chan from @channels.values()
+                       idx = chan.indexOf socket
+                       if idx != -1
+                               chan.splice idx, 1
+               return
+
+       push_update: (channel_name, param_match="") ->
+               return unless @channels.has channel_name
+
+               for sock in @channels.get channel_name
+                       if param_match != ""
+                               if sock.param == param_match
+                                       sock.emit 'update', {}
+                       else
+                               sock.emit 'update', {}
+               return
+
+update_forwarder = new UpdateForwarder()
+update_forwarder.add_channel "submission-updates"
+
+io.on 'connection', (socket) ->
+       # data is the submission id
+       socket.on 'request-submission-updates', (data) ->
+               socket.param = data
+               update_forwarder.join_channel "submission-updates", socket
+
+       socket.once 'disconnect', ->
+               update_forwarder.leave socket
+
+app.get '/submission_update', (req, res) ->
+       submission_id = req.query.submission_id
+       update_forwarder.push_update "submission-updates", submission_id
+
+       res.status 200
+       res.end()
+
+main = ->
+       port = 5000
+       console.log "Socket IO server running on port #{port}"
+       server.listen port
+
+module.exports = main
diff --git a/updater/main.coffee b/updater/main.coffee
new file mode 100644 (file)
index 0000000..0fd2414
--- /dev/null
@@ -0,0 +1,2 @@
+main = require "./app/app.js"
+main()
diff --git a/updater/package.json b/updater/package.json
new file mode 100644 (file)
index 0000000..6c5fd88
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "name": "codebox-socketio",
+  "version": "1.0.0",
+  "description": "SocketIO part of Codebox",
+  "main": "main.js",
+  "author": "Brendan Hansen",
+  "license": "MIT",
+  "private": true,
+  "dependencies": {
+    "express": "^4.17.1",
+    "socket.io": "^2.3.0"
+  }
+}