{"id":13483372,"url":"https://github.com/cable-cr/cable","last_synced_at":"2025-04-05T05:05:19.349Z","repository":{"id":42034500,"uuid":"196462498","full_name":"cable-cr/cable","owner":"cable-cr","description":"It's like ActionCable (100% compatible with JS Client), but you know, for Crystal","archived":false,"fork":false,"pushed_at":"2024-10-11T15:42:32.000Z","size":503,"stargazers_count":129,"open_issues_count":12,"forks_count":13,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-03-29T04:06:25.913Z","etag":null,"topics":["actioncable","crystal","crystal-language","hacktoberfest","lucky-framework","realtime","websockets"],"latest_commit_sha":null,"homepage":"","language":"Crystal","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cable-cr.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2019-07-11T20:42:01.000Z","updated_at":"2024-11-17T12:03:11.000Z","dependencies_parsed_at":"2023-09-21T18:43:37.041Z","dependency_job_id":"71e53be1-734e-4203-ac1a-432358641585","html_url":"https://github.com/cable-cr/cable","commit_stats":{"total_commits":75,"total_committers":10,"mean_commits":7.5,"dds":0.5466666666666666,"last_synced_commit":"5abfa6533ec8a8fe99cac0c6b518982746f9a50e"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cable-cr%2Fcable","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cable-cr%2Fcable/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cable-cr%2Fcable/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cable-cr%2Fcable/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cable-cr","download_url":"https://codeload.github.com/cable-cr/cable/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247289426,"owners_count":20914464,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actioncable","crystal","crystal-language","hacktoberfest","lucky-framework","realtime","websockets"],"created_at":"2024-07-31T17:01:10.535Z","updated_at":"2025-04-05T05:05:19.334Z","avatar_url":"https://github.com/cable-cr.png","language":"Crystal","funding_links":[],"categories":["HTTP"],"sub_categories":[],"readme":"# Cable\n\n[![ci workflow](https://github.com/cable-cr/cable/actions/workflows/ci.yml/badge.svg)](https://github.com/cable-cr/cable/actions/workflows/ci.yml)\n\nIt's like [ActionCable](https://guides.rubyonrails.org/action_cable_overview.html) (100% compatible with JS Client), but you know, for Crystal.\n\n## Installation\n\n1. Add the dependency to your `shard.yml`:\n\n```yaml\ndependencies:\n  cable:\n    github: cable-cr/cable\n    branch: master # or use the latest version\n  # Specify which backend you want to use\n  cable-redis:\n    github: cable-cr/cable-redis\n    branch: main\n```\n\nCable supports multiple backends. The most common one is Redis, but there's a few to choose from with more being added:\n\nSince there are multiple different versions of Redis for Crystal, you can choose which one you want to use.\n* [jgaskins/redis](https://github.com/cable-cr/cable-redis)\n* [stefanwille/crystal-redis](https://github.com/cable-cr/cable-redis-legacy)\n\nOr if you don't want to use Redis, you can try one of these alternatives\n\n* [NATS](https://github.com/cable-cr/cable-nats)\n\n2. Run `shards install`\n\n## Usage\n\nApplication code\n```crystal\nrequire \"cable\"\n# Or whichever backend you chose\nrequire \"cable-redis\"\n```\n\n## Lucky example\n\nTo help better illustrate how the entire setup looks, we'll use [Lucky](https://luckyframework.org), but this will work in any Crystal web framework.\n\n### Load the shard\n\n```crystal\n# src/shards.cr\n\nrequire \"cable\"\nrequire \"cable-redis\"\n```\n\n### Mount the middleware\n\nAdd the `Cable::Handler` before `Lucky::RouteHandler`\n\n```crystal\n# src/app_server.cr\n\nclass AppServer \u003c Lucky::BaseAppServer\n  def middleware\n    [\n      Cable::Handler(ApplicationCable::Connection).new, # place before the middleware below\n      Honeybadger::Handler.new,\n      Lucky::ErrorHandler.new(action: Errors::Show),\n      Lucky::RouteHandler.new,\n    ]\n   end\nend\n```\n\n### Configure cable settings\n\nAfter that, you can configure your `Cable server`. The defaults are:\n\n```crystal\n# config/cable.cr\n\nCable.configure do |settings|\n  settings.route = \"/cable\"    # the URL your JS Client will connect\n  settings.token = \"token\"     # The query string parameter used to get the token\n  settings.url = ENV.fetch(\"CABLE_BACKEND_URL\", \"redis://localhost:6379\")\n  settings.backend_class = Cable::RedisBackend\n  settings.backend_ping_interval = 15.seconds\n  settings.restart_error_allowance = 20\n  settings.on_error = -\u003e(error : Exception, message : String) do\n    # or whichever error reportings you're using\n    Bugsnag.report(error) do |event|\n      event.app.app_type = \"lucky\"\n      event.meta_data = {\n        \"error_class\" =\u003e JSON::Any.new(error.class.name),\n        \"message\"     =\u003e JSON::Any.new(message),\n      }\n    end\n  end\nend\n```\n\n### Configure logging level\n\nYou may want to tune how to report logging.\n\n```crystal\n# config/log.cr\n\nlog_levels = {\n  \"debug\" =\u003e Log::Severity::Debug,\n  \"info\"  =\u003e Log::Severity::Info,\n  \"error\" =\u003e Log::Severity::Error,\n}\n\n# use the `CABLE_DEBUG_LEVEL` env var to choose any of the 3 log levels above\nCable::Logger.level = log_levels[ENV.fetch(\"CABLE_DEBUG_LEVEL\", \"info\")]\n```\n\nAlternatively, use a global log level which matches you application log code also.\n\nSee [Crystal API docs](https://crystal-lang.org/api/1.6.1/Log.html#configure-logging-from-environment-variables) for more details..\n\n```crystal\n# config/log.cr\n\n# use the `LOG_LEVEL` env var\n\nCable::Logger.setup_from_env(default_level: :warn)\n```\n\n\u003e NOTE: The volume of logs produced are high... If log costs are a concern, use `warn` level to only receive critical logs\n\n### Setup the main application connection and channel classes\n\nThen you need to implement a few classes.\n\nThe connection class is how you are going to handle connections. It's referenced in the `src/app_server.cr` file when creating the handler.\n\n```crystal\n# src/channels/application_cable/connection.cr\n\nmodule ApplicationCable\n  class Connection \u003c Cable::Connection\n    # You need to specify how you identify the class, using something like:\n    # Remembering that it must be a String\n    # Tip: Use your `User#id` converted to String\n    identified_by :identifier\n\n    # If you'd like to keep a `User` instance together with the Connection, so\n    # there's no need to fetch from the database all the time, you can use the\n    # `owned_by` instruction\n    owned_by current_user : User\n\n    def connect\n      UserToken.decode_user_id(token.to_s).try do |user_id|\n        self.identifier = user_id.to_s\n        self.current_user =  UserQuery.find(user_id)\n      end\n    end\n  end\nend\n```\n\nThen you need you a base channel to make it easy to inherit your app's Cable logic.\n\n```crystal\n# src/channels/application_cable/channel.cr\n\nmodule ApplicationCable\n  class Channel \u003c Cable::Channel\n    # some potential shared logic or helpers\n  end\nend\n```\n\n### Create your app channels\n\n**Kitchen sink example**\n\nThen create your cables, as much as your want!! Let's set up a `ChatChannel` as an example:\n\n```crystal\n# src/channels/chat_channel.cr\n\nclass ChatChannel \u003c ApplicationCable::Channel\n  def subscribed\n    # We don't support stream_for, needs to generate your own unique string\n    stream_from \"chat_#{params[\"room\"]}\"\n  end\n\n  def receive(data)\n    broadcast_message = {} of String =\u003e String\n    broadcast_message[\"message\"] = data[\"message\"].to_s\n    broadcast_message[\"current_user_id\"] = connection.identifier\n    ChatChannel.broadcast_to(\"chat_#{params[\"room\"]}\", broadcast_message)\n  end\n\n  def perform(action, action_params)\n    user = UserQuery.new.find(connection.identifier)\n    # Perform actions on a user object. For example, you could manage\n    # its status by adding some .away and .status methods on it like below\n    # user.away if action == \"away\"\n    # user.status(action_params[\"status\"]) if action == \"status\"\n    ChatChannel.broadcast_to(\"chat_#{params[\"room\"]}\", {\n      \"user\"      =\u003e user.email,\n      \"performed\" =\u003e action.to_s,\n    })\n  end\n\n  def unsubscribed\n    #  Perform any action after the client closes the connection.\n    user = UserQuery.new.find(connection.identifier)\n\n    # You could, for example, call any method on your user\n    # user.logout\n  end\nend\n```\n\n**Rejection example**\n\nReject channel subscription if the request is invalid:\n\n```crystal\n# src/channels/chat_channel.cr\n\nclass ChatChannel \u003c ApplicationCable::Channel\n  def subscribed\n    reject if user_not_allowed_to_join_chat_room?\n\n    stream_from \"chat_#{params[\"room\"]}\"\n  end\nend\n```\n\n**Callbacks example**\n\nUse callbacks to perform actions or transmit messages once the connection/channel has been subscribed.\n\n```crystal\n# src/channels/chat_channel.cr\n\nclass ChatChannel \u003c ApplicationCable::Channel\n  # you can name these callbacks anything you want...\n  # `after_subscribed` can accept 1 or more callbacks to be run in order\n  after_subscribed :broadcast_welcome_pack_to_single_subscribed_user,\n                   :announce_user_joining_to_everyone_else_in_the_channel,\n                   :process_some_stuff\n\n  def subscribed\n    stream_from \"chat_#{params[\"room\"]}\"\n  end\n\n  # If you ONLY need to send the current_user a message\n  # and none of the other subscribers\n  #\n  # use -\u003e transmit(message), which accepts Hash(String, String) or String\n  def broadcast_welcome_pack_to_single_subscribed_user\n    transmit({ \"welcome_pack\" =\u003e \"some cool stuff for this single user\" })\n  end\n\n  # On the other hand,\n  # if you want to broadcast a message\n  # to all subscribers connected to this channel\n  #\n  # use -\u003e broadcast(message), which accepts Hash(String, String) or String\n  def announce_user_joining_to_everyone_else_in_the_channel\n    broadcast(\"username xyz just joined\")\n  end\n\n  # you don't need to use the transmit functionality\n  def process_some_stuff\n    send_welcome_email_to_user\n    update_their_profile\n  end\nend\n```\n\n## Error handling\n\nYou can setup a hook to report errors to any 3rd party service you choose.\n\n```crystal\n# config/cable.cr\nCable.configure do |settings|\n  settings.on_error = -\u003e(exception : Exception, message : String) do\n    # new 3rd part service handler\n    ExceptionService.notify(exception, message: message)\n    # default logic\n    Cable::Logger.error(exception: exception) { message }\n  end\nend\n```\n**Default Handler**\n\n```crystal\nHabitat.create do\n  setting on_error : Proc(Exception, String, Nil) = -\u003e(exception : Exception, message : String) do\n    Cable::Logger.error(exception: exception) { message }\n  end\nend\n```\n\n\u003e NOTE: The message field will contain details regarding which class/method raised the error\n\n## Client-Side\n\nCheck below on the JavaScript section how to communicate with the Cable backend.\n\n### JavaScript\n\nIt works with [ActionCable](https://www.npmjs.com/package/actioncable) JS Client out-of-the-box!! Yeah, that's really cool no? If you need to adapt, make a hack, or something like that?!\n\nNo, you don't need it! Just read the few lines below and start playing with Cable in 5 minutes!\n\n### ActionCable JS Example\n\n[examples/action-cable-js-client.md](examples/action-cable-js-client.md)\n\n### Vanilla JS Examples\n\nIf you want to use this shard with iOS clients or vanilla JS using react etc., there is an example in the [examples](examples/) folder.\n\n\u003e Note - If you are using a vanilla - non-action-cable JS client, you may want to disable the action cable response headers as they cause issues for clients who don't know how to handle them. Set a Habitat disable_sec_websocket_protocol_header like so to disable those headers;\n\n```crystal\n# config/cable.cr\n\nCable.configure do |settings|\n  settings.disable_sec_websocket_protocol_header = true\nend\n```\n\n## Debugging\n\nYou can create a JSON endpoint to ping the server and check how things are going.\n\n```crystal\n# src/actions/debug/index.cr\n\nclass Debug::Index \u003c ApiAction\n  include RequireAuthToken\n\n  get \"/debug\" do\n    json(Cable.server.debug_json) # Cable.server.debug_json is provided by this shard\n  end\nend\n```\n\nAlternatively, you can ping Redis directly using the redis-cli as follows;\n\n```bash\nPUBLISH _internal debug\n```\n\nThis will dump a debug status into the logs.\n\n## Contributing\n\n1. Fork it (\u003chttps://github.com/cable-cr/cable/fork\u003e)\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create a new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcable-cr%2Fcable","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcable-cr%2Fcable","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcable-cr%2Fcable/lists"}