├── test ├── test_helper.exs └── socketio_emitter_test.exs ├── .formatter.exs ├── .gitignore ├── LICENSE ├── config └── config.exs ├── mix.exs ├── .github └── workflows │ └── elixir.yml ├── lib └── socketio_emitter.ex ├── mix.lock └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrey Chugunov 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /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 | import Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :socketio_emitter, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:socketio_emitter, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule SocketioEmitter.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :socketio_emitter, 7 | version: "0.2.0", 8 | elixir: "~> 1.13.1", 9 | description: description(), 10 | package: package(), 11 | build_embedded: Mix.env() == :prod, 12 | start_permanent: Mix.env() == :prod, 13 | deps: deps(), 14 | source_url: "https://github.com/chugunov/socketio_emitter" 15 | ] 16 | end 17 | 18 | def application do 19 | # Specify extra applications you'll use from Erlang/Elixir 20 | [extra_applications: [:logger, :redix]] 21 | end 22 | 23 | defp description do 24 | """ 25 | `socketio_emitter` allows you to communicate with socket.io servers easily from Elixir processes. 26 | """ 27 | end 28 | 29 | defp package do 30 | [ 31 | name: :socketio_emitter, 32 | files: ["lib", "mix.exs", "README*", "LICENSE*"], 33 | maintainers: ["Andrey Chugunov"], 34 | licenses: ["MIT"], 35 | links: %{"GitHub" => "https://github.com/chugunov/socketio_emitter"} 36 | ] 37 | end 38 | 39 | defp deps do 40 | [ 41 | {:ex_doc, ">= 0.0.0", only: :dev}, 42 | {:redix, "~> 1.1"}, 43 | {:msgpax, "~> 2.3"} 44 | ] 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /test/socketio_emitter_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SocketIOEmitterTest do 2 | use ExUnit.Case 3 | doctest SocketIOEmitter 4 | 5 | setup_all do 6 | assert {:ok, _pid} = 7 | SocketIOEmitter.start_link( 8 | redix_config: [ 9 | host: "localhost", 10 | port: 6379 11 | ], 12 | pool_size: 3 13 | ) 14 | 15 | :ok 16 | end 17 | 18 | test "should successfully start supervisor and emit simple message" do 19 | msg = %{:text => "hello"} 20 | 21 | assert {:ok, _consumers_count} = 22 | SocketIOEmitter.emit(["private", msg], 23 | nsp: "/admin", 24 | rooms: ["username"], 25 | flags: [:broadcast] 26 | ) 27 | end 28 | 29 | test "should pack message with default parameters" do 30 | msg = %{:text => "hello"} 31 | {channel, binary_msg} = SocketIOEmitter.pack_msg!(["private", msg]) 32 | 33 | expected_pack = [ 34 | "emitter", 35 | %{data: ["private", msg], nsp: "/", type: 2}, 36 | %{flags: %{}, rooms: []} 37 | ] 38 | 39 | assert channel == "socket.io#/#" 40 | 41 | assert binary_msg == 42 | expected_pack 43 | |> Msgpax.pack!() 44 | |> IO.iodata_to_binary() 45 | end 46 | 47 | test "should pack message to namspace and room with broadcast flag" do 48 | msg = %{:text => "hello"} 49 | 50 | {channel, binary_msg} = 51 | SocketIOEmitter.pack_msg!(["private", msg], 52 | nsp: "/admin", 53 | rooms: ["username"], 54 | flags: [:broadcast] 55 | ) 56 | 57 | expected_pack = [ 58 | "emitter", 59 | %{data: ["private", msg], nsp: "/admin", type: 2}, 60 | %{flags: %{broadcast: true}, rooms: ["username"]} 61 | ] 62 | 63 | assert channel == "socket.io#/admin#username#" 64 | 65 | assert binary_msg == 66 | expected_pack 67 | |> Msgpax.pack!() 68 | |> IO.iodata_to_binary() 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | release: 9 | types: [created] 10 | 11 | jobs: 12 | build: 13 | 14 | name: Build and test OTP ${{matrix.otp-version}} / Elixir ${{matrix.elixir-version}} 15 | runs-on: ${{ matrix.os }} 16 | 17 | services: 18 | redis: 19 | image: redis 20 | options: >- 21 | --health-cmd "redis-cli ping" 22 | --health-interval 10s 23 | --health-timeout 5s 24 | --health-retries 5 25 | ports: 26 | - 6379:6379 27 | 28 | strategy: 29 | matrix: 30 | include: 31 | - otp-version: '24.2' 32 | elixir-version: '1.13.1' 33 | os: ubuntu-latest 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Set up Elixir 38 | uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f 39 | with: 40 | elixir-version: ${{ matrix.elixir-version }} 41 | otp-version: ${{ matrix.otp-version }} 42 | - name: Restore dependencies cache 43 | uses: actions/cache@v2 44 | with: 45 | path: deps 46 | key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} 47 | restore-keys: ${{ runner.os }}-mix- 48 | - name: Install dependencies 49 | run: mix deps.get 50 | - name: Run tests 51 | run: mix test 52 | 53 | publish: 54 | 55 | needs: build 56 | name: Publish to hex.pm 57 | runs-on: ubuntu-latest 58 | 59 | if: GitHub.event_name == 'release' 60 | steps: 61 | - name: Check out 62 | uses: actions/checkout@v2 63 | - name: Set up Elixir 64 | uses: erlef/setup-beam@988e02bfe678367a02564f65ca2e37726dc0268f 65 | with: 66 | elixir-version: '1.13.1' 67 | otp-version: '24.2' 68 | 69 | - name: Install dependencies 70 | run: mix deps.get 71 | - name: Build hex package 72 | run: mix hex.build 73 | - name: Publishing hex package 74 | run: mix hex.publish --yes 75 | env: 76 | HEX_API_KEY: ${{ secrets.HEX_API_KEY }} 77 | -------------------------------------------------------------------------------- /lib/socketio_emitter.ex: -------------------------------------------------------------------------------- 1 | defmodule SocketIOEmitter do 2 | @moduledoc """ 3 | Module allows you to communicate with socket.io servers easily from Elixir processes. 4 | """ 5 | 6 | use Supervisor 7 | 8 | @root_nsp "/" 9 | @prefix "socket.io" 10 | @uid "emitter" 11 | @parser_event_type 2 12 | @default_redix_config [redix_config: [], pool_size: 1] 13 | 14 | def start_link(redis_opts \\ []) 15 | 16 | def start_link(redis_opts) do 17 | Supervisor.start_link(__MODULE__, redis_opts, name: __MODULE__) 18 | end 19 | 20 | @impl true 21 | def init(redis_opts) do 22 | override_config(redis_opts) 23 | 24 | redix_workers = 25 | for i <- 0..(config(:pool_size) - 1) do 26 | Supervisor.child_spec({Redix, name: :"redix_#{i}"}, id: {Redix, i}) 27 | end 28 | 29 | opts = [strategy: :one_for_one, name: SocketIOEmitter.Supervisor] 30 | Supervisor.init(redix_workers, opts) 31 | end 32 | 33 | @doc """ 34 | Pack message 35 | """ 36 | def pack_msg!(args, opts \\ []) do 37 | nsp = Keyword.get(opts, :nsp, @root_nsp) 38 | rooms = Keyword.get(opts, :rooms, []) 39 | flags = Keyword.get(opts, :flags, []) 40 | prefix = Keyword.get(opts, :prefix, @prefix) 41 | 42 | packet = %{:type => @parser_event_type, :data => args, :nsp => nsp} 43 | flags_ = Enum.reduce(flags, %{}, &Map.put(&2, &1, true)) 44 | opts = %{:rooms => rooms, :flags => flags_} 45 | 46 | channel = 47 | case length(rooms) do 48 | 1 -> "#{prefix}##{nsp}##{hd(rooms)}#" 49 | _ -> "#{prefix}##{nsp}#" 50 | end 51 | 52 | binary_msg = 53 | [@uid, packet, opts] 54 | |> Msgpax.pack!() 55 | |> IO.iodata_to_binary() 56 | 57 | {channel, binary_msg} 58 | end 59 | 60 | @doc """ 61 | Emit message to socket.io 62 | """ 63 | def emit(args, opts \\ []) when is_list(args) do 64 | {:ok, emit!(args, opts)} 65 | rescue 66 | _error -> 67 | {:error, :wrong_data} 68 | end 69 | 70 | def emit!(args, opts \\ []) when is_list(args) do 71 | {channel, binary_msg} = pack_msg!(args, opts) 72 | Redix.command!(:"redix_#{random_index()}", ["PUBLISH", channel, binary_msg]) 73 | end 74 | 75 | defp random_index() do 76 | "#{rem(System.unique_integer([:positive]), config(:pool_size))}" 77 | end 78 | 79 | defp override_config(redis_opts) do 80 | new_config = Keyword.merge(config(), redis_opts) 81 | Application.put_env(:socketio_emitter, :redix_pool, new_config) 82 | end 83 | 84 | defp config() do 85 | curr_env_config = Application.get_env(:socketio_emitter, :redix_pool, []) 86 | Keyword.merge(@default_redix_config, curr_env_config) 87 | end 88 | 89 | defp config(key) when is_atom(key) do 90 | config()[key] 91 | end 92 | end 93 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], []}, 3 | "earmark": {:hex, :earmark, "1.2.2", "f718159d6b65068e8daeef709ccddae5f7fdc770707d82e7d126f584cd925b74", [:mix], [], "hexpm", "59514c4a207f9f25c5252e09974367718554b6a0f41fe39f7dc232168f9cb309"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.19", "de0d033d5ff9fc396a24eadc2fcf2afa3d120841eb3f1004d138cbf9273210e8", [:mix], [], "hexpm", "527ab6630b5c75c3a3960b75844c314ec305c76d9899bb30f71cb85952a9dc45"}, 5 | "ex_doc": {:hex, :ex_doc, "0.27.3", "d09ed7ab590b71123959d9017f6715b54a448d76b43cf909eb0b2e5a78a977b2", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "ee60b329d08195039bfeb25231a208749be4f2274eae42ce38f9be0538a2f2e6"}, 6 | "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 8 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 9 | "msgpax": {:hex, :msgpax, "2.3.0", "14f52ad249a3f77b5e2d59f6143e6c18a6e74f34666989e22bac0a465f9835cc", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "65c36846a62ed5615baf7d7d47babb6541313a6c0b6d2ff19354bd518f52df7e"}, 10 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, 11 | "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"}, 12 | "telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"}, 13 | } 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # socketio_emitter 2 | 3 | [![Build Status](https://api.travis-ci.org/chugunov/socketio_emitter.svg?branch=master)](https://travis-ci.org/chugunov/socketio_emitter) 4 | [![Hex.pm](https://img.shields.io/hexpm/v/socketio_emitter.svg)](https://hex.pm/packages/socketio_emitter) 5 | 6 | `socketio_emitter` allows you to communicate with socket.io servers easily from Elixir processes. Inspired by [socket.io-emitter](https://github.com/socketio/socket.io-emitter) 7 | 8 | ## Installation 9 | 10 | The package can be installed 11 | by adding `socketio_emitter` to your list of dependencies in `mix.exs`: 12 | 13 | ```elixir 14 | def deps do 15 | [{:socketio_emitter, "~> 0.1.2"}] 16 | end 17 | ``` 18 | 19 | ## How to use 20 | 21 | Register `socketio_emitter` supervisor at your supervisor tree: 22 | 23 | ```elixir 24 | defmodule ExampleApp do 25 | use Application 26 | 27 | def start(_type, _args) do 28 | import Supervisor.Spec 29 | 30 | children = [ 31 | # Add this line to your supervisor tree 32 | supervisor(SocketIOEmitter, []), 33 | ] 34 | 35 | opts = [strategy: :one_for_one, name: ExampleApp.Supervisor] 36 | Supervisor.start_link(children, opts) 37 | end 38 | end 39 | ``` 40 | 41 | Then call `SocketIOEmitter.emit/2`: 42 | 43 | ```elixir 44 | msg = %{:text => "hello"} 45 | 46 | # sending to all clients 47 | {:ok, _consumers_count} = SocketIOEmitter.emit ["broadcast", msg] 48 | 49 | # sending to all clients in 'game' room 50 | {:ok, _consumers_count} = SocketIOEmitter.emit ["new-game", msg], 51 | rooms: ["game"] 52 | 53 | # sending to individual socketid (private message) 54 | {:ok, _consumers_count} = SocketIOEmitter.emit ["private", msg], 55 | rooms: [socket_id] 56 | 57 | # sending to all clients in 'admin' namespace 58 | {:ok, _consumers_count} = SocketIOEmitter.emit ["namespace", msg], 59 | nsp: "/admin" 60 | 61 | # sending to all clients in 'admin' namespace and in 'notifications' room 62 | {:ok, _consumers_count} = SocketIOEmitter.emit ["namespace", msg], 63 | nsp: "/admin", 64 | rooms: ["notifications"] 65 | ``` 66 | 67 | ## Configuration 68 | 69 | You can configure `socketio_emitter` from your `config.exs`. 70 | See the [redix documentation](https://hexdocs.pm/redix/Redix.html#start_link/2) for the possible values of `redix_config`. 71 | 72 | ```elixir 73 | use Mix.Config 74 | 75 | config :socketio_emitter, :redix_pool, 76 | redix_config: [ 77 | # default value: localhost 78 | host: "example.com", 79 | # default value: 6379 80 | port: 5000, 81 | ], 82 | # 5 Redix processes will be available (default value: 1) 83 | pool_size: 5 84 | ``` 85 | Or passing by parameters directly to supervisor, in this way values from config will be **overridden**: 86 | 87 | ```elixir 88 | redix_pool = [redix_config: [ 89 | host: "localhost", 90 | port: 6379 91 | ], pool_size: 3] 92 | 93 | children = [ 94 | # Add this line to your supervisor tree 95 | supervisor(SocketIOEmitter, [redix_pool], [name: :socket_emitter]) 96 | ] 97 | ``` 98 | 99 | ## TODO 100 | 101 | - tests 102 | - documentation 103 | 104 | ## License 105 | 106 | MIT 107 | --------------------------------------------------------------------------------