{"id":15667616,"url":"https://github.com/joefreeman/topical","last_synced_at":"2026-01-24T21:32:28.346Z","repository":{"id":65131858,"uuid":"577078216","full_name":"joefreeman/topical","owner":"joefreeman","description":"Simple server-maintained state synchronisation.","archived":false,"fork":false,"pushed_at":"2026-01-21T13:06:37.000Z","size":485,"stargazers_count":13,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-21T23:47:59.896Z","etag":null,"topics":["elixir","javascript","react","typescript","websocket"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/joefreeman.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2022-12-11T22:27:53.000Z","updated_at":"2026-01-21T13:06:42.000Z","dependencies_parsed_at":"2024-10-03T14:04:56.028Z","dependency_job_id":"e51e9a20-368e-4873-b5b9-e354a5ffd41f","html_url":"https://github.com/joefreeman/topical","commit_stats":{"total_commits":131,"total_committers":1,"mean_commits":131.0,"dds":0.0,"last_synced_commit":"3a17d5dc353e24f7233bbcce079548c39f4d7bfd"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/joefreeman/topical","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joefreeman%2Ftopical","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joefreeman%2Ftopical/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joefreeman%2Ftopical/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joefreeman%2Ftopical/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/joefreeman","download_url":"https://codeload.github.com/joefreeman/topical/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/joefreeman%2Ftopical/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28737312,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-24T21:19:41.845Z","status":"ssl_error","status_checked_at":"2026-01-24T21:13:38.675Z","response_time":89,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["elixir","javascript","react","typescript","websocket"],"created_at":"2024-10-03T14:04:31.336Z","updated_at":"2026-01-24T21:32:28.323Z","avatar_url":"https://github.com/joefreeman.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cbr /\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"logo.png\" width=\"350\" alt=\"Topical\" /\u003e\n\u003c/p\u003e\n\n\u003cbr /\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://hex.pm/packages/topical\"\u003e\u003cimg src=\"https://img.shields.io/hexpm/v/topical.svg?color=6e4a7e\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@topical/core\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@topical/core.svg?color=3178c6\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@topical/react\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@topical/react.svg?color=087ea4\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cbr /\u003e\n\nTopical is an Elixir library for synchronising server-maintained state (_topics_) to connected clients. Topic lifecycle is managed by the server: topics are initialised as needed, shared between subscribing clients, and automatically shut down when not in use.\n\nThe accompanying JavaScript library (and React hooks) allow clients to easily connect to topics, and efficiently receive real-time updates. Clients can also send requests (or notifications) upstream to the server.\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"architecture.png\" width=\"400\" alt=\"Architecture diagram\" /\u003e\n\u003c/p\u003e\n\nSee the [Getting started](https://hexdocs.pm/topical/getting-started.html) guide.\n\n## Ephemeral or persistent state\n\nIn its simplest instance, a topic's state can be ephemeral - i.e., discarded when the topic is shut down. For example, for synchronising cursor positions of users (see [canvas](examples/canvas/) example).\n\nAlternatively state could be persisted - e.g., to a database - with the topic subscribing to updates from the database, which allows separating mutation logic, and replication of topics. In the case where lower durability can be afforded, state can be periodically flushed to disk.\n\n## Comparison to LiveView\n\nTopical solves a similar problem to Phoenix LiveView, but at a different abstraction level, by dealing only with the underlying state, rather than rendering HTML and handling UI events.\n\n## Adapters\n\nThere are WebSocket adapters for [Cowboy](https://github.com/ninenines/cowboy) and [WebSock](https://github.com/phoenixframework/websock) (compatible with [Plug](https://github.com/elixir-plug/plug) and [Bandit](https://github.com/mtrudel/bandit)), which allow adding Topical into an existing application. Either of these are required to support the JavaScript client and the full functionality of Topical. See [`examples/todo`](examples/todo/) for an example of both (running simultaneously).\n\nAdditionally, a REST-like adapter provides a way for clients to capture a snapshot of a topic (which is useful for supporing the incremental cache use case).\n\n## Example: todo list\n\nA partial implementation of a todo list topic might look like this:\n\n```elixir\ndefmodule MyApp.Topics.List do\n  use Topical.Topic, route: [\"lists\", :list_id]\n\n  # Initialise the topic\n  def init(params) do\n    # Get the ID from the route (unused here)\n    list_id = Keyword.fetch!(params, :list_id)\n\n    # TODO: subscribe to events from, e.g., database/pub-sub\n    # TODO: load list from, e.g., database/API\n\n    value = %{items: %{}, order: []}\n    {:ok, Topic.new(value)}\n  end\n\n  # Handle a message - e.g., from subscription\n  def handle_info({:done, id}, topic) do\n    topic = Topic.set(topic, [:items, id, :done], true)\n    {:ok, topic}\n  end\n\n  # Handle a request from a connected client\n  def handle_execute(\"add_item\", {text}, topic, _context) do\n    id =\n      topic.state.items\n      |\u003e Enum.count()\n      |\u003e Integer.to_string()\n\n    # Update the topic by putting the item in 'items', and appending the ID to 'order'\n    topic =\n      topic\n      |\u003e Topic.set([:items, id], %{text: text, done: false})\n      |\u003e Topic.insert([:order], id)\n\n    # Return the result (the ID), and the updated topic\n    {:ok, id, topic}\n  end\nend\n```\n\nAnd a corresponding React component:\n\n```typescript\nimport { SocketProvider, useTopic } from \"@topical/react\";\n\nfunction TodoList({ id }) {\n  const [list, { execute, loading, error }] = useTopic(\"lists\", id);\n  const handleAddClick = useCallback(\n    () =\u003e execute(\"add_item\", prompt()),\n    [execute]\n  );\n  if (loading) {\n    return \u003cp\u003eLoading...\u003c/p\u003e;\n  } else if (error) {\n    return \u003cp\u003eError.\u003c/p\u003e\n  } else {\n    return (\n      // ...\n    );\n  }\n}\n\nfunction App() {\n  return (\n    \u003cSocketProvider url=\"...\"\u003e\n      \u003cTodoList id=\"foo\" /\u003e\n      \u003cTodoList id=\"bar\" /\u003e\n    \u003c/SocketProvider\u003e\n  );\n}\n```\n\nSee [`examples/todo`](examples/todo/) for a more complete example.\n\n## Other examples\n\n- [`examples/todo`](examples/todo/) - A more complete todo example, with basic persistence.\n- [`examples/canvas`](examples/canvas/) - A simple canvas drawing example, with synchronised cursors.\n- [`examples/game_of_life`](examples/game_of_life/) - Conway's Game of Life.\n- [`examples/cache`](examples/cache/) - Using Topical as an incremental cache.\n\n## Documentation\n\nDocumentation is available on [HexDocs](https://hexdocs.pm/topical/).\n\n## Development\n\nThis repository is separated into:\n\n- [`server_ex`](server_ex/) - the Elixir library for implementing topic servers, including adapters.\n- [`client_js`](client_js/) - the vanilla JavaScript WebSocket client.\n- [`client_react`](client_react/) - React hooks built on top of the JavaScript client.\n\n## License\n\nTopical is released under the Apache License 2.0.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoefreeman%2Ftopical","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjoefreeman%2Ftopical","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjoefreeman%2Ftopical/lists"}