├── .gitignore
├── LICENSE
├── Procfile
├── README.md
├── assets
├── brunch-config.js
├── css
│ ├── app.css
│ └── phoenix.css
├── js
│ ├── app.js
│ └── web_console.js
├── package-lock.json
├── package.json
├── static
│ ├── favicon.ico
│ ├── images
│ │ └── phoenix.png
│ └── robots.txt
└── vendor
│ └── sha256.js
├── config
├── config.exs
├── dev.exs
├── prod.exs
├── prod.secret.exs
└── test.exs
├── elixir_buildpack.config
├── lib
├── counter
│ └── socket_counter.ex
├── ephemeral2.ex
├── ephemeral2
│ └── application.ex
├── ephemeral2_web.ex
└── ephemeral2_web
│ ├── channels
│ ├── all_channel.ex
│ ├── have_channel.ex
│ ├── user_socket.ex
│ └── want_channel.ex
│ ├── controllers
│ └── page_controller.ex
│ ├── endpoint.ex
│ ├── gettext.ex
│ ├── router.ex
│ ├── templates
│ ├── layout
│ │ └── app.html.eex
│ └── page
│ │ ├── new.html.eex
│ │ └── show.html.eex
│ └── views
│ ├── error_helpers.ex
│ ├── error_view.ex
│ ├── layout_view.ex
│ └── page_view.ex
├── mix.exs
├── mix.lock
├── priv
├── gettext
│ ├── en
│ │ └── LC_MESSAGES
│ │ │ └── errors.po
│ └── errors.pot
└── static
│ ├── css
│ ├── app.css
│ └── app.css.map
│ ├── favicon.ico
│ ├── images
│ └── phoenix.png
│ ├── js
│ ├── app.js
│ └── app.js.map
│ └── robots.txt
└── test
├── ephemeral2_web
├── controllers
│ └── page_controller_test.exs
└── views
│ ├── error_view_test.exs
│ ├── layout_view_test.exs
│ └── page_view_test.exs
├── support
├── channel_case.ex
└── conn_case.ex
└── test_helper.exs
/.gitignore:
--------------------------------------------------------------------------------
1 | # Mix artifacts
2 | /_build
3 | /deps
4 | /*.ez
5 |
6 | # Generate on crash by the VM
7 | erl_crash.dump
8 |
9 | # Static artifacts
10 | node_modules
11 |
12 | # Since we are building js and css from web/static,
13 | # we ignore priv/static/{css,js}. You may want to
14 | # comment this depending on your deployment strategy.
15 | # /priv/static/css
16 | # /priv/static/js
17 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Gabriel Durazo
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: mix phx.server
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ephemeral P2P
2 |
3 | See example implementation [running here](http://ephemeralp2p.durazo.us/). Discussion on Hacker News [here](https://news.ycombinator.com/item?id=9531265).
4 |
5 | This app hosts "P2P" pages that are "ephemeral". We say "P2P" because the clients host the page; new visitors retrieve the page contents from other visitors on the same page. It's "ephemeral" in that the server does not store the contents of any page, so once the last visitor leaves a particular page, it is gone.
6 |
7 | # How it works
8 |
9 | Ephemeral P2P is an Elixir/Phoenix app, taking advantage of Phoenix's excellent Channel functionality. All of the logic lies in two channels, the `HaveChannel` for clients who have a particular bit of content, and the `WantChannel` for visitors who want it. The content is addressed by its SHA256 (let us say `abc123`), and the two topics associated with that content are `have:abc123` and `want:abc123`.
10 |
11 | A new page is created from the homepage. A visitor fills in the textarea with whatever content they desire, and presses the "Submit" button (which is not a form submission). The client hashes the content (let's call the hash `abc123`) and uses the HTML5 history api to change the URL to `/abc123` for easy copy/paste-ability. The client then joins the "have:abc123" topic and begins listening for `"content_request"` messages, ready to respond with a `"content"` message that includes the page content which it has in memory.
12 |
13 | A subsequent visitor who loads `/abc123` joins the `want:abc123` topic and tries to obtain the content. First it listens for a `"content"` message that another visitor may have provoked. If the visitor does not receive it in 2 seconds, it will send a `"content_request"` message itself. The server will re-broadcast this message to all `have:abc123` subscribers, except that a `handle_out` will allow the message with probability `1/subscriber_count` and drop it otherwise. Any `have:abc123` subscribers who receive the message will respond with the content and the server will `broadcast` it to all `want:abc123` subscribers. The new visitor will send a `"content_request"` message every 2 seconds until it gets the content (for the case where the `handle_out` drops the message to everyone, or a `have` subscriber fails to respond for some reason.)
14 |
15 | When a `want:abc123` subscriber gets the content, it leaves the `want:abc123` topic and joins the `have:abc123` topic, ready to pass it along to newer visitors.
16 |
17 | Lastly, whenever a subscriber joins or leaves `have:abc123`, the new visitor count is broadcast, so all clients know the "health" of the page and how close it is to going away.
18 |
19 | # Run it yourself
20 |
21 | First you'll need to [install Elixir](http://elixir-lang.org/install.html). Once you have elixir set up, it should be as easy as:
22 |
23 | ```
24 | $ git clone git@github.com:losvedir/ephemeral2.git
25 | $ cd ephemeral2
26 | $ mix deps.get
27 | $ mix phoenix.server
28 | ```
29 |
30 | That should serve a copy of the app locally at `localhost:4000`. If you would like to modify the CSS or Javascript, you'll need to install [npm](https://www.npmjs.com/) and then brunch with:
31 |
32 | ```
33 | $ npm install -g brunch
34 | ```
35 |
--------------------------------------------------------------------------------
/assets/brunch-config.js:
--------------------------------------------------------------------------------
1 | exports.config = {
2 | // See http://brunch.io/#documentation for docs.
3 | files: {
4 | javascripts: {
5 | joinTo: "js/app.js"
6 |
7 | // To use a separate vendor.js bundle, specify two files path
8 | // http://brunch.io/docs/config#-files-
9 | // joinTo: {
10 | // "js/app.js": /^js/,
11 | // "js/vendor.js": /^(?!js)/
12 | // }
13 | //
14 | // To change the order of concatenation of files, explicitly mention here
15 | // order: {
16 | // before: [
17 | // "vendor/js/jquery-2.1.1.js",
18 | // "vendor/js/bootstrap.min.js"
19 | // ]
20 | // }
21 | },
22 | stylesheets: {
23 | joinTo: "css/app.css"
24 | },
25 | templates: {
26 | joinTo: "js/app.js"
27 | }
28 | },
29 |
30 | conventions: {
31 | // This option sets where we should place non-css and non-js assets in.
32 | // By default, we set this to "/assets/static". Files in this directory
33 | // will be copied to `paths.public`, which is "priv/static" by default.
34 | assets: /^(static)/
35 | },
36 |
37 | // Phoenix paths configuration
38 | paths: {
39 | // Dependencies and current project directories to watch
40 | watched: ["static", "css", "js", "vendor"],
41 | // Where to compile files to
42 | public: "../priv/static"
43 | },
44 |
45 | // Configure your plugins
46 | plugins: {
47 | babel: {
48 | // Do not use ES6 compiler in vendor code
49 | ignore: [/vendor/]
50 | }
51 | },
52 |
53 | modules: {
54 | autoRequire: {
55 | "js/app.js": ["js/app"]
56 | }
57 | },
58 |
59 | npm: {
60 | enabled: true
61 | }
62 | };
63 |
--------------------------------------------------------------------------------
/assets/css/app.css:
--------------------------------------------------------------------------------
1 | /* This file is for your main application css. */
2 |
3 | .web-console {height:75px; overflow:scroll; border:1px solid #e5e5e5}
4 | .web-console ul {padding:0; margin:0}
5 | .web-console li {list-style-type:none; padding:0 0 0 1em}
6 |
--------------------------------------------------------------------------------
/assets/js/app.js:
--------------------------------------------------------------------------------
1 | // Brunch automatically concatenates all files in your
2 | // watched paths. Those paths can be configured at
3 | // config.paths.watched in "brunch-config.js".
4 | //
5 | // However, those files will only be executed if
6 | // explicitly imported. The only exception are files
7 | // in vendor, which are never wrapped in imports and
8 | // therefore are always executed.
9 |
10 | // Import dependencies
11 | //
12 | // If you no longer want to use a dependency, remember
13 | // to also remove its path from "config.paths.watched".
14 | import "phoenix_html"
15 |
16 | // Import local files
17 | //
18 | // Local files can be imported directly using relative
19 | // paths "./socket" or full ones "web/static/js/socket".
20 |
21 | // import socket from "./socket"
22 |
23 | import {Socket} from "phoenix";
24 | import WebConsole from "./web_console";
25 |
26 | let hash;
27 | let content;
28 | let webConsole;
29 |
30 | document.addEventListener("DOMContentLoaded", () => {
31 | webConsole = new WebConsole(document.getElementById('js-console'));
32 | let homePageElement = document.getElementById("create-new-page");
33 | let showPageElement = document.getElementById("content-goes-here");
34 |
35 | webConsole.log("Connecting to websocket.");
36 | let socket = new Socket("/ws");
37 | socket.connect();
38 |
39 | let chan = socket.channel("all", {});
40 | chan.join().receive("ok", () => { webConsole.log("Connected!") });
41 |
42 | if ( homePageElement ) {
43 | homePageElement.addEventListener("click", () => {
44 | content = document.getElementById("new-page-content").value;
45 | hash = SHA256(content);
46 | history.pushState({}, "Your Page", hash);
47 | document.getElementById("content-goes-here").innerHTML = content;
48 | haveContent(socket, hash, content);
49 | });
50 | } else if ( showPageElement ) {
51 | wantContent(socket, window.location.pathname.substr(1), showPageElement);
52 | }
53 | });
54 |
55 | function haveContent(socket, hash, content) {
56 | let counter = document.getElementById("visitor-count");
57 |
58 | for( let i=0; i < socket.channels.length; i++ ) {
59 | if ( socket.channels[i].topic === 'have:' + hash ) {
60 | return;
61 | }
62 | }
63 |
64 | let chan = socket.channel("have:" + hash, {});
65 | chan.on("content_request", function(_msg) {
66 | webConsole.log("Request received...");
67 | chan.push("content", {content: content, hash: hash});
68 | webConsole.log("Content sent!");
69 | });
70 | chan.on("visitors_count", function(msg) {
71 | counter.innerHTML = msg.count;
72 | });
73 |
74 | chan.join().receive("ok", function(chan) {
75 | webConsole.log("Standing by... ready to share this content!")
76 | });
77 | }
78 |
79 | function wantContent(socket, hash, elem) {
80 | let requestContentInterval;
81 |
82 | let chan = socket.channel("want:" + hash, {});
83 | chan.on("content", function(msg) {
84 | clearInterval(requestContentInterval);
85 | webConsole.log(`Received content for hash ${hash}`);
86 | elem.innerHTML = msg.content;
87 | chan.leave();
88 | haveContent(socket, hash, msg.content);
89 | });
90 |
91 | chan.join().receive("ok", () => {
92 | webConsole.log(`Listening for content for hash ${hash}`);
93 |
94 | requestContentInterval = setInterval(() =>{
95 | webConsole.log("Requesting content.");
96 | chan.push("content_request", {hash: hash});
97 | }, 2000);
98 | });
99 | }
100 |
101 | let App = {
102 | }
103 |
104 | export default App
105 |
--------------------------------------------------------------------------------
/assets/js/web_console.js:
--------------------------------------------------------------------------------
1 | export default class WebConsole {
2 | // takes a
element
3 | constructor(list) {
4 | console.log(list);
5 | this.list = list;
6 | }
7 |
8 | log(msg) {
9 | let wasScrolledToTheBottom = this.isScrolledToTheBottom();
10 | this.addLine(msg);
11 | if (wasScrolledToTheBottom) {
12 | this.scrollToTheBottom();
13 | }
14 | }
15 |
16 | addLine(msg) {
17 | var li = document.createElement("li");
18 | var text = document.createTextNode(msg);
19 | li.appendChild(text);
20 | this.list.appendChild(li);
21 | }
22 |
23 | isScrolledToTheBottom() {
24 | return this.list.scrollTop >= this.list.scrollHeight - this.list.clientHeight;
25 | }
26 |
27 | scrollToTheBottom() {
28 | this.list.scrollTop = this.list.scrollHeight;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/assets/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "repository": {},
3 | "license": "MIT",
4 | "scripts": {
5 | "deploy": "brunch build --production",
6 | "watch": "brunch watch --stdin"
7 | },
8 | "dependencies": {
9 | "phoenix": "file:../deps/phoenix",
10 | "phoenix_html": "file:../deps/phoenix_html"
11 | },
12 | "devDependencies": {
13 | "babel-brunch": "6.1.1",
14 | "brunch": "2.10.9",
15 | "clean-css-brunch": "2.10.0",
16 | "uglify-js-brunch": "2.10.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/assets/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/losvedir/ephemeral2/4d48d6b7724127efc4f416ec45c4c2cb28472fc3/assets/static/favicon.ico
--------------------------------------------------------------------------------
/assets/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/losvedir/ephemeral2/4d48d6b7724127efc4f416ec45c4c2cb28472fc3/assets/static/images/phoenix.png
--------------------------------------------------------------------------------
/assets/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/assets/vendor/sha256.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * Secure Hash Algorithm (SHA256)
4 | * http://www.webtoolkit.info/
5 | *
6 | * Original code by Angel Marin, Paul Johnston.
7 | *
8 | **/
9 | function SHA256(s){
10 |
11 | var chrsz = 8;
12 | var hexcase = 0;
13 |
14 | function safe_add (x, y) {
15 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
16 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
17 | return (msw << 16) | (lsw & 0xFFFF);
18 | }
19 |
20 | function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
21 | function R (X, n) { return ( X >>> n ); }
22 | function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
23 | function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
24 | function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
25 | function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
26 | function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
27 | function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
28 |
29 | function core_sha256 (m, l) {
30 | var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
31 | var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
32 | var W = new Array(64);
33 | var a, b, c, d, e, f, g, h, i, j;
34 | var T1, T2;
35 |
36 | m[l >> 5] |= 0x80 << (24 - l % 32);
37 | m[((l + 64 >> 9) << 4) + 15] = l;
38 |
39 | for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
83 | }
84 | return bin;
85 | }
86 |
87 | function Utf8Encode(string) {
88 | string = string.replace(/\r\n/g,"\n");
89 | var utftext = "";
90 |
91 | for (var n = 0; n < string.length; n++) {
92 |
93 | var c = string.charCodeAt(n);
94 |
95 | if (c < 128) {
96 | utftext += String.fromCharCode(c);
97 | }
98 | else if((c > 127) && (c < 2048)) {
99 | utftext += String.fromCharCode((c >> 6) | 192);
100 | utftext += String.fromCharCode((c & 63) | 128);
101 | }
102 | else {
103 | utftext += String.fromCharCode((c >> 12) | 224);
104 | utftext += String.fromCharCode(((c >> 6) & 63) | 128);
105 | utftext += String.fromCharCode((c & 63) | 128);
106 | }
107 |
108 | }
109 |
110 | return utftext;
111 | }
112 |
113 | function binb2hex (binarray) {
114 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
115 | var str = "";
116 | for(var i = 0; i < binarray.length * 4; i++) {
117 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
118 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
119 | }
120 | return str;
121 | }
122 |
123 | s = Utf8Encode(s);
124 | return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/config/config.exs:
--------------------------------------------------------------------------------
1 | # This file is responsible for configuring your application
2 | # and its dependencies with the aid of the Mix.Config module.
3 | #
4 | # This configuration file is loaded before any dependency and
5 | # is restricted to this project.
6 | use Mix.Config
7 |
8 | # Configures the endpoint
9 | config :ephemeral2, Ephemeral2Web.Endpoint,
10 | url: [host: "localhost"],
11 | secret_key_base: "bS5WZOnD3ul0jryRS2Afj9VuloR5hXh3bzXIuDqTEk5lf4G66zeP3Qm9f7FHNerz",
12 | render_errors: [view: Ephemeral2Web.ErrorView, accepts: ~w(html json)],
13 | pubsub: [name: Ephemeral2.PubSub,
14 | adapter: Phoenix.PubSub.PG2]
15 |
16 | # Configures Elixir's Logger
17 | config :logger, :console,
18 | format: "$time $metadata[$level] $message\n",
19 | metadata: [:request_id]
20 |
21 | # Import environment specific config. This must remain at the bottom
22 | # of this file so it overrides the configuration defined above.
23 | import_config "#{Mix.env}.exs"
24 |
--------------------------------------------------------------------------------
/config/dev.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For development, we disable any cache and enable
4 | # debugging and code reloading.
5 | #
6 | # The watchers configuration can be used to run external
7 | # watchers to your application. For example, we use it
8 | # with brunch.io to recompile .js and .css sources.
9 | config :ephemeral2, Ephemeral2Web.Endpoint,
10 | http: [port: 4000],
11 | debug_errors: true,
12 | code_reloader: true,
13 | check_origin: false,
14 | watchers: [node: ["node_modules/brunch/bin/brunch", "watch", "--stdin",
15 | cd: Path.expand("../assets", __DIR__)]]
16 |
17 | # ## SSL Support
18 | #
19 | # In order to use HTTPS in development, a self-signed
20 | # certificate can be generated by running the following
21 | # command from your terminal:
22 | #
23 | # openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem
24 | #
25 | # The `http:` config above can be replaced with:
26 | #
27 | # https: [port: 4000, keyfile: "priv/server.key", certfile: "priv/server.pem"],
28 | #
29 | # If desired, both `http:` and `https:` keys can be
30 | # configured to run both http and https servers on
31 | # different ports.
32 |
33 | # Watch static and templates for browser reloading.
34 | config :ephemeral2, Ephemeral2Web.Endpoint,
35 | live_reload: [
36 | patterns: [
37 | ~r{priv/static/.*(js|css|png|jpeg|jpg|gif|svg)$},
38 | ~r{priv/gettext/.*(po)$},
39 | ~r{lib/ephemeral2_web/views/.*(ex)$},
40 | ~r{lib/ephemeral2_web/templates/.*(eex)$}
41 | ]
42 | ]
43 |
44 | # Do not include metadata nor timestamps in development logs
45 | config :logger, :console, format: "[$level] $message\n"
46 |
47 | # Set a higher stacktrace during development. Avoid configuring such
48 | # in production as building large stacktraces may be expensive.
49 | config :phoenix, :stacktrace_depth, 20
50 |
--------------------------------------------------------------------------------
/config/prod.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # For production, we often load configuration from external
4 | # sources, such as your system environment. For this reason,
5 | # you won't find the :http configuration below, but set inside
6 | # Ephemeral2Web.Endpoint.init/2 when load_from_system_env is
7 | # true. Any dynamic configuration should be done there.
8 | #
9 | # Don't forget to configure the url host to something meaningful,
10 | # Phoenix uses this information when generating URLs.
11 | #
12 | # Finally, we also include the path to a cache manifest
13 | # containing the digested version of static files. This
14 | # manifest is generated by the mix phx.digest task
15 | # which you typically run after static files are built.
16 | config :ephemeral2, Ephemeral2Web.Endpoint,
17 | load_from_system_env: true,
18 | url: [host: "example.com", port: 80],
19 | cache_static_manifest: "priv/static/cache_manifest.json"
20 |
21 | # Do not print debug messages in production
22 | config :logger, level: :info
23 |
24 | # ## SSL Support
25 | #
26 | # To get SSL working, you will need to add the `https` key
27 | # to the previous section and set your `:url` port to 443:
28 | #
29 | # config :ephemeral2, Ephemeral2Web.Endpoint,
30 | # ...
31 | # url: [host: "example.com", port: 443],
32 | # https: [:inet6,
33 | # port: 443,
34 | # keyfile: System.get_env("SOME_APP_SSL_KEY_PATH"),
35 | # certfile: System.get_env("SOME_APP_SSL_CERT_PATH")]
36 | #
37 | # Where those two env variables return an absolute path to
38 | # the key and cert in disk or a relative path inside priv,
39 | # for example "priv/ssl/server.key".
40 | #
41 | # We also recommend setting `force_ssl`, ensuring no data is
42 | # ever sent via http, always redirecting to https:
43 | #
44 | # config :ephemeral2, Ephemeral2Web.Endpoint,
45 | # force_ssl: [hsts: true]
46 | #
47 | # Check `Plug.SSL` for all available options in `force_ssl`.
48 |
49 | # ## Using releases
50 | #
51 | # If you are doing OTP releases, you need to instruct Phoenix
52 | # to start the server for all endpoints:
53 | #
54 | # config :phoenix, :serve_endpoints, true
55 | #
56 | # Alternatively, you can configure exactly which server to
57 | # start per endpoint:
58 | #
59 | # config :ephemeral2, Ephemeral2Web.Endpoint, server: true
60 | #
61 |
62 | # Finally import the config/prod.secret.exs
63 | # which should be versioned separately.
64 | import_config "prod.secret.exs"
65 |
--------------------------------------------------------------------------------
/config/prod.secret.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # In this file, we keep production configuration that
4 | # you'll likely want to automate and keep away from
5 | # your version control system.
6 | #
7 | # You should document the content of this
8 | # file or create a script for recreating it, since it's
9 | # kept out of version control and might be hard to recover
10 | # or recreate for your teammates (or yourself later on).
11 | config :ephemeral2, Ephemeral2Web.Endpoint,
12 | secret_key_base: "1OLIecTDDzkIAo+tDDWC519J1/RHrfS5lRYqZoSPH/G9lmGXMAKFhdxeamVNQM1z"
13 |
--------------------------------------------------------------------------------
/config/test.exs:
--------------------------------------------------------------------------------
1 | use Mix.Config
2 |
3 | # We don't run a server during test. If one is required,
4 | # you can enable the server option below.
5 | config :ephemeral2, Ephemeral2Web.Endpoint,
6 | http: [port: 4001],
7 | server: false
8 |
9 | # Print only warnings and errors during test
10 | config :logger, level: :warn
11 |
--------------------------------------------------------------------------------
/elixir_buildpack.config:
--------------------------------------------------------------------------------
1 | # Erlang version
2 | erlang_version=20.1.4
3 |
4 | # Elixir version
5 | elixir_version=1.5.2
6 |
7 | # Always rebuild from scratch on every deploy?
8 | always_rebuild=false
9 |
10 | # Export heroku config vars
11 | config_vars_to_export=(DATABASE_URL)
12 |
--------------------------------------------------------------------------------
/lib/counter/socket_counter.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2.SocketCounter do
2 | use GenServer
3 | require Logger
4 |
5 | def start_link() do
6 | GenServer.start_link(__MODULE__, [])
7 | end
8 |
9 | def init([]) do
10 | Logger.info "Init'ing SocketCounter"
11 | Process.send_after(self(), :log_stuff, 5_000)
12 | {:ok, []}
13 | end
14 |
15 | def handle_info(:log_stuff, state) do
16 | # YES THIS IS BAD AND SHOULD USE PHOENIX.PRESENCE
17 | Logger.info "total_subscribers=#{length(:ets.lookup(Elixir.Ephemeral2.PubSub.Local0, "all"))}"
18 | Process.send_after(self(), :log_stuff, 5_000)
19 | {:noreply, state}
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/ephemeral2.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2 do
2 | @moduledoc """
3 | Ephemeral2 keeps the contexts that define your domain
4 | and business logic.
5 |
6 | Contexts are also responsible for managing your data, regardless
7 | if it comes from the database, an external API or others.
8 | """
9 | end
10 |
--------------------------------------------------------------------------------
/lib/ephemeral2/application.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2.Application do
2 | use Application
3 |
4 | # See https://hexdocs.pm/elixir/Application.html
5 | # for more information on OTP Applications
6 | def start(_type, _args) do
7 | import Supervisor.Spec
8 |
9 | # Define workers and child supervisors to be supervised
10 | children = [
11 | # Start the endpoint when the application starts
12 | supervisor(Ephemeral2Web.Endpoint, []),
13 | worker(Ephemeral2.SocketCounter, [])
14 | ]
15 |
16 | # See https://hexdocs.pm/elixir/Supervisor.html
17 | # for other strategies and supported options
18 | opts = [strategy: :one_for_one, name: Ephemeral2.Supervisor]
19 | Supervisor.start_link(children, opts)
20 | end
21 |
22 | # Tell Phoenix to update the endpoint configuration
23 | # whenever the application is updated.
24 | def config_change(changed, _new, removed) do
25 | Ephemeral2Web.Endpoint.config_change(changed, removed)
26 | :ok
27 | end
28 | end
29 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web do
2 | @moduledoc """
3 | The entrypoint for defining your web interface, such
4 | as controllers, views, channels and so on.
5 |
6 | This can be used in your application as:
7 |
8 | use Ephemeral2Web, :controller
9 | use Ephemeral2Web, :view
10 |
11 | The definitions below will be executed for every view,
12 | controller, etc, so keep them short and clean, focused
13 | on imports, uses and aliases.
14 |
15 | Do NOT define functions inside the quoted expressions
16 | below. Instead, define any helper function in modules
17 | and import those modules here.
18 | """
19 |
20 | def controller do
21 | quote do
22 | use Phoenix.Controller, namespace: Ephemeral2Web
23 | import Plug.Conn
24 | import Ephemeral2Web.Router.Helpers
25 | import Ephemeral2Web.Gettext
26 | end
27 | end
28 |
29 | def view do
30 | quote do
31 | use Phoenix.View, root: "lib/ephemeral2_web/templates",
32 | namespace: Ephemeral2Web
33 |
34 | # Import convenience functions from controllers
35 | import Phoenix.Controller, only: [get_flash: 2, view_module: 1]
36 |
37 | # Use all HTML functionality (forms, tags, etc)
38 | use Phoenix.HTML
39 |
40 | import Ephemeral2Web.Router.Helpers
41 | import Ephemeral2Web.ErrorHelpers
42 | import Ephemeral2Web.Gettext
43 | end
44 | end
45 |
46 | def router do
47 | quote do
48 | use Phoenix.Router
49 | import Plug.Conn
50 | import Phoenix.Controller
51 | end
52 | end
53 |
54 | def channel do
55 | quote do
56 | use Phoenix.Channel
57 | import Ephemeral2Web.Gettext
58 | end
59 | end
60 |
61 | @doc """
62 | When used, dispatch to the appropriate controller/view/etc.
63 | """
64 | defmacro __using__(which) when is_atom(which) do
65 | apply(__MODULE__, which, [])
66 | end
67 | end
68 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/channels/all_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.AllChannel do
2 | use Phoenix.Channel
3 | require Logger
4 |
5 | def join("all", _message, socket) do
6 | Logger.info "Joined AllChannel"
7 | {:ok, %{}, socket}
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/channels/have_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.HaveChannel do
2 | use Phoenix.Channel
3 | require Logger
4 |
5 | def join("have:" <> hash, _message, socket) do
6 | :rand.seed(:exsp, :os.timestamp)
7 | Logger.info "Joined HaveChannel: #{hash}"
8 | send(self(), :broadcast_count)
9 | {:ok, %{}, socket}
10 | end
11 |
12 | def terminate(_reason, socket) do
13 | Logger.info "Left HaveChannel: #{socket.topic}"
14 | broadcast! socket, "visitors_count", %{"count" => visitor_count(socket) - 1}
15 | :ok
16 | end
17 |
18 | def handle_in("content", %{"content" => content, "hash" => hash}, socket) do
19 | verify_hash = :crypto.hash(:sha256, content) |> Base.encode16 |> String.downcase
20 |
21 | if verify_hash == hash do
22 | Ephemeral2Web.Endpoint.broadcast! "want:" <> hash, "content", %{"content" => content}
23 | {:noreply, socket}
24 | else
25 | {:stop, :bad_hash, socket}
26 | end
27 | end
28 |
29 | intercept ["content_request"]
30 |
31 | def handle_out("content_request", payload, socket) do
32 | threshhold = 1.0 / visitor_count(socket)
33 | rand = :rand.uniform
34 | Logger.info "threshhold: #{threshhold}, rand: #{rand}"
35 |
36 | if rand < threshhold do
37 | Logger.info "sending content_request"
38 | push socket, "content_request", payload
39 | else
40 | Logger.info "dropping content_request"
41 | end
42 |
43 | {:noreply, socket}
44 | end
45 |
46 | def handle_out(msg, payload, socket) do
47 | push socket, msg, payload
48 | {:noreply, socket}
49 | end
50 |
51 | def handle_info(:broadcast_count, socket) do
52 | broadcast! socket, "visitors_count", %{"count" => visitor_count(socket)}
53 | {:noreply, socket}
54 | end
55 |
56 | defp visitor_count(socket) do
57 | # YES THIS IS BAD AND SHOULD USE PHOENIX.PRESENCE
58 | length(:ets.lookup(Elixir.Ephemeral2.PubSub.Local0, socket.topic))
59 | end
60 | end
61 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/channels/user_socket.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.UserSocket do
2 | use Phoenix.Socket
3 |
4 | ## Channels
5 | # channel "room:*", Ephemeral2Web.RoomChannel
6 |
7 | channel "have:*", Ephemeral2Web.HaveChannel
8 | channel "want:*", Ephemeral2Web.WantChannel
9 | channel "all", Ephemeral2Web.AllChannel
10 |
11 | ## Transports
12 | transport :websocket, Phoenix.Transports.WebSocket
13 | # transport :longpoll, Phoenix.Transports.LongPoll
14 |
15 | # Socket params are passed from the client and can
16 | # be used to verify and authenticate a user. After
17 | # verification, you can put default assigns into
18 | # the socket that will be set for all channels, ie
19 | #
20 | # {:ok, assign(socket, :user_id, verified_user_id)}
21 | #
22 | # To deny connection, return `:error`.
23 | #
24 | # See `Phoenix.Token` documentation for examples in
25 | # performing token verification on connect.
26 | def connect(_params, socket) do
27 | {:ok, socket}
28 | end
29 |
30 | # Socket id's are topics that allow you to identify all sockets for a given user:
31 | #
32 | # def id(socket), do: "user_socket:#{socket.assigns.user_id}"
33 | #
34 | # Would allow you to broadcast a "disconnect" event and terminate
35 | # all active sockets and channels for a given user:
36 | #
37 | # Ephemeral2Web.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{})
38 | #
39 | # Returning `nil` makes this socket anonymous.
40 | def id(_socket), do: nil
41 | end
42 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/channels/want_channel.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.WantChannel do
2 | use Phoenix.Channel
3 | require Logger
4 |
5 | def join("want:" <> hash, _message, socket) do
6 | Logger.info "Joined WantChannel: #{hash}"
7 | {:ok, %{}, socket}
8 | end
9 |
10 | def handle_in("content_request", %{"hash" => hash}, socket) do
11 | Logger.info "handle_in WantChannel content_request"
12 | Ephemeral2Web.Endpoint.broadcast! "have:" <> hash, "content_request", %{}
13 | {:noreply, socket}
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/controllers/page_controller.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.PageController do
2 | use Ephemeral2Web, :controller
3 |
4 | def new(conn, _params) do
5 | render conn, "new.html"
6 | end
7 |
8 | def show(conn, %{"hash" => hash}) do
9 | render conn, "show.html", hash: hash
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/endpoint.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.Endpoint do
2 | use Phoenix.Endpoint, otp_app: :ephemeral2
3 |
4 | socket "/ws", Ephemeral2Web.UserSocket
5 |
6 | # Serve at "/" the static files from "priv/static" directory.
7 | #
8 | # You should set gzip to true if you are running phoenix.digest
9 | # when deploying your static files in production.
10 | plug Plug.Static,
11 | at: "/", from: :ephemeral2, gzip: false,
12 | only: ~w(css fonts images js favicon.ico robots.txt)
13 |
14 | # Code reloading can be explicitly enabled under the
15 | # :code_reloader configuration of your endpoint.
16 | if code_reloading? do
17 | socket "/phoenix/live_reload/socket", Phoenix.LiveReloader.Socket
18 | plug Phoenix.LiveReloader
19 | plug Phoenix.CodeReloader
20 | end
21 |
22 | plug Plug.RequestId
23 | plug Plug.Logger
24 |
25 | plug Plug.Parsers,
26 | parsers: [:urlencoded, :multipart, :json],
27 | pass: ["*/*"],
28 | json_decoder: Poison
29 |
30 | plug Plug.MethodOverride
31 | plug Plug.Head
32 |
33 | # The session will be stored in the cookie and signed,
34 | # this means its contents can be read but not tampered with.
35 | # Set :encryption_salt if you would also like to encrypt it.
36 | plug Plug.Session,
37 | store: :cookie,
38 | key: "_ephemeral2_key",
39 | signing_salt: "gvaq16iA"
40 |
41 | plug Ephemeral2Web.Router
42 |
43 | @doc """
44 | Callback invoked for dynamically configuring the endpoint.
45 |
46 | It receives the endpoint configuration and checks if
47 | configuration should be loaded from the system environment.
48 | """
49 | def init(_key, config) do
50 | if config[:load_from_system_env] do
51 | port = System.get_env("PORT") || raise "expected the PORT environment variable to be set"
52 | {:ok, Keyword.put(config, :http, [:inet6, port: port])}
53 | else
54 | {:ok, config}
55 | end
56 | end
57 | end
58 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/gettext.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.Gettext do
2 | @moduledoc """
3 | A module providing Internationalization with a gettext-based API.
4 |
5 | By using [Gettext](https://hexdocs.pm/gettext),
6 | your module gains a set of macros for translations, for example:
7 |
8 | import Ephemeral2Web.Gettext
9 |
10 | # Simple translation
11 | gettext "Here is the string to translate"
12 |
13 | # Plural translation
14 | ngettext "Here is the string to translate",
15 | "Here are the strings to translate",
16 | 3
17 |
18 | # Domain-based translation
19 | dgettext "errors", "Here is the error message to translate"
20 |
21 | See the [Gettext Docs](https://hexdocs.pm/gettext) for detailed usage.
22 | """
23 | use Gettext, otp_app: :ephemeral2
24 | end
25 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/router.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.Router do
2 | use Ephemeral2Web, :router
3 |
4 | pipeline :browser do
5 | plug :accepts, ["html"]
6 | plug :fetch_session
7 | plug :fetch_flash
8 | plug :protect_from_forgery
9 | plug :put_secure_browser_headers
10 | end
11 |
12 | pipeline :api do
13 | plug :accepts, ["json"]
14 | end
15 |
16 | scope "/", Ephemeral2Web do
17 | pipe_through :browser # Use the default browser stack
18 |
19 | get "/", PageController, :new
20 | get "/:hash", PageController, :show
21 | end
22 |
23 | # Other scopes may use custom stacks.
24 | # scope "/api", Ephemeral2Web do
25 | # pipe_through :api
26 | # end
27 | end
28 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/templates/layout/app.html.eex:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Ephemeral Hosting
11 | ">
12 |
13 |
14 |
15 |
Enter the page content you want below. After you submit, you will be redirected to your new page. That page will live as long as someone, somewhere is viewing it. Once the last person closes their browser, the page will be gone.
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/templates/page/show.html.eex:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/losvedir/ephemeral2/4d48d6b7724127efc4f416ec45c4c2cb28472fc3/lib/ephemeral2_web/templates/page/show.html.eex
--------------------------------------------------------------------------------
/lib/ephemeral2_web/views/error_helpers.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.ErrorHelpers do
2 | @moduledoc """
3 | Conveniences for translating and building error messages.
4 | """
5 |
6 | use Phoenix.HTML
7 |
8 | @doc """
9 | Generates tag for inlined form input errors.
10 | """
11 | def error_tag(form, field) do
12 | Enum.map(Keyword.get_values(form.errors, field), fn (error) ->
13 | content_tag :span, translate_error(error), class: "help-block"
14 | end)
15 | end
16 |
17 | @doc """
18 | Translates an error message using gettext.
19 | """
20 | def translate_error({msg, opts}) do
21 | # Because error messages were defined within Ecto, we must
22 | # call the Gettext module passing our Gettext backend. We
23 | # also use the "errors" domain as translations are placed
24 | # in the errors.po file.
25 | # Ecto will pass the :count keyword if the error message is
26 | # meant to be pluralized.
27 | # On your own code and templates, depending on whether you
28 | # need the message to be pluralized or not, this could be
29 | # written simply as:
30 | #
31 | # dngettext "errors", "1 file", "%{count} files", count
32 | # dgettext "errors", "is invalid"
33 | #
34 | if count = opts[:count] do
35 | Gettext.dngettext(Ephemeral2Web.Gettext, "errors", msg, msg, count, opts)
36 | else
37 | Gettext.dgettext(Ephemeral2Web.Gettext, "errors", msg, opts)
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/views/error_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.ErrorView do
2 | use Ephemeral2Web, :view
3 |
4 | def render("404.html", _assigns) do
5 | "Page not found"
6 | end
7 |
8 | def render("500.html", _assigns) do
9 | "Internal server error"
10 | end
11 |
12 | # In case no render clause matches or no
13 | # template is found, let's render it as 500
14 | def template_not_found(_template, assigns) do
15 | render "500.html", assigns
16 | end
17 | end
18 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/views/layout_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.LayoutView do
2 | use Ephemeral2Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/lib/ephemeral2_web/views/page_view.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.PageView do
2 | use Ephemeral2Web, :view
3 | end
4 |
--------------------------------------------------------------------------------
/mix.exs:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2.Mixfile do
2 | use Mix.Project
3 |
4 | def project do
5 | [
6 | app: :ephemeral2,
7 | version: "0.0.1",
8 | elixir: "~> 1.4",
9 | elixirc_paths: elixirc_paths(Mix.env),
10 | compilers: [:phoenix, :gettext] ++ Mix.compilers,
11 | start_permanent: Mix.env == :prod,
12 | deps: deps()
13 | ]
14 | end
15 |
16 | # Configuration for the OTP application.
17 | #
18 | # Type `mix help compile.app` for more information.
19 | def application do
20 | [
21 | mod: {Ephemeral2.Application, []},
22 | extra_applications: [:logger, :runtime_tools]
23 | ]
24 | end
25 |
26 | # Specifies which paths to compile per environment.
27 | defp elixirc_paths(:test), do: ["lib", "test/support"]
28 | defp elixirc_paths(_), do: ["lib"]
29 |
30 | # Specifies your project dependencies.
31 | #
32 | # Type `mix help deps` for examples and options.
33 | defp deps do
34 | [
35 | {:phoenix, "~> 1.3.0"},
36 | {:phoenix_pubsub, "~> 1.0"},
37 | {:phoenix_html, "~> 2.10"},
38 | {:phoenix_live_reload, "~> 1.0", only: :dev},
39 | {:gettext, "~> 0.11"},
40 | {:cowboy, "~> 1.0"}
41 | ]
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/mix.lock:
--------------------------------------------------------------------------------
1 | %{"cowboy": {:hex, :cowboy, "1.1.2", "61ac29ea970389a88eca5a65601460162d370a70018afe6f949a29dca91f3bb0", [:rebar3], [{:cowlib, "~> 1.0.2", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3.2", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"},
2 | "cowlib": {:hex, :cowlib, "1.0.2", "9d769a1d062c9c3ac753096f868ca121e2730b9a377de23dec0f7e08b1df84ee", [:make], [], "hexpm"},
3 | "file_system": {:hex, :file_system, "0.2.2", "7f1e9de4746f4eb8a4ca8f2fbab582d84a4e40fa394cce7bfcb068b988625b06", [], [], "hexpm"},
4 | "gettext": {:hex, :gettext, "0.13.1", "5e0daf4e7636d771c4c71ad5f3f53ba09a9ae5c250e1ab9c42ba9edccc476263", [], [], "hexpm"},
5 | "mime": {:hex, :mime, "1.1.0", "01c1d6f4083d8aa5c7b8c246ade95139620ef8effb009edde934e0ec3b28090a", [], [], "hexpm"},
6 | "phoenix": {:hex, :phoenix, "1.3.0", "1c01124caa1b4a7af46f2050ff11b267baa3edb441b45dbf243e979cd4c5891b", [:mix], [{:cowboy, "~> 1.0", [hex: :cowboy, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.3.3 or ~> 1.4", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
7 | "phoenix_html": {:hex, :phoenix_html, "2.10.5", "4f9df6b0fb7422a9440a73182a566cb9cbe0e3ffe8884ef9337ccf284fc1ef0a", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
8 | "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.1.3", "1d178429fc8950b12457d09c6afec247bfe1fcb6f36209e18fbb0221bdfe4d41", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.0 or ~> 1.2 or ~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm"},
9 | "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.0.2", "bfa7fd52788b5eaa09cb51ff9fcad1d9edfeb68251add458523f839392f034c1", [], [], "hexpm"},
10 | "plug": {:hex, :plug, "1.4.3", "236d77ce7bf3e3a2668dc0d32a9b6f1f9b1f05361019946aae49874904be4aed", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1", [hex: :cowboy, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm"},
11 | "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
12 | "ranch": {:hex, :ranch, "1.3.2", "e4965a144dc9fbe70e5c077c65e73c57165416a901bd02ea899cfd95aa890986", [:rebar3], [], "hexpm"}}
13 |
--------------------------------------------------------------------------------
/priv/gettext/en/LC_MESSAGES/errors.po:
--------------------------------------------------------------------------------
1 | ## `msgid`s in this file come from POT (.pot) files.
2 | ##
3 | ## Do not add, change, or remove `msgid`s manually here as
4 | ## they're tied to the ones in the corresponding POT file
5 | ## (with the same domain).
6 | ##
7 | ## Use `mix gettext.extract --merge` or `mix gettext.merge`
8 | ## to merge POT files into PO files.
9 | msgid ""
10 | msgstr ""
11 | "Language: en\n"
12 |
--------------------------------------------------------------------------------
/priv/gettext/errors.pot:
--------------------------------------------------------------------------------
1 | ## This file is a PO Template file.
2 | ##
3 | ## `msgid`s here are often extracted from source code.
4 | ## Add new translations manually only if they're dynamic
5 | ## translations that can't be statically extracted.
6 | ##
7 | ## Run `mix gettext.extract` to bring this file up to
8 | ## date. Leave `msgstr`s empty as changing them here as no
9 | ## effect: edit them in PO (`.po`) files instead.
10 |
11 |
--------------------------------------------------------------------------------
/priv/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/losvedir/ephemeral2/4d48d6b7724127efc4f416ec45c4c2cb28472fc3/priv/static/favicon.ico
--------------------------------------------------------------------------------
/priv/static/images/phoenix.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/losvedir/ephemeral2/4d48d6b7724127efc4f416ec45c4c2cb28472fc3/priv/static/images/phoenix.png
--------------------------------------------------------------------------------
/priv/static/js/app.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | 'use strict';
3 |
4 | var globals = typeof global === 'undefined' ? self : global;
5 | if (typeof globals.require === 'function') return;
6 |
7 | var modules = {};
8 | var cache = {};
9 | var aliases = {};
10 | var has = {}.hasOwnProperty;
11 |
12 | var expRe = /^\.\.?(\/|$)/;
13 | var expand = function(root, name) {
14 | var results = [], part;
15 | var parts = (expRe.test(name) ? root + '/' + name : name).split('/');
16 | for (var i = 0, length = parts.length; i < length; i++) {
17 | part = parts[i];
18 | if (part === '..') {
19 | results.pop();
20 | } else if (part !== '.' && part !== '') {
21 | results.push(part);
22 | }
23 | }
24 | return results.join('/');
25 | };
26 |
27 | var dirname = function(path) {
28 | return path.split('/').slice(0, -1).join('/');
29 | };
30 |
31 | var localRequire = function(path) {
32 | return function expanded(name) {
33 | var absolute = expand(dirname(path), name);
34 | return globals.require(absolute, path);
35 | };
36 | };
37 |
38 | var initModule = function(name, definition) {
39 | var hot = hmr && hmr.createHot(name);
40 | var module = {id: name, exports: {}, hot: hot};
41 | cache[name] = module;
42 | definition(module.exports, localRequire(name), module);
43 | return module.exports;
44 | };
45 |
46 | var expandAlias = function(name) {
47 | return aliases[name] ? expandAlias(aliases[name]) : name;
48 | };
49 |
50 | var _resolve = function(name, dep) {
51 | return expandAlias(expand(dirname(name), dep));
52 | };
53 |
54 | var require = function(name, loaderPath) {
55 | if (loaderPath == null) loaderPath = '/';
56 | var path = expandAlias(name);
57 |
58 | if (has.call(cache, path)) return cache[path].exports;
59 | if (has.call(modules, path)) return initModule(path, modules[path]);
60 |
61 | throw new Error("Cannot find module '" + name + "' from '" + loaderPath + "'");
62 | };
63 |
64 | require.alias = function(from, to) {
65 | aliases[to] = from;
66 | };
67 |
68 | var extRe = /\.[^.\/]+$/;
69 | var indexRe = /\/index(\.[^\/]+)?$/;
70 | var addExtensions = function(bundle) {
71 | if (extRe.test(bundle)) {
72 | var alias = bundle.replace(extRe, '');
73 | if (!has.call(aliases, alias) || aliases[alias].replace(extRe, '') === alias + '/index') {
74 | aliases[alias] = bundle;
75 | }
76 | }
77 |
78 | if (indexRe.test(bundle)) {
79 | var iAlias = bundle.replace(indexRe, '');
80 | if (!has.call(aliases, iAlias)) {
81 | aliases[iAlias] = bundle;
82 | }
83 | }
84 | };
85 |
86 | require.register = require.define = function(bundle, fn) {
87 | if (bundle && typeof bundle === 'object') {
88 | for (var key in bundle) {
89 | if (has.call(bundle, key)) {
90 | require.register(key, bundle[key]);
91 | }
92 | }
93 | } else {
94 | modules[bundle] = fn;
95 | delete cache[bundle];
96 | addExtensions(bundle);
97 | }
98 | };
99 |
100 | require.list = function() {
101 | var list = [];
102 | for (var item in modules) {
103 | if (has.call(modules, item)) {
104 | list.push(item);
105 | }
106 | }
107 | return list;
108 | };
109 |
110 | var hmr = globals._hmr && new globals._hmr(_resolve, require, modules, cache);
111 | require._cache = cache;
112 | require.hmr = hmr && hmr.wrap;
113 | require.brunch = true;
114 | globals.require = require;
115 | })();
116 |
117 | (function() {
118 | var global = typeof window === 'undefined' ? this : window;
119 | var __makeRelativeRequire = function(require, mappings, pref) {
120 | var none = {};
121 | var tryReq = function(name, pref) {
122 | var val;
123 | try {
124 | val = require(pref + '/node_modules/' + name);
125 | return val;
126 | } catch (e) {
127 | if (e.toString().indexOf('Cannot find module') === -1) {
128 | throw e;
129 | }
130 |
131 | if (pref.indexOf('node_modules') !== -1) {
132 | var s = pref.split('/');
133 | var i = s.lastIndexOf('node_modules');
134 | var newPref = s.slice(0, i).join('/');
135 | return tryReq(name, newPref);
136 | }
137 | }
138 | return none;
139 | };
140 | return function(name) {
141 | if (name in mappings) name = mappings[name];
142 | if (!name) return;
143 | if (name[0] !== '.' && pref) {
144 | var val = tryReq(name, pref);
145 | if (val !== none) return val;
146 | }
147 | return require(name);
148 | }
149 | };
150 |
151 | require.register("phoenix/priv/static/phoenix.js", function(exports, require, module) {
152 | require = __makeRelativeRequire(require, {}, "phoenix");
153 | (function() {
154 | (function (global, factory) {
155 | typeof exports === 'object' ? factory(exports) :
156 | typeof define === 'function' && define.amd ? define(['exports'], factory) :
157 | factory(global.Phoenix = global.Phoenix || {});
158 | }(this, (function (exports) {
159 | "use strict";
160 |
161 | Object.defineProperty(exports, "__esModule", {
162 | value: true
163 | });
164 |
165 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
166 |
167 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
168 |
169 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
170 |
171 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
172 |
173 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
174 |
175 | /**
176 | * Phoenix Channels JavaScript client
177 | *
178 | * ## Socket Connection
179 | *
180 | * A single connection is established to the server and
181 | * channels are multiplexed over the connection.
182 | * Connect to the server using the `Socket` class:
183 | *
184 | * ```javascript
185 | * let socket = new Socket("/socket", {params: {userToken: "123"}})
186 | * socket.connect()
187 | * ```
188 | *
189 | * The `Socket` constructor takes the mount point of the socket,
190 | * the authentication params, as well as options that can be found in
191 | * the Socket docs, such as configuring the `LongPoll` transport, and
192 | * heartbeat.
193 | *
194 | * ## Channels
195 | *
196 | * Channels are isolated, concurrent processes on the server that
197 | * subscribe to topics and broker events between the client and server.
198 | * To join a channel, you must provide the topic, and channel params for
199 | * authorization. Here's an example chat room example where `"new_msg"`
200 | * events are listened for, messages are pushed to the server, and
201 | * the channel is joined with ok/error/timeout matches:
202 | *
203 | * ```javascript
204 | * let channel = socket.channel("room:123", {token: roomToken})
205 | * channel.on("new_msg", msg => console.log("Got message", msg) )
206 | * $input.onEnter( e => {
207 | * channel.push("new_msg", {body: e.target.val}, 10000)
208 | * .receive("ok", (msg) => console.log("created message", msg) )
209 | * .receive("error", (reasons) => console.log("create failed", reasons) )
210 | * .receive("timeout", () => console.log("Networking issue...") )
211 | * })
212 | * channel.join()
213 | * .receive("ok", ({messages}) => console.log("catching up", messages) )
214 | * .receive("error", ({reason}) => console.log("failed join", reason) )
215 | * .receive("timeout", () => console.log("Networking issue. Still waiting...") )
216 | *```
217 | *
218 | * ## Joining
219 | *
220 | * Creating a channel with `socket.channel(topic, params)`, binds the params to
221 | * `channel.params`, which are sent up on `channel.join()`.
222 | * Subsequent rejoins will send up the modified params for
223 | * updating authorization params, or passing up last_message_id information.
224 | * Successful joins receive an "ok" status, while unsuccessful joins
225 | * receive "error".
226 | *
227 | * ## Duplicate Join Subscriptions
228 | *
229 | * While the client may join any number of topics on any number of channels,
230 | * the client may only hold a single subscription for each unique topic at any
231 | * given time. When attempting to create a duplicate subscription,
232 | * the server will close the existing channel, log a warning, and
233 | * spawn a new channel for the topic. The client will have their
234 | * `channel.onClose` callbacks fired for the existing channel, and the new
235 | * channel join will have its receive hooks processed as normal.
236 | *
237 | * ## Pushing Messages
238 | *
239 | * From the previous example, we can see that pushing messages to the server
240 | * can be done with `channel.push(eventName, payload)` and we can optionally
241 | * receive responses from the push. Additionally, we can use
242 | * `receive("timeout", callback)` to abort waiting for our other `receive` hooks
243 | * and take action after some period of waiting. The default timeout is 5000ms.
244 | *
245 | *
246 | * ## Socket Hooks
247 | *
248 | * Lifecycle events of the multiplexed connection can be hooked into via
249 | * `socket.onError()` and `socket.onClose()` events, ie:
250 | *
251 | * ```javascript
252 | * socket.onError( () => console.log("there was an error with the connection!") )
253 | * socket.onClose( () => console.log("the connection dropped") )
254 | * ```
255 | *
256 | *
257 | * ## Channel Hooks
258 | *
259 | * For each joined channel, you can bind to `onError` and `onClose` events
260 | * to monitor the channel lifecycle, ie:
261 | *
262 | * ```javascript
263 | * channel.onError( () => console.log("there was an error!") )
264 | * channel.onClose( () => console.log("the channel has gone away gracefully") )
265 | * ```
266 | *
267 | * ### onError hooks
268 | *
269 | * `onError` hooks are invoked if the socket connection drops, or the channel
270 | * crashes on the server. In either case, a channel rejoin is attempted
271 | * automatically in an exponential backoff manner.
272 | *
273 | * ### onClose hooks
274 | *
275 | * `onClose` hooks are invoked only in two cases. 1) the channel explicitly
276 | * closed on the server, or 2). The client explicitly closed, by calling
277 | * `channel.leave()`
278 | *
279 | *
280 | * ## Presence
281 | *
282 | * The `Presence` object provides features for syncing presence information
283 | * from the server with the client and handling presences joining and leaving.
284 | *
285 | * ### Syncing initial state from the server
286 | *
287 | * `Presence.syncState` is used to sync the list of presences on the server
288 | * with the client's state. An optional `onJoin` and `onLeave` callback can
289 | * be provided to react to changes in the client's local presences across
290 | * disconnects and reconnects with the server.
291 | *
292 | * `Presence.syncDiff` is used to sync a diff of presence join and leave
293 | * events from the server, as they happen. Like `syncState`, `syncDiff`
294 | * accepts optional `onJoin` and `onLeave` callbacks to react to a user
295 | * joining or leaving from a device.
296 | *
297 | * ### Listing Presences
298 | *
299 | * `Presence.list` is used to return a list of presence information
300 | * based on the local state of metadata. By default, all presence
301 | * metadata is returned, but a `listBy` function can be supplied to
302 | * allow the client to select which metadata to use for a given presence.
303 | * For example, you may have a user online from different devices with
304 | * a metadata status of "online", but they have set themselves to "away"
305 | * on another device. In this case, the app may choose to use the "away"
306 | * status for what appears on the UI. The example below defines a `listBy`
307 | * function which prioritizes the first metadata which was registered for
308 | * each user. This could be the first tab they opened, or the first device
309 | * they came online from:
310 | *
311 | * ```javascript
312 | * let state = {}
313 | * state = Presence.syncState(state, stateFromServer)
314 | * let listBy = (id, {metas: [first, ...rest]}) => {
315 | * first.count = rest.length + 1 // count of this user's presences
316 | * first.id = id
317 | * return first
318 | * }
319 | * let onlineUsers = Presence.list(state, listBy)
320 | * ```
321 | *
322 | *
323 | * ### Example Usage
324 | *```javascript
325 | * // detect if user has joined for the 1st time or from another tab/device
326 | * let onJoin = (id, current, newPres) => {
327 | * if(!current){
328 | * console.log("user has entered for the first time", newPres)
329 | * } else {
330 | * console.log("user additional presence", newPres)
331 | * }
332 | * }
333 | * // detect if user has left from all tabs/devices, or is still present
334 | * let onLeave = (id, current, leftPres) => {
335 | * if(current.metas.length === 0){
336 | * console.log("user has left from all devices", leftPres)
337 | * } else {
338 | * console.log("user left from a device", leftPres)
339 | * }
340 | * }
341 | * let presences = {} // client's initial empty presence state
342 | * // receive initial presence data from server, sent after join
343 | * myChannel.on("presence_state", state => {
344 | * presences = Presence.syncState(presences, state, onJoin, onLeave)
345 | * displayUsers(Presence.list(presences))
346 | * })
347 | * // receive "presence_diff" from server, containing join/leave events
348 | * myChannel.on("presence_diff", diff => {
349 | * presences = Presence.syncDiff(presences, diff, onJoin, onLeave)
350 | * this.setState({users: Presence.list(room.presences, listBy)})
351 | * })
352 | * ```
353 | * @module phoenix
354 | */
355 |
356 | var VSN = "2.0.0";
357 | var SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };
358 | var DEFAULT_TIMEOUT = 10000;
359 | var WS_CLOSE_NORMAL = 1000;
360 | var CHANNEL_STATES = {
361 | closed: "closed",
362 | errored: "errored",
363 | joined: "joined",
364 | joining: "joining",
365 | leaving: "leaving"
366 | };
367 | var CHANNEL_EVENTS = {
368 | close: "phx_close",
369 | error: "phx_error",
370 | join: "phx_join",
371 | reply: "phx_reply",
372 | leave: "phx_leave"
373 | };
374 | var CHANNEL_LIFECYCLE_EVENTS = [CHANNEL_EVENTS.close, CHANNEL_EVENTS.error, CHANNEL_EVENTS.join, CHANNEL_EVENTS.reply, CHANNEL_EVENTS.leave];
375 | var TRANSPORTS = {
376 | longpoll: "longpoll",
377 | websocket: "websocket"
378 | };
379 |
380 | /**
381 | * Initializes the Push
382 | * @param {Channel} channel - The Channel
383 | * @param {string} event - The event, for example `"phx_join"`
384 | * @param {Object} payload - The payload, for example `{user_id: 123}`
385 | * @param {number} timeout - The push timeout in milliseconds
386 | */
387 |
388 | var Push = function () {
389 | function Push(channel, event, payload, timeout) {
390 | _classCallCheck(this, Push);
391 |
392 | this.channel = channel;
393 | this.event = event;
394 | this.payload = payload || {};
395 | this.receivedResp = null;
396 | this.timeout = timeout;
397 | this.timeoutTimer = null;
398 | this.recHooks = [];
399 | this.sent = false;
400 | }
401 |
402 | /**
403 | *
404 | * @param {number} timeout
405 | */
406 |
407 |
408 | _createClass(Push, [{
409 | key: "resend",
410 | value: function resend(timeout) {
411 | this.timeout = timeout;
412 | this.reset();
413 | this.send();
414 | }
415 |
416 | /**
417 | *
418 | */
419 |
420 | }, {
421 | key: "send",
422 | value: function send() {
423 | if (this.hasReceived("timeout")) {
424 | return;
425 | }
426 | this.startTimeout();
427 | this.sent = true;
428 | this.channel.socket.push({
429 | topic: this.channel.topic,
430 | event: this.event,
431 | payload: this.payload,
432 | ref: this.ref,
433 | join_ref: this.channel.joinRef()
434 | });
435 | }
436 |
437 | /**
438 | *
439 | * @param {*} status
440 | * @param {*} callback
441 | */
442 |
443 | }, {
444 | key: "receive",
445 | value: function receive(status, callback) {
446 | if (this.hasReceived(status)) {
447 | callback(this.receivedResp.response);
448 | }
449 |
450 | this.recHooks.push({ status: status, callback: callback });
451 | return this;
452 | }
453 |
454 | // private
455 |
456 | }, {
457 | key: "reset",
458 | value: function reset() {
459 | this.cancelRefEvent();
460 | this.ref = null;
461 | this.refEvent = null;
462 | this.receivedResp = null;
463 | this.sent = false;
464 | }
465 | }, {
466 | key: "matchReceive",
467 | value: function matchReceive(_ref) {
468 | var status = _ref.status,
469 | response = _ref.response,
470 | ref = _ref.ref;
471 |
472 | this.recHooks.filter(function (h) {
473 | return h.status === status;
474 | }).forEach(function (h) {
475 | return h.callback(response);
476 | });
477 | }
478 | }, {
479 | key: "cancelRefEvent",
480 | value: function cancelRefEvent() {
481 | if (!this.refEvent) {
482 | return;
483 | }
484 | this.channel.off(this.refEvent);
485 | }
486 | }, {
487 | key: "cancelTimeout",
488 | value: function cancelTimeout() {
489 | clearTimeout(this.timeoutTimer);
490 | this.timeoutTimer = null;
491 | }
492 | }, {
493 | key: "startTimeout",
494 | value: function startTimeout() {
495 | var _this = this;
496 |
497 | if (this.timeoutTimer) {
498 | this.cancelTimeout();
499 | }
500 | this.ref = this.channel.socket.makeRef();
501 | this.refEvent = this.channel.replyEventName(this.ref);
502 |
503 | this.channel.on(this.refEvent, function (payload) {
504 | _this.cancelRefEvent();
505 | _this.cancelTimeout();
506 | _this.receivedResp = payload;
507 | _this.matchReceive(payload);
508 | });
509 |
510 | this.timeoutTimer = setTimeout(function () {
511 | _this.trigger("timeout", {});
512 | }, this.timeout);
513 | }
514 | }, {
515 | key: "hasReceived",
516 | value: function hasReceived(status) {
517 | return this.receivedResp && this.receivedResp.status === status;
518 | }
519 | }, {
520 | key: "trigger",
521 | value: function trigger(status, response) {
522 | this.channel.trigger(this.refEvent, { status: status, response: response });
523 | }
524 | }]);
525 |
526 | return Push;
527 | }();
528 |
529 | /**
530 | *
531 | * @param {string} topic
532 | * @param {Object} params
533 | * @param {Socket} socket
534 | */
535 |
536 |
537 | var Channel = exports.Channel = function () {
538 | function Channel(topic, params, socket) {
539 | var _this2 = this;
540 |
541 | _classCallCheck(this, Channel);
542 |
543 | this.state = CHANNEL_STATES.closed;
544 | this.topic = topic;
545 | this.params = params || {};
546 | this.socket = socket;
547 | this.bindings = [];
548 | this.timeout = this.socket.timeout;
549 | this.joinedOnce = false;
550 | this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);
551 | this.pushBuffer = [];
552 | this.rejoinTimer = new Timer(function () {
553 | return _this2.rejoinUntilConnected();
554 | }, this.socket.reconnectAfterMs);
555 | this.joinPush.receive("ok", function () {
556 | _this2.state = CHANNEL_STATES.joined;
557 | _this2.rejoinTimer.reset();
558 | _this2.pushBuffer.forEach(function (pushEvent) {
559 | return pushEvent.send();
560 | });
561 | _this2.pushBuffer = [];
562 | });
563 | this.onClose(function () {
564 | _this2.rejoinTimer.reset();
565 | _this2.socket.log("channel", "close " + _this2.topic + " " + _this2.joinRef());
566 | _this2.state = CHANNEL_STATES.closed;
567 | _this2.socket.remove(_this2);
568 | });
569 | this.onError(function (reason) {
570 | if (_this2.isLeaving() || _this2.isClosed()) {
571 | return;
572 | }
573 | _this2.socket.log("channel", "error " + _this2.topic, reason);
574 | _this2.state = CHANNEL_STATES.errored;
575 | _this2.rejoinTimer.scheduleTimeout();
576 | });
577 | this.joinPush.receive("timeout", function () {
578 | if (!_this2.isJoining()) {
579 | return;
580 | }
581 | _this2.socket.log("channel", "timeout " + _this2.topic + " (" + _this2.joinRef() + ")", _this2.joinPush.timeout);
582 | var leavePush = new Push(_this2, CHANNEL_EVENTS.leave, {}, _this2.timeout);
583 | leavePush.send();
584 | _this2.state = CHANNEL_STATES.errored;
585 | _this2.joinPush.reset();
586 | _this2.rejoinTimer.scheduleTimeout();
587 | });
588 | this.on(CHANNEL_EVENTS.reply, function (payload, ref) {
589 | _this2.trigger(_this2.replyEventName(ref), payload);
590 | });
591 | }
592 |
593 | _createClass(Channel, [{
594 | key: "rejoinUntilConnected",
595 | value: function rejoinUntilConnected() {
596 | this.rejoinTimer.scheduleTimeout();
597 | if (this.socket.isConnected()) {
598 | this.rejoin();
599 | }
600 | }
601 | }, {
602 | key: "join",
603 | value: function join() {
604 | var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
605 |
606 | if (this.joinedOnce) {
607 | throw "tried to join multiple times. 'join' can only be called a single time per channel instance";
608 | } else {
609 | this.joinedOnce = true;
610 | this.rejoin(timeout);
611 | return this.joinPush;
612 | }
613 | }
614 | }, {
615 | key: "onClose",
616 | value: function onClose(callback) {
617 | this.on(CHANNEL_EVENTS.close, callback);
618 | }
619 | }, {
620 | key: "onError",
621 | value: function onError(callback) {
622 | this.on(CHANNEL_EVENTS.error, function (reason) {
623 | return callback(reason);
624 | });
625 | }
626 | }, {
627 | key: "on",
628 | value: function on(event, callback) {
629 | this.bindings.push({ event: event, callback: callback });
630 | }
631 | }, {
632 | key: "off",
633 | value: function off(event) {
634 | this.bindings = this.bindings.filter(function (bind) {
635 | return bind.event !== event;
636 | });
637 | }
638 | }, {
639 | key: "canPush",
640 | value: function canPush() {
641 | return this.socket.isConnected() && this.isJoined();
642 | }
643 | }, {
644 | key: "push",
645 | value: function push(event, payload) {
646 | var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.timeout;
647 |
648 | if (!this.joinedOnce) {
649 | throw "tried to push '" + event + "' to '" + this.topic + "' before joining. Use channel.join() before pushing events";
650 | }
651 | var pushEvent = new Push(this, event, payload, timeout);
652 | if (this.canPush()) {
653 | pushEvent.send();
654 | } else {
655 | pushEvent.startTimeout();
656 | this.pushBuffer.push(pushEvent);
657 | }
658 |
659 | return pushEvent;
660 | }
661 |
662 | /** Leaves the channel
663 | *
664 | * Unsubscribes from server events, and
665 | * instructs channel to terminate on server
666 | *
667 | * Triggers onClose() hooks
668 | *
669 | * To receive leave acknowledgements, use the a `receive`
670 | * hook to bind to the server ack, ie:
671 | *
672 | * ```javascript
673 | * channel.leave().receive("ok", () => alert("left!") )
674 | * ```
675 | */
676 |
677 | }, {
678 | key: "leave",
679 | value: function leave() {
680 | var _this3 = this;
681 |
682 | var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
683 |
684 | this.state = CHANNEL_STATES.leaving;
685 | var onClose = function onClose() {
686 | _this3.socket.log("channel", "leave " + _this3.topic);
687 | _this3.trigger(CHANNEL_EVENTS.close, "leave");
688 | };
689 | var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);
690 | leavePush.receive("ok", function () {
691 | return onClose();
692 | }).receive("timeout", function () {
693 | return onClose();
694 | });
695 | leavePush.send();
696 | if (!this.canPush()) {
697 | leavePush.trigger("ok", {});
698 | }
699 |
700 | return leavePush;
701 | }
702 |
703 | /**
704 | * Overridable message hook
705 | *
706 | * Receives all events for specialized message handling
707 | * before dispatching to the channel callbacks.
708 | *
709 | * Must return the payload, modified or unmodified
710 | */
711 |
712 | }, {
713 | key: "onMessage",
714 | value: function onMessage(event, payload, ref) {
715 | return payload;
716 | }
717 |
718 | // private
719 |
720 | }, {
721 | key: "isMember",
722 | value: function isMember(topic, event, payload, joinRef) {
723 | if (this.topic !== topic) {
724 | return false;
725 | }
726 | var isLifecycleEvent = CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0;
727 |
728 | if (joinRef && isLifecycleEvent && joinRef !== this.joinRef()) {
729 | this.socket.log("channel", "dropping outdated message", { topic: topic, event: event, payload: payload, joinRef: joinRef });
730 | return false;
731 | } else {
732 | return true;
733 | }
734 | }
735 | }, {
736 | key: "joinRef",
737 | value: function joinRef() {
738 | return this.joinPush.ref;
739 | }
740 | }, {
741 | key: "sendJoin",
742 | value: function sendJoin(timeout) {
743 | this.state = CHANNEL_STATES.joining;
744 | this.joinPush.resend(timeout);
745 | }
746 | }, {
747 | key: "rejoin",
748 | value: function rejoin() {
749 | var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;
750 | if (this.isLeaving()) {
751 | return;
752 | }
753 | this.sendJoin(timeout);
754 | }
755 | }, {
756 | key: "trigger",
757 | value: function trigger(event, payload, ref, joinRef) {
758 | var _this4 = this;
759 |
760 | var handledPayload = this.onMessage(event, payload, ref, joinRef);
761 | if (payload && !handledPayload) {
762 | throw "channel onMessage callbacks must return the payload, modified or unmodified";
763 | }
764 |
765 | this.bindings.filter(function (bind) {
766 | return bind.event === event;
767 | }).map(function (bind) {
768 | return bind.callback(handledPayload, ref, joinRef || _this4.joinRef());
769 | });
770 | }
771 | }, {
772 | key: "replyEventName",
773 | value: function replyEventName(ref) {
774 | return "chan_reply_" + ref;
775 | }
776 | }, {
777 | key: "isClosed",
778 | value: function isClosed() {
779 | return this.state === CHANNEL_STATES.closed;
780 | }
781 | }, {
782 | key: "isErrored",
783 | value: function isErrored() {
784 | return this.state === CHANNEL_STATES.errored;
785 | }
786 | }, {
787 | key: "isJoined",
788 | value: function isJoined() {
789 | return this.state === CHANNEL_STATES.joined;
790 | }
791 | }, {
792 | key: "isJoining",
793 | value: function isJoining() {
794 | return this.state === CHANNEL_STATES.joining;
795 | }
796 | }, {
797 | key: "isLeaving",
798 | value: function isLeaving() {
799 | return this.state === CHANNEL_STATES.leaving;
800 | }
801 | }]);
802 |
803 | return Channel;
804 | }();
805 |
806 | var Serializer = {
807 | encode: function encode(msg, callback) {
808 | var payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload];
809 | return callback(JSON.stringify(payload));
810 | },
811 | decode: function decode(rawPayload, callback) {
812 | var _JSON$parse = JSON.parse(rawPayload),
813 | _JSON$parse2 = _slicedToArray(_JSON$parse, 5),
814 | join_ref = _JSON$parse2[0],
815 | ref = _JSON$parse2[1],
816 | topic = _JSON$parse2[2],
817 | event = _JSON$parse2[3],
818 | payload = _JSON$parse2[4];
819 |
820 | return callback({ join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload });
821 | }
822 | };
823 |
824 | /** Initializes the Socket
825 | *
826 | *
827 | * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)
828 | *
829 | * @param {string} endPoint - The string WebSocket endpoint, ie, `"ws://example.com/socket"`,
830 | * `"wss://example.com"`
831 | * `"/socket"` (inherited host & protocol)
832 | * @param {Object} opts - Optional configuration
833 | * @param {string} opts.transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.
834 | *
835 | * Defaults to WebSocket with automatic LongPoll fallback.
836 | * @param {Function} opts.encode - The function to encode outgoing messages.
837 | *
838 | * Defaults to JSON:
839 | *
840 | * ```javascript
841 | * (payload, callback) => callback(JSON.stringify(payload))
842 | * ```
843 | *
844 | * @param {Function} opts.decode - The function to decode incoming messages.
845 | *
846 | * Defaults to JSON:
847 | *
848 | * ```javascript
849 | * (payload, callback) => callback(JSON.parse(payload))
850 | * ```
851 | *
852 | * @param {number} opts.timeout - The default timeout in milliseconds to trigger push timeouts.
853 | *
854 | * Defaults `DEFAULT_TIMEOUT`
855 | * @param {number} opts.heartbeatIntervalMs - The millisec interval to send a heartbeat message
856 | * @param {number} opts.reconnectAfterMs - The optional function that returns the millsec reconnect interval.
857 | *
858 | * Defaults to stepped backoff of:
859 | *
860 | * ```javascript
861 | * function(tries){
862 | * return [1000, 5000, 10000][tries - 1] || 10000
863 | * }
864 | * ```
865 | * @param {Function} opts.logger - The optional function for specialized logging, ie:
866 | * ```javascript
867 | * logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }
868 | * ```
869 | *
870 | * @param {number} opts.longpollerTimeout - The maximum timeout of a long poll AJAX request.
871 | *
872 | * Defaults to 20s (double the server long poll timer).
873 | *
874 | * @param {Object} opts.params - The optional params to pass when connecting
875 | *
876 | *
877 | */
878 |
879 | var Socket = exports.Socket = function () {
880 | function Socket(endPoint) {
881 | var _this5 = this;
882 |
883 | var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
884 |
885 | _classCallCheck(this, Socket);
886 |
887 | this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };
888 | this.channels = [];
889 | this.sendBuffer = [];
890 | this.ref = 0;
891 | this.timeout = opts.timeout || DEFAULT_TIMEOUT;
892 | this.transport = opts.transport || window.WebSocket || LongPoll;
893 | this.defaultEncoder = Serializer.encode;
894 | this.defaultDecoder = Serializer.decode;
895 | if (this.transport !== LongPoll) {
896 | this.encode = opts.encode || this.defaultEncoder;
897 | this.decode = opts.decode || this.defaultDecoder;
898 | } else {
899 | this.encode = this.defaultEncoder;
900 | this.decode = this.defaultDecoder;
901 | }
902 | this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;
903 | this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) {
904 | return [1000, 2000, 5000, 10000][tries - 1] || 10000;
905 | };
906 | this.logger = opts.logger || function () {}; // noop
907 | this.longpollerTimeout = opts.longpollerTimeout || 20000;
908 | this.params = opts.params || {};
909 | this.endPoint = endPoint + "/" + TRANSPORTS.websocket;
910 | this.heartbeatTimer = null;
911 | this.pendingHeartbeatRef = null;
912 | this.reconnectTimer = new Timer(function () {
913 | _this5.disconnect(function () {
914 | return _this5.connect();
915 | });
916 | }, this.reconnectAfterMs);
917 | }
918 |
919 | _createClass(Socket, [{
920 | key: "protocol",
921 | value: function protocol() {
922 | return location.protocol.match(/^https/) ? "wss" : "ws";
923 | }
924 | }, {
925 | key: "endPointURL",
926 | value: function endPointURL() {
927 | var uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params), { vsn: VSN });
928 | if (uri.charAt(0) !== "/") {
929 | return uri;
930 | }
931 | if (uri.charAt(1) === "/") {
932 | return this.protocol() + ":" + uri;
933 | }
934 |
935 | return this.protocol() + "://" + location.host + uri;
936 | }
937 | }, {
938 | key: "disconnect",
939 | value: function disconnect(callback, code, reason) {
940 | if (this.conn) {
941 | this.conn.onclose = function () {}; // noop
942 | if (code) {
943 | this.conn.close(code, reason || "");
944 | } else {
945 | this.conn.close();
946 | }
947 | this.conn = null;
948 | }
949 | callback && callback();
950 | }
951 |
952 | /**
953 | *
954 | * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`
955 | */
956 |
957 | }, {
958 | key: "connect",
959 | value: function connect(params) {
960 | var _this6 = this;
961 |
962 | if (params) {
963 | console && console.log("passing params to connect is deprecated. Instead pass :params to the Socket constructor");
964 | this.params = params;
965 | }
966 | if (this.conn) {
967 | return;
968 | }
969 |
970 | this.conn = new this.transport(this.endPointURL());
971 | this.conn.timeout = this.longpollerTimeout;
972 | this.conn.onopen = function () {
973 | return _this6.onConnOpen();
974 | };
975 | this.conn.onerror = function (error) {
976 | return _this6.onConnError(error);
977 | };
978 | this.conn.onmessage = function (event) {
979 | return _this6.onConnMessage(event);
980 | };
981 | this.conn.onclose = function (event) {
982 | return _this6.onConnClose(event);
983 | };
984 | }
985 |
986 | /**
987 | * Logs the message. Override `this.logger` for specialized logging. noops by default
988 | * @param {string} kind
989 | * @param {string} msg
990 | * @param {Object} data
991 | */
992 |
993 | }, {
994 | key: "log",
995 | value: function log(kind, msg, data) {
996 | this.logger(kind, msg, data);
997 | }
998 |
999 | // Registers callbacks for connection state change events
1000 | //
1001 | // Examples
1002 | //
1003 | // socket.onError(function(error){ alert("An error occurred") })
1004 | //
1005 |
1006 | }, {
1007 | key: "onOpen",
1008 | value: function onOpen(callback) {
1009 | this.stateChangeCallbacks.open.push(callback);
1010 | }
1011 | }, {
1012 | key: "onClose",
1013 | value: function onClose(callback) {
1014 | this.stateChangeCallbacks.close.push(callback);
1015 | }
1016 | }, {
1017 | key: "onError",
1018 | value: function onError(callback) {
1019 | this.stateChangeCallbacks.error.push(callback);
1020 | }
1021 | }, {
1022 | key: "onMessage",
1023 | value: function onMessage(callback) {
1024 | this.stateChangeCallbacks.message.push(callback);
1025 | }
1026 | }, {
1027 | key: "onConnOpen",
1028 | value: function onConnOpen() {
1029 | var _this7 = this;
1030 |
1031 | this.log("transport", "connected to " + this.endPointURL());
1032 | this.flushSendBuffer();
1033 | this.reconnectTimer.reset();
1034 | if (!this.conn.skipHeartbeat) {
1035 | clearInterval(this.heartbeatTimer);
1036 | this.heartbeatTimer = setInterval(function () {
1037 | return _this7.sendHeartbeat();
1038 | }, this.heartbeatIntervalMs);
1039 | }
1040 | this.stateChangeCallbacks.open.forEach(function (callback) {
1041 | return callback();
1042 | });
1043 | }
1044 | }, {
1045 | key: "onConnClose",
1046 | value: function onConnClose(event) {
1047 | this.log("transport", "close", event);
1048 | this.triggerChanError();
1049 | clearInterval(this.heartbeatTimer);
1050 | this.reconnectTimer.scheduleTimeout();
1051 | this.stateChangeCallbacks.close.forEach(function (callback) {
1052 | return callback(event);
1053 | });
1054 | }
1055 | }, {
1056 | key: "onConnError",
1057 | value: function onConnError(error) {
1058 | this.log("transport", error);
1059 | this.triggerChanError();
1060 | this.stateChangeCallbacks.error.forEach(function (callback) {
1061 | return callback(error);
1062 | });
1063 | }
1064 | }, {
1065 | key: "triggerChanError",
1066 | value: function triggerChanError() {
1067 | this.channels.forEach(function (channel) {
1068 | return channel.trigger(CHANNEL_EVENTS.error);
1069 | });
1070 | }
1071 | }, {
1072 | key: "connectionState",
1073 | value: function connectionState() {
1074 | switch (this.conn && this.conn.readyState) {
1075 | case SOCKET_STATES.connecting:
1076 | return "connecting";
1077 | case SOCKET_STATES.open:
1078 | return "open";
1079 | case SOCKET_STATES.closing:
1080 | return "closing";
1081 | default:
1082 | return "closed";
1083 | }
1084 | }
1085 | }, {
1086 | key: "isConnected",
1087 | value: function isConnected() {
1088 | return this.connectionState() === "open";
1089 | }
1090 | }, {
1091 | key: "remove",
1092 | value: function remove(channel) {
1093 | this.channels = this.channels.filter(function (c) {
1094 | return c.joinRef() !== channel.joinRef();
1095 | });
1096 | }
1097 | }, {
1098 | key: "channel",
1099 | value: function channel(topic) {
1100 | var chanParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
1101 |
1102 | var chan = new Channel(topic, chanParams, this);
1103 | this.channels.push(chan);
1104 | return chan;
1105 | }
1106 | }, {
1107 | key: "push",
1108 | value: function push(data) {
1109 | var _this8 = this;
1110 |
1111 | var topic = data.topic,
1112 | event = data.event,
1113 | payload = data.payload,
1114 | ref = data.ref,
1115 | join_ref = data.join_ref;
1116 |
1117 | var callback = function callback() {
1118 | _this8.encode(data, function (result) {
1119 | _this8.conn.send(result);
1120 | });
1121 | };
1122 | this.log("push", topic + " " + event + " (" + join_ref + ", " + ref + ")", payload);
1123 | if (this.isConnected()) {
1124 | callback();
1125 | } else {
1126 | this.sendBuffer.push(callback);
1127 | }
1128 | }
1129 |
1130 | /**
1131 | * Return the next message ref, accounting for overflows
1132 | */
1133 |
1134 | }, {
1135 | key: "makeRef",
1136 | value: function makeRef() {
1137 | var newRef = this.ref + 1;
1138 | if (newRef === this.ref) {
1139 | this.ref = 0;
1140 | } else {
1141 | this.ref = newRef;
1142 | }
1143 |
1144 | return this.ref.toString();
1145 | }
1146 | }, {
1147 | key: "sendHeartbeat",
1148 | value: function sendHeartbeat() {
1149 | if (!this.isConnected()) {
1150 | return;
1151 | }
1152 | if (this.pendingHeartbeatRef) {
1153 | this.pendingHeartbeatRef = null;
1154 | this.log("transport", "heartbeat timeout. Attempting to re-establish connection");
1155 | this.conn.close(WS_CLOSE_NORMAL, "hearbeat timeout");
1156 | return;
1157 | }
1158 | this.pendingHeartbeatRef = this.makeRef();
1159 | this.push({ topic: "phoenix", event: "heartbeat", payload: {}, ref: this.pendingHeartbeatRef });
1160 | }
1161 | }, {
1162 | key: "flushSendBuffer",
1163 | value: function flushSendBuffer() {
1164 | if (this.isConnected() && this.sendBuffer.length > 0) {
1165 | this.sendBuffer.forEach(function (callback) {
1166 | return callback();
1167 | });
1168 | this.sendBuffer = [];
1169 | }
1170 | }
1171 | }, {
1172 | key: "onConnMessage",
1173 | value: function onConnMessage(rawMessage) {
1174 | var _this9 = this;
1175 |
1176 | this.decode(rawMessage.data, function (msg) {
1177 | var topic = msg.topic,
1178 | event = msg.event,
1179 | payload = msg.payload,
1180 | ref = msg.ref,
1181 | join_ref = msg.join_ref;
1182 |
1183 | if (ref && ref === _this9.pendingHeartbeatRef) {
1184 | _this9.pendingHeartbeatRef = null;
1185 | }
1186 |
1187 | _this9.log("receive", (payload.status || "") + " " + topic + " " + event + " " + (ref && "(" + ref + ")" || ""), payload);
1188 | _this9.channels.filter(function (channel) {
1189 | return channel.isMember(topic, event, payload, join_ref);
1190 | }).forEach(function (channel) {
1191 | return channel.trigger(event, payload, ref, join_ref);
1192 | });
1193 | _this9.stateChangeCallbacks.message.forEach(function (callback) {
1194 | return callback(msg);
1195 | });
1196 | });
1197 | }
1198 | }]);
1199 |
1200 | return Socket;
1201 | }();
1202 |
1203 | var LongPoll = exports.LongPoll = function () {
1204 | function LongPoll(endPoint) {
1205 | _classCallCheck(this, LongPoll);
1206 |
1207 | this.endPoint = null;
1208 | this.token = null;
1209 | this.skipHeartbeat = true;
1210 | this.onopen = function () {}; // noop
1211 | this.onerror = function () {}; // noop
1212 | this.onmessage = function () {}; // noop
1213 | this.onclose = function () {}; // noop
1214 | this.pollEndpoint = this.normalizeEndpoint(endPoint);
1215 | this.readyState = SOCKET_STATES.connecting;
1216 |
1217 | this.poll();
1218 | }
1219 |
1220 | _createClass(LongPoll, [{
1221 | key: "normalizeEndpoint",
1222 | value: function normalizeEndpoint(endPoint) {
1223 | return endPoint.replace("ws://", "http://").replace("wss://", "https://").replace(new RegExp("(.*)\/" + TRANSPORTS.websocket), "$1/" + TRANSPORTS.longpoll);
1224 | }
1225 | }, {
1226 | key: "endpointURL",
1227 | value: function endpointURL() {
1228 | return Ajax.appendParams(this.pollEndpoint, { token: this.token });
1229 | }
1230 | }, {
1231 | key: "closeAndRetry",
1232 | value: function closeAndRetry() {
1233 | this.close();
1234 | this.readyState = SOCKET_STATES.connecting;
1235 | }
1236 | }, {
1237 | key: "ontimeout",
1238 | value: function ontimeout() {
1239 | this.onerror("timeout");
1240 | this.closeAndRetry();
1241 | }
1242 | }, {
1243 | key: "poll",
1244 | value: function poll() {
1245 | var _this10 = this;
1246 |
1247 | if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) {
1248 | return;
1249 | }
1250 |
1251 | Ajax.request("GET", this.endpointURL(), "application/json", null, this.timeout, this.ontimeout.bind(this), function (resp) {
1252 | if (resp) {
1253 | var status = resp.status,
1254 | token = resp.token,
1255 | messages = resp.messages;
1256 |
1257 | _this10.token = token;
1258 | } else {
1259 | var status = 0;
1260 | }
1261 |
1262 | switch (status) {
1263 | case 200:
1264 | messages.forEach(function (msg) {
1265 | return _this10.onmessage({ data: msg });
1266 | });
1267 | _this10.poll();
1268 | break;
1269 | case 204:
1270 | _this10.poll();
1271 | break;
1272 | case 410:
1273 | _this10.readyState = SOCKET_STATES.open;
1274 | _this10.onopen();
1275 | _this10.poll();
1276 | break;
1277 | case 0:
1278 | case 500:
1279 | _this10.onerror();
1280 | _this10.closeAndRetry();
1281 | break;
1282 | default:
1283 | throw "unhandled poll status " + status;
1284 | }
1285 | });
1286 | }
1287 | }, {
1288 | key: "send",
1289 | value: function send(body) {
1290 | var _this11 = this;
1291 |
1292 | Ajax.request("POST", this.endpointURL(), "application/json", body, this.timeout, this.onerror.bind(this, "timeout"), function (resp) {
1293 | if (!resp || resp.status !== 200) {
1294 | _this11.onerror(resp && resp.status);
1295 | _this11.closeAndRetry();
1296 | }
1297 | });
1298 | }
1299 | }, {
1300 | key: "close",
1301 | value: function close(code, reason) {
1302 | this.readyState = SOCKET_STATES.closed;
1303 | this.onclose();
1304 | }
1305 | }]);
1306 |
1307 | return LongPoll;
1308 | }();
1309 |
1310 | var Ajax = exports.Ajax = function () {
1311 | function Ajax() {
1312 | _classCallCheck(this, Ajax);
1313 | }
1314 |
1315 | _createClass(Ajax, null, [{
1316 | key: "request",
1317 | value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {
1318 | if (window.XDomainRequest) {
1319 | var req = new XDomainRequest(); // IE8, IE9
1320 | this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);
1321 | } else {
1322 | var _req = window.XMLHttpRequest ? new window.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari
1323 | new ActiveXObject("Microsoft.XMLHTTP"); // IE6, IE5
1324 | this.xhrRequest(_req, method, endPoint, accept, body, timeout, ontimeout, callback);
1325 | }
1326 | }
1327 | }, {
1328 | key: "xdomainRequest",
1329 | value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {
1330 | var _this12 = this;
1331 |
1332 | req.timeout = timeout;
1333 | req.open(method, endPoint);
1334 | req.onload = function () {
1335 | var response = _this12.parseJSON(req.responseText);
1336 | callback && callback(response);
1337 | };
1338 | if (ontimeout) {
1339 | req.ontimeout = ontimeout;
1340 | }
1341 |
1342 | // Work around bug in IE9 that requires an attached onprogress handler
1343 | req.onprogress = function () {};
1344 |
1345 | req.send(body);
1346 | }
1347 | }, {
1348 | key: "xhrRequest",
1349 | value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {
1350 | var _this13 = this;
1351 |
1352 | req.open(method, endPoint, true);
1353 | req.timeout = timeout;
1354 | req.setRequestHeader("Content-Type", accept);
1355 | req.onerror = function () {
1356 | callback && callback(null);
1357 | };
1358 | req.onreadystatechange = function () {
1359 | if (req.readyState === _this13.states.complete && callback) {
1360 | var response = _this13.parseJSON(req.responseText);
1361 | callback(response);
1362 | }
1363 | };
1364 | if (ontimeout) {
1365 | req.ontimeout = ontimeout;
1366 | }
1367 |
1368 | req.send(body);
1369 | }
1370 | }, {
1371 | key: "parseJSON",
1372 | value: function parseJSON(resp) {
1373 | if (!resp || resp === "") {
1374 | return null;
1375 | }
1376 |
1377 | try {
1378 | return JSON.parse(resp);
1379 | } catch (e) {
1380 | console && console.log("failed to parse JSON response", resp);
1381 | return null;
1382 | }
1383 | }
1384 | }, {
1385 | key: "serialize",
1386 | value: function serialize(obj, parentKey) {
1387 | var queryStr = [];
1388 | for (var key in obj) {
1389 | if (!obj.hasOwnProperty(key)) {
1390 | continue;
1391 | }
1392 | var paramKey = parentKey ? parentKey + "[" + key + "]" : key;
1393 | var paramVal = obj[key];
1394 | if ((typeof paramVal === "undefined" ? "undefined" : _typeof(paramVal)) === "object") {
1395 | queryStr.push(this.serialize(paramVal, paramKey));
1396 | } else {
1397 | queryStr.push(encodeURIComponent(paramKey) + "=" + encodeURIComponent(paramVal));
1398 | }
1399 | }
1400 | return queryStr.join("&");
1401 | }
1402 | }, {
1403 | key: "appendParams",
1404 | value: function appendParams(url, params) {
1405 | if (Object.keys(params).length === 0) {
1406 | return url;
1407 | }
1408 |
1409 | var prefix = url.match(/\?/) ? "&" : "?";
1410 | return "" + url + prefix + this.serialize(params);
1411 | }
1412 | }]);
1413 |
1414 | return Ajax;
1415 | }();
1416 |
1417 | Ajax.states = { complete: 4 };
1418 |
1419 | var Presence = exports.Presence = {
1420 | syncState: function syncState(currentState, newState, onJoin, onLeave) {
1421 | var _this14 = this;
1422 |
1423 | var state = this.clone(currentState);
1424 | var joins = {};
1425 | var leaves = {};
1426 |
1427 | this.map(state, function (key, presence) {
1428 | if (!newState[key]) {
1429 | leaves[key] = presence;
1430 | }
1431 | });
1432 | this.map(newState, function (key, newPresence) {
1433 | var currentPresence = state[key];
1434 | if (currentPresence) {
1435 | var newRefs = newPresence.metas.map(function (m) {
1436 | return m.phx_ref;
1437 | });
1438 | var curRefs = currentPresence.metas.map(function (m) {
1439 | return m.phx_ref;
1440 | });
1441 | var joinedMetas = newPresence.metas.filter(function (m) {
1442 | return curRefs.indexOf(m.phx_ref) < 0;
1443 | });
1444 | var leftMetas = currentPresence.metas.filter(function (m) {
1445 | return newRefs.indexOf(m.phx_ref) < 0;
1446 | });
1447 | if (joinedMetas.length > 0) {
1448 | joins[key] = newPresence;
1449 | joins[key].metas = joinedMetas;
1450 | }
1451 | if (leftMetas.length > 0) {
1452 | leaves[key] = _this14.clone(currentPresence);
1453 | leaves[key].metas = leftMetas;
1454 | }
1455 | } else {
1456 | joins[key] = newPresence;
1457 | }
1458 | });
1459 | return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave);
1460 | },
1461 | syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) {
1462 | var joins = _ref2.joins,
1463 | leaves = _ref2.leaves;
1464 |
1465 | var state = this.clone(currentState);
1466 | if (!onJoin) {
1467 | onJoin = function onJoin() {};
1468 | }
1469 | if (!onLeave) {
1470 | onLeave = function onLeave() {};
1471 | }
1472 |
1473 | this.map(joins, function (key, newPresence) {
1474 | var currentPresence = state[key];
1475 | state[key] = newPresence;
1476 | if (currentPresence) {
1477 | var _state$key$metas;
1478 |
1479 | (_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas));
1480 | }
1481 | onJoin(key, currentPresence, newPresence);
1482 | });
1483 | this.map(leaves, function (key, leftPresence) {
1484 | var currentPresence = state[key];
1485 | if (!currentPresence) {
1486 | return;
1487 | }
1488 | var refsToRemove = leftPresence.metas.map(function (m) {
1489 | return m.phx_ref;
1490 | });
1491 | currentPresence.metas = currentPresence.metas.filter(function (p) {
1492 | return refsToRemove.indexOf(p.phx_ref) < 0;
1493 | });
1494 | onLeave(key, currentPresence, leftPresence);
1495 | if (currentPresence.metas.length === 0) {
1496 | delete state[key];
1497 | }
1498 | });
1499 | return state;
1500 | },
1501 | list: function list(presences, chooser) {
1502 | if (!chooser) {
1503 | chooser = function chooser(key, pres) {
1504 | return pres;
1505 | };
1506 | }
1507 |
1508 | return this.map(presences, function (key, presence) {
1509 | return chooser(key, presence);
1510 | });
1511 | },
1512 |
1513 |
1514 | // private
1515 |
1516 | map: function map(obj, func) {
1517 | return Object.getOwnPropertyNames(obj).map(function (key) {
1518 | return func(key, obj[key]);
1519 | });
1520 | },
1521 | clone: function clone(obj) {
1522 | return JSON.parse(JSON.stringify(obj));
1523 | }
1524 | };
1525 |
1526 | /**
1527 | *
1528 | * Creates a timer that accepts a `timerCalc` function to perform
1529 | * calculated timeout retries, such as exponential backoff.
1530 | *
1531 | * ## Examples
1532 | *
1533 | * ```javascript
1534 | * let reconnectTimer = new Timer(() => this.connect(), function(tries){
1535 | * return [1000, 5000, 10000][tries - 1] || 10000
1536 | * })
1537 | * reconnectTimer.scheduleTimeout() // fires after 1000
1538 | * reconnectTimer.scheduleTimeout() // fires after 5000
1539 | * reconnectTimer.reset()
1540 | * reconnectTimer.scheduleTimeout() // fires after 1000
1541 | * ```
1542 | * @param {Function} callback
1543 | * @param {Function} timerCalc
1544 | */
1545 |
1546 | var Timer = function () {
1547 | function Timer(callback, timerCalc) {
1548 | _classCallCheck(this, Timer);
1549 |
1550 | this.callback = callback;
1551 | this.timerCalc = timerCalc;
1552 | this.timer = null;
1553 | this.tries = 0;
1554 | }
1555 |
1556 | _createClass(Timer, [{
1557 | key: "reset",
1558 | value: function reset() {
1559 | this.tries = 0;
1560 | clearTimeout(this.timer);
1561 | }
1562 |
1563 | /**
1564 | * Cancels any previous scheduleTimeout and schedules callback
1565 | */
1566 |
1567 | }, {
1568 | key: "scheduleTimeout",
1569 | value: function scheduleTimeout() {
1570 | var _this15 = this;
1571 |
1572 | clearTimeout(this.timer);
1573 |
1574 | this.timer = setTimeout(function () {
1575 | _this15.tries = _this15.tries + 1;
1576 | _this15.callback();
1577 | }, this.timerCalc(this.tries + 1));
1578 | }
1579 | }]);
1580 |
1581 | return Timer;
1582 | }();
1583 |
1584 | })));
1585 | })();
1586 | });
1587 |
1588 | require.register("phoenix_html/priv/static/phoenix_html.js", function(exports, require, module) {
1589 | require = __makeRelativeRequire(require, {}, "phoenix_html");
1590 | (function() {
1591 | "use strict";
1592 |
1593 | (function() {
1594 | function buildHiddenInput(name, value) {
1595 | var input = document.createElement("input");
1596 | input.type = "hidden";
1597 | input.name = name;
1598 | input.value = value;
1599 | return input;
1600 | }
1601 |
1602 | function handleLinkClick(link) {
1603 | var message = link.getAttribute("data-confirm");
1604 | if(message && !window.confirm(message)) {
1605 | return;
1606 | }
1607 |
1608 | var to = link.getAttribute("data-to"),
1609 | method = buildHiddenInput("_method", link.getAttribute("data-method")),
1610 | csrf = buildHiddenInput("_csrf_token", link.getAttribute("data-csrf")),
1611 | form = document.createElement("form");
1612 |
1613 | form.method = (link.getAttribute("data-method") === "get") ? "get" : "post";
1614 | form.action = to;
1615 | form.style.display = "hidden";
1616 |
1617 | form.appendChild(csrf);
1618 | form.appendChild(method);
1619 | document.body.appendChild(form);
1620 | form.submit();
1621 | }
1622 |
1623 | window.addEventListener("click", function(e) {
1624 | var element = e.target;
1625 |
1626 | while (element && element.getAttribute) {
1627 | if(element.getAttribute("data-method")) {
1628 | handleLinkClick(element);
1629 | e.preventDefault();
1630 | return false;
1631 | } else {
1632 | element = element.parentNode;
1633 | }
1634 | }
1635 | }, false);
1636 | })();
1637 | })();
1638 | });
1639 | require.register("js/app.js", function(exports, require, module) {
1640 | "use strict";
1641 |
1642 | Object.defineProperty(exports, "__esModule", {
1643 | value: true
1644 | });
1645 |
1646 | require("phoenix_html");
1647 |
1648 | var _phoenix = require("phoenix");
1649 |
1650 | var _web_console = require("./web_console");
1651 |
1652 | var _web_console2 = _interopRequireDefault(_web_console);
1653 |
1654 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
1655 |
1656 | // Import local files
1657 | //
1658 | // Local files can be imported directly using relative
1659 | // paths "./socket" or full ones "web/static/js/socket".
1660 |
1661 | // import socket from "./socket"
1662 |
1663 | var hash = void 0; // Brunch automatically concatenates all files in your
1664 | // watched paths. Those paths can be configured at
1665 | // config.paths.watched in "brunch-config.js".
1666 | //
1667 | // However, those files will only be executed if
1668 | // explicitly imported. The only exception are files
1669 | // in vendor, which are never wrapped in imports and
1670 | // therefore are always executed.
1671 |
1672 | // Import dependencies
1673 | //
1674 | // If you no longer want to use a dependency, remember
1675 | // to also remove its path from "config.paths.watched".
1676 |
1677 | var content = void 0;
1678 | var webConsole = void 0;
1679 |
1680 | document.addEventListener("DOMContentLoaded", function () {
1681 | webConsole = new _web_console2.default(document.getElementById('js-console'));
1682 | var homePageElement = document.getElementById("create-new-page");
1683 | var showPageElement = document.getElementById("content-goes-here");
1684 |
1685 | webConsole.log("Connecting to websocket.");
1686 | var socket = new _phoenix.Socket("/ws");
1687 | socket.connect();
1688 |
1689 | var chan = socket.channel("all", {});
1690 | chan.join().receive("ok", function () {
1691 | webConsole.log("Connected!");
1692 | });
1693 |
1694 | if (homePageElement) {
1695 | homePageElement.addEventListener("click", function () {
1696 | content = document.getElementById("new-page-content").value;
1697 | hash = SHA256(content);
1698 | history.pushState({}, "Your Page", hash);
1699 | document.getElementById("content-goes-here").innerHTML = content;
1700 | haveContent(socket, hash, content);
1701 | });
1702 | } else if (showPageElement) {
1703 | wantContent(socket, window.location.pathname.substr(1), showPageElement);
1704 | }
1705 | });
1706 |
1707 | function haveContent(socket, hash, content) {
1708 | var counter = document.getElementById("visitor-count");
1709 |
1710 | for (var i = 0; i < socket.channels.length; i++) {
1711 | if (socket.channels[i].topic === 'have:' + hash) {
1712 | return;
1713 | }
1714 | }
1715 |
1716 | var chan = socket.channel("have:" + hash, {});
1717 | chan.on("content_request", function (_msg) {
1718 | webConsole.log("Request received...");
1719 | chan.push("content", { content: content, hash: hash });
1720 | webConsole.log("Content sent!");
1721 | });
1722 | chan.on("visitors_count", function (msg) {
1723 | counter.innerHTML = msg.count;
1724 | });
1725 |
1726 | chan.join().receive("ok", function (chan) {
1727 | webConsole.log("Standing by... ready to share this content!");
1728 | });
1729 | }
1730 |
1731 | function wantContent(socket, hash, elem) {
1732 | var requestContentInterval = void 0;
1733 |
1734 | var chan = socket.channel("want:" + hash, {});
1735 | chan.on("content", function (msg) {
1736 | clearInterval(requestContentInterval);
1737 | webConsole.log("Received content for hash " + hash);
1738 | elem.innerHTML = msg.content;
1739 | chan.leave();
1740 | haveContent(socket, hash, msg.content);
1741 | });
1742 |
1743 | chan.join().receive("ok", function () {
1744 | webConsole.log("Listening for content for hash " + hash);
1745 |
1746 | requestContentInterval = setInterval(function () {
1747 | webConsole.log("Requesting content.");
1748 | chan.push("content_request", { hash: hash });
1749 | }, 2000);
1750 | });
1751 | }
1752 |
1753 | var App = {};
1754 |
1755 | exports.default = App;
1756 |
1757 | });
1758 |
1759 | require.register("js/web_console.js", function(exports, require, module) {
1760 | "use strict";
1761 |
1762 | Object.defineProperty(exports, "__esModule", {
1763 | value: true
1764 | });
1765 |
1766 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
1767 |
1768 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
1769 |
1770 | var WebConsole = function () {
1771 | // takes a
element
1772 | function WebConsole(list) {
1773 | _classCallCheck(this, WebConsole);
1774 |
1775 | console.log(list);
1776 | this.list = list;
1777 | }
1778 |
1779 | _createClass(WebConsole, [{
1780 | key: "log",
1781 | value: function log(msg) {
1782 | var wasScrolledToTheBottom = this.isScrolledToTheBottom();
1783 | this.addLine(msg);
1784 | if (wasScrolledToTheBottom) {
1785 | this.scrollToTheBottom();
1786 | }
1787 | }
1788 | }, {
1789 | key: "addLine",
1790 | value: function addLine(msg) {
1791 | var li = document.createElement("li");
1792 | var text = document.createTextNode(msg);
1793 | li.appendChild(text);
1794 | this.list.appendChild(li);
1795 | }
1796 | }, {
1797 | key: "isScrolledToTheBottom",
1798 | value: function isScrolledToTheBottom() {
1799 | return this.list.scrollTop >= this.list.scrollHeight - this.list.clientHeight;
1800 | }
1801 | }, {
1802 | key: "scrollToTheBottom",
1803 | value: function scrollToTheBottom() {
1804 | this.list.scrollTop = this.list.scrollHeight;
1805 | }
1806 | }]);
1807 |
1808 | return WebConsole;
1809 | }();
1810 |
1811 | exports.default = WebConsole;
1812 |
1813 | });
1814 |
1815 | require.alias("phoenix/priv/static/phoenix.js", "phoenix");
1816 | require.alias("phoenix_html/priv/static/phoenix_html.js", "phoenix_html");require.register("___globals___", function(exports, require, module) {
1817 |
1818 | });})();require('___globals___');
1819 |
1820 | /**
1821 | *
1822 | * Secure Hash Algorithm (SHA256)
1823 | * http://www.webtoolkit.info/
1824 | *
1825 | * Original code by Angel Marin, Paul Johnston.
1826 | *
1827 | **/
1828 | function SHA256(s){
1829 |
1830 | var chrsz = 8;
1831 | var hexcase = 0;
1832 |
1833 | function safe_add (x, y) {
1834 | var lsw = (x & 0xFFFF) + (y & 0xFFFF);
1835 | var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
1836 | return (msw << 16) | (lsw & 0xFFFF);
1837 | }
1838 |
1839 | function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }
1840 | function R (X, n) { return ( X >>> n ); }
1841 | function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }
1842 | function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }
1843 | function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }
1844 | function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }
1845 | function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }
1846 | function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }
1847 |
1848 | function core_sha256 (m, l) {
1849 | var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);
1850 | var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);
1851 | var W = new Array(64);
1852 | var a, b, c, d, e, f, g, h, i, j;
1853 | var T1, T2;
1854 |
1855 | m[l >> 5] |= 0x80 << (24 - l % 32);
1856 | m[((l + 64 >> 9) << 4) + 15] = l;
1857 |
1858 | for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);
1902 | }
1903 | return bin;
1904 | }
1905 |
1906 | function Utf8Encode(string) {
1907 | string = string.replace(/\r\n/g,"\n");
1908 | var utftext = "";
1909 |
1910 | for (var n = 0; n < string.length; n++) {
1911 |
1912 | var c = string.charCodeAt(n);
1913 |
1914 | if (c < 128) {
1915 | utftext += String.fromCharCode(c);
1916 | }
1917 | else if((c > 127) && (c < 2048)) {
1918 | utftext += String.fromCharCode((c >> 6) | 192);
1919 | utftext += String.fromCharCode((c & 63) | 128);
1920 | }
1921 | else {
1922 | utftext += String.fromCharCode((c >> 12) | 224);
1923 | utftext += String.fromCharCode(((c >> 6) & 63) | 128);
1924 | utftext += String.fromCharCode((c & 63) | 128);
1925 | }
1926 |
1927 | }
1928 |
1929 | return utftext;
1930 | }
1931 |
1932 | function binb2hex (binarray) {
1933 | var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
1934 | var str = "";
1935 | for(var i = 0; i < binarray.length * 4; i++) {
1936 | str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +
1937 | hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);
1938 | }
1939 | return str;
1940 | }
1941 |
1942 | s = Utf8Encode(s);
1943 | return binb2hex(core_sha256(str2binb(s), s.length * chrsz));
1944 |
1945 | }
1946 |
1947 | ;require('js/app');
1948 | //# sourceMappingURL=app.js.map
--------------------------------------------------------------------------------
/priv/static/js/app.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["node_modules/phoenix/priv/static/phoenix.js","node_modules/phoenix_html/priv/static/phoenix_html.js","js/app.js","js/web_console.js","vendor/sha256.js"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AC55CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;ACnDA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AApHA;AAAA;ACAA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AApDA;AAAA;;;;;;ACAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"../priv/static/js/app.js","sourcesContent":["\nrequire.register(\"phoenix/priv/static/phoenix.js\", function(exports, require, module) {\n require = __makeRelativeRequire(require, {}, \"phoenix\");\n (function() {\n (function (global, factory) {\ntypeof exports === 'object' ? factory(exports) :\ntypeof define === 'function' && define.amd ? define(['exports'], factory) :\nfactory(global.Phoenix = global.Phoenix || {});\n}(this, (function (exports) {\n\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\nvar _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i[\"return\"]) _i[\"return\"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError(\"Invalid attempt to destructure non-iterable instance\"); } }; }();\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nfunction _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\n/**\n * Phoenix Channels JavaScript client\n *\n * ## Socket Connection\n *\n * A single connection is established to the server and\n * channels are multiplexed over the connection.\n * Connect to the server using the `Socket` class:\n *\n * ```javascript\n * let socket = new Socket(\"/socket\", {params: {userToken: \"123\"}})\n * socket.connect()\n * ```\n *\n * The `Socket` constructor takes the mount point of the socket,\n * the authentication params, as well as options that can be found in\n * the Socket docs, such as configuring the `LongPoll` transport, and\n * heartbeat.\n *\n * ## Channels\n *\n * Channels are isolated, concurrent processes on the server that\n * subscribe to topics and broker events between the client and server.\n * To join a channel, you must provide the topic, and channel params for\n * authorization. Here's an example chat room example where `\"new_msg\"`\n * events are listened for, messages are pushed to the server, and\n * the channel is joined with ok/error/timeout matches:\n *\n * ```javascript\n * let channel = socket.channel(\"room:123\", {token: roomToken})\n * channel.on(\"new_msg\", msg => console.log(\"Got message\", msg) )\n * $input.onEnter( e => {\n * channel.push(\"new_msg\", {body: e.target.val}, 10000)\n * .receive(\"ok\", (msg) => console.log(\"created message\", msg) )\n * .receive(\"error\", (reasons) => console.log(\"create failed\", reasons) )\n * .receive(\"timeout\", () => console.log(\"Networking issue...\") )\n * })\n * channel.join()\n * .receive(\"ok\", ({messages}) => console.log(\"catching up\", messages) )\n * .receive(\"error\", ({reason}) => console.log(\"failed join\", reason) )\n * .receive(\"timeout\", () => console.log(\"Networking issue. Still waiting...\") )\n *```\n *\n * ## Joining\n *\n * Creating a channel with `socket.channel(topic, params)`, binds the params to\n * `channel.params`, which are sent up on `channel.join()`.\n * Subsequent rejoins will send up the modified params for\n * updating authorization params, or passing up last_message_id information.\n * Successful joins receive an \"ok\" status, while unsuccessful joins\n * receive \"error\".\n *\n * ## Duplicate Join Subscriptions\n *\n * While the client may join any number of topics on any number of channels,\n * the client may only hold a single subscription for each unique topic at any\n * given time. When attempting to create a duplicate subscription,\n * the server will close the existing channel, log a warning, and\n * spawn a new channel for the topic. The client will have their\n * `channel.onClose` callbacks fired for the existing channel, and the new\n * channel join will have its receive hooks processed as normal.\n *\n * ## Pushing Messages\n *\n * From the previous example, we can see that pushing messages to the server\n * can be done with `channel.push(eventName, payload)` and we can optionally\n * receive responses from the push. Additionally, we can use\n * `receive(\"timeout\", callback)` to abort waiting for our other `receive` hooks\n * and take action after some period of waiting. The default timeout is 5000ms.\n *\n *\n * ## Socket Hooks\n *\n * Lifecycle events of the multiplexed connection can be hooked into via\n * `socket.onError()` and `socket.onClose()` events, ie:\n *\n * ```javascript\n * socket.onError( () => console.log(\"there was an error with the connection!\") )\n * socket.onClose( () => console.log(\"the connection dropped\") )\n * ```\n *\n *\n * ## Channel Hooks\n *\n * For each joined channel, you can bind to `onError` and `onClose` events\n * to monitor the channel lifecycle, ie:\n *\n * ```javascript\n * channel.onError( () => console.log(\"there was an error!\") )\n * channel.onClose( () => console.log(\"the channel has gone away gracefully\") )\n * ```\n *\n * ### onError hooks\n *\n * `onError` hooks are invoked if the socket connection drops, or the channel\n * crashes on the server. In either case, a channel rejoin is attempted\n * automatically in an exponential backoff manner.\n *\n * ### onClose hooks\n *\n * `onClose` hooks are invoked only in two cases. 1) the channel explicitly\n * closed on the server, or 2). The client explicitly closed, by calling\n * `channel.leave()`\n *\n *\n * ## Presence\n *\n * The `Presence` object provides features for syncing presence information\n * from the server with the client and handling presences joining and leaving.\n *\n * ### Syncing initial state from the server\n *\n * `Presence.syncState` is used to sync the list of presences on the server\n * with the client's state. An optional `onJoin` and `onLeave` callback can\n * be provided to react to changes in the client's local presences across\n * disconnects and reconnects with the server.\n *\n * `Presence.syncDiff` is used to sync a diff of presence join and leave\n * events from the server, as they happen. Like `syncState`, `syncDiff`\n * accepts optional `onJoin` and `onLeave` callbacks to react to a user\n * joining or leaving from a device.\n *\n * ### Listing Presences\n *\n * `Presence.list` is used to return a list of presence information\n * based on the local state of metadata. By default, all presence\n * metadata is returned, but a `listBy` function can be supplied to\n * allow the client to select which metadata to use for a given presence.\n * For example, you may have a user online from different devices with\n * a metadata status of \"online\", but they have set themselves to \"away\"\n * on another device. In this case, the app may choose to use the \"away\"\n * status for what appears on the UI. The example below defines a `listBy`\n * function which prioritizes the first metadata which was registered for\n * each user. This could be the first tab they opened, or the first device\n * they came online from:\n *\n * ```javascript\n * let state = {}\n * state = Presence.syncState(state, stateFromServer)\n * let listBy = (id, {metas: [first, ...rest]}) => {\n * first.count = rest.length + 1 // count of this user's presences\n * first.id = id\n * return first\n * }\n * let onlineUsers = Presence.list(state, listBy)\n * ```\n *\n *\n * ### Example Usage\n *```javascript\n * // detect if user has joined for the 1st time or from another tab/device\n * let onJoin = (id, current, newPres) => {\n * if(!current){\n * console.log(\"user has entered for the first time\", newPres)\n * } else {\n * console.log(\"user additional presence\", newPres)\n * }\n * }\n * // detect if user has left from all tabs/devices, or is still present\n * let onLeave = (id, current, leftPres) => {\n * if(current.metas.length === 0){\n * console.log(\"user has left from all devices\", leftPres)\n * } else {\n * console.log(\"user left from a device\", leftPres)\n * }\n * }\n * let presences = {} // client's initial empty presence state\n * // receive initial presence data from server, sent after join\n * myChannel.on(\"presence_state\", state => {\n * presences = Presence.syncState(presences, state, onJoin, onLeave)\n * displayUsers(Presence.list(presences))\n * })\n * // receive \"presence_diff\" from server, containing join/leave events\n * myChannel.on(\"presence_diff\", diff => {\n * presences = Presence.syncDiff(presences, diff, onJoin, onLeave)\n * this.setState({users: Presence.list(room.presences, listBy)})\n * })\n * ```\n * @module phoenix\n */\n\nvar VSN = \"2.0.0\";\nvar SOCKET_STATES = { connecting: 0, open: 1, closing: 2, closed: 3 };\nvar DEFAULT_TIMEOUT = 10000;\nvar WS_CLOSE_NORMAL = 1000;\nvar CHANNEL_STATES = {\n closed: \"closed\",\n errored: \"errored\",\n joined: \"joined\",\n joining: \"joining\",\n leaving: \"leaving\"\n};\nvar CHANNEL_EVENTS = {\n close: \"phx_close\",\n error: \"phx_error\",\n join: \"phx_join\",\n reply: \"phx_reply\",\n leave: \"phx_leave\"\n};\nvar CHANNEL_LIFECYCLE_EVENTS = [CHANNEL_EVENTS.close, CHANNEL_EVENTS.error, CHANNEL_EVENTS.join, CHANNEL_EVENTS.reply, CHANNEL_EVENTS.leave];\nvar TRANSPORTS = {\n longpoll: \"longpoll\",\n websocket: \"websocket\"\n};\n\n/**\n * Initializes the Push\n * @param {Channel} channel - The Channel\n * @param {string} event - The event, for example `\"phx_join\"`\n * @param {Object} payload - The payload, for example `{user_id: 123}`\n * @param {number} timeout - The push timeout in milliseconds\n */\n\nvar Push = function () {\n function Push(channel, event, payload, timeout) {\n _classCallCheck(this, Push);\n\n this.channel = channel;\n this.event = event;\n this.payload = payload || {};\n this.receivedResp = null;\n this.timeout = timeout;\n this.timeoutTimer = null;\n this.recHooks = [];\n this.sent = false;\n }\n\n /**\n *\n * @param {number} timeout\n */\n\n\n _createClass(Push, [{\n key: \"resend\",\n value: function resend(timeout) {\n this.timeout = timeout;\n this.reset();\n this.send();\n }\n\n /**\n *\n */\n\n }, {\n key: \"send\",\n value: function send() {\n if (this.hasReceived(\"timeout\")) {\n return;\n }\n this.startTimeout();\n this.sent = true;\n this.channel.socket.push({\n topic: this.channel.topic,\n event: this.event,\n payload: this.payload,\n ref: this.ref,\n join_ref: this.channel.joinRef()\n });\n }\n\n /**\n *\n * @param {*} status\n * @param {*} callback\n */\n\n }, {\n key: \"receive\",\n value: function receive(status, callback) {\n if (this.hasReceived(status)) {\n callback(this.receivedResp.response);\n }\n\n this.recHooks.push({ status: status, callback: callback });\n return this;\n }\n\n // private\n\n }, {\n key: \"reset\",\n value: function reset() {\n this.cancelRefEvent();\n this.ref = null;\n this.refEvent = null;\n this.receivedResp = null;\n this.sent = false;\n }\n }, {\n key: \"matchReceive\",\n value: function matchReceive(_ref) {\n var status = _ref.status,\n response = _ref.response,\n ref = _ref.ref;\n\n this.recHooks.filter(function (h) {\n return h.status === status;\n }).forEach(function (h) {\n return h.callback(response);\n });\n }\n }, {\n key: \"cancelRefEvent\",\n value: function cancelRefEvent() {\n if (!this.refEvent) {\n return;\n }\n this.channel.off(this.refEvent);\n }\n }, {\n key: \"cancelTimeout\",\n value: function cancelTimeout() {\n clearTimeout(this.timeoutTimer);\n this.timeoutTimer = null;\n }\n }, {\n key: \"startTimeout\",\n value: function startTimeout() {\n var _this = this;\n\n if (this.timeoutTimer) {\n this.cancelTimeout();\n }\n this.ref = this.channel.socket.makeRef();\n this.refEvent = this.channel.replyEventName(this.ref);\n\n this.channel.on(this.refEvent, function (payload) {\n _this.cancelRefEvent();\n _this.cancelTimeout();\n _this.receivedResp = payload;\n _this.matchReceive(payload);\n });\n\n this.timeoutTimer = setTimeout(function () {\n _this.trigger(\"timeout\", {});\n }, this.timeout);\n }\n }, {\n key: \"hasReceived\",\n value: function hasReceived(status) {\n return this.receivedResp && this.receivedResp.status === status;\n }\n }, {\n key: \"trigger\",\n value: function trigger(status, response) {\n this.channel.trigger(this.refEvent, { status: status, response: response });\n }\n }]);\n\n return Push;\n}();\n\n/**\n *\n * @param {string} topic\n * @param {Object} params\n * @param {Socket} socket\n */\n\n\nvar Channel = exports.Channel = function () {\n function Channel(topic, params, socket) {\n var _this2 = this;\n\n _classCallCheck(this, Channel);\n\n this.state = CHANNEL_STATES.closed;\n this.topic = topic;\n this.params = params || {};\n this.socket = socket;\n this.bindings = [];\n this.timeout = this.socket.timeout;\n this.joinedOnce = false;\n this.joinPush = new Push(this, CHANNEL_EVENTS.join, this.params, this.timeout);\n this.pushBuffer = [];\n this.rejoinTimer = new Timer(function () {\n return _this2.rejoinUntilConnected();\n }, this.socket.reconnectAfterMs);\n this.joinPush.receive(\"ok\", function () {\n _this2.state = CHANNEL_STATES.joined;\n _this2.rejoinTimer.reset();\n _this2.pushBuffer.forEach(function (pushEvent) {\n return pushEvent.send();\n });\n _this2.pushBuffer = [];\n });\n this.onClose(function () {\n _this2.rejoinTimer.reset();\n _this2.socket.log(\"channel\", \"close \" + _this2.topic + \" \" + _this2.joinRef());\n _this2.state = CHANNEL_STATES.closed;\n _this2.socket.remove(_this2);\n });\n this.onError(function (reason) {\n if (_this2.isLeaving() || _this2.isClosed()) {\n return;\n }\n _this2.socket.log(\"channel\", \"error \" + _this2.topic, reason);\n _this2.state = CHANNEL_STATES.errored;\n _this2.rejoinTimer.scheduleTimeout();\n });\n this.joinPush.receive(\"timeout\", function () {\n if (!_this2.isJoining()) {\n return;\n }\n _this2.socket.log(\"channel\", \"timeout \" + _this2.topic + \" (\" + _this2.joinRef() + \")\", _this2.joinPush.timeout);\n var leavePush = new Push(_this2, CHANNEL_EVENTS.leave, {}, _this2.timeout);\n leavePush.send();\n _this2.state = CHANNEL_STATES.errored;\n _this2.joinPush.reset();\n _this2.rejoinTimer.scheduleTimeout();\n });\n this.on(CHANNEL_EVENTS.reply, function (payload, ref) {\n _this2.trigger(_this2.replyEventName(ref), payload);\n });\n }\n\n _createClass(Channel, [{\n key: \"rejoinUntilConnected\",\n value: function rejoinUntilConnected() {\n this.rejoinTimer.scheduleTimeout();\n if (this.socket.isConnected()) {\n this.rejoin();\n }\n }\n }, {\n key: \"join\",\n value: function join() {\n var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;\n\n if (this.joinedOnce) {\n throw \"tried to join multiple times. 'join' can only be called a single time per channel instance\";\n } else {\n this.joinedOnce = true;\n this.rejoin(timeout);\n return this.joinPush;\n }\n }\n }, {\n key: \"onClose\",\n value: function onClose(callback) {\n this.on(CHANNEL_EVENTS.close, callback);\n }\n }, {\n key: \"onError\",\n value: function onError(callback) {\n this.on(CHANNEL_EVENTS.error, function (reason) {\n return callback(reason);\n });\n }\n }, {\n key: \"on\",\n value: function on(event, callback) {\n this.bindings.push({ event: event, callback: callback });\n }\n }, {\n key: \"off\",\n value: function off(event) {\n this.bindings = this.bindings.filter(function (bind) {\n return bind.event !== event;\n });\n }\n }, {\n key: \"canPush\",\n value: function canPush() {\n return this.socket.isConnected() && this.isJoined();\n }\n }, {\n key: \"push\",\n value: function push(event, payload) {\n var timeout = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.timeout;\n\n if (!this.joinedOnce) {\n throw \"tried to push '\" + event + \"' to '\" + this.topic + \"' before joining. Use channel.join() before pushing events\";\n }\n var pushEvent = new Push(this, event, payload, timeout);\n if (this.canPush()) {\n pushEvent.send();\n } else {\n pushEvent.startTimeout();\n this.pushBuffer.push(pushEvent);\n }\n\n return pushEvent;\n }\n\n /** Leaves the channel\n *\n * Unsubscribes from server events, and\n * instructs channel to terminate on server\n *\n * Triggers onClose() hooks\n *\n * To receive leave acknowledgements, use the a `receive`\n * hook to bind to the server ack, ie:\n *\n * ```javascript\n * channel.leave().receive(\"ok\", () => alert(\"left!\") )\n * ```\n */\n\n }, {\n key: \"leave\",\n value: function leave() {\n var _this3 = this;\n\n var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;\n\n this.state = CHANNEL_STATES.leaving;\n var onClose = function onClose() {\n _this3.socket.log(\"channel\", \"leave \" + _this3.topic);\n _this3.trigger(CHANNEL_EVENTS.close, \"leave\");\n };\n var leavePush = new Push(this, CHANNEL_EVENTS.leave, {}, timeout);\n leavePush.receive(\"ok\", function () {\n return onClose();\n }).receive(\"timeout\", function () {\n return onClose();\n });\n leavePush.send();\n if (!this.canPush()) {\n leavePush.trigger(\"ok\", {});\n }\n\n return leavePush;\n }\n\n /**\n * Overridable message hook\n *\n * Receives all events for specialized message handling\n * before dispatching to the channel callbacks.\n *\n * Must return the payload, modified or unmodified\n */\n\n }, {\n key: \"onMessage\",\n value: function onMessage(event, payload, ref) {\n return payload;\n }\n\n // private\n\n }, {\n key: \"isMember\",\n value: function isMember(topic, event, payload, joinRef) {\n if (this.topic !== topic) {\n return false;\n }\n var isLifecycleEvent = CHANNEL_LIFECYCLE_EVENTS.indexOf(event) >= 0;\n\n if (joinRef && isLifecycleEvent && joinRef !== this.joinRef()) {\n this.socket.log(\"channel\", \"dropping outdated message\", { topic: topic, event: event, payload: payload, joinRef: joinRef });\n return false;\n } else {\n return true;\n }\n }\n }, {\n key: \"joinRef\",\n value: function joinRef() {\n return this.joinPush.ref;\n }\n }, {\n key: \"sendJoin\",\n value: function sendJoin(timeout) {\n this.state = CHANNEL_STATES.joining;\n this.joinPush.resend(timeout);\n }\n }, {\n key: \"rejoin\",\n value: function rejoin() {\n var timeout = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.timeout;\n if (this.isLeaving()) {\n return;\n }\n this.sendJoin(timeout);\n }\n }, {\n key: \"trigger\",\n value: function trigger(event, payload, ref, joinRef) {\n var _this4 = this;\n\n var handledPayload = this.onMessage(event, payload, ref, joinRef);\n if (payload && !handledPayload) {\n throw \"channel onMessage callbacks must return the payload, modified or unmodified\";\n }\n\n this.bindings.filter(function (bind) {\n return bind.event === event;\n }).map(function (bind) {\n return bind.callback(handledPayload, ref, joinRef || _this4.joinRef());\n });\n }\n }, {\n key: \"replyEventName\",\n value: function replyEventName(ref) {\n return \"chan_reply_\" + ref;\n }\n }, {\n key: \"isClosed\",\n value: function isClosed() {\n return this.state === CHANNEL_STATES.closed;\n }\n }, {\n key: \"isErrored\",\n value: function isErrored() {\n return this.state === CHANNEL_STATES.errored;\n }\n }, {\n key: \"isJoined\",\n value: function isJoined() {\n return this.state === CHANNEL_STATES.joined;\n }\n }, {\n key: \"isJoining\",\n value: function isJoining() {\n return this.state === CHANNEL_STATES.joining;\n }\n }, {\n key: \"isLeaving\",\n value: function isLeaving() {\n return this.state === CHANNEL_STATES.leaving;\n }\n }]);\n\n return Channel;\n}();\n\nvar Serializer = {\n encode: function encode(msg, callback) {\n var payload = [msg.join_ref, msg.ref, msg.topic, msg.event, msg.payload];\n return callback(JSON.stringify(payload));\n },\n decode: function decode(rawPayload, callback) {\n var _JSON$parse = JSON.parse(rawPayload),\n _JSON$parse2 = _slicedToArray(_JSON$parse, 5),\n join_ref = _JSON$parse2[0],\n ref = _JSON$parse2[1],\n topic = _JSON$parse2[2],\n event = _JSON$parse2[3],\n payload = _JSON$parse2[4];\n\n return callback({ join_ref: join_ref, ref: ref, topic: topic, event: event, payload: payload });\n }\n};\n\n/** Initializes the Socket\n *\n *\n * For IE8 support use an ES5-shim (https://github.com/es-shims/es5-shim)\n *\n * @param {string} endPoint - The string WebSocket endpoint, ie, `\"ws://example.com/socket\"`,\n * `\"wss://example.com\"`\n * `\"/socket\"` (inherited host & protocol)\n * @param {Object} opts - Optional configuration\n * @param {string} opts.transport - The Websocket Transport, for example WebSocket or Phoenix.LongPoll.\n *\n * Defaults to WebSocket with automatic LongPoll fallback.\n * @param {Function} opts.encode - The function to encode outgoing messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.stringify(payload))\n * ```\n *\n * @param {Function} opts.decode - The function to decode incoming messages.\n *\n * Defaults to JSON:\n *\n * ```javascript\n * (payload, callback) => callback(JSON.parse(payload))\n * ```\n *\n * @param {number} opts.timeout - The default timeout in milliseconds to trigger push timeouts.\n *\n * Defaults `DEFAULT_TIMEOUT`\n * @param {number} opts.heartbeatIntervalMs - The millisec interval to send a heartbeat message\n * @param {number} opts.reconnectAfterMs - The optional function that returns the millsec reconnect interval.\n *\n * Defaults to stepped backoff of:\n *\n * ```javascript\n * function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * }\n * ```\n * @param {Function} opts.logger - The optional function for specialized logging, ie:\n * ```javascript\n * logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) }\n * ```\n *\n * @param {number} opts.longpollerTimeout - The maximum timeout of a long poll AJAX request.\n *\n * Defaults to 20s (double the server long poll timer).\n *\n * @param {Object} opts.params - The optional params to pass when connecting\n *\n *\n*/\n\nvar Socket = exports.Socket = function () {\n function Socket(endPoint) {\n var _this5 = this;\n\n var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n _classCallCheck(this, Socket);\n\n this.stateChangeCallbacks = { open: [], close: [], error: [], message: [] };\n this.channels = [];\n this.sendBuffer = [];\n this.ref = 0;\n this.timeout = opts.timeout || DEFAULT_TIMEOUT;\n this.transport = opts.transport || window.WebSocket || LongPoll;\n this.defaultEncoder = Serializer.encode;\n this.defaultDecoder = Serializer.decode;\n if (this.transport !== LongPoll) {\n this.encode = opts.encode || this.defaultEncoder;\n this.decode = opts.decode || this.defaultDecoder;\n } else {\n this.encode = this.defaultEncoder;\n this.decode = this.defaultDecoder;\n }\n this.heartbeatIntervalMs = opts.heartbeatIntervalMs || 30000;\n this.reconnectAfterMs = opts.reconnectAfterMs || function (tries) {\n return [1000, 2000, 5000, 10000][tries - 1] || 10000;\n };\n this.logger = opts.logger || function () {}; // noop\n this.longpollerTimeout = opts.longpollerTimeout || 20000;\n this.params = opts.params || {};\n this.endPoint = endPoint + \"/\" + TRANSPORTS.websocket;\n this.heartbeatTimer = null;\n this.pendingHeartbeatRef = null;\n this.reconnectTimer = new Timer(function () {\n _this5.disconnect(function () {\n return _this5.connect();\n });\n }, this.reconnectAfterMs);\n }\n\n _createClass(Socket, [{\n key: \"protocol\",\n value: function protocol() {\n return location.protocol.match(/^https/) ? \"wss\" : \"ws\";\n }\n }, {\n key: \"endPointURL\",\n value: function endPointURL() {\n var uri = Ajax.appendParams(Ajax.appendParams(this.endPoint, this.params), { vsn: VSN });\n if (uri.charAt(0) !== \"/\") {\n return uri;\n }\n if (uri.charAt(1) === \"/\") {\n return this.protocol() + \":\" + uri;\n }\n\n return this.protocol() + \"://\" + location.host + uri;\n }\n }, {\n key: \"disconnect\",\n value: function disconnect(callback, code, reason) {\n if (this.conn) {\n this.conn.onclose = function () {}; // noop\n if (code) {\n this.conn.close(code, reason || \"\");\n } else {\n this.conn.close();\n }\n this.conn = null;\n }\n callback && callback();\n }\n\n /**\n *\n * @param {Object} params - The params to send when connecting, for example `{user_id: userToken}`\n */\n\n }, {\n key: \"connect\",\n value: function connect(params) {\n var _this6 = this;\n\n if (params) {\n console && console.log(\"passing params to connect is deprecated. Instead pass :params to the Socket constructor\");\n this.params = params;\n }\n if (this.conn) {\n return;\n }\n\n this.conn = new this.transport(this.endPointURL());\n this.conn.timeout = this.longpollerTimeout;\n this.conn.onopen = function () {\n return _this6.onConnOpen();\n };\n this.conn.onerror = function (error) {\n return _this6.onConnError(error);\n };\n this.conn.onmessage = function (event) {\n return _this6.onConnMessage(event);\n };\n this.conn.onclose = function (event) {\n return _this6.onConnClose(event);\n };\n }\n\n /**\n * Logs the message. Override `this.logger` for specialized logging. noops by default\n * @param {string} kind\n * @param {string} msg\n * @param {Object} data\n */\n\n }, {\n key: \"log\",\n value: function log(kind, msg, data) {\n this.logger(kind, msg, data);\n }\n\n // Registers callbacks for connection state change events\n //\n // Examples\n //\n // socket.onError(function(error){ alert(\"An error occurred\") })\n //\n\n }, {\n key: \"onOpen\",\n value: function onOpen(callback) {\n this.stateChangeCallbacks.open.push(callback);\n }\n }, {\n key: \"onClose\",\n value: function onClose(callback) {\n this.stateChangeCallbacks.close.push(callback);\n }\n }, {\n key: \"onError\",\n value: function onError(callback) {\n this.stateChangeCallbacks.error.push(callback);\n }\n }, {\n key: \"onMessage\",\n value: function onMessage(callback) {\n this.stateChangeCallbacks.message.push(callback);\n }\n }, {\n key: \"onConnOpen\",\n value: function onConnOpen() {\n var _this7 = this;\n\n this.log(\"transport\", \"connected to \" + this.endPointURL());\n this.flushSendBuffer();\n this.reconnectTimer.reset();\n if (!this.conn.skipHeartbeat) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = setInterval(function () {\n return _this7.sendHeartbeat();\n }, this.heartbeatIntervalMs);\n }\n this.stateChangeCallbacks.open.forEach(function (callback) {\n return callback();\n });\n }\n }, {\n key: \"onConnClose\",\n value: function onConnClose(event) {\n this.log(\"transport\", \"close\", event);\n this.triggerChanError();\n clearInterval(this.heartbeatTimer);\n this.reconnectTimer.scheduleTimeout();\n this.stateChangeCallbacks.close.forEach(function (callback) {\n return callback(event);\n });\n }\n }, {\n key: \"onConnError\",\n value: function onConnError(error) {\n this.log(\"transport\", error);\n this.triggerChanError();\n this.stateChangeCallbacks.error.forEach(function (callback) {\n return callback(error);\n });\n }\n }, {\n key: \"triggerChanError\",\n value: function triggerChanError() {\n this.channels.forEach(function (channel) {\n return channel.trigger(CHANNEL_EVENTS.error);\n });\n }\n }, {\n key: \"connectionState\",\n value: function connectionState() {\n switch (this.conn && this.conn.readyState) {\n case SOCKET_STATES.connecting:\n return \"connecting\";\n case SOCKET_STATES.open:\n return \"open\";\n case SOCKET_STATES.closing:\n return \"closing\";\n default:\n return \"closed\";\n }\n }\n }, {\n key: \"isConnected\",\n value: function isConnected() {\n return this.connectionState() === \"open\";\n }\n }, {\n key: \"remove\",\n value: function remove(channel) {\n this.channels = this.channels.filter(function (c) {\n return c.joinRef() !== channel.joinRef();\n });\n }\n }, {\n key: \"channel\",\n value: function channel(topic) {\n var chanParams = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};\n\n var chan = new Channel(topic, chanParams, this);\n this.channels.push(chan);\n return chan;\n }\n }, {\n key: \"push\",\n value: function push(data) {\n var _this8 = this;\n\n var topic = data.topic,\n event = data.event,\n payload = data.payload,\n ref = data.ref,\n join_ref = data.join_ref;\n\n var callback = function callback() {\n _this8.encode(data, function (result) {\n _this8.conn.send(result);\n });\n };\n this.log(\"push\", topic + \" \" + event + \" (\" + join_ref + \", \" + ref + \")\", payload);\n if (this.isConnected()) {\n callback();\n } else {\n this.sendBuffer.push(callback);\n }\n }\n\n /**\n * Return the next message ref, accounting for overflows\n */\n\n }, {\n key: \"makeRef\",\n value: function makeRef() {\n var newRef = this.ref + 1;\n if (newRef === this.ref) {\n this.ref = 0;\n } else {\n this.ref = newRef;\n }\n\n return this.ref.toString();\n }\n }, {\n key: \"sendHeartbeat\",\n value: function sendHeartbeat() {\n if (!this.isConnected()) {\n return;\n }\n if (this.pendingHeartbeatRef) {\n this.pendingHeartbeatRef = null;\n this.log(\"transport\", \"heartbeat timeout. Attempting to re-establish connection\");\n this.conn.close(WS_CLOSE_NORMAL, \"hearbeat timeout\");\n return;\n }\n this.pendingHeartbeatRef = this.makeRef();\n this.push({ topic: \"phoenix\", event: \"heartbeat\", payload: {}, ref: this.pendingHeartbeatRef });\n }\n }, {\n key: \"flushSendBuffer\",\n value: function flushSendBuffer() {\n if (this.isConnected() && this.sendBuffer.length > 0) {\n this.sendBuffer.forEach(function (callback) {\n return callback();\n });\n this.sendBuffer = [];\n }\n }\n }, {\n key: \"onConnMessage\",\n value: function onConnMessage(rawMessage) {\n var _this9 = this;\n\n this.decode(rawMessage.data, function (msg) {\n var topic = msg.topic,\n event = msg.event,\n payload = msg.payload,\n ref = msg.ref,\n join_ref = msg.join_ref;\n\n if (ref && ref === _this9.pendingHeartbeatRef) {\n _this9.pendingHeartbeatRef = null;\n }\n\n _this9.log(\"receive\", (payload.status || \"\") + \" \" + topic + \" \" + event + \" \" + (ref && \"(\" + ref + \")\" || \"\"), payload);\n _this9.channels.filter(function (channel) {\n return channel.isMember(topic, event, payload, join_ref);\n }).forEach(function (channel) {\n return channel.trigger(event, payload, ref, join_ref);\n });\n _this9.stateChangeCallbacks.message.forEach(function (callback) {\n return callback(msg);\n });\n });\n }\n }]);\n\n return Socket;\n}();\n\nvar LongPoll = exports.LongPoll = function () {\n function LongPoll(endPoint) {\n _classCallCheck(this, LongPoll);\n\n this.endPoint = null;\n this.token = null;\n this.skipHeartbeat = true;\n this.onopen = function () {}; // noop\n this.onerror = function () {}; // noop\n this.onmessage = function () {}; // noop\n this.onclose = function () {}; // noop\n this.pollEndpoint = this.normalizeEndpoint(endPoint);\n this.readyState = SOCKET_STATES.connecting;\n\n this.poll();\n }\n\n _createClass(LongPoll, [{\n key: \"normalizeEndpoint\",\n value: function normalizeEndpoint(endPoint) {\n return endPoint.replace(\"ws://\", \"http://\").replace(\"wss://\", \"https://\").replace(new RegExp(\"(.*)\\/\" + TRANSPORTS.websocket), \"$1/\" + TRANSPORTS.longpoll);\n }\n }, {\n key: \"endpointURL\",\n value: function endpointURL() {\n return Ajax.appendParams(this.pollEndpoint, { token: this.token });\n }\n }, {\n key: \"closeAndRetry\",\n value: function closeAndRetry() {\n this.close();\n this.readyState = SOCKET_STATES.connecting;\n }\n }, {\n key: \"ontimeout\",\n value: function ontimeout() {\n this.onerror(\"timeout\");\n this.closeAndRetry();\n }\n }, {\n key: \"poll\",\n value: function poll() {\n var _this10 = this;\n\n if (!(this.readyState === SOCKET_STATES.open || this.readyState === SOCKET_STATES.connecting)) {\n return;\n }\n\n Ajax.request(\"GET\", this.endpointURL(), \"application/json\", null, this.timeout, this.ontimeout.bind(this), function (resp) {\n if (resp) {\n var status = resp.status,\n token = resp.token,\n messages = resp.messages;\n\n _this10.token = token;\n } else {\n var status = 0;\n }\n\n switch (status) {\n case 200:\n messages.forEach(function (msg) {\n return _this10.onmessage({ data: msg });\n });\n _this10.poll();\n break;\n case 204:\n _this10.poll();\n break;\n case 410:\n _this10.readyState = SOCKET_STATES.open;\n _this10.onopen();\n _this10.poll();\n break;\n case 0:\n case 500:\n _this10.onerror();\n _this10.closeAndRetry();\n break;\n default:\n throw \"unhandled poll status \" + status;\n }\n });\n }\n }, {\n key: \"send\",\n value: function send(body) {\n var _this11 = this;\n\n Ajax.request(\"POST\", this.endpointURL(), \"application/json\", body, this.timeout, this.onerror.bind(this, \"timeout\"), function (resp) {\n if (!resp || resp.status !== 200) {\n _this11.onerror(resp && resp.status);\n _this11.closeAndRetry();\n }\n });\n }\n }, {\n key: \"close\",\n value: function close(code, reason) {\n this.readyState = SOCKET_STATES.closed;\n this.onclose();\n }\n }]);\n\n return LongPoll;\n}();\n\nvar Ajax = exports.Ajax = function () {\n function Ajax() {\n _classCallCheck(this, Ajax);\n }\n\n _createClass(Ajax, null, [{\n key: \"request\",\n value: function request(method, endPoint, accept, body, timeout, ontimeout, callback) {\n if (window.XDomainRequest) {\n var req = new XDomainRequest(); // IE8, IE9\n this.xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback);\n } else {\n var _req = window.XMLHttpRequest ? new window.XMLHttpRequest() : // IE7+, Firefox, Chrome, Opera, Safari\n new ActiveXObject(\"Microsoft.XMLHTTP\"); // IE6, IE5\n this.xhrRequest(_req, method, endPoint, accept, body, timeout, ontimeout, callback);\n }\n }\n }, {\n key: \"xdomainRequest\",\n value: function xdomainRequest(req, method, endPoint, body, timeout, ontimeout, callback) {\n var _this12 = this;\n\n req.timeout = timeout;\n req.open(method, endPoint);\n req.onload = function () {\n var response = _this12.parseJSON(req.responseText);\n callback && callback(response);\n };\n if (ontimeout) {\n req.ontimeout = ontimeout;\n }\n\n // Work around bug in IE9 that requires an attached onprogress handler\n req.onprogress = function () {};\n\n req.send(body);\n }\n }, {\n key: \"xhrRequest\",\n value: function xhrRequest(req, method, endPoint, accept, body, timeout, ontimeout, callback) {\n var _this13 = this;\n\n req.open(method, endPoint, true);\n req.timeout = timeout;\n req.setRequestHeader(\"Content-Type\", accept);\n req.onerror = function () {\n callback && callback(null);\n };\n req.onreadystatechange = function () {\n if (req.readyState === _this13.states.complete && callback) {\n var response = _this13.parseJSON(req.responseText);\n callback(response);\n }\n };\n if (ontimeout) {\n req.ontimeout = ontimeout;\n }\n\n req.send(body);\n }\n }, {\n key: \"parseJSON\",\n value: function parseJSON(resp) {\n if (!resp || resp === \"\") {\n return null;\n }\n\n try {\n return JSON.parse(resp);\n } catch (e) {\n console && console.log(\"failed to parse JSON response\", resp);\n return null;\n }\n }\n }, {\n key: \"serialize\",\n value: function serialize(obj, parentKey) {\n var queryStr = [];\n for (var key in obj) {\n if (!obj.hasOwnProperty(key)) {\n continue;\n }\n var paramKey = parentKey ? parentKey + \"[\" + key + \"]\" : key;\n var paramVal = obj[key];\n if ((typeof paramVal === \"undefined\" ? \"undefined\" : _typeof(paramVal)) === \"object\") {\n queryStr.push(this.serialize(paramVal, paramKey));\n } else {\n queryStr.push(encodeURIComponent(paramKey) + \"=\" + encodeURIComponent(paramVal));\n }\n }\n return queryStr.join(\"&\");\n }\n }, {\n key: \"appendParams\",\n value: function appendParams(url, params) {\n if (Object.keys(params).length === 0) {\n return url;\n }\n\n var prefix = url.match(/\\?/) ? \"&\" : \"?\";\n return \"\" + url + prefix + this.serialize(params);\n }\n }]);\n\n return Ajax;\n}();\n\nAjax.states = { complete: 4 };\n\nvar Presence = exports.Presence = {\n syncState: function syncState(currentState, newState, onJoin, onLeave) {\n var _this14 = this;\n\n var state = this.clone(currentState);\n var joins = {};\n var leaves = {};\n\n this.map(state, function (key, presence) {\n if (!newState[key]) {\n leaves[key] = presence;\n }\n });\n this.map(newState, function (key, newPresence) {\n var currentPresence = state[key];\n if (currentPresence) {\n var newRefs = newPresence.metas.map(function (m) {\n return m.phx_ref;\n });\n var curRefs = currentPresence.metas.map(function (m) {\n return m.phx_ref;\n });\n var joinedMetas = newPresence.metas.filter(function (m) {\n return curRefs.indexOf(m.phx_ref) < 0;\n });\n var leftMetas = currentPresence.metas.filter(function (m) {\n return newRefs.indexOf(m.phx_ref) < 0;\n });\n if (joinedMetas.length > 0) {\n joins[key] = newPresence;\n joins[key].metas = joinedMetas;\n }\n if (leftMetas.length > 0) {\n leaves[key] = _this14.clone(currentPresence);\n leaves[key].metas = leftMetas;\n }\n } else {\n joins[key] = newPresence;\n }\n });\n return this.syncDiff(state, { joins: joins, leaves: leaves }, onJoin, onLeave);\n },\n syncDiff: function syncDiff(currentState, _ref2, onJoin, onLeave) {\n var joins = _ref2.joins,\n leaves = _ref2.leaves;\n\n var state = this.clone(currentState);\n if (!onJoin) {\n onJoin = function onJoin() {};\n }\n if (!onLeave) {\n onLeave = function onLeave() {};\n }\n\n this.map(joins, function (key, newPresence) {\n var currentPresence = state[key];\n state[key] = newPresence;\n if (currentPresence) {\n var _state$key$metas;\n\n (_state$key$metas = state[key].metas).unshift.apply(_state$key$metas, _toConsumableArray(currentPresence.metas));\n }\n onJoin(key, currentPresence, newPresence);\n });\n this.map(leaves, function (key, leftPresence) {\n var currentPresence = state[key];\n if (!currentPresence) {\n return;\n }\n var refsToRemove = leftPresence.metas.map(function (m) {\n return m.phx_ref;\n });\n currentPresence.metas = currentPresence.metas.filter(function (p) {\n return refsToRemove.indexOf(p.phx_ref) < 0;\n });\n onLeave(key, currentPresence, leftPresence);\n if (currentPresence.metas.length === 0) {\n delete state[key];\n }\n });\n return state;\n },\n list: function list(presences, chooser) {\n if (!chooser) {\n chooser = function chooser(key, pres) {\n return pres;\n };\n }\n\n return this.map(presences, function (key, presence) {\n return chooser(key, presence);\n });\n },\n\n\n // private\n\n map: function map(obj, func) {\n return Object.getOwnPropertyNames(obj).map(function (key) {\n return func(key, obj[key]);\n });\n },\n clone: function clone(obj) {\n return JSON.parse(JSON.stringify(obj));\n }\n};\n\n/**\n *\n * Creates a timer that accepts a `timerCalc` function to perform\n * calculated timeout retries, such as exponential backoff.\n *\n * ## Examples\n *\n * ```javascript\n * let reconnectTimer = new Timer(() => this.connect(), function(tries){\n * return [1000, 5000, 10000][tries - 1] || 10000\n * })\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * reconnectTimer.scheduleTimeout() // fires after 5000\n * reconnectTimer.reset()\n * reconnectTimer.scheduleTimeout() // fires after 1000\n * ```\n * @param {Function} callback\n * @param {Function} timerCalc\n */\n\nvar Timer = function () {\n function Timer(callback, timerCalc) {\n _classCallCheck(this, Timer);\n\n this.callback = callback;\n this.timerCalc = timerCalc;\n this.timer = null;\n this.tries = 0;\n }\n\n _createClass(Timer, [{\n key: \"reset\",\n value: function reset() {\n this.tries = 0;\n clearTimeout(this.timer);\n }\n\n /**\n * Cancels any previous scheduleTimeout and schedules callback\n */\n\n }, {\n key: \"scheduleTimeout\",\n value: function scheduleTimeout() {\n var _this15 = this;\n\n clearTimeout(this.timer);\n\n this.timer = setTimeout(function () {\n _this15.tries = _this15.tries + 1;\n _this15.callback();\n }, this.timerCalc(this.tries + 1));\n }\n }]);\n\n return Timer;\n}();\n\n})));\n })();\n});","\nrequire.register(\"phoenix_html/priv/static/phoenix_html.js\", function(exports, require, module) {\n require = __makeRelativeRequire(require, {}, \"phoenix_html\");\n (function() {\n \"use strict\";\n\n(function() {\n function buildHiddenInput(name, value) {\n var input = document.createElement(\"input\");\n input.type = \"hidden\";\n input.name = name;\n input.value = value;\n return input;\n }\n\n function handleLinkClick(link) {\n var message = link.getAttribute(\"data-confirm\");\n if(message && !window.confirm(message)) {\n return;\n }\n\n var to = link.getAttribute(\"data-to\"),\n method = buildHiddenInput(\"_method\", link.getAttribute(\"data-method\")),\n csrf = buildHiddenInput(\"_csrf_token\", link.getAttribute(\"data-csrf\")),\n form = document.createElement(\"form\");\n\n form.method = (link.getAttribute(\"data-method\") === \"get\") ? \"get\" : \"post\";\n form.action = to;\n form.style.display = \"hidden\";\n\n form.appendChild(csrf);\n form.appendChild(method);\n document.body.appendChild(form);\n form.submit();\n }\n\n window.addEventListener(\"click\", function(e) {\n var element = e.target;\n\n while (element && element.getAttribute) {\n if(element.getAttribute(\"data-method\")) {\n handleLinkClick(element);\n e.preventDefault();\n return false;\n } else {\n element = element.parentNode;\n }\n }\n }, false);\n})();\n })();\n});","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nrequire(\"phoenix_html\");\n\nvar _phoenix = require(\"phoenix\");\n\nvar _web_console = require(\"./web_console\");\n\nvar _web_console2 = _interopRequireDefault(_web_console);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\n// Import local files\n//\n// Local files can be imported directly using relative\n// paths \"./socket\" or full ones \"web/static/js/socket\".\n\n// import socket from \"./socket\"\n\nvar hash = void 0; // Brunch automatically concatenates all files in your\n// watched paths. Those paths can be configured at\n// config.paths.watched in \"brunch-config.js\".\n//\n// However, those files will only be executed if\n// explicitly imported. The only exception are files\n// in vendor, which are never wrapped in imports and\n// therefore are always executed.\n\n// Import dependencies\n//\n// If you no longer want to use a dependency, remember\n// to also remove its path from \"config.paths.watched\".\n\nvar content = void 0;\nvar webConsole = void 0;\n\ndocument.addEventListener(\"DOMContentLoaded\", function () {\n webConsole = new _web_console2.default(document.getElementById('js-console'));\n var homePageElement = document.getElementById(\"create-new-page\");\n var showPageElement = document.getElementById(\"content-goes-here\");\n\n webConsole.log(\"Connecting to websocket.\");\n var socket = new _phoenix.Socket(\"/ws\");\n socket.connect();\n\n var chan = socket.channel(\"all\", {});\n chan.join().receive(\"ok\", function () {\n webConsole.log(\"Connected!\");\n });\n\n if (homePageElement) {\n homePageElement.addEventListener(\"click\", function () {\n content = document.getElementById(\"new-page-content\").value;\n hash = SHA256(content);\n history.pushState({}, \"Your Page\", hash);\n document.getElementById(\"content-goes-here\").innerHTML = content;\n haveContent(socket, hash, content);\n });\n } else if (showPageElement) {\n wantContent(socket, window.location.pathname.substr(1), showPageElement);\n }\n});\n\nfunction haveContent(socket, hash, content) {\n var counter = document.getElementById(\"visitor-count\");\n\n for (var i = 0; i < socket.channels.length; i++) {\n if (socket.channels[i].topic === 'have:' + hash) {\n return;\n }\n }\n\n var chan = socket.channel(\"have:\" + hash, {});\n chan.on(\"content_request\", function (_msg) {\n webConsole.log(\"Request received...\");\n chan.push(\"content\", { content: content, hash: hash });\n webConsole.log(\"Content sent!\");\n });\n chan.on(\"visitors_count\", function (msg) {\n counter.innerHTML = msg.count;\n });\n\n chan.join().receive(\"ok\", function (chan) {\n webConsole.log(\"Standing by... ready to share this content!\");\n });\n}\n\nfunction wantContent(socket, hash, elem) {\n var requestContentInterval = void 0;\n\n var chan = socket.channel(\"want:\" + hash, {});\n chan.on(\"content\", function (msg) {\n clearInterval(requestContentInterval);\n webConsole.log(\"Received content for hash \" + hash);\n elem.innerHTML = msg.content;\n chan.leave();\n haveContent(socket, hash, msg.content);\n });\n\n chan.join().receive(\"ok\", function () {\n webConsole.log(\"Listening for content for hash \" + hash);\n\n requestContentInterval = setInterval(function () {\n webConsole.log(\"Requesting content.\");\n chan.push(\"content_request\", { hash: hash });\n }, 2000);\n });\n}\n\nvar App = {};\n\nexports.default = App;\n","\"use strict\";\n\nObject.defineProperty(exports, \"__esModule\", {\n value: true\n});\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nvar WebConsole = function () {\n // takes a
element\n function WebConsole(list) {\n _classCallCheck(this, WebConsole);\n\n console.log(list);\n this.list = list;\n }\n\n _createClass(WebConsole, [{\n key: \"log\",\n value: function log(msg) {\n var wasScrolledToTheBottom = this.isScrolledToTheBottom();\n this.addLine(msg);\n if (wasScrolledToTheBottom) {\n this.scrollToTheBottom();\n }\n }\n }, {\n key: \"addLine\",\n value: function addLine(msg) {\n var li = document.createElement(\"li\");\n var text = document.createTextNode(msg);\n li.appendChild(text);\n this.list.appendChild(li);\n }\n }, {\n key: \"isScrolledToTheBottom\",\n value: function isScrolledToTheBottom() {\n return this.list.scrollTop >= this.list.scrollHeight - this.list.clientHeight;\n }\n }, {\n key: \"scrollToTheBottom\",\n value: function scrollToTheBottom() {\n this.list.scrollTop = this.list.scrollHeight;\n }\n }]);\n\n return WebConsole;\n}();\n\nexports.default = WebConsole;\n","/**\n*\n* Secure Hash Algorithm (SHA256)\n* http://www.webtoolkit.info/\n*\n* Original code by Angel Marin, Paul Johnston.\n*\n**/\nfunction SHA256(s){\n\n var chrsz = 8;\n var hexcase = 0;\n\n function safe_add (x, y) {\n var lsw = (x & 0xFFFF) + (y & 0xFFFF);\n var msw = (x >> 16) + (y >> 16) + (lsw >> 16);\n return (msw << 16) | (lsw & 0xFFFF);\n }\n\n function S (X, n) { return ( X >>> n ) | (X << (32 - n)); }\n function R (X, n) { return ( X >>> n ); }\n function Ch(x, y, z) { return ((x & y) ^ ((~x) & z)); }\n function Maj(x, y, z) { return ((x & y) ^ (x & z) ^ (y & z)); }\n function Sigma0256(x) { return (S(x, 2) ^ S(x, 13) ^ S(x, 22)); }\n function Sigma1256(x) { return (S(x, 6) ^ S(x, 11) ^ S(x, 25)); }\n function Gamma0256(x) { return (S(x, 7) ^ S(x, 18) ^ R(x, 3)); }\n function Gamma1256(x) { return (S(x, 17) ^ S(x, 19) ^ R(x, 10)); }\n\n function core_sha256 (m, l) {\n var K = new Array(0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5, 0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5, 0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3, 0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174, 0xE49B69C1, 0xEFBE4786, 0xFC19DC6, 0x240CA1CC, 0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA, 0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7, 0xC6E00BF3, 0xD5A79147, 0x6CA6351, 0x14292967, 0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13, 0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85, 0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3, 0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070, 0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5, 0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3, 0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208, 0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2);\n var HASH = new Array(0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19);\n var W = new Array(64);\n var a, b, c, d, e, f, g, h, i, j;\n var T1, T2;\n\n m[l >> 5] |= 0x80 << (24 - l % 32);\n m[((l + 64 >> 9) << 4) + 15] = l;\n\n for ( var i = 0; i>5] |= (str.charCodeAt(i / chrsz) & mask) << (24 - i%32);\n }\n return bin;\n }\n\n function Utf8Encode(string) {\n string = string.replace(/\\r\\n/g,\"\\n\");\n var utftext = \"\";\n\n for (var n = 0; n < string.length; n++) {\n\n var c = string.charCodeAt(n);\n\n if (c < 128) {\n utftext += String.fromCharCode(c);\n }\n else if((c > 127) && (c < 2048)) {\n utftext += String.fromCharCode((c >> 6) | 192);\n utftext += String.fromCharCode((c & 63) | 128);\n }\n else {\n utftext += String.fromCharCode((c >> 12) | 224);\n utftext += String.fromCharCode(((c >> 6) & 63) | 128);\n utftext += String.fromCharCode((c & 63) | 128);\n }\n\n }\n\n return utftext;\n }\n\n function binb2hex (binarray) {\n var hex_tab = hexcase ? \"0123456789ABCDEF\" : \"0123456789abcdef\";\n var str = \"\";\n for(var i = 0; i < binarray.length * 4; i++) {\n str += hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8+4)) & 0xF) +\n hex_tab.charAt((binarray[i>>2] >> ((3 - i%4)*8 )) & 0xF);\n }\n return str;\n }\n\n s = Utf8Encode(s);\n return binb2hex(core_sha256(str2binb(s), s.length * chrsz));\n\n}\n"]}
--------------------------------------------------------------------------------
/priv/static/robots.txt:
--------------------------------------------------------------------------------
1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
2 | #
3 | # To ban all spiders from the entire site uncomment the next two lines:
4 | # User-agent: *
5 | # Disallow: /
6 |
--------------------------------------------------------------------------------
/test/ephemeral2_web/controllers/page_controller_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.PageControllerTest do
2 | use Ephemeral2Web.ConnCase
3 |
4 | test "GET /", %{conn: conn} do
5 | conn = get conn, "/"
6 | assert html_response(conn, 200) =~ "Welcome to Phoenix!"
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/test/ephemeral2_web/views/error_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.ErrorViewTest do
2 | use Ephemeral2Web.ConnCase, async: true
3 |
4 | # Bring render/3 and render_to_string/3 for testing custom views
5 | import Phoenix.View
6 |
7 | test "renders 404.html" do
8 | assert render_to_string(Ephemeral2Web.ErrorView, "404.html", []) ==
9 | "Page not found"
10 | end
11 |
12 | test "render 500.html" do
13 | assert render_to_string(Ephemeral2Web.ErrorView, "500.html", []) ==
14 | "Internal server error"
15 | end
16 |
17 | test "render any other" do
18 | assert render_to_string(Ephemeral2Web.ErrorView, "505.html", []) ==
19 | "Internal server error"
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/test/ephemeral2_web/views/layout_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.LayoutViewTest do
2 | use Ephemeral2Web.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/ephemeral2_web/views/page_view_test.exs:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.PageViewTest do
2 | use Ephemeral2Web.ConnCase, async: true
3 | end
4 |
--------------------------------------------------------------------------------
/test/support/channel_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.ChannelCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | channel tests.
5 |
6 | Such tests rely on `Phoenix.ChannelTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with channels
21 | use Phoenix.ChannelTest
22 |
23 | # The default endpoint for testing
24 | @endpoint Ephemeral2Web.Endpoint
25 | end
26 | end
27 |
28 |
29 | setup _tags do
30 | :ok
31 | end
32 |
33 | end
34 |
--------------------------------------------------------------------------------
/test/support/conn_case.ex:
--------------------------------------------------------------------------------
1 | defmodule Ephemeral2Web.ConnCase do
2 | @moduledoc """
3 | This module defines the test case to be used by
4 | tests that require setting up a connection.
5 |
6 | Such tests rely on `Phoenix.ConnTest` and also
7 | import other functionality to make it easier
8 | to build common datastructures and query the data layer.
9 |
10 | Finally, if the test case interacts with the database,
11 | it cannot be async. For this reason, every test runs
12 | inside a transaction which is reset at the beginning
13 | of the test unless the test case is marked as async.
14 | """
15 |
16 | use ExUnit.CaseTemplate
17 |
18 | using do
19 | quote do
20 | # Import conveniences for testing with connections
21 | use Phoenix.ConnTest
22 | import Ephemeral2Web.Router.Helpers
23 |
24 | # The default endpoint for testing
25 | @endpoint Ephemeral2Web.Endpoint
26 | end
27 | end
28 |
29 |
30 | setup _tags do
31 | {:ok, conn: Phoenix.ConnTest.build_conn()}
32 | end
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/test/test_helper.exs:
--------------------------------------------------------------------------------
1 | ExUnit.start()
2 |
3 |
--------------------------------------------------------------------------------