├── .gitignore ├── .idea ├── .name ├── codeStyleSettings.xml ├── codeStyles │ └── Project.xml ├── dictionaries │ └── jshore.xml ├── encodings.xml ├── jsLibraryMappings.xml ├── jsLinters │ ├── jshint.xml │ └── jslint.xml ├── libraries │ └── sass_stdlib.xml ├── misc.xml ├── modules.xml ├── scopes │ └── scope_settings.xml ├── vcs.xml ├── watcherTasks.xml └── weewikipaint.iml ├── Jakefile.js ├── LICENSE.TXT ├── Procfile ├── autojake.js ├── design ├── WeeWiki-404.jpg ├── WeeWiki-buttons-blue.jpg ├── WeeWiki-buttons.jpg ├── WeeWiki-main.jpg ├── arrow.png ├── arrow.svg ├── design.txt ├── weewiki-logo.png └── weewiki-logo.svg ├── jake.bat ├── jake.sh ├── package.json ├── readme.md ├── spikes ├── node_http_get │ ├── http_get.js │ └── run.sh ├── node_http_servefile │ ├── file.html │ ├── http_server.js │ └── run.sh ├── node_http_server │ ├── http_server.js │ └── run.sh ├── phantomjs │ └── hello.js ├── socket.io │ ├── app.js │ ├── client-race.js │ ├── index.html │ ├── readme.md │ ├── server-hang.html │ └── server-hang.js ├── socket_io_client_close_race │ ├── readme.md │ └── run.js ├── socket_io_disconnection │ ├── app.js │ ├── index.html │ └── readme.md ├── socket_io_emit_on_disconnect_hang │ ├── readme.md │ └── run.js └── socket_io_http_close_race │ ├── readme.md │ └── run.js ├── src ├── _release_test.js ├── _run_server.js ├── _smoke_test.js ├── client │ ├── content │ │ ├── 404.html │ │ ├── _404_test.js │ │ ├── _button_css_test.js │ │ ├── _css_test_helper.js │ │ ├── _drawing_area_css_test.js │ │ ├── _ghost_pointer_css_test.js │ │ ├── _index_test.js │ │ ├── _layout_css_test.js │ │ ├── _logo_css_test.js │ │ ├── _not_found_css_test.js │ │ ├── _theme_css_test.js │ │ ├── images │ │ │ ├── arrow.png │ │ │ ├── cursor.png │ │ │ └── weewiki-logo.png │ │ ├── index.html │ │ ├── screen.css │ │ └── vendor │ │ │ ├── normalize-3.0.2.css │ │ │ └── quixote-0.9.0.js │ ├── network │ │ ├── __test_harness_client.js │ │ ├── __test_harness_server.js │ │ ├── __test_harness_shared.js │ │ ├── _real_time_connection_test.js │ │ ├── real_time_connection.js │ │ └── vendor │ │ │ ├── async-1.5.2.js │ │ │ ├── emitter-1.2.1.js │ │ │ └── socket.io-2.0.4.js │ └── ui │ │ ├── _client_test.js │ │ ├── _html_coordinate_test.js │ │ ├── _html_element_test.js │ │ ├── _svg_canvas_test.js │ │ ├── browser.js │ │ ├── client.js │ │ ├── html_coordinate.js │ │ ├── html_element.js │ │ ├── svg_canvas.js │ │ └── vendor │ │ ├── jquery-1.8.3.min.js │ │ ├── modernizr.custom-2.8.3.min.js │ │ └── raphael-2.1.2.min.js ├── node_modules │ ├── _assert.js │ ├── _fail_fast_test.js │ ├── fail_fast.js │ └── vendor │ │ ├── big-object-diff-0.7.0.js │ │ └── proclaim-2.0.0.js ├── server │ ├── __socket_io_client.js │ ├── _clock_test.js │ ├── _http_server_test.js │ ├── _message_repository_test.js │ ├── _real_time_logic_test.js │ ├── _real_time_server_test.js │ ├── _server_test.js │ ├── clock.js │ ├── http_server.js │ ├── message_repository.js │ ├── real_time_logic.js │ ├── real_time_server.js │ ├── run.js │ └── server.js └── shared │ ├── _client_clear_screen_message_test.js │ ├── _client_draw_message_test.js │ ├── _client_pointer_message_test.js │ ├── _client_remove_pointer_message_test.js │ ├── _server_clear_screen_message_test.js │ ├── _server_draw_message_test.js │ ├── _server_pointer_message_test.js │ ├── _server_remove_pointer_message_test.js │ ├── client_clear_screen_message.js │ ├── client_draw_message.js │ ├── client_pointer_message.js │ ├── client_remove_pointer_message.js │ ├── server_clear_screen_message.js │ ├── server_draw_message.js │ ├── server_pointer_message.js │ └── server_remove_pointer_message.js └── todo.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | .DS_Store 3 | 4 | # WebStorm 5 | .idea/workspace.xml 6 | 7 | # npm 8 | node_modules/**/.bin/ 9 | 10 | # Build artifacts 11 | generated/ 12 | 13 | 14 | ### npm modules' build artifacts ### 15 | 16 | # Selenium WebDriverJS & Karma: Optional dependencies that don't build on vanilla Windows 17 | node_modules/fsevents/ 18 | 19 | # Socket.IO 20 | node_modules/uws/build/ 21 | node_modules/uws/build_log.txt 22 | 23 | # uws 24 | node_modules/uws/uws_darwin_59.node 25 | 26 | # Karma 27 | node_modules/karma/static/context.js 28 | node_modules/karma/static/karma.js -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | weewikipaint -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/dictionaries/jshore.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | async 5 | eqeqeq 6 | firefox 7 | forin 8 | gitignore 9 | gurdiga 10 | immed 11 | jshint 12 | latedef 13 | minify 14 | newcap 15 | noarg 16 | nodeunit 17 | noempty 18 | nonew 19 | testacular 20 | undef 21 | 22 | 23 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/jsLinters/jshint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | -------------------------------------------------------------------------------- /.idea/jsLinters/jslint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/libraries/sass_stdlib.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | http://www.w3.org/1999/xhtml 8 | 9 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/scopes/scope_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/weewikipaint.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.TXT: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | Copyright (c) 2012-2016 Titanium I.T. LLC except for Third-Party Material 4 | stated below. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a 7 | copy of this software and associated documentation files (the "Software"), 8 | to deal in the Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 10 | and/or sell copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following conditions: 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | 22 | Third-Party Material 23 | -------------------- 24 | Copyrights for some files belong to third parties. They have been included 25 | in their redistributable form and any existing copyright and license notices 26 | remain intact. These files may be found in the following locations: 27 | 28 | * `node_modules` 29 | * various `vendor` directories 30 | 31 | In addition, the file `src/client/content/images/cursor.png` is based on 32 | "near me" icon included in Google's Material Icons set, which is licensed 33 | under the Apache License Version 2.0. For more information, see: 34 | https://design.google.com/icons/ 35 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node generated/dist/server/run.js $PORT 2 | -------------------------------------------------------------------------------- /autojake.js: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/node 2 | 3 | // Automatically runs Jake when files change. 4 | // 5 | // Thanks to Davide Alberto Molin for contributing this code. 6 | // See http://www.letscodejavascript.com/v3/comments/live/7 for details. 7 | // 8 | // NOTE: The "COMMAND" variable must be changed for this to work on Windows. 9 | 10 | (function() { 11 | "use strict"; 12 | 13 | var gaze; 14 | try { gaze = require("gaze"); } 15 | catch (err) { console.log("To use this script, run 'npm install gaze'."); process.exit(1); } 16 | 17 | var spawn = require("child_process").spawn; 18 | 19 | var WATCH = "src/**/*.js"; 20 | 21 | var COMMAND = "./jake.sh"; // Mac/Unix 22 | // var COMMAND = "jake.bat"; // Windows 23 | var COMMAND_ARGS = ["loose=true"]; 24 | 25 | var buildRunning = false; 26 | 27 | gaze(WATCH, function(err, watcher) { 28 | console.log("Will run " + COMMAND + " when " + WATCH + " changes."); 29 | watcher.on("all", function(evt, filepath) { 30 | if (buildRunning) return; 31 | buildRunning = true; 32 | 33 | console.log("\n> " + COMMAND + " " + COMMAND_ARGS.join(" ")); 34 | var jake = spawn(COMMAND, COMMAND_ARGS, { stdio: "inherit" }); 35 | 36 | jake.on("exit", function(code) { 37 | buildRunning = false; 38 | }); 39 | }); 40 | }); 41 | 42 | }()); 43 | -------------------------------------------------------------------------------- /design/WeeWiki-404.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-404.jpg -------------------------------------------------------------------------------- /design/WeeWiki-buttons-blue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-buttons-blue.jpg -------------------------------------------------------------------------------- /design/WeeWiki-buttons.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-buttons.jpg -------------------------------------------------------------------------------- /design/WeeWiki-main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/WeeWiki-main.jpg -------------------------------------------------------------------------------- /design/arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/arrow.png -------------------------------------------------------------------------------- /design/arrow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /design/design.txt: -------------------------------------------------------------------------------- 1 | Design guidelines from Espen Brunborg of primate.co.uk: 2 | 3 | Colours: 4 | Blue (main background): #41a9cc 5 | Medium blue (button background): #00799c 6 | Darkened medium blue (mainly used for hover states on buttons): #006f8f 7 | Dark blue (for button dropshadows and text): #0d576d 8 | Gray (for button): #e5e5e5 9 | Darkened gray (for button hover state): #d9d9d9 10 | medium gray (for button dropshadow): #a7a9ab 11 | dark gray (for button text): #595959 12 | 13 | // If any of the colours are inconsistent with the JS site, please feel free to use what you already have. 14 | 15 | Typography: 16 | Intro text: Alwyn new rounded, regular, 14px (or em equivalent), dark blue 17 | Call-out text at the bottom: Alwyn new rounded, regular, 14px,white 18 | 19 | Clear button: 20 | Text: Alwyn new rounded, bold, 12px, dark gray, uppercase 21 | Background: gray 22 | Dimensions: try to keep it 30px tall, with a generous padding left/right 23 | Dropshadow: medium gray, 2px vertical offset, no blur 24 | 25 | Call-out button: 26 | Text: Alwyn new rounded, bold, 14px, white, uppercase 27 | Background: medium blue 28 | Dimensions, try to keep it 35px tall, generous padding left/right (maybe even have a min-width of 175px) 29 | 30 | 404 page: 31 | 404 Text: Alwyn new rounded, bold, 220px, darkened medium blue 32 | Strapline text: Same as intro text 33 | Button: same as call-out button 34 | -------------------------------------------------------------------------------- /design/weewiki-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesshore/lets_code_javascript/HEAD/design/weewiki-logo.png -------------------------------------------------------------------------------- /design/weewiki-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 14 | 17 | 20 | 25 | 27 | 30 | 32 | 35 | 39 | 41 | 44 | 48 | 49 | 50 | 55 | 58 | 61 | 66 | 68 | 71 | 73 | 76 | 80 | 82 | 85 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /jake.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if not exist node_modules\.bin\jake.cmd call npm rebuild 3 | node node_modules\jake\bin\cli.js %* 4 | -------------------------------------------------------------------------------- /jake.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | [ ! -f node_modules/.bin/jake ] && npm rebuild 4 | node_modules/.bin/jake $* 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weewikipaint", 3 | "version": "0.0.1", 4 | "engines": { 5 | "node": "9.11.1" 6 | }, 7 | "devDependencies": { 8 | "async": "^2.6.0", 9 | "browserify": "^16.1.0", 10 | "eslint": "^4.18.2", 11 | "glob": "^7.1.2", 12 | "hashcat": "^0.3.1", 13 | "jake": "^8.0.15", 14 | "karma": "^2.0.0", 15 | "karma-commonjs": "^1.0.0", 16 | "karma-mocha": "^1.3.0", 17 | "mocha": "^5.0.1", 18 | "procfile": "^0.1.1", 19 | "selenium-webdriver": "^3.6.0", 20 | "semver": "^5.5.0", 21 | "shelljs": "^0.8.1", 22 | "simplebuild-karma": "^1.0.0", 23 | "socket.io-client": "^2.0.4" 24 | }, 25 | "dependencies": { 26 | "lolex": "^2.3.2", 27 | "send": "^0.16.2", 28 | "socket.io": "^2.0.4" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Let's Code: Test-Driven Javascript 2 | ================================== 3 | 4 | "Let's Code: Test-Driven Javascript" is a screencast series focusing on 5 | rigorous, professional web development. For more information, visit 6 | http://letscodejavascript.com . 7 | 8 | This repository contains the source code for WeeWikiPaint, the application 9 | being developed in the series. 10 | 11 | (Wondering why we check in things like `node_modules` or IDE settings? See "[The Reliable Build](http://www.letscodejavascript.com/v3/blog/2014/12/the_reliable_build)".) 12 | 13 | 14 | Before building or running for the first time: 15 | ----------------------------------- 16 | 17 | 1. Install [Node.js](http://nodejs.org/download/) 18 | 2. Install [Git](http://git-scm.com/downloads) 19 | 3. Install [Firefox](http://getfirefox.com) (for smoke tests) 20 | 4. Install geckodriver into path (for smoke tests). On Mac, this is most easily done with [Homebrew](http://brew.sh/): `brew install geckodriver`. On Windows, [download geckodriver](https://github.com/mozilla/geckodriver/releases/) and manually add it to your path. On Linux, either download geckodriver manually or use your favorite package manager. 21 | 5. Clone source repository: `git clone https://github.com/jamesshore/lets_code_javascript.git` 22 | 6. All commands must run from root of repository: `cd lets_code_javascript` 23 | 24 | *Note:* If you update the repository (with `git pull` or similar), be sure to erase generated files with `git clean -fdx` first. (Note that this will erase any files you've added, so be sure to check in what you want to keep first.) 25 | 26 | 27 | Running old episodes: 28 | --------------------- 29 | 30 | Every episode's source code has an associated `episodeXX` tag. You can check out and run those episodes, but some episodes' code won't work on the current version of Node. You'll need to install the exact version of Node the code was written for. 31 | 32 | ### To check out an old episode: 33 | 34 | 1. If you made any changes, check them in. 35 | 2. Erase generated files: `git clean -fdx` 36 | 3. Reset any changes: `git reset --hard` 37 | 4. Check out old version: `git checkout episodeXX` (For example, `git checkout episode200`.) 38 | 39 | Compatibility notes: 40 | 41 | * Episodes 1-9 don't work on case-sensitive file systems. To fix the problem, rename `jakefile.js` to `Jakefile.js` (with a capital 'J'). 42 | * Episodes 37-39 don't work on Windows. A workaround is included in the episode 37 video. 43 | * Episodes 269-441 and 469+ may fail when running smoke tests. They use Selenium for smoke testing and download the appropriate Firefox driver as needed. Those drivers may be missing or incompatible with your current version of Firefox. Starting with episode 469, Selenium uses geckodriver, which is installed separately from the rest of Selenium, which may make the smoke tests more reliable. 44 | 45 | ### To change Node versions and run the code: 46 | 47 | 1. Look at the `engines.node` property of `package.json` to see which version of Node the code runs on. Prior to episode 31, the Node version was documented in `readme.md`. Prior to episode 10, the version wasn't documented; those episodes used v0.6.17. 48 | 49 | 2. Install the correct version of Node. On Unix and Mac, [n](https://github.com/visionmedia/n) is a convenient tool for switching Node versions. On Windows, you can use [nvmw](https://github.com/hakobera/nvmw). 50 | 51 | 3. To see how to run the code, look at the episode's `readme.md` or watch the episode in question. Note that some episodes end with non-working code. 52 | 53 | 54 | ### Known version issues: 55 | 56 | Node has introduced breaking changes with newer versions. Here are the issues we're aware of. I've included some workarounds, but the best way to run old code is to install the exact version of Node that the code was written for. 57 | 58 | * Some episodes include a version of Jake that doesn't run on Node 0.10+. You might be able to resolve this problem by running `npm install jake`. 59 | 60 | * Some episodes include a version of NodeUnit that relies on an internal 'evals' module that was removed in Node 0.12. (See Node.js [issue #291](https://github.com/caolan/nodeunit/issues/291).) You might be able to resolve this problem by running `npm install nodeunit`. 61 | 62 | * Some episodes include a version of Testacular (now named "Karma") that crashes when you capture a browser in Node 0.10+. There's no easy workaround for this problem, so just install Node 0.8 if you want to run the code in those episodes. 63 | 64 | * A few episodes rely on a feature of Node.js streams that was removed in Node 0.10. A workaround is included in the video for the episodes in question. 65 | 66 | * Most episodes have a test that checks how [server.close()](http://nodejs.org/api/net.html#net_server_close_callback) handles errors. This behavior was changed in Node 0.12, so the test will fail. (In previous versions, it threw an exception, but now it passes an `err` object to the server.close callback.) You can just delete the test in question, or see [episode 14](http://www.letscodejavascript.com/v3/comments/live/14#comment-1870243150) for a workaround. 67 | 68 | 69 | To build and test this episode: 70 | ------------------------------- 71 | 72 | 1. Run `./jake.sh karma` (Unix/Mac) or `jake karma` (Windows) 73 | 2. Navigate at least one browser to http://localhost:9876 74 | 3. Run `./jake.sh loose=true` (Unix/Mac) or `jake loose=true` (Windows) 75 | 76 | You can also run `./jake.sh quick loose=true` for a faster but less thorough set of tests. 77 | 78 | *Note:* The master branch is not guaranteed to build successfully. For a known-good build (tested on Mac and Windows, and assumed to work on Linux), use the integration branch. To change branches, follow the steps under "Running old episodes" (above), but replace `episodeXX` with `integration` (for the known-good integration branch) or `master` (for the latest code). 79 | 80 | 81 | To run this episode locally: 82 | ---------------------------- 83 | 84 | 1. Run `./jake.sh run` (Unix/Mac) or `jake run` (Windows) 85 | 2. Navigate a browser to http://localhost:5000 86 | 87 | *Note:* The master branch is not guaranteed to run successfully. For a known-good build, use the integration branch as described above. 88 | 89 | 90 | To deploy: 91 | ---------- 92 | 93 | Before deploying for the first time: 94 | 95 | 1. Make sure code is in Git repository (clone GitHub repo, or 'git init' yours) 96 | 2. Install [Heroku Toolbelt](https://toolbelt.heroku.com/) 97 | 3. Sign up for a [Heroku account](https://id.heroku.com/signup) 98 | 4. Run `heroku create ` (requires git repository and Heroku account) 99 | 5. Search codebase for `weewikipaint.herokuapp.com` URLs and change them to refer to `` 100 | 6. Push known-good deploy to Heroku: `git push heroku episode321:master` 101 | 102 | Then, to deploy: 103 | 104 | 1. Run `./jake.sh deploy` (Unix/Mac) or `jake deploy` (Windows) for instructions 105 | 106 | *Note:* The master and integration branches are not guaranteed to deploy successfully. The last known-good deploy was commit 0eabb0cd7b9f16a9375cb8b16a1d449570d23162. We'll establish better deployment practices in a future chapter of the screencast. 107 | 108 | 109 | Finding your way around: 110 | ------------------------ 111 | 112 | * `build`: Scripts and utilities used to build and test 113 | * `design`: Initial design concept 114 | * `generated`: Files generated by the build 115 | * `generated/dist`: Distribution files--what actually runs in production 116 | * `node_modules`: npm modules 117 | * `spikes`: One-off experiments 118 | * `src`: All the source code 119 | * `src/client`: Browser-side code 120 | * `src/client/content`: HTML, CSS, images, etc., and CSS tests 121 | * `src/client/network`: Code used to communicate with the server 122 | * `src/client/ui`: Code used to render the UI 123 | * `src/node_modules`: Commonly-used utility modules. Note that these are *not* npm modules; they are part of the application source code. 124 | * `src/server`: Server-side code 125 | * `src/shared`: Code shared between client and server 126 | 127 | Files that start with an underscore are test-related and not used in production. -------------------------------------------------------------------------------- /spikes/node_http_get/http_get.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // This spike shows how to get a URL using Node's HTTP module. 4 | "use strict"; 5 | 6 | var http = require("http"); 7 | 8 | http.get("http://www.google.com/index.html", function(res) { 9 | console.log("Got response: " + res.statusCode); 10 | }).on('error', function(e) { 11 | console.log("Got error: " + e.message); 12 | }); 13 | -------------------------------------------------------------------------------- /spikes/node_http_get/run.sh: -------------------------------------------------------------------------------- 1 | node http_get.js 2 | -------------------------------------------------------------------------------- /spikes/node_http_servefile/file.html: -------------------------------------------------------------------------------- 1 | 2 |

This is an HTML file that's been changed.

3 | 4 | -------------------------------------------------------------------------------- /spikes/node_http_servefile/http_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // This spike demonstrates how to serve a static file. 4 | // 5 | // It's not robust and it reflects a very basic understanding of node; use it 6 | // as a starting point, not a production-quality example. 7 | "use strict"; 8 | 9 | var http = require("http"); 10 | var fs = require("fs"); 11 | 12 | var server = http.createServer(); 13 | 14 | server.on("request", function(request, response) { 15 | console.log("Received request"); 16 | 17 | fs.readFile("file.html", function (err, data) { 18 | if (err) throw err; 19 | response.end(data); 20 | }); 21 | }); 22 | 23 | server.listen(8080); 24 | 25 | console.log("Server started"); -------------------------------------------------------------------------------- /spikes/node_http_servefile/run.sh: -------------------------------------------------------------------------------- 1 | node http_server.js 2 | -------------------------------------------------------------------------------- /spikes/node_http_server/http_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | 3 | // This is a simple spike of Node's HTTP module. The goal was to show 4 | // how to serve a very simple HTML page using Node. 5 | // It's not robust and it reflects a very basic understanding of node; use it 6 | // as a starting point, not a production-quality example. 7 | "use strict"; 8 | 9 | var http = require("http"); 10 | 11 | var server = http.createServer(); 12 | 13 | server.on("request", function(request, response) { 14 | console.log("Received request"); 15 | 16 | var body = "Node HTTP Spike" + 17 | "

This is a spike of Node's HTTP server.

"; 18 | 19 | // The following two approaches are equivalent: 20 | // The verbose way... 21 | // response.statusCode = 200; 22 | // response.write(body); 23 | // response.end(); 24 | 25 | // The concise way... 26 | response.end(body); 27 | }); 28 | 29 | server.listen(8080); 30 | 31 | console.log("Server started"); -------------------------------------------------------------------------------- /spikes/node_http_server/run.sh: -------------------------------------------------------------------------------- 1 | node http_server.js 2 | -------------------------------------------------------------------------------- /spikes/phantomjs/hello.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | (function() { 3 | "use strict"; 4 | 5 | var page = require("webpage").create(); 6 | 7 | console.log("Hello world"); 8 | 9 | page.onConsoleMessage = function(message) { 10 | console.log("CONSOLE: " + message); 11 | }; 12 | 13 | page.open("http://localhost:5000", function(success) { 14 | console.log("Success: " + success); 15 | 16 | page.evaluate(inBrowser); 17 | 18 | page.render("wwp.png"); 19 | phantom.exit(0); 20 | }); 21 | 22 | function inBrowser() { 23 | console.log("Hi"); 24 | console.log("defined? " + isDefined(wwp.HtmlElement)); 25 | 26 | var drawingArea = new wwp.HtmlElement($("#drawingArea")); 27 | drawingArea.triggerMouseDown(10, 20); 28 | drawingArea.triggerMouseMove(50, 60); 29 | drawingArea.triggerMouseUp(50, 60); 30 | 31 | function isDefined(obj) { 32 | return typeof(obj) !== "undefined"; 33 | } 34 | } 35 | 36 | }()); -------------------------------------------------------------------------------- /spikes/socket.io/app.js: -------------------------------------------------------------------------------- 1 | var app = require('http').createServer(handler) 2 | var io = require('socket.io')(app); 3 | var fs = require('fs'); 4 | 5 | var port = process.argv[2] || 8080; 6 | 7 | app.listen(port); 8 | 9 | console.log("Server started on port " + port); 10 | function handler (req, res) { 11 | console.log("Request received"); 12 | 13 | fs.readFile(__dirname + '/index.html', 14 | function (err, data) { 15 | if (err) { 16 | res.writeHead(500); 17 | return res.end('Error loading index.html'); 18 | } 19 | 20 | res.writeHead(200); 21 | res.end(data); 22 | console.log("index.html served"); 23 | }); 24 | } 25 | 26 | io.on('connection', function (socket) { 27 | console.log("Connection created"); 28 | 29 | socket.on("message", function(message) { 30 | console.log("Message posted: " + message); 31 | io.emit("serverMessage", message); 32 | }); 33 | 34 | }); -------------------------------------------------------------------------------- /spikes/socket.io/client-race.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var clientIo = require("socket.io-client"); 6 | var http = require("http"); 7 | var io = require('socket.io'); 8 | 9 | var httpServer; 10 | var server; 11 | 12 | var PORT = 5020; 13 | 14 | startServer(function() { 15 | console.log("SERVER STARTED"); 16 | 17 | var client = clientIo("http://localhost:" + PORT); 18 | 19 | client.on('connect', function() { 20 | client.disconnect(); 21 | stopServer(function() { 22 | console.log("COMPLETE! NODE SHOULD EXIT NOW."); 23 | }); 24 | }); 25 | }); 26 | 27 | function startServer(callback) { 28 | httpServer = http.createServer(); 29 | server = io(httpServer); 30 | httpServer.listen(PORT, callback); 31 | }; 32 | 33 | function stopServer(callback) { 34 | httpServer.on("close", function() { 35 | console.log("SERVER CLOSED"); 36 | callback(); 37 | }); 38 | 39 | setTimeout(function() { 40 | server.close(); 41 | }, 500); 42 | }; 43 | 44 | }()); -------------------------------------------------------------------------------- /spikes/socket.io/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | socket.io spike 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | Server messages go here 14 |
15 | 16 | 17 | 18 | 19 | 20 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /spikes/socket.io/readme.md: -------------------------------------------------------------------------------- 1 | Example of using socket.io for multi-user real-time web 2 | 3 | To try it: 4 | 5 | 1. Start server: `node spikes/socket.io/app.js` 6 | 2. Open multiple client tabs in a browser: `http://localhost:8080` 7 | 3. Look at server and client consoles 8 | 9 | 10 | Also: 11 | 12 | * client-race.js demonstrates a race condition in the socket.io client. It was exposed by our attempt to test socket.io. The issue was fixed in v1.4.5. It was reported here: https://github.com/socketio/socket.io-client/issues/935 13 | 14 | * server-hang.js and server-hang.html demonstrates a bug in the socket.io server that prevents the server process from exiting. The issue was reported here: https://github.com/socketio/socket.io/issues/1602 -------------------------------------------------------------------------------- /spikes/socket.io/server-hang.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Server won't exit 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |

Look at the console!

14 | 15 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /spikes/socket.io/server-hang.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var PORT = 5020; 6 | var TIMEOUT = 5000; 7 | 8 | var io = require('socket.io')(PORT); 9 | 10 | console.log("Waiting " + (TIMEOUT / 1000) + " seconds..."); 11 | setTimeout(function() { 12 | io.close(); 13 | console.log("PROCESS SHOULD NOW EXIT"); 14 | }, TIMEOUT); 15 | 16 | }()); -------------------------------------------------------------------------------- /spikes/socket_io_client_close_race/readme.md: -------------------------------------------------------------------------------- 1 | Code to reproduce client-side race condition in Socket.IO. 2 | 3 | The issue: 4 | 5 | 1. Open a Socket.IO connection to the server 6 | 2. After the server connects, but before the client-side "connect" event has fired, close the client connection. 7 | 3. Socket.IO never shuts down the connection. 8 | 9 | 10 | To try it: 11 | 12 | 1. Run `node spikes/socket_io_client_close_race/run.js` 13 | 14 | 15 | Reported on socketio/socket.io-client GitHub as issue #1133: 16 | https://github.com/socketio/socket.io-client/issues/1133 -------------------------------------------------------------------------------- /spikes/socket_io_client_close_race/run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var clientIo = require("socket.io-client"); 5 | var http = require("http"); 6 | var io = require("socket.io"); 7 | 8 | var httpServer; 9 | var ioServer; 10 | 11 | var PORT = 5020; 12 | 13 | startServer(function() { 14 | console.log("SERVER STARTED"); 15 | 16 | var connections = {}; 17 | logAndTrackServerConnections(connections); 18 | 19 | console.log("** connecting client"); 20 | var client = clientIo("http://localhost:" + PORT); 21 | logClientConnections(client); 22 | 23 | ioServer.once("connect", function() { 24 | console.log("** disconnecting client"); 25 | client.disconnect(); 26 | console.log("** waiting for server to disconnect"); 27 | setTimeout(function() { 28 | console.log("#### Does the client think it's connected? (Expect 'false'):", client.connected); 29 | console.log("#### How many connections does the server have? (Expect 0):", numberOfServerConnections(connections)); 30 | stopServer(function() { 31 | console.log("** end of test") 32 | }); 33 | }, 500); 34 | }); 35 | }); 36 | 37 | function logAndTrackServerConnections(connections) { 38 | ioServer.on("connect", function(socket) { 39 | var key = socket.id; 40 | console.log("SERVER CONNECTED", key); 41 | connections[key] = socket; 42 | socket.on("disconnect", function() { 43 | console.log("SERVER DISCONNECTED", key); 44 | delete connections[key]; 45 | }); 46 | }); 47 | } 48 | 49 | function logClientConnections(socket) { 50 | var id; 51 | socket.on("connect", function() { 52 | id = socket.id; 53 | console.log("CLIENT CONNECTED", id); 54 | }); 55 | socket.on("disconnect", function() { 56 | console.log("CLIENT DISCONNECTED", id); 57 | }); 58 | } 59 | 60 | function numberOfServerConnections(connections) { 61 | return Object.keys(connections).length; 62 | } 63 | 64 | function startServer(callback) { 65 | console.log("** starting server"); 66 | httpServer = http.createServer(); 67 | ioServer = io(httpServer); 68 | httpServer.listen(PORT, callback); 69 | }; 70 | 71 | function stopServer(callback) { 72 | console.log("** stopping server"); 73 | 74 | httpServer.on("close", function() { 75 | console.log("SERVER CLOSED"); 76 | callback(); 77 | }); 78 | 79 | ioServer.close(); 80 | }; 81 | 82 | }()); -------------------------------------------------------------------------------- /spikes/socket_io_disconnection/app.js: -------------------------------------------------------------------------------- 1 | var app = require('http').createServer(handler) 2 | var io = require('socket.io')(app); 3 | var fs = require('fs'); 4 | 5 | var port = process.argv[2] || 8080; 6 | 7 | app.listen(port); 8 | 9 | console.log("Server started on port " + port); 10 | function handler (req, res) { 11 | console.log("Request received"); 12 | 13 | fs.readFile(__dirname + '/index.html', 14 | function (err, data) { 15 | if (err) { 16 | res.writeHead(500); 17 | return res.end('Error loading index.html'); 18 | } 19 | 20 | res.writeHead(200); 21 | res.end(data); 22 | console.log("index.html served"); 23 | }); 24 | } 25 | 26 | io.on('connection', function (socket) { 27 | console.log("\nConnection created"); 28 | 29 | socket.on("message", function(message) { 30 | console.log("Message posted: " + message); 31 | io.emit("serverMessage", message); 32 | }); 33 | 34 | socket.on("disconnect", function(reason) { 35 | console.log("Disconnect: ", reason); 36 | }); 37 | 38 | }); -------------------------------------------------------------------------------- /spikes/socket_io_disconnection/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | socket.io spike 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /spikes/socket_io_disconnection/readme.md: -------------------------------------------------------------------------------- 1 | How does Socket.IO behave when the browser disconnects? Use this spike to manually check the following on multiple browsers. Does the server get a disconnect event when... 2 | * ...when the 'disconnect' button is pushed? 3 | * ...the page is reloaded? 4 | * ...the user navigates away? 5 | * ...the tab is closed? 6 | * ...the browser is closed? 7 | 8 | To try it: 9 | 10 | 1. Start server: `node spikes/socket_io_disconnection/app.js` 11 | 2. Open client tabs in a browser: `http://localhost:8080` 12 | 3. Look at server and client consoles 13 | 14 | 15 | Results: (is the disconnect event received by the server?) 16 | * Firefox 54: yes in all cases 17 | * Chrome 59: yes in all cases 18 | * Safari 10.1.2 (desktop): yes in all cases 19 | * IE11: yes in all cases 20 | * MS Edge 14.14393.0: yes in all cases 21 | * Mobile Safari 10.0.0: yes in all cases 22 | * Chrome Mobile 44.0.2403: yes in all cases -------------------------------------------------------------------------------- /spikes/socket_io_emit_on_disconnect_hang/readme.md: -------------------------------------------------------------------------------- 1 | An attempt to reproduce a client-side hang with Socket.IO. 2 | 3 | The issue was that our tests were failing to exit. This code was an attempt to reproduce the issue. Although we *did* find a "didn't exit" problem, it isn't the same as the one we were seeing in our tests. In fact, I think it may be the same as the `socket_io_client_close_race` spike, reported as https://github.com/socketio/socket.io-client/issues/1133. 4 | 5 | Ultimately, we fixed our tests by having it open sockets sequentially rather than in parallel. -------------------------------------------------------------------------------- /spikes/socket_io_emit_on_disconnect_hang/run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | const socketIoClient = require("socket.io-client"); 5 | const socketIoServer = require("socket.io"); 6 | const http = require("http"); 7 | const util = require("util"); 8 | 9 | let httpServer; 10 | let ioServer; 11 | 12 | const PORT = 5020; 13 | 14 | startServer(async function() { 15 | console.log("SERVER STARTED"); 16 | 17 | const connections = {}; 18 | logAndTrackServerConnections(connections); 19 | logHttpConnections(); 20 | 21 | console.log("** setting up server event handlers"); 22 | ioServer.on("connect", function(serverSocket) { 23 | serverSocket.on("client_event", () => { 24 | console.log("CLIENT EVENT RECEIVED BY SERVER"); 25 | console.log("** emitting server event"); 26 | ioServer.emit("server_event", "server event sent in response to client event"); 27 | }); 28 | serverSocket.on("disconnect", (reason) => { 29 | // comment out following lines to prevent issue 30 | console.log("** emitting server event in disconnect handler"); 31 | ioServer.emit("arbitrary_server_event", "server event sent during disconnect"); 32 | }); 33 | }); 34 | 35 | console.log("** creating client sockets"); 36 | const clientSocket1 = await createClientSocket(); 37 | const clientSocket2 = await createClientSocket(); 38 | 39 | console.log("** emitting client event"); 40 | clientSocket1.emit("client_event", "client event"); 41 | 42 | console.log("** waiting for server event"); 43 | await new Promise((resolve) => { 44 | clientSocket1.on("server_event", () => { 45 | console.log("SERVER EVENT RECEIVED BY CLIENT"); 46 | // use setTimeout, instead of calling resolve() directly, to prevent issue 47 | setTimeout(resolve, 200); 48 | resolve(); 49 | }); 50 | }); 51 | 52 | console.log("** closing client sockets"); 53 | await closeClientSocket(clientSocket1); 54 | await closeClientSocket(clientSocket2); 55 | 56 | // uncomment this block of code to prevent issue 57 | // console.log("** waiting for server sockets to disconnect"); 58 | // const serverSockets = Object.values(connections); 59 | // const promises = serverSockets.map((serverSocket) => { 60 | // return new Promise((resolve) => serverSocket.on("disconnect", resolve)); 61 | // }); 62 | // await Promise.all(promises); 63 | 64 | await stopServer(ioServer); 65 | console.log("** end of test, Node.js should now exit") 66 | }); 67 | 68 | function parallelCreateSockets(numSockets) { 69 | let createPromises = []; 70 | for (let i = 0; i < numSockets; i++) { 71 | createPromises.push(createSocket()); 72 | } 73 | return Promise.all(createPromises); 74 | } 75 | 76 | function logAndTrackServerConnections(connections) { 77 | ioServer.on("connect", function(socket) { 78 | const key = socket.id; 79 | console.log("SERVER CONNECTED", key); 80 | connections[key] = socket; 81 | socket.on("disconnect", function(reason) { 82 | console.log(`SERVER DISCONNECTED; reason ${reason}; id ${socket.id}`); 83 | delete connections[key]; 84 | }); 85 | }); 86 | } 87 | 88 | function logHttpConnections() { 89 | httpServer.on('connection', function(socket) { 90 | const id = socket.remoteAddress + ':' + socket.remotePort; 91 | console.log("HTTP CONNECT", id); 92 | socket.on("close", function() { 93 | console.log("HTTP DISCONNECT", id); 94 | }); 95 | }); 96 | } 97 | 98 | function logClientConnections(socket) { 99 | let id; 100 | socket.on("connect", function() { 101 | id = socket.id; 102 | console.log("CLIENT CONNECTED", id); 103 | }); 104 | socket.on("disconnect", function() { 105 | console.log("CLIENT DISCONNECTED", id); 106 | }); 107 | } 108 | 109 | function startServer(callback) { 110 | console.log("** starting server"); 111 | httpServer = http.createServer(); 112 | ioServer = socketIoServer(httpServer); 113 | httpServer.listen(PORT, callback); 114 | }; 115 | 116 | async function stopServer(ioServer) { 117 | console.log("** stopping server"); 118 | 119 | const close = util.promisify(ioServer.close.bind(ioServer)); 120 | await close(); 121 | console.log("SERVER STOPPED"); 122 | } 123 | 124 | function createClientSocket() { 125 | const socket = socketIoClient("http://localhost:" + PORT); 126 | logClientConnections(socket); 127 | return new Promise(function(resolve) { 128 | socket.on("connect", function() { 129 | return resolve(socket); 130 | }); 131 | }); 132 | } 133 | 134 | function closeClientSocket(clientSocket) { 135 | const closePromise = new Promise(function(resolve) { 136 | clientSocket.on("disconnect", function() { 137 | return resolve(); 138 | }); 139 | }); 140 | clientSocket.disconnect(); 141 | 142 | return closePromise; 143 | } 144 | 145 | }()); -------------------------------------------------------------------------------- /spikes/socket_io_http_close_race/readme.md: -------------------------------------------------------------------------------- 1 | Code to reproduce client-side race condition in Socket.IO. 2 | 3 | The issue: 4 | 5 | 1. Open a Socket.IO connection to the server 6 | 2. Immediately after the client-side "connect" event has fired, close the connection 7 | 3. Immediately after the server-side "disconnect" event has fired, shutdown the server using httpServer.close() 8 | 4. Socket.IO opens several HTTP connections, but doesn't close them all, causing server to hang 9 | 10 | To try it: 11 | 12 | 1. Run `node spikes/socket_io_http_close_race/run.js` 13 | 14 | Reported to Socket.IO here: https://github.com/socketio/socket.io/issues/2975 -------------------------------------------------------------------------------- /spikes/socket_io_http_close_race/run.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | "use strict"; 3 | 4 | var clientIo = require("socket.io-client"); 5 | var http = require("http"); 6 | var io = require("socket.io"); 7 | 8 | var httpServer; 9 | var ioServer; 10 | 11 | var PORT = 5020; 12 | 13 | startServer(function() { 14 | console.log("SERVER STARTED"); 15 | 16 | var connections = {}; 17 | logAndTrackServerConnections(connections); 18 | logHttpConnections(); 19 | 20 | var serverSocket; 21 | ioServer.once("connect", function(socket) { 22 | console.log("** stored server socket", socket.id); 23 | serverSocket = socket; 24 | }); 25 | 26 | console.log("** connecting client"); 27 | var client = clientIo("http://localhost:" + PORT); 28 | logClientConnections(client); 29 | 30 | client.once("connect", function() { 31 | console.log("** disconnecting client"); 32 | client.once("disconnect", function() { 33 | console.log("** waiting for server to disconnect"); 34 | serverSocket.once("disconnect", function() { 35 | 36 | // console.log("** waiting to stop server"); // uncommenting this timeout prevents hang 37 | // setTimeout(function() { 38 | stopServer(function() { 39 | console.log("** end of test, Node.js should now exit") 40 | }); 41 | // }, 500); 42 | }); 43 | }); 44 | client.disconnect(); 45 | }); 46 | }); 47 | 48 | function logAndTrackServerConnections(connections) { 49 | ioServer.on("connect", function(socket) { 50 | var key = socket.id; 51 | console.log("SERVER CONNECTED", key); 52 | connections[key] = socket; 53 | socket.on("disconnect", function() { 54 | console.log("SERVER DISCONNECTED", key); 55 | delete connections[key]; 56 | }); 57 | }); 58 | } 59 | 60 | function logHttpConnections() { 61 | httpServer.on('connection', function(socket) { 62 | var id = socket.remoteAddress + ':' + socket.remotePort; 63 | console.log("HTTP CONNECT", id); 64 | socket.on("close", function() { 65 | console.log("HTTP DISCONNECT", id); 66 | }); 67 | }); 68 | } 69 | 70 | function logClientConnections(socket) { 71 | var id; 72 | socket.on("connect", function() { 73 | id = socket.id; 74 | console.log("CLIENT CONNECTED", id); 75 | }); 76 | socket.on("disconnect", function() { 77 | console.log("CLIENT DISCONNECTED", id); 78 | }); 79 | } 80 | 81 | function startServer(callback) { 82 | console.log("** starting server"); 83 | httpServer = http.createServer(); 84 | ioServer = io(httpServer); 85 | httpServer.listen(PORT, callback); 86 | }; 87 | 88 | function stopServer(callback) { 89 | console.log("** stopping server"); 90 | 91 | httpServer.close(function() { // using ioServer.close() instead of httpServer.close() prevents hang 92 | console.log("SERVER CLOSED"); 93 | callback(); 94 | }); 95 | }; 96 | 97 | }()); -------------------------------------------------------------------------------- /src/_release_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | /*jshint regexp:false*/ 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | var jake = require("jake"); 8 | var child_process = require("child_process"); 9 | var http = require("http"); 10 | var fs = require("fs"); 11 | var procfile = require("procfile"); 12 | var assert = require("_assert"); 13 | 14 | describe("Release", function() { 15 | /*eslint no-invalid-this:off */ 16 | this.timeout(10 * 1000); 17 | 18 | it("is on web", function(done) { 19 | httpGet("http://weewikipaint.herokuapp.com", function(response, receivedData) { 20 | var foundHomePage = receivedData.indexOf("WeeWikiPaint home page") !== -1; 21 | assert.equal(foundHomePage, true, "home page should have contained test marker"); 22 | done(); 23 | }); 24 | }); 25 | 26 | }); 27 | 28 | function httpGet(url, callback) { 29 | var request = http.get(url); 30 | request.on("response", function(response) { 31 | var receivedData = ""; 32 | response.setEncoding("utf8"); 33 | 34 | response.on("data", function(chunk) { 35 | receivedData += chunk; 36 | }); 37 | response.on("end", function() { 38 | callback(response, receivedData); 39 | }); 40 | }); 41 | } 42 | 43 | }()); -------------------------------------------------------------------------------- /src/_run_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | (function() { 3 | "use strict"; 4 | 5 | var child_process = require("child_process"); 6 | var fs = require("fs"); 7 | var procfile = require("procfile"); 8 | 9 | exports.runInteractively = function() { 10 | return run("inherit"); 11 | }; 12 | 13 | exports.runProgrammatically = function(callback) { 14 | var serverProcess = run(["pipe", "pipe", process.stderr]); 15 | 16 | serverProcess.stdout.setEncoding("utf8"); 17 | serverProcess.stdout.on("data", function(chunk) { 18 | if (chunk.trim().indexOf("Server started") !== -1) callback(serverProcess); 19 | }); 20 | }; 21 | 22 | function run(stdioOptions) { 23 | var commandLine = parseProcFile(); 24 | return child_process.spawn(commandLine.command, commandLine.options, {stdio: stdioOptions }); 25 | } 26 | 27 | function parseProcFile() { 28 | var fileData = fs.readFileSync("Procfile", "utf8"); 29 | var webCommand = procfile.parse(fileData).web; 30 | webCommand.options = webCommand.options.map(function(element) { 31 | if (element === "$PORT") return "5000"; 32 | else return element; 33 | }); 34 | return webCommand; 35 | } 36 | 37 | }()); -------------------------------------------------------------------------------- /src/client/content/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 404: Not Found 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 |

There’s only one page on this site...

69 | 70 | Go Draw Something 71 | 72 | -------------------------------------------------------------------------------- /src/client/content/_404_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var cssHelper = require("./_css_test_helper.js"); 7 | var quixote = require("./vendor/quixote-0.9.0.js"); 8 | 9 | describe("CSS: 404 page", function() { 10 | 11 | var frame; 12 | var page; 13 | var viewport; 14 | 15 | var logo; 16 | var header; 17 | var tagline; 18 | var drawSomething; 19 | 20 | before(function(done) { 21 | /*eslint no-invalid-this:off */ 22 | this.timeout(10 * 1000); 23 | var options = { 24 | src: "/base/src/client/content/404.html", 25 | width: cssHelper.IOS_BROWSER_WIDTH, 26 | height: cssHelper.IPAD_LANDSCAPE_HEIGHT_WITH_BROWSER_TABS 27 | }; 28 | frame = quixote.createFrame(options, done); 29 | }); 30 | 31 | after(function() { 32 | frame.remove(); 33 | }); 34 | 35 | beforeEach(function() { 36 | frame.reset(); 37 | 38 | page = frame.page(); 39 | viewport = frame.viewport(); 40 | 41 | logo = frame.get("#logo"); 42 | header = frame.get("#header"); 43 | tagline = frame.get("#tagline"); 44 | drawSomething = frame.get("#draw-something"); 45 | }); 46 | 47 | it("fits perfectly within viewport", function() { 48 | page.assert({ 49 | width: viewport.width, 50 | height: viewport.height 51 | }, "page should not be larger than viewport"); 52 | }); 53 | 54 | it("has a nice margin when viewport is smaller than the page", function() { 55 | frame.resize(50, 50); 56 | 57 | drawSomething.assert({ 58 | bottom: page.bottom.minus(13) 59 | }, "bottom element should have a nice margin before the bottom of the page"); 60 | }); 61 | 62 | it("has an overall layout", function() { 63 | logo.assert({ 64 | top: logo.height.times(2), 65 | center: page.center, 66 | height: 30 67 | }, "logo should be centered at top of page"); 68 | assert.equal(cssHelper.fontSize(logo), "30px", "logo font size"); 69 | assert.equal(cssHelper.textAlign(logo), "center", "logo text should be centered"); 70 | header.assert({ 71 | top: logo.bottom, 72 | center: viewport.center, 73 | height: 200 74 | }, "404 header should be centered under logo"); 75 | assert.equal(cssHelper.fontSize(header), "200px", "header font size"); 76 | assert.equal(cssHelper.textAlign(header), "center", "header text should be centered"); 77 | tagline.assert({ 78 | top: header.bottom.plus(tagline.height), 79 | center: viewport.center, 80 | height: 18 81 | }, "tagline should be centered under 404 header"); 82 | assert.equal(cssHelper.fontSize(tagline), "15px", "tagline font size"); 83 | assert.equal(cssHelper.textAlign(tagline), "center", "tagline text should be centered"); 84 | drawSomething.assert({ 85 | top: tagline.bottom.plus(tagline.height), 86 | center: page.center, 87 | height: 35, 88 | width: 225 89 | }, "button should be centered below tagline"); 90 | assert.equal(cssHelper.textAlign(drawSomething), "center", "button text should be centered"); 91 | }); 92 | 93 | it("has a color scheme", function() { 94 | assert.equal(cssHelper.backgroundColor(frame.body()), cssHelper.BACKGROUND_BLUE, "page background should be light blue"); 95 | assert.equal(cssHelper.textColor(logo), cssHelper.WHITE, "logo text should be white"); 96 | assert.equal(cssHelper.textColor(header), cssHelper.DARK_BLUE, "header should be dark blue"); 97 | assert.equal(cssHelper.textColor(tagline), cssHelper.DARK_BLUE, "tagline should be dark blue"); 98 | assert.equal(cssHelper.backgroundColor(drawSomething), cssHelper.MEDIUM_BLUE, "button background should be medium blue"); 99 | assert.equal(cssHelper.textColor(drawSomething), cssHelper.WHITE, "button text should be white"); 100 | }); 101 | 102 | it("has a typographic scheme", function() { 103 | assert.equal(cssHelper.fontFamily(logo), cssHelper.STANDARD_FONT, "logo font"); 104 | assert.equal(cssHelper.fontWeight(logo), cssHelper.HEADLINE_WEIGHT, "logo weight"); 105 | assert.equal(cssHelper.fontFamily(header), cssHelper.STANDARD_FONT, "header font"); 106 | assert.equal(cssHelper.fontWeight(header), cssHelper.HEADLINE_WEIGHT, "header weight"); 107 | assert.equal(cssHelper.fontFamily(tagline), cssHelper.STANDARD_FONT, "tagline font"); 108 | assert.equal(cssHelper.fontWeight(tagline), cssHelper.BODY_TEXT_WEIGHT, "tagline weight"); 109 | assert.equal(cssHelper.fontFamily(drawSomething), cssHelper.STANDARD_FONT, "draw something button family"); 110 | assert.equal(cssHelper.fontWeight(drawSomething), cssHelper.LINK_BUTTON_WEIGHT, "draw something button weight"); 111 | }); 112 | 113 | 114 | describe("button", function() { 115 | 116 | it("has common styling", function() { 117 | assertStandardButtonStyling(drawSomething, "draw something button"); 118 | }); 119 | 120 | it("has rounded corners", function() { 121 | assert.equal(cssHelper.roundedCorners(drawSomething), cssHelper.CORNER_ROUNDING, "draw something button"); 122 | }); 123 | 124 | it("has a drop shadow", function() { 125 | assert.equal(cssHelper.dropShadow(drawSomething), cssHelper.DARK_BLUE + cssHelper.BUTTON_DROP_SHADOW, "draw something button drop shadow"); 126 | }); 127 | 128 | it("darkens when user hovers over them", function() { 129 | cssHelper.assertHoverStyle(drawSomething, cssHelper.DARKENED_MEDIUM_BLUE, "draw something button"); 130 | }); 131 | 132 | it("appears to depress when user activates them", function() { 133 | cssHelper.assertActivateDepresses(drawSomething, tagline.bottom.plus(19), "draw something button"); 134 | }); 135 | 136 | }); 137 | }); 138 | 139 | 140 | function assertStandardButtonStyling(button, description) { 141 | assert.equal(cssHelper.textAlign(button), "center", description + "text horizontal centering"); 142 | assert.equal(cssHelper.isTextVerticallyCentered(button), true, description + " text vertical centering"); 143 | assert.equal(cssHelper.textIsUnderlined(button), false, description + " text underline"); 144 | assert.equal(cssHelper.textIsUppercase(button), true, description + " text uppercase"); 145 | assert.equal(cssHelper.hasBorder(button), false, description + " border"); 146 | } 147 | 148 | }()); 149 | -------------------------------------------------------------------------------- /src/client/content/_button_css_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var cssHelper = require("./_css_test_helper.js"); 7 | 8 | describe("CSS: Button block", function() { 9 | 10 | cssHelper.setupUnitTests(); 11 | 12 | var INHERITED_FONT = "inherit-this-font"; 13 | 14 | var linkTag; 15 | var buttonTag; 16 | 17 | beforeEach(function() { 18 | cssHelper.frame.add( 19 | "
" + 20 | " foo" + 21 | " " + 22 | "
" 23 | ); 24 | 25 | linkTag = cssHelper.frame.get("#a_tag"); 26 | buttonTag = cssHelper.frame.get("#button_tag"); 27 | }); 28 | 29 | it("text", function() { 30 | assert.equal(cssHelper.textAlign(linkTag), "center", "should be horizontally centered"); 31 | assert.equal(cssHelper.textIsUnderlined(linkTag), false, "text should not be underlined"); 32 | assert.equal(cssHelper.textIsUppercase(linkTag), true, "text should be uppercase"); 33 | assert.equal(cssHelper.fontFamily(buttonTag), INHERITED_FONT, "", "", " 79 | 80 | 81 | 84 | 85 | Join Us! 86 | 87 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/client/content/screen.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | margin-bottom: 13px; 5 | } 6 | 7 | 8 | /* "Let's Code" theme (our only theme) */ 9 | 10 | .theme-lets-code { 11 | font-family: "alwyn-new-rounded-web", "Helvetica", sans-serif; 12 | background-color: #41a9cc; 13 | } 14 | 15 | p { 16 | font-size: 15px; 17 | font-weight: 300; 18 | line-height: 18px; 19 | 20 | color: #0d576d; 21 | } 22 | 23 | strong { 24 | font-weight: 300; 25 | color: white; 26 | } 27 | 28 | 29 | /* Layout */ 30 | 31 | .layout-width-full { 32 | width: 980px; 33 | } 34 | 35 | .layout-width-button { 36 | width: 225px; 37 | } 38 | 39 | .layout-center { 40 | display: block; 41 | margin-left: auto; 42 | margin-right: auto; 43 | } 44 | 45 | .layout-center-text { 46 | text-align: center; 47 | } 48 | 49 | 50 | 51 | /* Button block */ 52 | 53 | .button { 54 | display: block; 55 | padding: 0; 56 | 57 | font-family: inherit; 58 | text-align: center; 59 | text-transform: uppercase; 60 | text-decoration: none; 61 | 62 | border: none; 63 | border-radius: 2px; 64 | } 65 | 66 | .button:active, .button._active_ { 67 | box-shadow: none; 68 | position: relative; 69 | top: 1px; 70 | } 71 | 72 | .button--action { 73 | height: 35px; 74 | 75 | font-size: 16px; 76 | font-weight: 400; 77 | line-height: 35px; /* vertical centering */ 78 | 79 | color: white; 80 | background-color: #00799c; 81 | box-shadow: 0 1px 0 0 #0d576d; 82 | } 83 | 84 | .button--action:hover, .button--action._hover_ { 85 | background-color: #006f8f; 86 | } 87 | 88 | .button--drawing { 89 | height: 30px; 90 | 91 | font-size: 12px; 92 | font-weight: 600; 93 | line-height: 30px; /* vertical centering */ 94 | 95 | background-color: #e5e5e5; 96 | color: #595959; 97 | box-shadow: 0 1px 0 0 #a7a9ab; 98 | } 99 | 100 | .button--drawing:hover, .button--drawing._hover_ { 101 | background-color: #d9d9d9; 102 | } 103 | 104 | 105 | /* Drawing Area block */ 106 | 107 | .drawing-area { 108 | position: relative; 109 | } 110 | 111 | .drawing-area__canvas { 112 | height: 474px; 113 | background-color: white; 114 | border-radius: 2px; 115 | } 116 | 117 | .drawing-area__arrow { 118 | background-image: url(/images/arrow.png); 119 | background-repeat: no-repeat; 120 | background-position: center; 121 | height: 9px; 122 | 123 | position: absolute; 124 | top: 0px; 125 | left: 0px; 126 | right: 0px; 127 | } 128 | 129 | .drawing-area__button { 130 | position: absolute; 131 | top: 15px; 132 | right: 15px; 133 | 134 | width: 70px; 135 | } 136 | 137 | .drawing-area__button:active, .drawing-area__button._active_ { 138 | position: absolute; 139 | top: 16px; 140 | } 141 | 142 | 143 | /* Logo block */ 144 | 145 | .logo { 146 | height: 30px; 147 | 148 | text-align: center; 149 | line-height: 30px; /* vertical centering */ 150 | font-size: 30px; 151 | font-weight: 600; 152 | 153 | color: white; 154 | } 155 | 156 | 157 | /* Ghost Pointer block */ 158 | 159 | .ghost-pointer { 160 | opacity: 0.5; 161 | } 162 | 163 | 164 | /* 'Not Found' block */ 165 | 166 | .not-found { 167 | text-align: center; 168 | line-height: 200px; /* vertical centering */ 169 | font-size: 200px; 170 | font-weight: 600; 171 | 172 | color: #0d576d; 173 | } 174 | -------------------------------------------------------------------------------- /src/client/network/__test_harness_client.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | /* global io:false, $:false */ 3 | (function() { 4 | "use strict"; 5 | 6 | var shared = require("./__test_harness_shared.js"); 7 | 8 | var endpoints = shared.endpoints; 9 | 10 | exports.PORT = shared.PORT; 11 | 12 | exports.waitForServerDisconnect = function(connection, callback) { 13 | ajax({ 14 | connection: connection, 15 | endpoint: endpoints.WAIT_FOR_SERVER_DISCONNECT, 16 | async: true 17 | }, function(err, responseText) { 18 | return callback(err); 19 | }); 20 | }; 21 | 22 | exports.isConnected = function(connection) { 23 | var responseText = ajax({ 24 | connection: connection, 25 | endpoint: endpoints.IS_CONNECTED, 26 | async: false 27 | }); 28 | 29 | var connectedIds = JSON.parse(responseText); 30 | return connectedIds.indexOf(connection.getSocketId()) !== -1; 31 | }; 32 | 33 | exports.sendMessage = function(connection, message, callback) { 34 | ajax({ 35 | connection: connection, 36 | endpoint: endpoints.SEND_MESSAGE, 37 | async: true, 38 | data: { 39 | messageName: message.name(), 40 | messageData: message.payload() 41 | } 42 | }, function(err, responseText) { 43 | callback(err); 44 | }); 45 | }; 46 | 47 | exports.waitForMessage = function(connection, messageConstructor, callback) { 48 | ajax({ 49 | connection: connection, 50 | endpoint: endpoints.WAIT_FOR_MESSAGE, 51 | data: { 52 | messageName: messageConstructor.MESSAGE_NAME 53 | }, 54 | async: true 55 | }, function(err, responseText) { 56 | return callback(err, JSON.parse(responseText)); 57 | }); 58 | }; 59 | 60 | function ajax(options, callback) { 61 | var origin = window.location.protocol + "//" + window.location.hostname + ":" + exports.PORT; 62 | var request = $.ajax({ 63 | type: "GET", 64 | url: origin + options.endpoint, 65 | data: { 66 | data: JSON.stringify(options.data), 67 | socketId: options.connection.getSocketId() 68 | }, 69 | async: options.async, 70 | cache: false 71 | }); 72 | 73 | if (options.async) { 74 | request.done(function() { 75 | if (request.status !== 200) throw new Error("Invalid status: " + request.status); 76 | return callback(null, request.responseText); 77 | }); 78 | request.fail(function(_, errorText) { 79 | if (request.status !== 200) throw new Error("Invalid status: " + request.status); 80 | throw new Error(errorText); 81 | }); 82 | } 83 | else { 84 | if (request.status !== 200) throw new Error("Invalid status: " + request.status); 85 | else return request.responseText; 86 | } 87 | } 88 | 89 | }()); -------------------------------------------------------------------------------- /src/client/network/__test_harness_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | /* global io:false, $:false */ 3 | (function() { 4 | "use strict"; 5 | 6 | var shared = require("./__test_harness_shared.js"); 7 | var failFast = require("fail_fast"); 8 | var http = require("http"); 9 | var socketIo = require("socket.io"); 10 | var url = require("url"); 11 | var querystring = require("querystring"); 12 | var ClientPointerMessage = require("../../shared/client_pointer_message.js"); 13 | var ClientDrawMessage = require("../../shared/client_draw_message.js"); 14 | 15 | var endpoints = shared.endpoints; 16 | 17 | // The network test harness is started inside of the build script before the network tests are run 18 | exports.start = function() { 19 | var httpServer = http.createServer(); 20 | httpServer.on("request", handleRequest); 21 | var io = socketIo(httpServer); 22 | httpServer.listen(shared.PORT); 23 | 24 | var endpointMap = {}; 25 | endpointMap[endpoints.IS_CONNECTED] = setupIsConnected(io); 26 | endpointMap[endpoints.WAIT_FOR_SERVER_DISCONNECT] = setupWaitForServerDisconnect(); 27 | endpointMap[endpoints.SEND_MESSAGE] = setupSendMessage(); 28 | endpointMap[endpoints.WAIT_FOR_MESSAGE] = setupWaitForMessage(io); 29 | 30 | return stopFn(httpServer, io); 31 | 32 | function handleRequest(request, response) { 33 | response.setHeader("Access-Control-Allow-Origin", "*"); 34 | 35 | var parsedUrl = url.parse(request.url); 36 | var path = parsedUrl.pathname; 37 | 38 | var endpoint = endpointMap[path]; 39 | if (endpoint !== undefined) { 40 | var parsedQuery = querystring.parse(parsedUrl.query); 41 | var parsedData = parsedQuery.data !== undefined ? JSON.parse(parsedQuery.data) : undefined; 42 | endpoint(getSocket(io, parsedQuery.socketId), parsedData, request, response); 43 | } 44 | else { 45 | response.statusCode = 404; 46 | response.end("Not Found"); 47 | } 48 | } 49 | }; 50 | 51 | function getSocket(io, clientSocketId) { 52 | return io.sockets.sockets[clientSocketId]; 53 | } 54 | 55 | function stopFn(httpServer, io) { 56 | // Socket.IO doesn't exit cleanly, so we have to manually collect the connections 57 | // and unref() them so the server process will exit. 58 | // See bug #1602: https://github.com/socketio/socket.io/issues/1602 59 | var connections = []; 60 | httpServer.on("connection", function(socket) { 61 | connections.push(socket); 62 | }); 63 | 64 | return function(callback) { 65 | return function() { 66 | io.close(); 67 | connections.forEach(function(socket) { 68 | socket.unref(); 69 | }); 70 | callback(); 71 | }; 72 | }; 73 | } 74 | 75 | function setupIsConnected(io) { 76 | return function isConnectedEndpoint(socket, data, request, response) { 77 | var socketIds = Object.keys(io.sockets.connected); 78 | response.end(JSON.stringify(socketIds)); 79 | }; 80 | } 81 | 82 | function setupWaitForServerDisconnect() { 83 | return function waitForServerDisconnectEndpoint(socket, data, request, response) { 84 | if (socket === undefined || socket.disconnected) return response.end("disconnected"); 85 | socket.on("disconnect", function() { 86 | return response.end("disconnected"); 87 | }); 88 | }; 89 | } 90 | 91 | function setupSendMessage() { 92 | return function sendMessageEndpoint(socket, data, request, response) { 93 | socket.emit(data.messageName, data.messageData); 94 | return response.end("ok"); 95 | }; 96 | } 97 | 98 | function setupWaitForMessage(io) { 99 | var lastDrawMessage = {}; 100 | 101 | var TESTABLE_MESSAGES = [ 102 | ClientDrawMessage.MESSAGE_NAME, 103 | ClientPointerMessage.MESSAGE_NAME 104 | ]; 105 | 106 | io.on("connection", function(socket) { 107 | TESTABLE_MESSAGES.forEach(function(messageName) { 108 | socket.on(messageName, function(data) { 109 | lastDrawMessage[messageDataKey(socket.id, messageName)] = data; 110 | }); 111 | }); 112 | }); 113 | 114 | return function waitForMessageEndpoint(socket, data, request, response) { 115 | var messageName = data.messageName; 116 | failFast.unlessTrue( 117 | TESTABLE_MESSAGES.indexOf(messageName) !== -1, 118 | messageName + " not yet supported; add it to TESTABLE_MESSAGES constant in test harness server." 119 | ); 120 | var key = messageDataKey(socket.id, messageName); 121 | 122 | var result = lastDrawMessage[key]; 123 | if (result === undefined) { 124 | socket.once(messageName, sendResponse); 125 | } 126 | else { 127 | sendResponse(result); 128 | } 129 | 130 | function sendResponse(data) { 131 | response.end(JSON.stringify(data)); 132 | delete lastDrawMessage[key]; 133 | } 134 | }; 135 | 136 | function messageDataKey(socketId, messageName) { 137 | return socketId + "|" + messageName; 138 | } 139 | } 140 | 141 | }()); -------------------------------------------------------------------------------- /src/client/network/__test_harness_shared.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | /* global io:false, $:false */ 3 | (function() { 4 | "use strict"; 5 | 6 | exports.endpoints = { 7 | IS_CONNECTED: "/is-connected", 8 | WAIT_FOR_SERVER_DISCONNECT: "/wait-for-server-disconnect", 9 | 10 | SEND_MESSAGE: "/send-message", 11 | WAIT_FOR_MESSAGE: "/wait-for-message" 12 | }; 13 | 14 | exports.PORT = "5030"; 15 | 16 | }()); -------------------------------------------------------------------------------- /src/client/network/_real_time_connection_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var harness = require("./__test_harness_client.js"); 7 | var Connection = require("./real_time_connection.js"); 8 | var async = require("./vendor/async-1.5.2.js"); 9 | var ServerDrawMessage = require("../../shared/server_draw_message.js"); 10 | var ClientDrawMessage = require("../../shared/client_draw_message.js"); 11 | 12 | describe("NET: RealTimeConnection", function() { 13 | 14 | var connection; 15 | 16 | beforeEach(function() { 17 | connection = new Connection(); 18 | }); 19 | 20 | it("connects and disconnects from Socket.IO server", function(done) { 21 | connection.connect(harness.PORT, function(err) { 22 | assert.equal(err, null, "connect() should not have error"); 23 | assert.equal(harness.isConnected(connection), true, "client should have connected to server"); 24 | 25 | connection.disconnect(function(err2) { 26 | assert.equal(err2, null, "disconnect() should not have error"); 27 | harness.waitForServerDisconnect(connection, done); // will timeout if disconnect doesn't work 28 | }); 29 | }); 30 | }); 31 | 32 | it("only calls connect() and disconnect() callbacks once", function(done) { 33 | var connectCallback = 0; 34 | var disconnectCallback = 0; 35 | 36 | connection.connect(harness.PORT, function(err) { 37 | if (err) return done(err); 38 | connectCallback++; 39 | 40 | connection.disconnect(function(err) { 41 | if (err) return done(err); 42 | disconnectCallback++; 43 | 44 | connection.connect(harness.PORT, function(err) { 45 | if (err) return done(err); 46 | 47 | assert.equal(connectCallback, 1, "connect callback"); 48 | assert.equal(disconnectCallback, 1, "disconnect callback"); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | }); 54 | 55 | it("connect() can be called without callback", function(done) { 56 | connection.connect(harness.PORT); 57 | async.until(test, fn, done); 58 | 59 | function test() { 60 | return connection.isConnected(); 61 | } 62 | 63 | function fn(callback) { 64 | setTimeout(callback, 50); 65 | } 66 | }); 67 | 68 | it("sends messages to Socket.IO server", function(done) { 69 | connection.connect(harness.PORT, function() { 70 | var message = new ClientDrawMessage(1, 2, 3, 4); 71 | 72 | connection.sendMessage(message); 73 | 74 | harness.waitForMessage(connection, ClientDrawMessage, function(error, messageData) { 75 | assert.deepEqual(messageData, message.payload()); 76 | connection.disconnect(done); 77 | }); 78 | }); 79 | }); 80 | 81 | it("gets most recent message sent to Socket.IO server, even if it hasn't be received yet", function(done) { 82 | var DRAW_MESSAGE = new ClientDrawMessage(1, 2, 3, 4); 83 | 84 | connection.connect(harness.PORT, function() { 85 | assert.deepEqual(connection.getLastSentMessage(), null, "should not have message if nothing sent"); 86 | connection.sendMessage(DRAW_MESSAGE); 87 | assert.deepEqual(connection.getLastSentMessage(), DRAW_MESSAGE, "should return last sent message"); 88 | connection.disconnect(done); 89 | }); 90 | }); 91 | 92 | it("receives messages from Socket.IO server", function(done) { 93 | var DRAW_MESSAGE = new ServerDrawMessage(1, 2, 3, 4); 94 | 95 | connection.connect(harness.PORT, function() { 96 | 97 | connection.onMessage(ServerDrawMessage, function(message) { 98 | assert.deepEqual(message, DRAW_MESSAGE); 99 | connection.disconnect(done); 100 | }); 101 | harness.sendMessage(connection, DRAW_MESSAGE, function() {}); 102 | }); 103 | }); 104 | 105 | it("can trigger messages manually", function(done) { 106 | var DRAW_MESSAGE = new ServerDrawMessage(1, 2, 3, 4); 107 | 108 | connection.connect(harness.PORT, function() { 109 | connection.onMessage(ServerDrawMessage, function(message) { 110 | assert.deepEqual(message, DRAW_MESSAGE); 111 | connection.disconnect(done); 112 | }); 113 | 114 | connection.triggerMessage(DRAW_MESSAGE); 115 | // if triggerMessage doesn't do anything, the test will time out 116 | }); 117 | }); 118 | 119 | it("provides socket ID", function(done) { 120 | connection.connect(harness.PORT, function() { 121 | var socketId = connection.getSocketId(); 122 | assert.defined(socketId, "should return socket ID after connecting"); 123 | connection.disconnect(function() { 124 | assert.equal(connection.getSocketId(), null, "should return null after disconnecting"); 125 | done(); 126 | }); 127 | }); 128 | }); 129 | 130 | it("provides server port", function(done) { 131 | connection.connect(harness.PORT, function() { 132 | assert.equal(connection.getPort(), harness.PORT, "should return connection port after connecting"); 133 | connection.disconnect(function() { 134 | assert.equal(connection.getPort(), null, "should return null after disconnecting"); 135 | done(); 136 | }); 137 | }); 138 | }); 139 | 140 | it("checks status of connection", function(done) { 141 | assert.equal(connection.isConnected(), false, "should not be connected before connect() is called"); 142 | 143 | connection.connect(harness.PORT, function() { 144 | assert.equal(connection.isConnected(), true, "should be connected after connect() is complete"); 145 | connection.disconnect(function() { 146 | assert.equal(connection.isConnected(), false, "should not be connected after disconnect() is complete"); 147 | done(); 148 | }); 149 | }); 150 | }); 151 | 152 | it("fails fast when methods are called before connect() is called", function() { 153 | var expectedMessage = "Connection used before connect() called"; 154 | 155 | assert.throws(connection.disconnect.bind(connection, callback), expectedMessage, "disconnect()"); 156 | assert.throws(connection.sendMessage.bind(connection), expectedMessage, "sendMessage()"); 157 | assert.throws(connection.onMessage.bind(connection, ServerDrawMessage, callback), expectedMessage, "onMessage()"); 158 | assert.throws(connection.triggerMessage.bind(connection), expectedMessage, "triggerMessage()"); 159 | assert.throws(connection.getSocketId.bind(connection), expectedMessage, "getSocketId()"); 160 | assert.throws(connection.getPort.bind(connection), expectedMessage, "getPort()"); 161 | 162 | function callback() { 163 | assert.fail("Callback should never be called"); 164 | } 165 | }); 166 | 167 | }); 168 | 169 | 170 | describe("NET: RealTimeConnection._nullIo", function() { 171 | 172 | var IRRELEVANT_SERVER = "http://irrelevant_server"; 173 | 174 | var nullIo = Connection._nullIo; 175 | 176 | it("mimics Socket.IO variables (without actually talking to a server)", function() { 177 | var socket = nullIo("http://any.host:9283"); 178 | 179 | assert.equal(socket.connected, true, "connected"); 180 | assert.equal(socket.id, "NullConnection", "id"); 181 | assert.deepEqual(socket.io, { 182 | engine: { 183 | port: "9283" 184 | } 185 | }, "io"); 186 | }); 187 | 188 | it("emits connect event upon construction", function(done) { 189 | var socket = nullIo(IRRELEVANT_SERVER); 190 | socket.once("connect", function() { 191 | done(); 192 | }); 193 | // test times out if connect event not sent 194 | }); 195 | 196 | it("silently swallows all events that would be sent to server", function(done) { 197 | var socket = nullIo(IRRELEVANT_SERVER); 198 | 199 | var eventHandler = false; 200 | socket.on("my_event", function() { 201 | eventHandler = true; 202 | done(); // test will fail if done is called twice 203 | }); 204 | socket.emit("my_event"); 205 | 206 | assert.equal(eventHandler, false, "events should be swallowed"); 207 | done(); 208 | }); 209 | 210 | it("'closes' socket by emitting asynchronous disconnect event and changing state", function(done) { 211 | var socket = nullIo(IRRELEVANT_SERVER); 212 | 213 | socket.close(); 214 | // by putting event handler after close(), we test that the event is asynchronous 215 | socket.once("disconnect", function() { 216 | assert.equal(socket.connected, false, "socket should no longer be connected"); 217 | done(); 218 | }); 219 | // test times out if disconnect event not sent 220 | }); 221 | 222 | }); 223 | 224 | }()); -------------------------------------------------------------------------------- /src/client/network/real_time_connection.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | /* global io:false */ 3 | (function() { 4 | "use strict"; 5 | 6 | var failFast = require("fail_fast"); 7 | var EventEmitter = require("./vendor/emitter-1.2.1.js"); 8 | 9 | var Connection = module.exports = function() { 10 | return initialize(this, window.io); 11 | }; 12 | 13 | Connection.createNull = function() { 14 | return initialize(new Connection(), Connection._nullIo); 15 | }; 16 | 17 | function initialize(self, ioToInject) { 18 | self._io = ioToInject; 19 | self._connectCalled = false; 20 | self._socket = null; 21 | self._lastSentMessage = null; 22 | self._localEmitter = new EventEmitter(); 23 | return self; 24 | } 25 | 26 | Connection.prototype.connect = function(port, callback) { 27 | this._connectCalled = true; 28 | var origin = window.location.protocol + "//" + window.location.hostname + ":" + port; 29 | this._socket = this._io(origin); 30 | // Choice of calling .once() instead of .on() is not tested. It only comes into play when the server 31 | // connection is interrupted, which we don't support yet 32 | if (callback !== undefined) this._socket.once("connect", function() { 33 | return callback(null); 34 | }); 35 | }; 36 | 37 | Connection.prototype.disconnect = function(callback) { 38 | failFastUnlessConnectCalled(this); 39 | 40 | // Choice of calling .once() instead of .on() is not tested. It only comes into play when the server 41 | // connection is interrupted, which we don't support yet 42 | this._socket.once("disconnect", function() { 43 | return callback(null); 44 | }); 45 | this._socket.close(); 46 | }; 47 | 48 | Connection.prototype.sendMessage = function(message) { 49 | failFastUnlessConnectCalled(this); 50 | 51 | this._lastSentMessage = message; 52 | this._socket.emit(message.name(), message.payload()); 53 | }; 54 | 55 | Connection.prototype.getLastSentMessage = function() { 56 | return this._lastSentMessage; 57 | }; 58 | 59 | Connection.prototype.onMessage = function(messageConstructor, handler) { 60 | failFastUnlessConnectCalled(this); 61 | failFast.unlessDefined(messageConstructor.MESSAGE_NAME, "messageConstructor.MESSAGE_NAME"); 62 | 63 | this._localEmitter.on(messageConstructor.MESSAGE_NAME, handler); 64 | this._socket.on(messageConstructor.MESSAGE_NAME, function(messageData) { 65 | return handler(messageConstructor.fromPayload(messageData)); 66 | }); 67 | }; 68 | 69 | Connection.prototype.triggerMessage = function(message) { 70 | failFastUnlessConnectCalled(this); 71 | 72 | this._localEmitter.emit(message.name(), message); 73 | }; 74 | 75 | Connection.prototype.getSocketId = function() { 76 | failFastUnlessConnectCalled(this); 77 | if (!this.isConnected()) return null; 78 | 79 | else return this._socket.id; 80 | }; 81 | 82 | Connection.prototype.getPort = function() { 83 | failFastUnlessConnectCalled(this); 84 | if (!this.isConnected()) return null; 85 | 86 | return this._socket.io.engine.port; 87 | }; 88 | 89 | Connection.prototype.isConnected = function() { 90 | return this._socket !== null && this._socket.connected; 91 | }; 92 | 93 | function failFastUnlessConnectCalled(self) { 94 | failFast.unlessTrue(self._connectCalled, "Connection used before connect() called"); 95 | } 96 | 97 | 98 | //**** nullIo mimics the socket.io interface, but doesn't talk over the network 99 | 100 | // We're exposing this for test purposes only. 101 | Connection._nullIo = function(origin) { 102 | return new NullSocket(parsePort(origin)); 103 | }; 104 | 105 | // This code based on https://gist.github.com/jlong/2428561 106 | function parsePort(url) { 107 | var parser = document.createElement('a'); 108 | parser.href = url; 109 | return parser.port; 110 | } 111 | 112 | function NullSocket(port) { 113 | this._emitter = new EventEmitter(); 114 | 115 | this.connected = true; 116 | this.id = "NullConnection"; 117 | this.io = { 118 | engine: { port: port } 119 | }; 120 | 121 | asynchronousEmit(this._emitter, "connect"); 122 | } 123 | 124 | NullSocket.prototype.emit = function() { 125 | // ignore all events (that's what makes this a "Null" Socket) 126 | }; 127 | 128 | NullSocket.prototype.on = function(event, handler) { 129 | // ignore all events 130 | }; 131 | 132 | NullSocket.prototype.once = function(event, handler) { 133 | if (event === "disconnect") return this._emitter.once(event, handler); 134 | if (event === "connect") return this._emitter.once(event, handler); 135 | // ignore all other events 136 | }; 137 | 138 | NullSocket.prototype.close = function() { 139 | this.connected = false; 140 | asynchronousEmit(this._emitter, "disconnect"); 141 | }; 142 | 143 | function asynchronousEmit(emitter, event) { 144 | setTimeout(function() { 145 | emitter.emit(event); 146 | }, 0); 147 | } 148 | 149 | }()); -------------------------------------------------------------------------------- /src/client/network/vendor/emitter-1.2.1.js: -------------------------------------------------------------------------------- 1 | // Retrieved on 4 Nov 2016 from https://github.com/component/emitter/tree/1.2.1 2 | // Copyright (c) 2014 Component contributors 3 | // MIT License: https://github.com/component/emitter/blob/1.2.1/LICENSE 4 | 5 | 6 | /** 7 | * Expose `Emitter`. 8 | */ 9 | 10 | if (typeof module !== 'undefined') { 11 | module.exports = Emitter; 12 | } 13 | 14 | /** 15 | * Initialize a new `Emitter`. 16 | * 17 | * @api public 18 | */ 19 | 20 | function Emitter(obj) { 21 | if (obj) return mixin(obj); 22 | }; 23 | 24 | /** 25 | * Mixin the emitter properties. 26 | * 27 | * @param {Object} obj 28 | * @return {Object} 29 | * @api private 30 | */ 31 | 32 | function mixin(obj) { 33 | for (var key in Emitter.prototype) { 34 | obj[key] = Emitter.prototype[key]; 35 | } 36 | return obj; 37 | } 38 | 39 | /** 40 | * Listen on the given `event` with `fn`. 41 | * 42 | * @param {String} event 43 | * @param {Function} fn 44 | * @return {Emitter} 45 | * @api public 46 | */ 47 | 48 | Emitter.prototype.on = 49 | Emitter.prototype.addEventListener = function(event, fn){ 50 | this._callbacks = this._callbacks || {}; 51 | (this._callbacks['$' + event] = this._callbacks['$' + event] || []) 52 | .push(fn); 53 | return this; 54 | }; 55 | 56 | /** 57 | * Adds an `event` listener that will be invoked a single 58 | * time then automatically removed. 59 | * 60 | * @param {String} event 61 | * @param {Function} fn 62 | * @return {Emitter} 63 | * @api public 64 | */ 65 | 66 | Emitter.prototype.once = function(event, fn){ 67 | function on() { 68 | this.off(event, on); 69 | fn.apply(this, arguments); 70 | } 71 | 72 | on.fn = fn; 73 | this.on(event, on); 74 | return this; 75 | }; 76 | 77 | /** 78 | * Remove the given callback for `event` or all 79 | * registered callbacks. 80 | * 81 | * @param {String} event 82 | * @param {Function} fn 83 | * @return {Emitter} 84 | * @api public 85 | */ 86 | 87 | Emitter.prototype.off = 88 | Emitter.prototype.removeListener = 89 | Emitter.prototype.removeAllListeners = 90 | Emitter.prototype.removeEventListener = function(event, fn){ 91 | this._callbacks = this._callbacks || {}; 92 | 93 | // all 94 | if (0 == arguments.length) { 95 | this._callbacks = {}; 96 | return this; 97 | } 98 | 99 | // specific event 100 | var callbacks = this._callbacks['$' + event]; 101 | if (!callbacks) return this; 102 | 103 | // remove all handlers 104 | if (1 == arguments.length) { 105 | delete this._callbacks['$' + event]; 106 | return this; 107 | } 108 | 109 | // remove specific handler 110 | var cb; 111 | for (var i = 0; i < callbacks.length; i++) { 112 | cb = callbacks[i]; 113 | if (cb === fn || cb.fn === fn) { 114 | callbacks.splice(i, 1); 115 | break; 116 | } 117 | } 118 | return this; 119 | }; 120 | 121 | /** 122 | * Emit `event` with the given args. 123 | * 124 | * @param {String} event 125 | * @param {Mixed} ... 126 | * @return {Emitter} 127 | */ 128 | 129 | Emitter.prototype.emit = function(event){ 130 | this._callbacks = this._callbacks || {}; 131 | var args = [].slice.call(arguments, 1) 132 | , callbacks = this._callbacks['$' + event]; 133 | 134 | if (callbacks) { 135 | callbacks = callbacks.slice(0); 136 | for (var i = 0, len = callbacks.length; i < len; ++i) { 137 | callbacks[i].apply(this, args); 138 | } 139 | } 140 | 141 | return this; 142 | }; 143 | 144 | /** 145 | * Return array of callbacks for `event`. 146 | * 147 | * @param {String} event 148 | * @return {Array} 149 | * @api public 150 | */ 151 | 152 | Emitter.prototype.listeners = function(event){ 153 | this._callbacks = this._callbacks || {}; 154 | return this._callbacks['$' + event] || []; 155 | }; 156 | 157 | /** 158 | * Check if this emitter has `event` handlers. 159 | * 160 | * @param {String} event 161 | * @return {Boolean} 162 | * @api public 163 | */ 164 | 165 | Emitter.prototype.hasListeners = function(event){ 166 | return !! this.listeners(event).length; 167 | }; 168 | -------------------------------------------------------------------------------- /src/client/ui/_html_coordinate_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var HtmlCoordinate = require("./html_coordinate.js"); 7 | var HtmlElement = require("./html_element.js"); 8 | 9 | describe("UI: HtmlCoordinate", function() { 10 | 11 | var element; 12 | 13 | beforeEach(function() { 14 | element = HtmlElement.fromHtml("
"); 15 | element.appendSelfToBody(); 16 | }); 17 | 18 | afterEach(function() { 19 | element.remove(); 20 | }); 21 | 22 | it("provides relative x and y coordinates", function() { 23 | var coord = HtmlCoordinate.fromRelativeOffset(element, 10, 20); 24 | assert.deepEqual(coord.toRelativeOffset(element), { x: 10, y: 20 }); 25 | }); 26 | 27 | it("provides page x and y coordinates", function() { 28 | var coord = HtmlCoordinate.fromPageOffset(13, 42); 29 | assert.deepEqual(coord.toPageOffset(), { x: 13, y: 42 }); 30 | }); 31 | 32 | it("converts page offsets to relative offsets", function() { 33 | var coord = HtmlCoordinate.fromPageOffset(10, 20); 34 | assert.deepEqual(coord.toRelativeOffset(element), { x: -5, y: 5 }); 35 | }); 36 | 37 | it("converts positions relative to one element to be relative to another", function() { 38 | var element2 = HtmlElement.fromHtml("
"); 39 | element2.appendSelfToBody(); 40 | 41 | try { 42 | var coord = HtmlCoordinate.fromRelativeOffset(element2, 10, 20); 43 | var offset = coord.toRelativeOffset(element); 44 | assert.deepEqual(offset, { x: 95, y: 105 }); 45 | } 46 | finally { 47 | element2.remove(); 48 | } 49 | }); 50 | 51 | it("converts to string for debugging purposes", function() { 52 | var coord = HtmlCoordinate.fromPageOffset(10, 20); 53 | assert.equal(coord.toString(), "[HtmlCoordinate page offset (10, 20)]"); 54 | }); 55 | 56 | describe("relative offset conversion", function() { 57 | 58 | var COLLAPSING_BODY_MARGIN = 8; 59 | var scrollEnabler; 60 | var scroller; 61 | 62 | beforeEach(function() { 63 | scrollEnabler = HtmlElement.fromHtml( 64 | "
scroll enabler
" 65 | ); 66 | scrollEnabler.appendSelfToBody(); 67 | scroller = scrollEnabler.toDomElement().ownerDocument.defaultView; 68 | }); 69 | 70 | afterEach(function() { 71 | scroller.scroll(0, 0); 72 | scrollEnabler.remove(); 73 | }); 74 | 75 | 76 | it("ignores margin", function() { 77 | checkFromRelativeOffsetCalculation("margin-top: 13px;", 0, 13 - COLLAPSING_BODY_MARGIN); 78 | checkFromRelativeOffsetCalculation("margin-left: 13px;", 13, 0); 79 | checkFromRelativeOffsetCalculation("margin: 13px;", 13, 13 - COLLAPSING_BODY_MARGIN); 80 | checkFromRelativeOffsetCalculation("margin: 1em; font-size: 16px", 16, 16 - COLLAPSING_BODY_MARGIN); 81 | 82 | checkToRelativeOffsetCalculation("margin-top: 13px;", 0, 13 - COLLAPSING_BODY_MARGIN); 83 | checkToRelativeOffsetCalculation("margin-left: 13px;", 13, 0); 84 | checkToRelativeOffsetCalculation("margin: 13px;", 13, 13 - COLLAPSING_BODY_MARGIN); 85 | checkToRelativeOffsetCalculation("margin: 1em; font-size: 16px", 16, 16 - COLLAPSING_BODY_MARGIN); 86 | }); 87 | 88 | it("accounts for page scrolling", function() { 89 | var pageOffset = HtmlCoordinate.fromPageOffset(100, 100); 90 | 91 | var preScrollFromRelative = HtmlCoordinate.fromRelativeOffset(element, 0, 0); 92 | var preScrollToRelative = pageOffset.toRelativeOffset(element); 93 | 94 | scroller.scroll(80, 90); 95 | 96 | var postScrollFromRelative = HtmlCoordinate.fromRelativeOffset(element, 0, 0); 97 | var postScrollToRelative = pageOffset.toRelativeOffset(element); 98 | 99 | assert.objEqual(postScrollFromRelative, preScrollFromRelative); 100 | assert.deepEqual(postScrollToRelative, preScrollToRelative); 101 | }); 102 | 103 | it("fails fails fast if there is any padding", function() { 104 | expectFailFast("padding-top: 13px;"); 105 | expectFailFast("padding-left: 13px;"); 106 | expectFailFast("padding: 13px;"); 107 | expectFailFast("padding: 1em; font-size: 16px"); 108 | 109 | // IE 8 weirdness 110 | expectFailFast("padding-top: 20%"); 111 | expectFailFast("padding-left: 20%"); 112 | }); 113 | 114 | it("fails fast if there is any border", function() { 115 | expectFailFast("border-top: 13px solid;"); 116 | expectFailFast("border-left: 13px solid;"); 117 | expectFailFast("border: 13px solid;"); 118 | expectFailFast("border: 1em solid; font-size: 16px"); 119 | 120 | // IE 8 weirdness 121 | expectFailFast("border: thin solid"); 122 | expectFailFast("border: medium solid"); 123 | expectFailFast("border: thick solid"); 124 | checkFromRelativeOffsetCalculation("border: 13px none", 0, 0); 125 | checkToRelativeOffsetCalculation("border: 13px none", 0, 0); 126 | }); 127 | 128 | function expectFailFast(elementStyle) { 129 | var styledElement = HtmlElement.fromHtml("
"); 130 | try { 131 | styledElement.appendSelfToBody(); 132 | assert.throws(function() { 133 | HtmlCoordinate.fromRelativeOffset(styledElement, 100, 150); 134 | }); 135 | assert.throws(function() { 136 | HtmlCoordinate.fromPageOffset(100, 150).toRelativeOffset(styledElement); 137 | }); 138 | } 139 | finally { 140 | styledElement.remove(); 141 | } 142 | } 143 | 144 | function checkFromRelativeOffsetCalculation(elementStyle, additionalXOffset, additionalYOffset) { 145 | var BASE_STYLE = "width: 120px; height: 80px; border: 0px none;"; 146 | 147 | var unstyledElement = HtmlElement.fromHtml("
"); 148 | unstyledElement.appendSelfToBody(); 149 | var expectedCoord = HtmlCoordinate.fromRelativeOffset( 150 | unstyledElement, 151 | 100 + additionalXOffset, 152 | 150 + additionalYOffset 153 | ); 154 | unstyledElement.remove(); 155 | 156 | var styledElement = HtmlElement.fromHtml("
"); 157 | try { 158 | styledElement.appendSelfToBody(); 159 | var actualCoord = HtmlCoordinate.fromRelativeOffset(styledElement, 100, 150); 160 | assert.objEqual(expectedCoord, actualCoord); 161 | } 162 | finally { 163 | styledElement.remove(); 164 | } 165 | } 166 | 167 | function checkToRelativeOffsetCalculation(elementStyle, additionalXOffset, additionalYOffset) { 168 | var BASE_STYLE = "width: 120px; height: 80px; border: 0px none;"; 169 | 170 | var unstyledElement = HtmlElement.fromHtml("
"); 171 | unstyledElement.appendSelfToBody(); 172 | var expectedOffset = HtmlCoordinate.fromPageOffset( 173 | 100 - additionalXOffset, 174 | 150 - additionalYOffset 175 | ).toRelativeOffset(unstyledElement); 176 | unstyledElement.remove(); 177 | 178 | var styledElement = HtmlElement.fromHtml("
"); 179 | try { 180 | styledElement.appendSelfToBody(); 181 | var actualOffset = HtmlCoordinate.fromPageOffset(100, 150).toRelativeOffset(styledElement); 182 | assert.deepEqual(expectedOffset, actualOffset); 183 | } 184 | finally { 185 | styledElement.remove(); 186 | } 187 | } 188 | 189 | }); 190 | 191 | describe("equality", function() { 192 | 193 | it("is equal when based on the same page data", function() { 194 | var coord1 = HtmlCoordinate.fromPageOffset(25, 35); 195 | var coord2 = HtmlCoordinate.fromRelativeOffset(element, 10, 20); 196 | 197 | assert.objEqual(coord1, coord2); 198 | }); 199 | 200 | it("is not equal when x values are different", function() { 201 | var coord1 = HtmlCoordinate.fromPageOffset(10, 20); 202 | var coord2 = HtmlCoordinate.fromPageOffset(15, 20); 203 | 204 | assert.objNotEqual(coord1, coord2); 205 | }); 206 | 207 | it("is not equal when y values are different", function() { 208 | var coord1 = HtmlCoordinate.fromPageOffset(10, 20); 209 | var coord2 = HtmlCoordinate.fromPageOffset(10, 25); 210 | 211 | assert.objNotEqual(coord1, coord2); 212 | }); 213 | 214 | it("ignores minor rendering corrections", function() { 215 | // When an element is rendered, it's sometimes positioned in a slighly different location than our 216 | // calculated coordinate. We still want that position to be considered equal to the one we calculated. 217 | assert.objEqual(HtmlCoordinate.fromPageOffset(10, 20), HtmlCoordinate.fromPageOffset(10.0099, 20), "x"); 218 | assert.objEqual(HtmlCoordinate.fromPageOffset(10, 20), HtmlCoordinate.fromPageOffset(10, 19.991), "y"); 219 | }); 220 | 221 | }); 222 | }); 223 | 224 | }()); -------------------------------------------------------------------------------- /src/client/ui/_svg_canvas_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | /*global HtmlElement, $, Raphael:true */ 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | var SvgCanvas = require("./svg_canvas.js"); 8 | var HtmlElement = require("./html_element.js"); 9 | var HtmlCoordinate = require("./html_coordinate.js"); 10 | var assert = require("_assert"); 11 | 12 | describe("UI: SvgCanvas", function() { 13 | 14 | var canvasElement; 15 | var svgCanvas; 16 | 17 | beforeEach(function() { 18 | canvasElement = HtmlElement.fromHtml("
hi
"); 19 | canvasElement.appendSelfToBody(); 20 | svgCanvas = new SvgCanvas(canvasElement); 21 | }); 22 | 23 | afterEach(function() { 24 | canvasElement.remove(); 25 | }); 26 | 27 | it("has the same dimensions as its enclosing div, regardless of border", function() { 28 | // There might be a better way of coding this that doesn't use a spy. 29 | // See Martin Grandrath's suggestion at http://www.letscodejavascript.com/v3/comments/live/185#comment-1292169079 30 | 31 | var realRaphael = Raphael; 32 | try { 33 | Raphael = SpyRaphael; 34 | svgCanvas = new SvgCanvas(canvasElement); 35 | assert.equal(Raphael.width, 200); 36 | assert.equal(Raphael.height, 900); 37 | } 38 | finally { 39 | Raphael = realRaphael; 40 | } 41 | 42 | function SpyRaphael(element, width, height) { 43 | SpyRaphael.width = width; 44 | SpyRaphael.height = height; 45 | } 46 | }); 47 | 48 | it("returns zero line segments", function() { 49 | assert.deepEqual(svgCanvas.lineSegments(), []); 50 | }); 51 | 52 | it("draws and returns one line segment", function() { 53 | svgCanvas.drawLine(coord(1, 2), coord(5, 10)); 54 | assert.deepEqual(svgCanvas.lineSegments(), [[1, 2, 5, 10]]); 55 | }); 56 | 57 | it("draws and returns multiple line segments", function() { 58 | svgCanvas.drawLine(coord(1, 2), coord(5, 10)); 59 | svgCanvas.drawLine(coord(20, 60), coord(2, 3)); 60 | svgCanvas.drawLine(coord(0, 0), coord(100, 200)); 61 | assert.deepEqual(svgCanvas.lineSegments(), [ 62 | [1, 2, 5, 10], 63 | [20, 60, 2, 3], 64 | [0, 0, 100, 200] 65 | ]); 66 | }); 67 | 68 | it("draws dots and styles them nicely", function() { 69 | svgCanvas.drawDot(coord(5, 10)); 70 | 71 | var elements = svgCanvas.elementsForTestingOnly(); 72 | assert.equal(elements.length, 1); 73 | 74 | assert.equal(elements[0].type, "circle"); 75 | 76 | var attrs = elements[0].attrs; 77 | assert.equal(attrs.cx, 5); 78 | assert.equal(attrs.cy, 10); 79 | assert.equal(attrs.r, SvgCanvas.STROKE_WIDTH / 2); 80 | assert.equal(attrs.stroke, SvgCanvas.LINE_COLOR); 81 | assert.equal(attrs.fill, SvgCanvas.LINE_COLOR); 82 | }); 83 | 84 | it("styles lines nicely", function() { 85 | svgCanvas.drawLine(coord(3, 3), coord(4, 4)); 86 | var attrs = svgCanvas.elementsForTestingOnly()[0].attrs; 87 | assert.equal(attrs.stroke, SvgCanvas.LINE_COLOR); 88 | assert.equal(attrs["stroke-width"], SvgCanvas.STROKE_WIDTH); 89 | assert.equal(attrs["stroke-linecap"], SvgCanvas.LINE_CAP); 90 | }); 91 | 92 | it("clears everything", function() { 93 | svgCanvas.drawLine(coord(3, 3), coord(4, 4)); 94 | svgCanvas.clear(); 95 | assert.deepEqual(svgCanvas.lineSegments(), []); 96 | }); 97 | 98 | function coord(x, y) { 99 | return HtmlCoordinate.fromRelativeOffset(canvasElement, x, y); 100 | } 101 | 102 | }); 103 | 104 | }()); -------------------------------------------------------------------------------- /src/client/ui/browser.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | /*global Modernizr, $ */ 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | exports.supportsTouchEvents = function() { 8 | return askModernizr("touch"); 9 | }; 10 | 11 | exports.supportsTouchEventConstructor = function() { 12 | try { 13 | var unused = new TouchEvent("touchstart", {}); 14 | return true; 15 | } 16 | catch (err) { 17 | if (!(err instanceof TypeError)) throw err; 18 | return false; 19 | } 20 | }; 21 | 22 | exports.usesAndroidInitTouchEventParameterOrder = function() { 23 | var touchEvent = document.createEvent("TouchEvent"); 24 | var touches = document.createTouchList(); 25 | 26 | try { 27 | touchEvent.initTouchEvent(touches); 28 | return touchEvent.touches === touches; 29 | } 30 | catch (err) { 31 | if (!(err instanceof TypeError)) throw err; 32 | return false; 33 | } 34 | }; 35 | 36 | function askModernizr(feature) { 37 | var result = Modernizr[feature]; 38 | if (result === undefined) throw new Error(feature + " is not checked by the installed version of Modernizr"); 39 | 40 | return result; 41 | } 42 | 43 | }()); -------------------------------------------------------------------------------- /src/client/ui/client.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | /*global Raphael, $ */ 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | var SvgCanvas = require("./svg_canvas.js"); 8 | var HtmlElement = require("./html_element.js"); 9 | var HtmlCoordinate = require("./html_coordinate.js"); 10 | var browser = require("./browser.js"); 11 | var failFast = require("fail_fast"); 12 | var ClientDrawMessage = require("../../shared/client_draw_message.js"); 13 | var ServerDrawMessage = require("../../shared/server_draw_message.js"); 14 | var ClientPointerMessage = require("../../shared/client_pointer_message.js"); 15 | var ServerPointerMessage = require("../../shared/server_pointer_message.js"); 16 | var ClientRemovePointerMessage = require("../../shared/client_remove_pointer_message.js"); 17 | var ServerRemovePointerMessage = require("../../shared/server_remove_pointer_message.js"); 18 | var ClientClearScreenMessage = require("../../shared/client_clear_screen_message.js"); 19 | var ServerClearScreenMessage = require("../../shared/server_clear_screen_message.js"); 20 | 21 | var svgCanvas = null; 22 | var start = null; 23 | var lineDrawn = false; 24 | var drawingArea; 25 | var clearScreenButton; 26 | var pointerHtml; 27 | var documentBody; 28 | var windowElement; 29 | var network; 30 | var ghostPointerElements; 31 | 32 | exports.initializeDrawingArea = function(elements, realTimeConnection) { 33 | if (svgCanvas !== null) throw new Error("Client.js is not re-entrant"); 34 | 35 | drawingArea = elements.drawingAreaDiv; 36 | clearScreenButton = elements.clearScreenButton; 37 | pointerHtml = elements.pointerHtml; 38 | 39 | failFast.unlessDefined(drawingArea, "elements.drawingArea"); 40 | failFast.unlessDefined(clearScreenButton, "elements.clearScreenButton"); 41 | failFast.unlessDefined(pointerHtml, "elements.pointerHtml"); 42 | 43 | documentBody = new HtmlElement(document.body); 44 | windowElement = new HtmlElement(window); 45 | svgCanvas = new SvgCanvas(drawingArea); 46 | network = realTimeConnection; 47 | ghostPointerElements = {}; 48 | 49 | network.connect(window.location.port); 50 | 51 | handlePointerMovement(); 52 | handleClearScreenAction(); 53 | handleDrawing(); 54 | 55 | return svgCanvas; 56 | }; 57 | 58 | exports.drawingAreaHasBeenRemovedFromDom = function() { 59 | svgCanvas = null; 60 | }; 61 | 62 | 63 | //*** Pointers 64 | 65 | function handlePointerMovement() { 66 | documentBody.onMouseMove(sendPointerMessage); 67 | documentBody.onMouseLeave(sendRemovePointerMessage); 68 | drawingArea.onSingleTouchMove(sendPointerMessage); 69 | drawingArea.onTouchEnd(sendRemovePointerMessage); 70 | network.onMessage(ServerPointerMessage, displayNetworkPointer); 71 | network.onMessage(ServerRemovePointerMessage, removeNetworkPointer); 72 | } 73 | 74 | function sendPointerMessage(coordinate) { 75 | var relativeOffset = coordinate.toRelativeOffset(drawingArea); 76 | network.sendMessage(new ClientPointerMessage(relativeOffset.x, relativeOffset.y)); 77 | } 78 | 79 | function sendRemovePointerMessage() { 80 | network.sendMessage(new ClientRemovePointerMessage()); 81 | } 82 | 83 | function displayNetworkPointer(serverMessage) { 84 | var pointerElement = ghostPointerElements[serverMessage.id]; 85 | if (pointerElement === undefined) { 86 | pointerElement = HtmlElement.appendHtmlToBody(pointerHtml); 87 | ghostPointerElements[serverMessage.id] = pointerElement; 88 | } 89 | pointerElement.setAbsolutePosition(HtmlCoordinate.fromRelativeOffset(drawingArea, serverMessage.x, serverMessage.y)); 90 | } 91 | 92 | function removeNetworkPointer(serverMessage) { 93 | var pointerElement = ghostPointerElements[serverMessage.id]; 94 | if (pointerElement === undefined) return; 95 | 96 | delete ghostPointerElements[serverMessage.id]; 97 | pointerElement.remove(); 98 | } 99 | 100 | 101 | //*** Clear Screen 102 | 103 | function handleClearScreenAction() { 104 | clearScreenButton.onMouseClick(clearDrawingAreaAndSendMessage); 105 | network.onMessage(ServerClearScreenMessage, clearDrawingArea); 106 | } 107 | 108 | function clearDrawingAreaAndSendMessage() { 109 | clearDrawingArea(); 110 | network.sendMessage(new ClientClearScreenMessage()); 111 | } 112 | 113 | function clearDrawingArea() { 114 | svgCanvas.clear(); 115 | } 116 | 117 | 118 | //*** Drawing 119 | 120 | function handleDrawing() { 121 | drawingArea.preventBrowserDragDefaults(); 122 | handleMouseDragGesture(); 123 | handleTouchDragGesture(); 124 | handleNetworkDrawing(); 125 | } 126 | 127 | function handleNetworkDrawing() { 128 | network.onMessage(ServerDrawMessage, function(message) { 129 | var from = HtmlCoordinate.fromRelativeOffset(drawingArea, message.from.x, message.from.y); 130 | var to = HtmlCoordinate.fromRelativeOffset(drawingArea, message.to.x, message.to.y); 131 | drawLineSegment(from, to); 132 | }); 133 | } 134 | 135 | function handleMouseDragGesture() { 136 | drawingArea.onMouseDown(startDrag); 137 | documentBody.onMouseMove(continueDrag); 138 | windowElement.onMouseUp(endDrag); 139 | } 140 | 141 | function handleTouchDragGesture() { 142 | drawingArea.onSingleTouchStart(startDrag); 143 | drawingArea.onSingleTouchMove(continueDrag); 144 | drawingArea.onTouchEnd(endDrag); 145 | drawingArea.onTouchCancel(endDrag); 146 | 147 | drawingArea.onMultiTouchStart(endDrag); 148 | } 149 | 150 | function startDrag(coordinate) { 151 | start = coordinate; 152 | } 153 | 154 | function continueDrag(coordinate) { 155 | if (!isCurrentlyDrawing()) return; 156 | 157 | var end = coordinate; 158 | if (!start.equals(end)) { 159 | drawLineSegmentAndSendDrawMessage(start, end); 160 | start = end; 161 | lineDrawn = true; 162 | } 163 | } 164 | 165 | function endDrag() { 166 | if (!isCurrentlyDrawing()) return; 167 | 168 | if (!lineDrawn) drawLineSegmentAndSendDrawMessage(start, start); 169 | 170 | start = null; 171 | lineDrawn = false; 172 | } 173 | 174 | function isCurrentlyDrawing() { 175 | return start !== null; 176 | } 177 | 178 | function drawLineSegmentAndSendDrawMessage(start, end) { 179 | drawLineSegment(start, end); 180 | sendDrawMessage(start, end); 181 | } 182 | 183 | function drawLineSegment(start, end) { 184 | if (start.equals(end)) svgCanvas.drawDot(start); 185 | else svgCanvas.drawLine(start, end); 186 | } 187 | 188 | function sendDrawMessage(start, end) { 189 | var startOffset = start.toRelativeOffset(drawingArea); 190 | var endOffset = end.toRelativeOffset(drawingArea); 191 | network.sendMessage(new ClientDrawMessage(startOffset.x, startOffset.y, endOffset.x, endOffset.y)); 192 | } 193 | 194 | }()); -------------------------------------------------------------------------------- /src/client/ui/html_coordinate.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var failFast = require("fail_fast.js"); 6 | 7 | var HtmlCoordinate = module.exports = function HtmlCoordinate(pageX, pageY) { 8 | this._pageX = pageX; 9 | this._pageY = pageY; 10 | }; 11 | 12 | HtmlCoordinate.fromPageOffset = function(x, y) { 13 | return new HtmlCoordinate(x, y); 14 | }; 15 | 16 | HtmlCoordinate.prototype.toPageOffset = function() { 17 | return { 18 | x: this._pageX, 19 | y: this._pageY 20 | }; 21 | }; 22 | 23 | HtmlCoordinate.fromRelativeOffset = function(htmlElement, relativeX, relativeY) { 24 | failFastIfStylingPresent(htmlElement); 25 | 26 | var scroll = scrollOffset(htmlElement); 27 | var element = elementOffset(htmlElement); 28 | return new HtmlCoordinate( 29 | scroll.x + element.x + relativeX, 30 | scroll.y + element.y + relativeY 31 | ); 32 | }; 33 | 34 | HtmlCoordinate.prototype.toRelativeOffset = function(htmlElement) { 35 | failFastIfStylingPresent(htmlElement); 36 | 37 | var scroll = scrollOffset(htmlElement); 38 | var element = elementOffset(htmlElement); 39 | return { 40 | x: this._pageX - scroll.x - element.x, 41 | y: this._pageY - scroll.y - element.y 42 | }; 43 | }; 44 | 45 | HtmlCoordinate.prototype.equals = function(that) { 46 | failFast.unlessTrue(that instanceof HtmlCoordinate, "tried to compare HtmlCoordinate with a different type of object"); 47 | 48 | var xDiff = Math.abs(this._pageX - that._pageX); 49 | var yDiff = Math.abs(this._pageY - that._pageY); 50 | 51 | return xDiff < 0.01 && yDiff < 0.01; 52 | }; 53 | 54 | HtmlCoordinate.prototype.toString = function() { 55 | return "[HtmlCoordinate page offset (" + this._pageX + ", " + this._pageY + ")]"; 56 | }; 57 | 58 | function scrollOffset(element) { 59 | var domElement = element.toDomElement(); 60 | return { 61 | x: domElement.ownerDocument.defaultView.pageXOffset, 62 | y: domElement.ownerDocument.defaultView.pageYOffset 63 | }; 64 | } 65 | 66 | function elementOffset(element) { 67 | var domElement = element.toDomElement(); 68 | var elementPosition = domElement.getBoundingClientRect(); 69 | return { 70 | x: elementPosition.left, 71 | y: elementPosition.top 72 | }; 73 | } 74 | 75 | function failFastIfStylingPresent(element) { 76 | var style = window.getComputedStyle(element.toDomElement()); 77 | 78 | failFastIfPaddingPresent("top"); 79 | failFastIfPaddingPresent("left"); 80 | failFastIfBorderPresent("top"); 81 | failFastIfBorderPresent("left"); 82 | 83 | function failFastIfPaddingPresent(side) { 84 | var css = style.getPropertyValue("padding-" + side); 85 | if (css !== "0px") throw new Error("HtmlCoordinate cannot convert relative offsets for elements that have padding (" + side + " padding was '" + css + "')"); 86 | } 87 | 88 | function failFastIfBorderPresent(side) { 89 | var css = style.getPropertyValue("border-" + side + "-width"); 90 | if (css !== "0px") throw new Error("HtmlCoordinate cannot convert relative offsets for elements that have border (" + side + " border was '" + css + "')"); 91 | } 92 | } 93 | 94 | }()); -------------------------------------------------------------------------------- /src/client/ui/svg_canvas.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | /*global Raphael */ 3 | 4 | (function() { 5 | "use strict"; 6 | 7 | var SvgCanvas = module.exports = function(htmlElement) { 8 | var dimensions = htmlElement.getDimensions(); 9 | this._element = htmlElement; 10 | this._paper = new Raphael(htmlElement.toDomElement(), dimensions.width, dimensions.height); 11 | }; 12 | 13 | SvgCanvas.LINE_COLOR = "black"; 14 | SvgCanvas.STROKE_WIDTH = 2; 15 | SvgCanvas.LINE_CAP = "round"; 16 | 17 | SvgCanvas.prototype.clear = function() { 18 | this._paper.clear(); 19 | }; 20 | 21 | SvgCanvas.prototype.drawLine = function(start, end) { 22 | var startOffset = start.toRelativeOffset(this._element); 23 | var endOffset = end.toRelativeOffset(this._element); 24 | 25 | this._paper.path("M" + startOffset.x + "," + startOffset.y + "L" + endOffset.x + "," + endOffset.y) 26 | .attr({ 27 | "stroke": SvgCanvas.LINE_COLOR, 28 | "stroke-width": SvgCanvas.STROKE_WIDTH, 29 | "stroke-linecap": SvgCanvas.LINE_CAP 30 | }); 31 | }; 32 | 33 | SvgCanvas.prototype.drawDot = function(coord) { 34 | var offset = coord.toRelativeOffset(this._element); 35 | 36 | this._paper.circle(offset.x, offset.y, SvgCanvas.STROKE_WIDTH / 2) 37 | .attr({ 38 | "stroke": SvgCanvas.LINE_COLOR, 39 | "fill": SvgCanvas.LINE_COLOR 40 | }); 41 | }; 42 | 43 | SvgCanvas.prototype.lineSegments = function() { 44 | var result = []; 45 | this._paper.forEach(function(element) { 46 | result.push(normalizeToLineSegment(element)); 47 | }); 48 | return result; 49 | }; 50 | 51 | SvgCanvas.prototype.elementsForTestingOnly = function() { 52 | var result = []; 53 | this._paper.forEach(function(element) { 54 | result.push(element); 55 | }); 56 | return result; 57 | }; 58 | 59 | function normalizeToLineSegment(element) { 60 | switch (element.type) { 61 | case "path": 62 | return normalizePath(element); 63 | case "circle": 64 | return normalizeCircle(element); 65 | default: 66 | throw new Error("Unknown element type: " + element.type); 67 | } 68 | } 69 | 70 | function normalizeCircle(element) { 71 | return [ 72 | element.attrs.cx, 73 | element.attrs.cy 74 | ]; 75 | } 76 | 77 | function normalizePath(element) { 78 | if (!Raphael.svg) throw new Error("Unknown Raphael rendering engine"); 79 | 80 | return normalizeSvgPath(element); 81 | } 82 | 83 | function normalizeSvgPath(element) { 84 | var pathRegex; 85 | 86 | var path = element.node.attributes.d.value; 87 | if (path.indexOf(",") !== -1) 88 | // We're in Firefox, Safari, Chrome, which uses format "M20,30L30,300" 89 | { 90 | pathRegex = /M(\d+),(\d+)L(\d+),(\d+)/; 91 | } 92 | else { 93 | // We're in IE9, which uses format "M 20 30 L 30 300" 94 | pathRegex = /M (\d+) (\d+) L (\d+) (\d+)/; 95 | } 96 | var pathComponents = path.match(pathRegex); 97 | 98 | return [ 99 | parseInt(pathComponents[1], 10), 100 | parseInt(pathComponents[2], 10), 101 | parseInt(pathComponents[3], 10), 102 | parseInt(pathComponents[4], 10) 103 | ]; 104 | } 105 | 106 | }()); -------------------------------------------------------------------------------- /src/client/ui/vendor/modernizr.custom-2.8.3.min.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.8.3 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-borderradius-boxshadow-touch-teststyles-prefixes 3 | */ 4 | ;window.Modernizr=function(a,b,c){function y(a){i.cssText=a}function z(a,b){return y(l.join(a+";")+(b||""))}function A(a,b){return typeof a===b}function B(a,b){return!!~(""+a).indexOf(b)}function C(a,b){for(var d in a){var e=a[d];if(!B(e,"-")&&i[e]!==c)return b=="pfx"?e:!0}return!1}function D(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:A(f,"function")?f.bind(d||b):f}return!1}function E(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+n.join(d+" ")+d).split(" ");return A(b,"string")||A(b,"undefined")?C(e,b):(e=(a+" "+o.join(d+" ")+d).split(" "),D(e,b,c))}var d="2.8.3",e={},f=b.documentElement,g="modernizr",h=b.createElement(g),i=h.style,j,k={}.toString,l=" -webkit- -moz- -o- -ms- ".split(" "),m="Webkit Moz O ms",n=m.split(" "),o=m.toLowerCase().split(" "),p={},q={},r={},s=[],t=s.slice,u,v=function(a,c,d,e){var h,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:g+(d+1),l.appendChild(j);return h=["­",'"].join(""),l.id=g,(m?l:n).innerHTML+=h,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=f.style.overflow,f.style.overflow="hidden",f.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),f.style.overflow=k),!!i},w={}.hasOwnProperty,x;!A(w,"undefined")&&!A(w.call,"undefined")?x=function(a,b){return w.call(a,b)}:x=function(a,b){return b in a&&A(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=t.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(t.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(t.call(arguments)))};return e}),p.touch=function(){var c;return"ontouchstart"in a||a.DocumentTouch&&b instanceof DocumentTouch?c=!0:v(["@media (",l.join("touch-enabled),("),g,")","{#modernizr{top:9px;position:absolute}}"].join(""),function(a){c=a.offsetTop===9}),c},p.borderradius=function(){return E("borderRadius")},p.boxshadow=function(){return E("boxShadow")};for(var F in p)x(p,F)&&(u=F.toLowerCase(),e[u]=p[F](),s.push((e[u]?"":"no-")+u));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)x(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof enableClasses!="undefined"&&enableClasses&&(f.className+=" "+(b?"":"no-")+a),e[a]=b}return e},y(""),h=j=null,e._version=d,e._prefixes=l,e._domPrefixes=o,e._cssomPrefixes=n,e.testProp=function(a){return C([a])},e.testAllProps=E,e.testStyles=v,e}(this,this.document); -------------------------------------------------------------------------------- /src/node_modules/_assert.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | // **** 6 | // An assertion library that works the way *I* want it to. Get off my lawn! 7 | // **** 8 | 9 | // We use Proclaim rather than Chai because Chai doesn't support IE 8. 10 | // But Proclaim error messages are terrible, so we end up doing a lot ourselves. 11 | var proclaim = require("./vendor/proclaim-2.0.0.js"); 12 | var objectDiff = require("./vendor/big-object-diff-0.7.0.js"); 13 | 14 | var shim = { 15 | Function: { 16 | // WORKAROUND IE 8, IE 9, IE 10, IE 11: no function.name 17 | name: function name(fn) { 18 | if (fn.name) return fn.name; 19 | 20 | // Based on code by Jason Bunting et al, http://stackoverflow.com/a/332429 21 | var funcNameRegex = /function\s+(.{1,})\s*\(/; 22 | var results = (funcNameRegex).exec((fn).toString()); 23 | return (results && results.length > 1) ? results[1] : ""; 24 | } 25 | } 26 | }; 27 | 28 | exports.fail = function(message) { 29 | proclaim.fail(null, null, message); 30 | }; 31 | 32 | exports.defined = function(value, message) { 33 | message = message ? message + ": " : ""; 34 | proclaim.isDefined(value, message + "expected any value, but was undefined"); 35 | }; 36 | 37 | exports.undefined = function(value, message) { 38 | message = message ? message + ": " : ""; 39 | proclaim.isUndefined(value, message + "expected undefined, but was '" + value + "'"); 40 | }; 41 | 42 | exports.type = function(obj, expectedType, message) { 43 | message = message ? message + ": " : ""; 44 | proclaim.isInstanceOf( 45 | obj, 46 | expectedType, 47 | message + "expected object to be instance of " + 48 | shim.Function.name(expectedType) + ", but was " + describeObject(obj) 49 | ); 50 | }; 51 | 52 | exports.equal = function(actual, expected, message) { 53 | message = message ? message + ": " : ""; 54 | var expectedType = typeof expected; 55 | var actualType = typeof actual; 56 | 57 | if (actual !== undefined && expected !== undefined) { 58 | proclaim.strictEqual( 59 | actualType, expectedType, 60 | message + "expected " + expectedType + " '" + expected + "', but got " + actualType + " '" + actual + "'" 61 | ); 62 | } 63 | proclaim.strictEqual(actual, expected, message + "expected '" + expected + "', but got '" + actual + "'"); 64 | }; 65 | 66 | exports.notEqual = function(actual, expected, message) { 67 | message = message ? message + ": " : ""; 68 | 69 | proclaim.notEqual(actual, expected, message + "expected '" + expected + "' to be different from '" + actual + "'"); 70 | }; 71 | 72 | exports.lte = function(actual, expected, message) { 73 | message = message ? message + ": " : ""; 74 | 75 | proclaim.isTrue(actual <= expected, message + "expected <= '" + expected + "', but got '" + actual + "'"); 76 | }; 77 | 78 | exports.gte = function(actual, expected, message) { 79 | message = message ? message + ": " : ""; 80 | 81 | proclaim.isTrue(actual >= expected, message + "expected >= '" + expected + "', but got '" + actual + "'"); 82 | }; 83 | 84 | exports.objEqual = function(actual, expected, message) { 85 | message = message ? message + ": " : ""; 86 | proclaim.isDefined(actual, message + "expected object, but was undefined"); 87 | proclaim.isTrue(actual.equals(expected), message + "object equality expected '" + expected + "', but got '" + actual + "'"); 88 | }; 89 | 90 | exports.objNotEqual = function(actual, expected, message) { 91 | message = message ? message + ": " : ""; 92 | proclaim.isFalse(actual.equals(expected), message + "expected '" + expected + "' and '" + actual + "' to be not be equal(), but they were"); 93 | }; 94 | 95 | exports.deepEqual = function(actual, expected, message) { 96 | message = message ? message + ": " : ""; 97 | 98 | // We use objectDiff.match() instead of proclaim.deepEqual() because Proclaim doesn't do strict 99 | // equality checking in its deepEqual() assertion and objectDiff does. 100 | if (!objectDiff.match(actual, expected)) { 101 | var expectedString = JSON.stringify(expected); 102 | var actualString = JSON.stringify(actual); 103 | 104 | if (expectedString !== actualString) message += "expected " + expectedString + ", but got " + actualString; 105 | else message += "object prototype expected " + describeObject(expected) + ", but got " + describeObject(actual); 106 | 107 | proclaim.fail( 108 | actual, 109 | expected, 110 | message 111 | ); 112 | } 113 | }; 114 | 115 | exports.match = function(actual, expectedRegex, message) { 116 | message = message ? message + ": " : ""; 117 | proclaim.match(actual, expectedRegex, message + "expected string to match " + expectedRegex + ", but got '" + actual + "'"); 118 | }; 119 | 120 | exports.noException = function(fn, message) { 121 | try { 122 | fn(); 123 | } 124 | catch (e) { 125 | message = message ? message + ": " : ""; 126 | exports.fail(message + "expected no exception, but got '" + e + "'"); 127 | } 128 | }; 129 | 130 | exports.exception = function(fn, expected, message) { 131 | message = message ? message + ": " : ""; 132 | var noException = false; 133 | try { 134 | fn(); 135 | noException = true; 136 | } 137 | catch (e) { 138 | if (typeof expected === "string") { 139 | proclaim.strictEqual( 140 | e.message, 141 | expected, 142 | message + "expected exception message to be '" + expected + "', but was '" + e.message + "'" 143 | ); 144 | } 145 | else if (expected instanceof RegExp) proclaim.match( 146 | e.message, 147 | expected, 148 | message + "expected exception message to match " + expected + ", but was '" + e.message + "'" 149 | ); 150 | else if (typeof expected === "function") proclaim.isInstanceOf( 151 | e, 152 | expected, 153 | message + "expected exception to be of type " + shim.Function.name(expected) + ", but was " + describeObject(e) 154 | ); 155 | else if (expected !== undefined) throw new Error("Unrecognized 'expected' parameter in assertion: " + expected); 156 | } 157 | if (noException) exports.fail(message + "expected exception"); 158 | }; 159 | 160 | exports.throws = exports.exception; 161 | exports.doesNotThrow = exports.noException; 162 | 163 | function describeObject(obj) { 164 | var actualType = "unknown"; 165 | var prototype = Object.getPrototypeOf(obj); 166 | if (prototype === null) actualType = "object without a prototype"; 167 | else if (prototype.constructor) actualType = shim.Function.name(prototype.constructor); 168 | return actualType; 169 | } 170 | 171 | }()); -------------------------------------------------------------------------------- /src/node_modules/_fail_fast_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | (function() { 3 | "use strict"; 4 | 5 | var failFast = require("./fail_fast.js"); 6 | var FailFastException = failFast.FailFastException; 7 | var assert = require("_assert"); 8 | 9 | describe("SHARED: Fail Fast module", function() { 10 | 11 | it("uses custom exception", function() { 12 | try { 13 | throw new FailFastException("foo"); 14 | } 15 | catch (e) { 16 | assert.equal(e.name, "FailFastException"); 17 | assert.equal(e.constructor, FailFastException); 18 | assert.equal("" + e, "FailFastException"); 19 | } 20 | }); 21 | 22 | it("checks if variable is defined", function() { 23 | assert.doesNotThrow(unlessDefined("foo")); 24 | assert.doesNotThrow(unlessDefined(null)); 25 | assert.throws(unlessDefined(undefined), /^Required variable was not defined$/); 26 | assert.throws(unlessDefined(undefined, "myVariable"), /^Required variable \[myVariable\] was not defined$/); 27 | 28 | function unlessDefined(variable, variableName) { 29 | return function() { 30 | failFast.unlessDefined(variable, variableName); 31 | }; 32 | } 33 | }); 34 | 35 | it("checks if expression is true", function() { 36 | assert.doesNotThrow(unlessTrue(true)); 37 | assert.throws(unlessTrue(false), /^Expected condition to be true$/); 38 | assert.throws(unlessTrue(false, "a message"), /^a message$/); 39 | assert.throws(unlessTrue("foo"), /^Expected condition to be true or false$/); 40 | assert.throws(unlessTrue("foo", "ignoredMessage"), /^Expected condition to be true or false$/); 41 | 42 | function unlessTrue(variable, message) { 43 | return function() { 44 | failFast.unlessTrue(variable, message); 45 | }; 46 | } 47 | }); 48 | 49 | it("fails when statement is unreachable", function() { 50 | assert.throws(unreachable(), /^Unreachable code executed$/); 51 | assert.throws(unreachable("foo"), /^foo$/); 52 | 53 | function unreachable(message) { 54 | return function() { 55 | failFast.unreachable(message); 56 | }; 57 | } 58 | }); 59 | }); 60 | 61 | }()); -------------------------------------------------------------------------------- /src/node_modules/fail_fast.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2013 Titanium I.T. LLC. All rights reserved. See LICENSE.TXT for details. 2 | (function() { 3 | "use strict"; 4 | 5 | exports.unlessDefined = function(variable, variableName) { 6 | variableName = variableName ? " [" + variableName + "] " : " "; 7 | if (variable === undefined) throw new FailFastException(exports.unlessDefined, "Required variable" + variableName + "was not defined"); 8 | }; 9 | 10 | exports.unlessTrue = function(variable, message) { 11 | if (message === undefined) message = "Expected condition to be true"; 12 | 13 | if (variable === false) throw new FailFastException(exports.unlessTrue, message); 14 | if (variable !== true) throw new FailFastException(exports.unlessTrue, "Expected condition to be true or false"); 15 | }; 16 | 17 | exports.unreachable = function(message) { 18 | if (!message) message = "Unreachable code executed"; 19 | 20 | throw new FailFastException(exports.unreachable, message); 21 | }; 22 | 23 | var FailFastException = exports.FailFastException = function(fnToRemoveFromStackTrace, message) { 24 | if (Error.captureStackTrace) Error.captureStackTrace(this, fnToRemoveFromStackTrace); // only works on Chrome/V8 25 | this.message = message; 26 | }; 27 | FailFastException.prototype = new Error(); 28 | FailFastException.prototype.constructor = FailFastException; 29 | FailFastException.prototype.name = "FailFastException"; 30 | 31 | }()); -------------------------------------------------------------------------------- /src/node_modules/vendor/big-object-diff-0.7.0.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | "use strict"; 3 | 4 | var INDENT_TEXT = " "; 5 | 6 | exports.renderDiff = function(expected, actual) { 7 | return renderDiffWithIndent("", expected, actual); 8 | }; 9 | 10 | function renderDiffWithIndent(indent, expected, actual) { 11 | if (exports.match(expected, actual)) return ""; 12 | 13 | if (isArray(expected) && isArray(actual)) return arrayRenderDiff(indent, expected, actual); 14 | if (isObject(actual) && isObject(expected)) return objectRenderDiff(indent, expected, actual); 15 | else return flatRenderDiff(expected, actual); 16 | } 17 | 18 | function flatRenderDiff(expected, actual) { 19 | var renderedActual = flatRender(actual); 20 | var renderedExpected = flatRender(expected); 21 | 22 | if (isFunction(expected) && isFunction(actual) && renderedActual === renderedExpected) { 23 | renderedExpected = "different " + renderedExpected; 24 | } 25 | 26 | return renderedActual + " // expected " + renderedExpected; 27 | } 28 | 29 | function arrayRenderDiff(indent, expected, actual) { 30 | return "[" + renderPropertiesDiff(indent, expected, actual, true) + "\n" + indent + "]"; 31 | } 32 | 33 | function objectRenderDiff(indent, expected, actual) { 34 | return "{" + renderPropertiesDiff(indent, expected, actual, false) + "\n" + indent + "}"; 35 | } 36 | 37 | function renderPropertiesDiff(oldIndent, expected, actual, ignoreLengthProperty) { 38 | var indent = oldIndent + INDENT_TEXT; 39 | 40 | var unionKeys = []; 41 | var extraKeys = []; 42 | var missingKeys = []; 43 | 44 | analyzeKeys(); 45 | return incorrectProperties() + missingProperties() + extraProperties() + mismatchedPrototype(); 46 | 47 | function analyzeKeys() { 48 | var expectedKeys = Object.getOwnPropertyNames(expected); 49 | var actualKeys = Object.getOwnPropertyNames(actual); 50 | 51 | expectedKeys.forEach(function(key) { 52 | if (actual.hasOwnProperty(key)) unionKeys.push(key); 53 | else missingKeys.push(key); 54 | }); 55 | extraKeys = actualKeys.filter(function(key) { 56 | return (!expected.hasOwnProperty(key)); 57 | }); 58 | } 59 | 60 | function incorrectProperties() { 61 | return unionKeys.reduce(function(accumulated, key) { 62 | if (ignoreLengthProperty && key === "length") return accumulated; 63 | 64 | var diff = renderDiffWithIndent(indent, expected[key], actual[key]); 65 | if (!diff) return accumulated; 66 | 67 | return accumulated + "\n" + indent + key + ": " + diff; 68 | }, ""); 69 | } 70 | 71 | function missingProperties() { 72 | return propertyBlock(expected, missingKeys, "missing properties"); 73 | } 74 | 75 | function extraProperties() { 76 | return propertyBlock(actual, extraKeys, "extra properties"); 77 | } 78 | 79 | function propertyBlock(obj, keys, title) { 80 | if (keys.length === 0) return ""; 81 | return "\n" + indent + "// " + title + ":" + renderProperties(oldIndent, obj, keys, false, true); 82 | } 83 | 84 | function mismatchedPrototype() { 85 | var expectedProto = Object.getPrototypeOf(expected); 86 | var actualProto = Object.getPrototypeOf(actual); 87 | 88 | if (expectedProto !== actualProto) return "\n" + indent + "// objects have different prototypes"; 89 | else return ""; 90 | } 91 | 92 | } 93 | 94 | exports.render = function(obj) { 95 | return renderWithIndent("", obj, false); 96 | }; 97 | 98 | function renderWithIndent(indent, obj, collapseObjects) { 99 | if (collapseObjects) return flatRender(obj); 100 | else if (isArray(obj)) return arrayRender(indent, obj); 101 | else if (isObject(obj)) return objectRender(indent, obj); 102 | else return flatRender(obj); 103 | } 104 | 105 | function flatRender(obj) { 106 | if (obj === undefined) return "undefined"; 107 | if (obj === null) return "null"; 108 | if (typeof obj === "string") { 109 | var str = JSON.stringify(obj); 110 | if (str.length > 61) str = str.substr(0, 60) + '"...'; // >61, not >60, because of trailing quote 111 | return str; 112 | } 113 | if (isArray(obj)) { 114 | if (obj.length === 0) return "[]"; 115 | return "[...]"; 116 | } 117 | if (isObject(obj)) { 118 | if (Object.getOwnPropertyNames(obj).length === 0) return "{}"; 119 | else return "{...}"; 120 | } 121 | if (isFunction(obj)) { 122 | if (!obj.name) return "()"; 123 | else return obj.name + "()"; 124 | } 125 | 126 | return obj.toString(); 127 | } 128 | 129 | function arrayRender(indent, obj) { 130 | if (obj.length === 0) return "[]"; 131 | 132 | var properties = renderProperties(indent, obj, Object.getOwnPropertyNames(obj), true, false); 133 | return "[" + properties + "\n" + indent + "]"; 134 | } 135 | 136 | function objectRender(indent, obj) { 137 | if (Object.getOwnPropertyNames(obj).length === 0) return "{}"; 138 | 139 | var properties = renderProperties(indent, obj, Object.getOwnPropertyNames(obj), false, false); 140 | return "{" + properties + "\n" + indent + "}"; 141 | } 142 | 143 | function renderProperties(indent, obj, keys, ignoreLengthProperty, collapseObjects) { 144 | var newIndent = indent + INDENT_TEXT; 145 | var properties = keys.reduce(function(accumulated, key) { 146 | if (ignoreLengthProperty && key === "length") return accumulated; 147 | return accumulated + "\n" + newIndent + key + ": " + renderWithIndent(newIndent, obj[key], collapseObjects); 148 | }, ""); 149 | return properties; 150 | } 151 | 152 | exports.match = function(a, b) { 153 | if (typeof a === "object" && typeof b === "object") return objectAndArrayMatch(a, b); 154 | else return flatMatch(a, b); 155 | }; 156 | 157 | function flatMatch(a, b) { 158 | if (typeof a === "number" && isNaN(a)) return isNaN(b); 159 | 160 | return a === b; 161 | } 162 | 163 | function objectAndArrayMatch(a, b) { 164 | if (a === b) return true; 165 | if (a === null) return b === null; 166 | if (b === null) return a === null; 167 | 168 | if (!exports.match(Object.getPrototypeOf(a), Object.getPrototypeOf(b))) return false; 169 | 170 | var aKeys = Object.getOwnPropertyNames(a); 171 | var bKeys = Object.getOwnPropertyNames(b); 172 | if (aKeys.length !== bKeys.length) return false; 173 | 174 | return aKeys.every(function(key) { 175 | return exports.match(a[key], b[key]); 176 | }); 177 | } 178 | 179 | function isArray(obj) { 180 | return Array.isArray(obj); 181 | } 182 | 183 | function isObject(obj) { 184 | return typeof obj === "object" && obj !== null && !isArray(obj); 185 | } 186 | 187 | function isFunction(obj) { 188 | return typeof obj === "function"; 189 | } 190 | -------------------------------------------------------------------------------- /src/server/__socket_io_client.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const io = require("socket.io-client"); 6 | 7 | module.exports = class SocketIoClient { 8 | 9 | constructor(serverUrl, realTimeServer) { 10 | this._serverUrl = serverUrl; 11 | this._realTimeServer = realTimeServer; 12 | } 13 | 14 | createSockets(numSockets) { 15 | let sockets = []; 16 | for (let i = 0; i < numSockets; i++) { 17 | sockets.push(this.createSocket()); 18 | } 19 | return Promise.all(sockets); 20 | } 21 | 22 | async closeSockets(...sockets) { 23 | await Promise.all(sockets.map(async (socket) => { 24 | await this.closeSocket(socket); 25 | })); 26 | } 27 | 28 | createSocket() { 29 | const socket = this.createSocketWithoutWaiting(); 30 | return new Promise((resolve, reject) => { 31 | socket.on("connect", () => { 32 | waitForServerToConnect(socket.id, this._realTimeServer) 33 | .then(() => resolve(socket)) 34 | .catch(reject); 35 | }); 36 | }); 37 | } 38 | 39 | createSocketWithoutWaiting() { 40 | return io(this._serverUrl); 41 | } 42 | 43 | closeSocket(socket) { 44 | return new Promise((resolve, reject) => { 45 | const id = socket.id; 46 | socket.on("disconnect", () => { 47 | waitForServerToDisconnect(id, this._realTimeServer) 48 | .then(() => resolve(socket)) 49 | .catch(reject); 50 | }); 51 | socket.disconnect(); 52 | }); 53 | } 54 | }; 55 | 56 | function waitForServerToConnect(socketId, realTimeServer) { 57 | return waitForServerSocketState(true, socketId, realTimeServer); 58 | } 59 | 60 | function waitForServerToDisconnect(socketId, realTimeServer) { 61 | return waitForServerSocketState(false, socketId, realTimeServer); 62 | } 63 | 64 | async function waitForServerSocketState(expectedConnectionState, socketId, realTimeServer) { 65 | // We wait for sockets to be created or destroyed on the server because, when we don't, 66 | // we seem to trigger all kinds of Socket.IO nastiness. Sometimes Socket.IO won't close 67 | // a connection, and sometimes the tests just never exit. Waiting for the server seems 68 | // to prevent those problems, which apparently are caused by creating and destroying sockets 69 | // too quickly. 70 | 71 | const TIMEOUT = 1000; // milliseconds 72 | const RETRY_PERIOD = 10; // milliseconds 73 | 74 | const startTime = Date.now(); 75 | let success = !expectedConnectionState; 76 | 77 | while(success !== expectedConnectionState && !isTimeUp()) { 78 | await timeoutPromise(RETRY_PERIOD); 79 | success = realTimeServer.isClientConnected(socketId); 80 | } 81 | if (isTimeUp()) throw new Error("socket " + socketId + " failed to connect to server"); 82 | 83 | function isTimeUp() { 84 | return (startTime + TIMEOUT) < Date.now(); 85 | } 86 | 87 | function timeoutPromise(milliseconds) { 88 | return new Promise((resolve) => { 89 | setTimeout(resolve, milliseconds); 90 | }); 91 | } 92 | } 93 | 94 | }()); -------------------------------------------------------------------------------- /src/server/_clock_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const assert = require("_assert"); 6 | const Clock = require("./clock.js"); 7 | 8 | describe("Clock", function() { 9 | 10 | let realClock; 11 | let fakeClock; 12 | 13 | beforeEach(function() { 14 | realClock = new Clock(); 15 | fakeClock = Clock.createFake(); 16 | }); 17 | 18 | it("attaches to real system clock by default", function(done) { 19 | const startTime = realClock.now(); 20 | setTimeout(() => { 21 | try { 22 | const elapsedTime = realClock.now() - startTime; 23 | assert.gte(elapsedTime, 10); 24 | done(); 25 | } 26 | catch (err) { done(err); } 27 | }, 10); 28 | }); 29 | 30 | it("can use fake clock instead of real system clock", function() { 31 | assert.equal(fakeClock.now(), 424242); 32 | }); 33 | 34 | it("ticks the fake clock", function() { 35 | const startTime = fakeClock.now(); 36 | fakeClock.tick(10000); 37 | assert.equal(fakeClock.now(), startTime + 10000); 38 | }); 39 | 40 | it("fails fast when attempting to tick system clock", function() { 41 | assert.exception(() => realClock.tick()); 42 | }); 43 | 44 | it("tells us how many milliseconds have elapsed", function() { 45 | const startTime = fakeClock.now(); 46 | fakeClock.tick(999); 47 | assert.equal(fakeClock.millisecondsSince(startTime), 999); 48 | }); 49 | 50 | it("fake clock runs a function every n milliseconds", function(done) { 51 | const interval = fakeClock.setInterval(done, 10000); 52 | fakeClock.tick(10000); 53 | interval.clear(); 54 | fakeClock.tick(10000); // if clear() didn't work, done() will called twice and the test will fail 55 | }); 56 | 57 | it("real clock runs a function every >=n milliseconds", function(done) { 58 | let intervalCalled = false; 59 | const startTime = realClock.now(); 60 | const interval = realClock.setInterval(() => { 61 | try { 62 | // If there's a GC cycle or other delay, this function may get called twice; prevent it 63 | if (!intervalCalled) { 64 | intervalCalled = true; 65 | assert.gte(realClock.millisecondsSince(startTime), 10); 66 | interval.clear(); 67 | done(); 68 | } 69 | } 70 | catch(err) { done(err); } 71 | }, 10); 72 | }); 73 | 74 | }); 75 | 76 | }()); -------------------------------------------------------------------------------- /src/server/_http_server_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const HttpServer = require("./http_server.js"); 6 | const assert = require("_assert"); 7 | const http = require("http"); 8 | const fs = require("fs"); 9 | const util = require("util"); 10 | 11 | const writeFile = util.promisify(fs.writeFile); 12 | const unlink = util.promisify(fs.unlink); 13 | 14 | const CONTENT_DIR = "generated/test"; 15 | 16 | const INDEX_PAGE = "index.html"; 17 | const OTHER_PAGE = "other.html"; 18 | const NOT_FOUND_PAGE = "test404.html"; 19 | 20 | const INDEX_PAGE_DATA = "This is index page file"; 21 | const OTHER_PAGE_DATA = "This is another page"; 22 | const NOT_FOUND_DATA = "This is 404 page file"; 23 | 24 | const PORT = 5020; 25 | const BASE_URL = "http://localhost:" + PORT; 26 | 27 | const TEST_FILES = [ 28 | [ CONTENT_DIR + "/" + INDEX_PAGE, INDEX_PAGE_DATA], 29 | [ CONTENT_DIR + "/" + OTHER_PAGE, OTHER_PAGE_DATA], 30 | [ CONTENT_DIR + "/" + NOT_FOUND_PAGE, NOT_FOUND_DATA] 31 | ]; 32 | 33 | describe("HTTP Server", function() { 34 | 35 | let server = new HttpServer(CONTENT_DIR, NOT_FOUND_PAGE); 36 | 37 | beforeEach(async () => { 38 | await Promise.all([ 39 | createTestFiles(), 40 | server.start(PORT) 41 | ]); 42 | }); 43 | 44 | afterEach(async () => { 45 | await Promise.all([ 46 | deleteTestFiles(), 47 | server.stop() 48 | ]); 49 | }); 50 | 51 | it("serves files from directory", async () => { 52 | let [ response, responseData ] = await httpGet(BASE_URL + "/" + INDEX_PAGE); 53 | assert.equal(response.statusCode, 200, "status code"); 54 | assert.equal(responseData, INDEX_PAGE_DATA, "response text"); 55 | }); 56 | 57 | it("sets content-type and charset for HTML files", async () => { 58 | let [ response ] = await httpGet(BASE_URL + "/" + INDEX_PAGE); 59 | assert.equal(response.headers["content-type"], "text/html; charset=UTF-8", "content-type header"); 60 | }); 61 | 62 | it("supports multiple files", async () => { 63 | let [ response, responseData ] = await httpGet(BASE_URL + "/" + OTHER_PAGE); 64 | assert.equal(response.statusCode, 200, "status code"); 65 | assert.equal(responseData, OTHER_PAGE_DATA, "response text"); 66 | }); 67 | 68 | it("serves index.html when asked for home page", async () => { 69 | let [ response, responseData ] = await httpGet(BASE_URL); 70 | assert.equal(response.statusCode, 200, "status code"); 71 | assert.equal(responseData, INDEX_PAGE_DATA, "response text"); 72 | }); 73 | 74 | it("returns 404 when file doesn't exist", async () => { 75 | let [ response, responseData ] = await httpGet(BASE_URL + "/bargle"); 76 | assert.equal(response.statusCode, 404, "status code"); 77 | assert.equal(responseData, NOT_FOUND_DATA, "404 text"); 78 | }); 79 | 80 | it("sets content-type and charset for 404 page", async () => { 81 | let [ response ] = await httpGet(BASE_URL + "/bargle"); 82 | assert.equal(response.headers["content-type"], "text/html; charset=UTF-8", "content-type header"); 83 | }); 84 | 85 | function httpGet(url) { 86 | return new Promise((resolve, reject) => { 87 | http.get(url, function(response) { 88 | let receivedData = ""; 89 | response.setEncoding("utf8"); 90 | 91 | response.on("data", function(chunk) { 92 | receivedData += chunk; 93 | }); 94 | response.on("error", reject); 95 | response.on("end", function() { 96 | resolve([ response, receivedData ]); 97 | }); 98 | }); 99 | }); 100 | } 101 | 102 | }); 103 | 104 | function createTestFiles() { 105 | return Promise.all(TEST_FILES.map(([ file, data ]) => { 106 | return writeFile(file, data); 107 | })); 108 | } 109 | 110 | function deleteTestFiles() { 111 | return Promise.all(TEST_FILES.map(([ file ]) => { 112 | return unlink(file); 113 | })); 114 | } 115 | 116 | }()); -------------------------------------------------------------------------------- /src/server/_message_repository_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const assert = require("_assert"); 6 | const MessageRepository = require("./message_repository.js"); 7 | const ServerClearScreenMessage = require("../shared/server_clear_screen_message.js"); 8 | 9 | describe("Message Repository", function() { 10 | 11 | let repo; 12 | 13 | beforeEach(function() { 14 | repo = new MessageRepository(); 15 | }); 16 | 17 | it("replays no messages when there aren't any", function() { 18 | assert.deepEqual(repo.replay(), []); 19 | }); 20 | 21 | it("stores and replays multiple messages", function() { 22 | repo.store(new ServerClearScreenMessage()); 23 | repo.store(new ServerClearScreenMessage()); 24 | assert.deepEqual(repo.replay(), [ 25 | new ServerClearScreenMessage(), 26 | new ServerClearScreenMessage() 27 | ]); 28 | }); 29 | 30 | it("isolates its data from changes to returned results", function() { 31 | const messages = repo.replay(); 32 | messages.push("change to our copy of repo's messages"); 33 | assert.deepEqual(repo.replay(), [], "repo's messages shouldn't change"); 34 | }); 35 | 36 | }); 37 | 38 | }()); 39 | -------------------------------------------------------------------------------- /src/server/_server_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | (function() { 3 | "use strict"; 4 | 5 | const Server = require("./server.js"); 6 | const fs = require("fs"); 7 | const http = require("http"); 8 | const assert = require("_assert"); 9 | const ClientPointerMessage = require("../shared/client_pointer_message.js"); 10 | const ServerPointerMessage = require("../shared/server_pointer_message.js"); 11 | const SocketIoClient = require("./__socket_io_client.js"); 12 | 13 | const CONTENT_DIR = "generated/test/"; 14 | const NOT_FOUND_PAGE = "test404.html"; 15 | const PORT = 5020; 16 | 17 | const INDEX_PAGE = "index.html"; 18 | const INDEX_PAGE_CONTENTS = "This is the index page."; 19 | 20 | describe("Server", function() { 21 | 22 | let socketIoClient; 23 | let server; 24 | 25 | beforeEach(function(done) { 26 | fs.writeFile(CONTENT_DIR + INDEX_PAGE, INDEX_PAGE_CONTENTS, done); 27 | }); 28 | 29 | afterEach(function(done) { 30 | fs.unlink(CONTENT_DIR + INDEX_PAGE, done); 31 | }); 32 | 33 | beforeEach(async function() { 34 | server = new Server(); 35 | await server.start(CONTENT_DIR, NOT_FOUND_PAGE, PORT); 36 | socketIoClient = new SocketIoClient("http://localhost:" + PORT, server._realTimeLogic._realTimeServer); 37 | }); 38 | 39 | afterEach(async function() { 40 | try { 41 | const realTimeServer = server._realTimeServer; 42 | assert.equal(realTimeServer.numberOfActiveConnections(), 0, "afterEach() requires all sockets to be closed"); 43 | } 44 | finally { 45 | await server.stop(); 46 | } 47 | }); 48 | 49 | it("serves HTML", function(done) { 50 | http.get("http://localhost:" + PORT, function(response) { 51 | let receivedData = ""; 52 | response.setEncoding("utf8"); 53 | 54 | response.on("data", function(chunk) { 55 | receivedData += chunk; 56 | }); 57 | response.on("error", function(err) { 58 | assert.fail(err); 59 | }); 60 | response.on("end", function() { 61 | assert.equal(receivedData, INDEX_PAGE_CONTENTS); 62 | done(); 63 | }); 64 | }); 65 | }); 66 | 67 | it("services real-time events", async function() { 68 | // Need to create our sockets serially because the tests won't exit if we don't. 69 | // I believe it's a bug in Socket.IO but I haven't been able to reproduce with a 70 | // trimmed-down test case. If you want to try converting this back to a parallel 71 | // implementation, be sure to run the tests about ten times because the issue doesn't 72 | // always occur. -JDLS 4 Aug 2017 73 | 74 | const emitter = await socketIoClient.createSocket(); 75 | const receiver = await socketIoClient.createSocket(); 76 | const clientMessage = new ClientPointerMessage(100, 200); 77 | 78 | emitter.emit(clientMessage.name(), clientMessage.payload()); 79 | 80 | const actualPayload = await new Promise((resolve) => { 81 | receiver.once(ServerPointerMessage.MESSAGE_NAME, resolve); 82 | }); 83 | 84 | assert.deepEqual(actualPayload, clientMessage.toServerMessage(emitter.id).payload()); 85 | await new Promise((resolve) => setTimeout(resolve, 0)); 86 | await socketIoClient.closeSocket(emitter); 87 | await socketIoClient.closeSocket(receiver); 88 | }); 89 | 90 | }); 91 | 92 | }()); -------------------------------------------------------------------------------- /src/server/clock.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const lolex = require("lolex"); 6 | 7 | const FAKE_START_TIME = 424242; 8 | 9 | class RealClock { 10 | get Date() { return Date; } 11 | tick() { throw new Error("Attempted to tick() system clock. Should be a fake clock instead."); } 12 | setInterval(fn, milliseconds) { return setInterval(fn, milliseconds); } 13 | clearInterval(id) { clearInterval(id); } 14 | } 15 | 16 | module.exports = class Clock { 17 | 18 | constructor() { 19 | this._clock = new RealClock(); 20 | } 21 | 22 | static createFake() { 23 | var clock = new Clock(true); 24 | clock._clock = lolex.createClock(FAKE_START_TIME); 25 | return clock; 26 | } 27 | 28 | now() { 29 | return this._clock.Date.now(); 30 | } 31 | 32 | tick(milliseconds) { 33 | this._clock.tick(milliseconds); 34 | } 35 | 36 | millisecondsSince(startTimeInMilliseconds) { 37 | return this.now() - startTimeInMilliseconds; 38 | } 39 | 40 | setInterval(fn, intervalInMilliseconds) { 41 | var handle = this._clock.setInterval(fn, intervalInMilliseconds); 42 | return { 43 | clear: function() { 44 | this._clock.clearInterval(handle); 45 | }.bind(this) 46 | }; 47 | } 48 | 49 | }; 50 | 51 | }()); -------------------------------------------------------------------------------- /src/server/http_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const http = require("http"); 6 | const fs = require("fs"); 7 | const send = require("send"); 8 | const util = require("util"); 9 | 10 | module.exports = class HttpServer { 11 | 12 | constructor(contentDir, notFoundPageToServe) { 13 | this._httpServer = http.createServer(); 14 | 15 | handleHttpRequests(this._httpServer, contentDir, notFoundPageToServe); 16 | } 17 | 18 | start(portNumber) { 19 | const listen = util.promisify(this._httpServer.listen.bind(this._httpServer)); 20 | return listen(portNumber); 21 | } 22 | 23 | stop() { 24 | const close = util.promisify(this._httpServer.close.bind(this._httpServer)); 25 | return close(); 26 | } 27 | 28 | getNodeServer() { 29 | return this._httpServer; 30 | } 31 | 32 | }; 33 | 34 | function handleHttpRequests(httpServer, contentDir, notFoundPageToServe) { 35 | httpServer.on("request", function(request, response) { 36 | send(request, request.url, { root: contentDir }).on("error", handleError).pipe(response); 37 | 38 | function handleError(err) { 39 | if (err.status === 404) serveErrorFile(response, 404, contentDir + "/" + notFoundPageToServe); 40 | else throw err; 41 | } 42 | }); 43 | } 44 | 45 | function serveErrorFile(response, statusCode, file) { 46 | response.statusCode = statusCode; 47 | response.setHeader("Content-Type", "text/html; charset=UTF-8"); 48 | fs.readFile(file, function(err, data) { 49 | if (err) throw err; 50 | response.end(data); 51 | }); 52 | } 53 | 54 | }()); -------------------------------------------------------------------------------- /src/server/message_repository.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | module.exports = class MessageRepository { 6 | constructor() { 7 | this._data = []; 8 | } 9 | 10 | store(message) { 11 | this._data.push(message); 12 | } 13 | 14 | replay() { 15 | return this._data.slice(); 16 | } 17 | }; 18 | 19 | }()); -------------------------------------------------------------------------------- /src/server/real_time_logic.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const ServerRemovePointerMessage = require("../shared/server_remove_pointer_message.js"); 6 | const ServerPointerMessage = require("../shared/server_pointer_message.js"); 7 | const ClientDrawMessage = require("../shared/client_draw_message.js"); 8 | const MessageRepository = require("./message_repository.js"); 9 | const Clock = require("./clock.js"); 10 | const RealTimeServer = require("./real_time_server.js"); 11 | 12 | // Consider Jay Bazuzi's suggestions from E494 comments (direct connection from client to server when testing) 13 | // http://disq.us/p/1gobws6 http://www.letscodejavascript.com/v3/comments/live/494 14 | 15 | const CLIENT_TIMEOUT = 3 * 1000; 16 | 17 | const RealTimeLogic = module.exports = class RealTimeLogic { 18 | 19 | constructor(realTimeServer, clock = new Clock()) { 20 | this._realTimeServer = realTimeServer; 21 | this._clock = clock; 22 | this._messageRepo = new MessageRepository(); 23 | } 24 | 25 | start() { 26 | handleClientTimeouts(this); 27 | handleRealTimeEvents(this); 28 | } 29 | 30 | stop() { 31 | stopHandlingClientTimeouts(this); 32 | } 33 | 34 | }; 35 | 36 | RealTimeLogic.CLIENT_TIMEOUT = CLIENT_TIMEOUT; 37 | 38 | function handleRealTimeEvents(self) { 39 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_CONNECT, replayPreviousMessages); 40 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_MESSAGE, processClientMessage); 41 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_DISCONNECT, removeClientPointer); 42 | 43 | function replayPreviousMessages(clientId) { 44 | self._messageRepo.replay().forEach((message) => { 45 | self._realTimeServer.sendToOneClient(clientId, message); 46 | }); 47 | } 48 | 49 | function processClientMessage({ clientId, message }) { 50 | broadcastAndStoreMessage(self, clientId, message.toServerMessage(clientId)); 51 | } 52 | 53 | function removeClientPointer(clientId) { 54 | broadcastAndStoreMessage(self, null, new ServerRemovePointerMessage(clientId)); 55 | } 56 | } 57 | 58 | function handleClientTimeouts(self) { 59 | self._lastActivity = {}; 60 | 61 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_CONNECT, startTrackingClient); 62 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_MESSAGE, updateClient); 63 | self._realTimeServer.on(RealTimeServer.EVENT.CLIENT_DISCONNECT, stopTrackingClient); 64 | self._interval = self._clock.setInterval(timeOutInactiveClients, 100); 65 | 66 | function startTrackingClient(clientId) { 67 | resetClientTimeout(clientId); 68 | } 69 | 70 | function updateClient({ clientId, message }) { 71 | if (!isTrackingClient(clientId)) { 72 | startTrackingClient(clientId); 73 | if (message.name() === ClientDrawMessage.MESSAGE_NAME) { 74 | const pointerLocation = message.getPointerLocation(); 75 | const pointerMessage = new ServerPointerMessage(clientId, pointerLocation.x, pointerLocation.y); 76 | broadcastAndStoreMessage(self, clientId, pointerMessage); 77 | } 78 | } 79 | else { 80 | resetClientTimeout(clientId); 81 | } 82 | } 83 | 84 | function stopTrackingClient(clientId) { 85 | delete self._lastActivity[clientId]; 86 | } 87 | 88 | function timeOutInactiveClients() { 89 | Object.keys(self._lastActivity).forEach((clientId) => { 90 | const lastActivity = self._lastActivity[clientId]; 91 | if (self._clock.millisecondsSince(lastActivity) >= CLIENT_TIMEOUT) { 92 | broadcastAndStoreMessage(self, null, new ServerRemovePointerMessage(clientId)); 93 | stopTrackingClient(clientId); 94 | } 95 | }); 96 | } 97 | 98 | function isTrackingClient(clientId) { 99 | return self._lastActivity[clientId] !== undefined; 100 | } 101 | 102 | function resetClientTimeout(clientId) { 103 | self._lastActivity[clientId] = self._clock.now(); 104 | } 105 | } 106 | 107 | function stopHandlingClientTimeouts(self) { 108 | self._interval.clear(); 109 | } 110 | 111 | function broadcastAndStoreMessage(self, clientIdOrNull, message) { 112 | self._messageRepo.store(message); 113 | if (clientIdOrNull) self._realTimeServer.broadcastToAllClientsButOne(clientIdOrNull, message); 114 | else self._realTimeServer.broadcastToAllClients(message); 115 | } 116 | 117 | }()); -------------------------------------------------------------------------------- /src/server/real_time_server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | const io = require("socket.io"); 6 | const failFast = require("fail_fast.js"); 7 | const util = require("util"); 8 | const EventEmitter = require("events"); 9 | const ClientPointerMessage = require("../shared/client_pointer_message.js"); 10 | const ClientRemovePointerMessage = require("../shared/client_remove_pointer_message.js"); 11 | const ClientDrawMessage = require("../shared/client_draw_message.js"); 12 | const ClientClearScreenMessage = require("../shared/client_clear_screen_message.js"); 13 | 14 | const SUPPORTED_MESSAGES = [ 15 | ClientPointerMessage, 16 | ClientRemovePointerMessage, 17 | ClientDrawMessage, 18 | ClientClearScreenMessage 19 | ]; 20 | 21 | const RealTimeServer = module.exports = class RealTimeServer extends EventEmitter { 22 | 23 | constructor(httpServer) { 24 | super(); 25 | 26 | failFast.unlessDefined(httpServer, "httpServer"); 27 | this._nodeHttpServer = httpServer.getNodeServer(); 28 | 29 | this._socketIoConnections = {}; 30 | this._io = io; 31 | } 32 | 33 | static createNull() { 34 | const server = new RealTimeServer(new NullHttpServer()); 35 | server._io = nullIo; 36 | return server; 37 | } 38 | 39 | start() { 40 | this._ioServer = this._io(this._nodeHttpServer); 41 | this._nodeHttpServer.on("close", failFastIfHttpServerClosed); 42 | 43 | trackSocketIoConnections(this, this._socketIoConnections, this._ioServer); 44 | listenForClientMessages(this, this._ioServer); 45 | } 46 | 47 | stop() { 48 | const close = util.promisify(this._ioServer.close.bind(this._ioServer)); 49 | 50 | this._nodeHttpServer.removeListener("close", failFastIfHttpServerClosed); 51 | return close(); 52 | } 53 | 54 | sendToOneClient(clientId, message) { 55 | const socket = lookUpSocket(this, clientId); 56 | socket.emit(message.name(), message.payload()); 57 | recordServerMessage(this, { 58 | message, 59 | clientId, 60 | type: RealTimeServer.SEND_TYPE.ONE_CLIENT 61 | }); 62 | } 63 | 64 | broadcastToAllClients(message) { 65 | this._ioServer.emit(message.name(), message.payload()); 66 | recordServerMessage(this, { 67 | message, 68 | type: RealTimeServer.SEND_TYPE.ALL_CLIENTS 69 | }); 70 | } 71 | 72 | broadcastToAllClientsButOne(clientToExclude, message) { 73 | const socket = lookUpSocket(this, clientToExclude); 74 | socket.broadcast.emit(message.name(), message.payload()); 75 | recordServerMessage(this, { 76 | message, 77 | clientId: clientToExclude, 78 | type: RealTimeServer.SEND_TYPE.ALL_CLIENTS_BUT_ONE 79 | }); 80 | } 81 | 82 | getLastSentMessage() { 83 | return this._lastSentMessage; 84 | } 85 | 86 | isClientConnected(clientId) { 87 | return this._socketIoConnections[clientId] !== undefined; 88 | } 89 | 90 | numberOfActiveConnections() { 91 | return Object.keys(this._socketIoConnections).length; 92 | } 93 | 94 | simulateClientMessage(clientId, message) { 95 | handleClientMessage(this, clientId, message); 96 | } 97 | 98 | connectNullClient(clientId) { 99 | connectClient(this, new NullSocket(clientId)); 100 | } 101 | 102 | disconnectNullClient(clientId) { 103 | const socket = lookUpSocket(this, clientId); 104 | failFast.unlessTrue(socket.isNull === true, `Attempted to disconnect non-null client: [${clientId}]`); 105 | disconnectClient(this, socket); 106 | } 107 | }; 108 | 109 | RealTimeServer.EVENT = { 110 | CLIENT_DISCONNECT: "clientDisconnect", 111 | CLIENT_CONNECT: "clientConnect", 112 | CLIENT_MESSAGE: "clientMessage", 113 | SERVER_MESSAGE: "serverMessage" 114 | }; 115 | 116 | RealTimeServer.SEND_TYPE = { 117 | ONE_CLIENT: "one_client", 118 | ALL_CLIENTS: "all_clients", 119 | ALL_CLIENTS_BUT_ONE: "all_clients_but_one" 120 | }; 121 | 122 | function recordServerMessage(self, messageInfo) { 123 | self._lastSentMessage = messageInfo; 124 | self.emit(RealTimeServer.EVENT.SERVER_MESSAGE, messageInfo); 125 | } 126 | 127 | function trackSocketIoConnections(self, connections, ioServer) { 128 | // Inspired by isaacs 129 | // https://github.com/isaacs/server-destroy/commit/71f1a988e1b05c395e879b18b850713d1774fa92 130 | ioServer.on("connection", function(socket) { 131 | connectClient(self, socket); 132 | socket.on("disconnect", function() { 133 | disconnectClient(self, socket); 134 | }); 135 | }); 136 | } 137 | 138 | function listenForClientMessages(self, ioServer) { 139 | ioServer.on("connect", (socket) => { 140 | SUPPORTED_MESSAGES.forEach(function(messageConstructor) { 141 | socket.on(messageConstructor.MESSAGE_NAME, function(payload) { 142 | handleClientMessage(self, socket.id, messageConstructor.fromPayload(payload)); 143 | }); 144 | }); 145 | }); 146 | } 147 | 148 | function handleClientMessage(self, clientId, message) { 149 | self.emit(RealTimeServer.EVENT.CLIENT_MESSAGE, { clientId, message }); 150 | } 151 | 152 | function connectClient(self, socket) { 153 | const key = socket.id; 154 | failFast.unlessDefined(key, "socket.id"); 155 | 156 | self._socketIoConnections[key] = socket; 157 | self.emit(RealTimeServer.EVENT.CLIENT_CONNECT, key); 158 | } 159 | 160 | function disconnectClient(self, socket) { 161 | const key = socket.id; 162 | failFast.unlessDefined(key, "socket.id"); 163 | 164 | delete self._socketIoConnections[key]; 165 | self.emit(RealTimeServer.EVENT.CLIENT_DISCONNECT, key); 166 | } 167 | 168 | function lookUpSocket(self, clientId) { 169 | const socket = self._socketIoConnections[clientId]; 170 | failFast.unlessTrue(socket !== undefined, `attempted to look up socket that isn't connected: [${clientId}]`); 171 | return socket; 172 | } 173 | 174 | function failFastIfHttpServerClosed() { 175 | throw new Error( 176 | "Do not call httpServer.stop() when using RealTimeServer--it will trigger this bug: " + 177 | "https://github.com/socketio/socket.io/issues/2975" 178 | ); 179 | } 180 | 181 | 182 | class NullHttpServer { 183 | getNodeServer() { 184 | return { 185 | on: noOp, 186 | removeListener: noOp 187 | }; 188 | } 189 | } 190 | 191 | class NullIoServer { 192 | on() {} 193 | emit() {} 194 | close(done) { return done(); } 195 | } 196 | 197 | class NullSocket { 198 | constructor(id) { this.id = id; } 199 | get isNull() { return true; } 200 | emit() {} 201 | get broadcast() { return { emit: noOp }; } 202 | } 203 | 204 | function nullIo() { 205 | return new NullIoServer(); 206 | } 207 | 208 | function noOp() {} 209 | 210 | }()); -------------------------------------------------------------------------------- /src/server/run.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2017 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | (async function() { 3 | "use strict"; 4 | 5 | const Server = require("./server.js"); 6 | 7 | var CONTENT_DIR = "./generated/dist/client"; 8 | 9 | const port = process.argv[2]; 10 | const server = new Server(); 11 | 12 | await server.start(CONTENT_DIR, "404.html", port); 13 | console.log("Server started"); 14 | 15 | }()); -------------------------------------------------------------------------------- /src/server/server.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2017 Titanium I.T. LLC. All rights reserved. See LICENSE.txt for details. 2 | (function() { 3 | "use strict"; 4 | 5 | const HttpServer = require("./http_server.js"); 6 | const RealTimeServer = require("./real_time_server.js"); 7 | const RealTimeLogic = require("./real_time_logic.js"); 8 | 9 | module.exports = class Server { 10 | 11 | async start(contentDir, notFoundPageToServe, portNumber) { 12 | if (!portNumber) throw new Error("port number is required"); 13 | 14 | this._httpServer = new HttpServer(contentDir, notFoundPageToServe); 15 | await this._httpServer.start(portNumber); 16 | 17 | // Consider Martin Grandrath's suggestions from E509 comments (different server initialization) 18 | // http://disq.us/p/1i1xydn http://www.letscodejavascript.com/v3/comments/live/509 19 | 20 | this._realTimeServer = new RealTimeServer(this._httpServer); 21 | this._realTimeServer.start(); 22 | 23 | this._realTimeLogic = new RealTimeLogic(this._realTimeServer); 24 | this._realTimeLogic.start(); 25 | } 26 | 27 | async stop() { 28 | if (this._realTimeLogic === undefined) throw new Error("stop() called before server started"); 29 | 30 | this._realTimeLogic.stop(); 31 | await this._realTimeServer.stop(); 32 | } 33 | 34 | }; 35 | 36 | }()); -------------------------------------------------------------------------------- /src/shared/_client_clear_screen_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ClientClearScreenMessage = require("./client_clear_screen_message.js"); 7 | var ServerClearScreenMessage = require("./server_clear_screen_message.js"); 8 | 9 | describe("SHARED: ClientClearScreenMessage", function() { 10 | 11 | it("converts serializable objects to ClientClearScreenMessages and back", function() { 12 | var bareObject = {}; 13 | var messageObject = new ClientClearScreenMessage(); 14 | 15 | assert.deepEqual(ClientClearScreenMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 16 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 17 | }); 18 | 19 | it("translates to ServerClearScreenMessage", function() { 20 | var expected = new ServerClearScreenMessage(); 21 | var actual = new ClientClearScreenMessage().toServerMessage(); 22 | 23 | assert.deepEqual(actual, expected); 24 | }); 25 | 26 | it("instances know their network message name", function() { 27 | assert.equal(new ClientClearScreenMessage().name(), ClientClearScreenMessage.MESSAGE_NAME); 28 | }); 29 | 30 | }); 31 | 32 | }()); -------------------------------------------------------------------------------- /src/shared/_client_draw_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ClientDrawMessage = require("./client_draw_message.js"); 7 | var ServerDrawMessage = require("./server_draw_message.js"); 8 | 9 | describe("SHARED: ClientDrawMessage", function() { 10 | 11 | it("converts bare objects to ClientDrawMessages and back", function() { 12 | var bareObject = { fromX: 1, fromY: 2, toX: 3, toY: 4 }; 13 | var messageObject = new ClientDrawMessage(1, 2, 3, 4); 14 | 15 | assert.deepEqual(ClientDrawMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 16 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 17 | }); 18 | 19 | it("translates to ServerDrawMessage", function() { 20 | var expected = new ServerDrawMessage(1, 2, 3, 4); 21 | var actual = new ClientDrawMessage(1, 2, 3, 4).toServerMessage(); 22 | 23 | assert.deepEqual(actual, expected); 24 | }); 25 | 26 | it("instances know their network message name", function() { 27 | assert.equal(new ClientDrawMessage(1, 2, 3, 4).name(), ClientDrawMessage.MESSAGE_NAME); 28 | }); 29 | 30 | it("knows where the pointer was", function() { 31 | assert.deepEqual(new ClientDrawMessage(1, 2, 3, 4).getPointerLocation(), { x: 3, y: 4 }, "line"); 32 | assert.deepEqual(new ClientDrawMessage(10, 10, 10, 10).getPointerLocation(), { x: 10, y: 10 }, "dot"); 33 | }); 34 | 35 | }); 36 | 37 | }()); -------------------------------------------------------------------------------- /src/shared/_client_pointer_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ClientPointerMessage = require("./client_pointer_message.js"); 7 | var ServerPointerMessage = require("./server_pointer_message.js"); 8 | 9 | describe("SHARED: ClientPointerMessage", function() { 10 | 11 | it("converts bare objects to ClientPointerMessages and back", function() { 12 | var bareObject = { x: 1, y: 2 }; 13 | var messageObject = new ClientPointerMessage(1, 2); 14 | 15 | assert.deepEqual(ClientPointerMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 16 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 17 | }); 18 | 19 | it("translates to ServerPointerMessage", function() { 20 | var expected = new ServerPointerMessage("a", 1, 2); 21 | var actual = new ClientPointerMessage(1, 2).toServerMessage("a"); 22 | 23 | assert.deepEqual(actual, expected); 24 | }); 25 | 26 | it("instances know their network message name", function() { 27 | assert.equal(new ClientPointerMessage(1, 2).name(), ClientPointerMessage.MESSAGE_NAME); 28 | }); 29 | 30 | }); 31 | 32 | }()); -------------------------------------------------------------------------------- /src/shared/_client_remove_pointer_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ClientRemovePointerMessage = require("./client_remove_pointer_message.js"); 7 | var ServerRemovePointerMessage = require("./server_remove_pointer_message.js"); 8 | 9 | describe("SHARED: ClientRemovePointerMessage", function() { 10 | 11 | it("converts serializable objects to ClientRemovePointerMessages and back", function() { 12 | var bareObject = {}; 13 | var messageObject = new ClientRemovePointerMessage(); 14 | 15 | assert.deepEqual(ClientRemovePointerMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 16 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 17 | }); 18 | 19 | it("translates to ServerRemovePointerMessage", function() { 20 | var expected = new ServerRemovePointerMessage("a"); 21 | var actual = new ClientRemovePointerMessage().toServerMessage("a"); 22 | 23 | assert.deepEqual(actual, expected); 24 | }); 25 | 26 | it("instances know their network message name", function() { 27 | assert.equal(new ClientRemovePointerMessage().name(), ClientRemovePointerMessage.MESSAGE_NAME); 28 | }); 29 | 30 | }); 31 | 32 | }()); -------------------------------------------------------------------------------- /src/shared/_server_clear_screen_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ServerClearScreenMessage = require("./server_clear_screen_message.js"); 7 | 8 | describe("SHARED: ServerClearScreenMessage", function() { 9 | 10 | it("converts bare objects to message objects and back", function() { 11 | var bareObject = {}; 12 | var messageObject = new ServerClearScreenMessage(); 13 | 14 | assert.deepEqual(ServerClearScreenMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 15 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 16 | }); 17 | 18 | it("instances know their network message name", function() { 19 | assert.equal(new ServerClearScreenMessage().name(), ServerClearScreenMessage.MESSAGE_NAME); 20 | }); 21 | 22 | }); 23 | 24 | }()); -------------------------------------------------------------------------------- /src/shared/_server_draw_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ServerDrawMessage = require("./server_draw_message.js"); 7 | 8 | describe("SHARED: ServerDrawMessage", function() { 9 | 10 | it("converts bare objects to ServerDrawMessages and back", function() { 11 | var bareObject = { fromX: 1, fromY: 2, toX: 3, toY: 4 }; 12 | var messageObject = new ServerDrawMessage(1, 2, 3, 4); 13 | 14 | assert.deepEqual(ServerDrawMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 15 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 16 | }); 17 | 18 | it("instances know their network message name", function() { 19 | assert.equal(new ServerDrawMessage(1, 2, 3, 4).name(), ServerDrawMessage.MESSAGE_NAME); 20 | }); 21 | 22 | }); 23 | 24 | }()); -------------------------------------------------------------------------------- /src/shared/_server_pointer_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ServerPointerMessage = require("./server_pointer_message.js"); 7 | 8 | describe("SHARED: ServerPointerMessage", function() { 9 | 10 | it("converts bare objects to ServerPointerMessages and back", function() { 11 | var bareObject = { id: "a", x: 1, y: 2 }; 12 | var messageObject = new ServerPointerMessage("a", 1, 2); 13 | 14 | assert.deepEqual(ServerPointerMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 15 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 16 | }); 17 | 18 | it("instances know their network message name", function() { 19 | assert.equal(new ServerPointerMessage(1, 2, 3, 4).name(), ServerPointerMessage.MESSAGE_NAME); 20 | }); 21 | 22 | }); 23 | 24 | }()); -------------------------------------------------------------------------------- /src/shared/_server_remove_pointer_message_test.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var assert = require("_assert"); 6 | var ServerRemovePointerMessage = require("./server_remove_pointer_message.js"); 7 | 8 | describe("SHARED: ServerRemovePointerMessage", function() { 9 | 10 | it("converts bare objects to message objects and back", function() { 11 | var bareObject = { id: "a" }; 12 | var messageObject = new ServerRemovePointerMessage("a"); 13 | 14 | assert.deepEqual(ServerRemovePointerMessage.fromPayload(bareObject), messageObject, "fromPayload()"); 15 | assert.deepEqual(messageObject.payload(), bareObject, "payload()"); 16 | }); 17 | 18 | it("instances know their network message name", function() { 19 | assert.equal(new ServerRemovePointerMessage().name(), ServerRemovePointerMessage.MESSAGE_NAME); 20 | }); 21 | 22 | }); 23 | 24 | }()); -------------------------------------------------------------------------------- /src/shared/client_clear_screen_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerClearScreenMessage = require("./server_clear_screen_message.js"); 6 | 7 | var ClientClearScreenMessage = module.exports = function() { 8 | }; 9 | 10 | ClientClearScreenMessage.MESSAGE_NAME = "client_clear_screen_message"; 11 | ClientClearScreenMessage.prototype.name = function() { return ClientClearScreenMessage.MESSAGE_NAME; }; 12 | 13 | ClientClearScreenMessage.fromPayload = function(obj) { 14 | return new ClientClearScreenMessage(); 15 | }; 16 | 17 | ClientClearScreenMessage.prototype.payload = function() { 18 | return {}; 19 | }; 20 | 21 | ClientClearScreenMessage.prototype.toServerMessage = function() { 22 | return new ServerClearScreenMessage(); 23 | }; 24 | 25 | }()); -------------------------------------------------------------------------------- /src/shared/client_draw_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerDrawMessage = require("./server_draw_message.js"); 6 | 7 | var ClientDrawMessage = module.exports = function(fromX, fromY, toX, toY) { 8 | this._data = { 9 | fromX: fromX, 10 | fromY: fromY, 11 | toX: toX, 12 | toY: toY 13 | }; 14 | }; 15 | 16 | ClientDrawMessage.MESSAGE_NAME = "client_line_message"; 17 | ClientDrawMessage.prototype.name = function() { return ClientDrawMessage.MESSAGE_NAME; }; 18 | 19 | ClientDrawMessage.fromPayload = function(obj) { 20 | return new ClientDrawMessage(obj.fromX, obj.fromY, obj.toX, obj.toY); 21 | }; 22 | 23 | ClientDrawMessage.prototype.payload = function() { 24 | return this._data; 25 | }; 26 | 27 | ClientDrawMessage.prototype.toServerMessage = function() { 28 | return new ServerDrawMessage(this._data.fromX, this._data.fromY, this._data.toX, this._data.toY); 29 | }; 30 | 31 | ClientDrawMessage.prototype.getPointerLocation = function() { 32 | return { 33 | x: this._data.toX, 34 | y: this._data.toY 35 | }; 36 | }; 37 | 38 | }()); -------------------------------------------------------------------------------- /src/shared/client_pointer_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerPointerMessage = require("./server_pointer_message.js"); 6 | 7 | var ClientPointerMessage = module.exports = function(x, y) { 8 | this._x = x; 9 | this._y = y; 10 | }; 11 | 12 | ClientPointerMessage.MESSAGE_NAME = "client_pointer_message"; 13 | ClientPointerMessage.prototype.name = function() { return ClientPointerMessage.MESSAGE_NAME; }; 14 | 15 | ClientPointerMessage.fromPayload = function(obj) { 16 | return new ClientPointerMessage(obj.x, obj.y); 17 | }; 18 | 19 | ClientPointerMessage.prototype.payload = function() { 20 | return { 21 | x: this._x, 22 | y: this._y 23 | }; 24 | }; 25 | 26 | ClientPointerMessage.prototype.toServerMessage = function(id) { 27 | return new ServerPointerMessage(id, this._x, this._y); 28 | }; 29 | 30 | }()); -------------------------------------------------------------------------------- /src/shared/client_remove_pointer_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerRemovePointerMessage = require("./server_remove_pointer_message.js"); 6 | 7 | var ClientRemovePointerMessage = module.exports = function() { 8 | }; 9 | 10 | ClientRemovePointerMessage.MESSAGE_NAME = "client_remove_pointer_message"; 11 | ClientRemovePointerMessage.prototype.name = function() { return ClientRemovePointerMessage.MESSAGE_NAME; }; 12 | 13 | ClientRemovePointerMessage.fromPayload = function(obj) { 14 | return new ClientRemovePointerMessage(); 15 | }; 16 | 17 | ClientRemovePointerMessage.prototype.payload = function() { 18 | return {}; 19 | }; 20 | 21 | ClientRemovePointerMessage.prototype.toServerMessage = function(clientId) { 22 | return new ServerRemovePointerMessage(clientId); 23 | }; 24 | 25 | }()); -------------------------------------------------------------------------------- /src/shared/server_clear_screen_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerClearScreenMessage = module.exports = function() { 6 | }; 7 | 8 | ServerClearScreenMessage.MESSAGE_NAME = "server_clear_screen_message"; 9 | ServerClearScreenMessage.prototype.name = function() { return ServerClearScreenMessage.MESSAGE_NAME; }; 10 | 11 | ServerClearScreenMessage.fromPayload = function(obj) { 12 | return new ServerClearScreenMessage(); 13 | }; 14 | 15 | ServerClearScreenMessage.prototype.payload = function() { 16 | return {}; 17 | }; 18 | 19 | }()); -------------------------------------------------------------------------------- /src/shared/server_draw_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerDrawMessage = module.exports = function(fromX, fromY, toX, toY) { 6 | this.from = { 7 | x: fromX, 8 | y: fromY 9 | }; 10 | this.to = { 11 | x: toX, 12 | y: toY 13 | }; 14 | }; 15 | 16 | ServerDrawMessage.MESSAGE_NAME = "server_draw_message"; 17 | ServerDrawMessage.prototype.name = function() { return ServerDrawMessage.MESSAGE_NAME; }; 18 | 19 | ServerDrawMessage.fromPayload = function(obj) { 20 | return new ServerDrawMessage(obj.fromX, obj.fromY, obj.toX, obj.toY); 21 | }; 22 | 23 | ServerDrawMessage.prototype.payload = function() { 24 | return { 25 | fromX: this.from.x, 26 | fromY: this.from.y, 27 | toX: this.to.x, 28 | toY: this.to.y 29 | }; 30 | }; 31 | 32 | }()); -------------------------------------------------------------------------------- /src/shared/server_pointer_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerPointerMessage = module.exports = function ServerPointerMessage(id, x, y) { 6 | this.id = id; 7 | this.x = x; 8 | this.y = y; 9 | }; 10 | 11 | ServerPointerMessage.MESSAGE_NAME = "server_pointer_message"; 12 | ServerPointerMessage.prototype.name = function() { return ServerPointerMessage.MESSAGE_NAME; }; 13 | 14 | ServerPointerMessage.fromPayload = function(obj) { 15 | return new ServerPointerMessage(obj.id, obj.x, obj.y); 16 | }; 17 | 18 | ServerPointerMessage.prototype.payload = function() { 19 | return { 20 | id: this.id, 21 | x: this.x, 22 | y: this.y 23 | }; 24 | }; 25 | 26 | }()); -------------------------------------------------------------------------------- /src/shared/server_remove_pointer_message.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2016-2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file. 2 | (function() { 3 | "use strict"; 4 | 5 | var ServerRemovePointerMessage = module.exports = function(clientId) { 6 | this.id = clientId; 7 | }; 8 | 9 | ServerRemovePointerMessage.MESSAGE_NAME = "server_remove_pointer_message"; 10 | ServerRemovePointerMessage.prototype.name = function() { return ServerRemovePointerMessage.MESSAGE_NAME; }; 11 | 12 | ServerRemovePointerMessage.fromPayload = function(obj) { 13 | return new ServerRemovePointerMessage(obj.id); 14 | }; 15 | 16 | ServerRemovePointerMessage.prototype.payload = function() { 17 | return { id: this.id }; 18 | }; 19 | 20 | }()); -------------------------------------------------------------------------------- /todo.txt: -------------------------------------------------------------------------------- 1 | WeeWikiPaint 2 | 3 | Minimum Marketable Features: 4 | + marketing home page (episodes 1-32) 5 | + single-user painting on home page (episodes 33-200) 6 | + replace home page with professional design (episodes 201-321) 7 | + IE 11 support (and get rid of IE 8) (episodes 322-324) 8 | + Android support (episodes 325-327) 9 | + iOS 8 support (episode 328) 10 | + replace 404 page with professional design (episodes 329-369) 11 | * collaborative painting (episode 370+) 12 | - multiple servers 13 | - responsive design 14 | - accessibility 15 | - polish 16 | - clear button retains focus when drawing (IE8, 9, Chrome) 17 | - drawing glitches occur when exiting drawing area that is flush against side of browser 18 | - one-pixel gap on right side of drawing area (Firefox, others unknown) 19 | - favicon 20 | - 'clear' button flashes rather than depressing (iOS) 21 | - 'clear' button depresses diagonally rather than vertically (IE 9, IE 11) 22 | - multiple painting/erasing tools 23 | - save the pages (persistence / databases) 24 | - wiki-fy the pages 25 | - more complex painting? 26 | - more collaborative features? 27 | 28 | User Stories to finish current MMF: 29 | - reconnect and resync when client is disconnected/reconnected 30 | - show disconnection warning / other UX when client is disconnected 31 | - handle server reset smoothly (clear the drawing? re-sync to browsers? not persistence/DB) 32 | - handle packet loss (What packet loss is possible? TCP handles this? What about unstable cellular connections?) 33 | - at least handle glitches in Socket.IO where we don't receive a message 34 | 35 | - version synchronization across client/server 36 | - collision-handling / conflicts / race conditions 37 | - load management 38 | - server monitoring / error handling 39 | - security (handling bad data, denial of service attacks) 40 | - nsp, snyk 41 | - performance optimization 42 | - garbage collection pauses 43 | - network tick rate is currently unbounded 44 | - event repo stores everything, no culling 45 | - bug: shows ghost pointer on other clients when tapping 'clear' button on mobile browsers 46 | - see onTouchClick_spike branch (but note document.elementFromPoint may not be testable on IE 11) 47 | - retire support for IE 11 first? 48 | - bug: ghost pointers block other actions 49 | - e.g., starting a drag on top of a ghost pointer drags the pointer instead of drawing a line 50 | - e.g., clicking clear button if ghost pointer is in the way prevents button from clicking 51 | - investigate 'pointer-events: none' CSS 52 | - polish: 53 | - make 'clear' button depress when clear event received? 54 | - when user tabs away, tabs back, then clicks, the mouse pointer doesn't re-appear 55 | 56 | Engineering Tasks: 57 | 58 | To Do on current task: --------------------------------------------------------------------------------