├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .tool-versions ├── README.md ├── assets ├── css │ ├── app.scss │ └── phoenix.css ├── js │ ├── app.js │ ├── socket.js │ └── view │ │ ├── message_list.js │ │ ├── metrics_list.js │ │ └── operation_box.js ├── package-lock.json ├── package.json ├── static │ ├── favicon.ico │ ├── images │ │ └── phoenix.png │ └── robots.txt ├── webpack.config.js └── yarn.lock ├── config ├── config.exs ├── dev.exs ├── prod.exs └── test.exs ├── lib ├── delicate_chat.ex ├── delicate_chat │ ├── application.ex │ ├── metrics.ex │ ├── metrics_notifier.ex │ ├── violence_text_judgement.ex │ └── xml_validator.ex ├── delicate_chat_web.ex └── delicate_chat_web │ ├── channels │ ├── room_channel.ex │ └── user_socket.ex │ ├── controllers │ └── page_controller.ex │ ├── endpoint.ex │ ├── gettext.ex │ ├── router.ex │ ├── templates │ ├── layout │ │ └── app.html.eex │ └── page │ │ └── index.html.eex │ └── views │ ├── error_helpers.ex │ ├── error_view.ex │ ├── layout_view.ex │ └── page_view.ex ├── mix.exs ├── mix.lock ├── package.json ├── priv └── gettext │ ├── en │ └── LC_MESSAGES │ │ └── errors.po │ └── errors.pot ├── test ├── delicate_chat_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 └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | web/static/vendor/ 2 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "browser": true, 4 | "jquery": true, 5 | }, 6 | "parserOptions": { 7 | "ecmaVersion": 7, 8 | "sourceType": "module" 9 | }, 10 | "globals": { 11 | }, 12 | "extends": "eslint:recommended", 13 | "rules": { 14 | "indent": [ 15 | "error", 16 | 2 17 | ], 18 | "linebreak-style": [ 19 | "error", 20 | "unix" 21 | ], 22 | "no-console": "off", 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ], 31 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }] 32 | } 33 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # App artifacts 2 | /_build 3 | /db 4 | /deps 5 | /*.ez 6 | 7 | # Generated on crash by the VM 8 | erl_crash.dump 9 | 10 | # Generated on crash by NPM 11 | npm-debug.log 12 | 13 | # Static artifacts 14 | /assets/node_modules 15 | 16 | # Since we are building assets from assets/, 17 | # we ignore priv/static. You may want to comment 18 | # this depending on your deployment strategy. 19 | /priv/static/ 20 | 21 | # Files matching config/*.secret.exs pattern contain sensitive 22 | # data and you should not commit them into version control. 23 | # 24 | # Alternatively, you may comment the line below and commit the 25 | # secrets files as long as you replace their contents by environment 26 | # variables. 27 | /config/*.secret.exs -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | nodejs 8.4.0 2 | elixir 1.5.2 3 | erlang 20.1 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DelicateChat(α) 2 | 3 | Elixirのアンチーパターンとそれで発生する問題時のErlangVMの状態を体験するためのWebチャット 4 | 5 | ## 発表スライドへのリンク 6 | 7 | [Elixirを利用した繊細なwebチャットの開発](https://www.slideshare.net/ndruger/elixirweb-82742732) 8 | 9 | ## To start your Phoenix server: 10 | 11 | * Install dependencies with `mix deps.get` 12 | * Install Node.js dependencies with `cd assets && yarn` 13 | * Start Phoenix endpoint with `iex -S mix phx.server` 14 | 15 | Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. 16 | 17 | ## 対応済みアンチパターン 18 | 19 | - 1. ユーザー入力から動的にAtomを生成する。 20 | - 2. GenServerで処理量より多くのメッセージを定常的に受け取る。 21 | 22 | ## 対応予定アンチパターン 23 | 24 | - GenServerでのinit/1でのコネクション接続によるエラー 25 | - メモリリーク 26 | - プロセスリーク 27 | - ファイルでスクリプタリーク 28 | - パターンマッチミスによるリーク 29 | - NIFによるErlangVMクラッシュ 30 | 31 | -------------------------------------------------------------------------------- /assets/css/app.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | font-size: large 4 | } 5 | 6 | textarea.form-control { 7 | font-size: large 8 | } 9 | 10 | select.form-control { 11 | font-size: large 12 | } 13 | 14 | .client-base { 15 | display: flex; 16 | flex-direction: column; 17 | flex-grow: 1; 18 | height: 100%; 19 | } 20 | 21 | .client-container { 22 | display: flex; 23 | flex-grow: 1; 24 | overflow: hidden; 25 | } 26 | 27 | .side-bar { 28 | background-color: #000; 29 | color: #0f0; 30 | overflow: scroll; 31 | position: relative; 32 | display: flex; 33 | flex-direction: column; 34 | flex-basis: 440px; 35 | flex-shrink: 0; 36 | padding: 15px; 37 | .system-metrics { 38 | font-size: 18pt; 39 | } 40 | .metrics-group { 41 | background-color: rgba(255, 255, 255, 0.2); 42 | padding: 2px 10px; 43 | margin: 8px; 44 | .metrics-group-label { 45 | text-align: center; 46 | margin-bottom: 4px; 47 | } 48 | .metrics-body { 49 | white-space: pre; 50 | } 51 | } 52 | .chart-container { 53 | $height: 40px; 54 | height: $height; 55 | position: relative; 56 | canvas { 57 | height: $height !important; 58 | } 59 | } 60 | } 61 | 62 | .client-main { 63 | position: relative; 64 | display: flex; 65 | flex-direction: column; 66 | flex-grow: 1; 67 | } 68 | 69 | .message-list-container { 70 | overflow: scroll; 71 | flex: 1; 72 | padding: 15px; 73 | .message-list-pad { 74 | height: 100vh; 75 | } 76 | } 77 | 78 | .message { 79 | border-top: solid 2px #aaa; 80 | padding: 8px; 81 | .message-header { 82 | .message-name { 83 | font-weight: bold; 84 | &.message-name-system { 85 | color: red; 86 | } 87 | } 88 | .message-type { 89 | margin-left: 10px; 90 | color: #5a5; 91 | } 92 | } 93 | .message-body { 94 | white-space: pre; 95 | } 96 | .message-body-main-xml { 97 | border: solid 2px #ccc; 98 | padding: 2px; 99 | border-radius: 5px; 100 | } 101 | } 102 | 103 | .operation-box { 104 | border-top: solid 2px #aaa; 105 | padding: 15px; 106 | display: flex; 107 | .opeartion-message-form { 108 | flex-grow: 1; 109 | .operation-type { 110 | width: 180px; 111 | } 112 | } 113 | .opeartion-trouble { 114 | padding: 10px; 115 | flex-basis: 440px; 116 | .operation-button-group { 117 | float: right; 118 | } 119 | } 120 | } 121 | 122 | -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import 'phoenix_html'; 2 | import {Socket} from 'phoenix'; 3 | import * as OperationBox from './view/operation_box'; 4 | import * as MessageList from './view/message_list'; 5 | import * as MetricsList from './view/metrics_list'; 6 | 7 | $(() => { 8 | 9 | const socket = new Socket('/socket', { 10 | params: {token: window.userToken}, 11 | // logger: ((kind, msg, data) => { 12 | // console.log(`${kind}: ${msg}`, data); 13 | // }) 14 | }); 15 | 16 | socket.connect(); 17 | const chan = socket.channel('room:chat', {}); 18 | 19 | OperationBox.load(chan); 20 | 21 | chan.join().receive('ok', () => { 22 | console.log('joined'); 23 | 24 | chan.on('new_msg', (msg) => { 25 | MessageList.handleMsg(msg); 26 | }); 27 | 28 | chan.on('system', (msg) => { 29 | MetricsList.handleMsg(msg); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /assets/js/socket.js: -------------------------------------------------------------------------------- 1 | // NOTE: The contents of this file will only be executed if 2 | // you uncomment its entry in "assets/js/app.js". 3 | 4 | // To use Phoenix channels, the first step is to import Socket 5 | // and connect at the socket path in "lib/web/endpoint.ex": 6 | import {Socket} from "phoenix" 7 | 8 | let socket = new Socket("/socket", {params: {token: window.userToken}}) 9 | 10 | // When you connect, you'll often need to authenticate the client. 11 | // For example, imagine you have an authentication plug, `MyAuth`, 12 | // which authenticates the session and assigns a `:current_user`. 13 | // If the current user exists you can assign the user's token in 14 | // the connection for use in the layout. 15 | // 16 | // In your "lib/web/router.ex": 17 | // 18 | // pipeline :browser do 19 | // ... 20 | // plug MyAuth 21 | // plug :put_user_token 22 | // end 23 | // 24 | // defp put_user_token(conn, _) do 25 | // if current_user = conn.assigns[:current_user] do 26 | // token = Phoenix.Token.sign(conn, "user socket", current_user.id) 27 | // assign(conn, :user_token, token) 28 | // else 29 | // conn 30 | // end 31 | // end 32 | // 33 | // Now you need to pass this token to JavaScript. You can do so 34 | // inside a script tag in "lib/web/templates/layout/app.html.eex": 35 | // 36 | // 37 | // 38 | // You will need to verify the user token in the "connect/2" function 39 | // in "lib/web/channels/user_socket.ex": 40 | // 41 | // def connect(%{"token" => token}, socket) do 42 | // # max_age: 1209600 is equivalent to two weeks in seconds 43 | // case Phoenix.Token.verify(socket, "user socket", token, max_age: 1209600) do 44 | // {:ok, user_id} -> 45 | // {:ok, assign(socket, :user, user_id)} 46 | // {:error, reason} -> 47 | // :error 48 | // end 49 | // end 50 | // 51 | // Finally, pass the token on connect as below. Or remove it 52 | // from connect if you don't care about authentication. 53 | 54 | socket.connect() 55 | 56 | // Now that you are connected, you can join channels with a topic: 57 | let channel = socket.channel("topic:subtopic", {}) 58 | channel.join() 59 | .receive("ok", resp => { console.log("Joined successfully", resp) }) 60 | .receive("error", resp => { console.log("Unable to join", resp) }) 61 | 62 | export default socket 63 | -------------------------------------------------------------------------------- /assets/js/view/message_list.js: -------------------------------------------------------------------------------- 1 | import {pd as PrettyData} from 'pretty-data'; 2 | 3 | const messageMax = 20; 4 | 5 | function createTextBody({body}) { 6 | const $el = $('