{"id":13726641,"url":"https://github.com/eldh/credt","last_synced_at":"2026-02-13T02:03:11.002Z","repository":{"id":149717416,"uuid":"257177869","full_name":"eldh/credt","owner":"eldh","description":"CRDT-like data structures for building distributed, offline-first applications","archived":false,"fork":false,"pushed_at":"2020-07-29T21:15:42.000Z","size":229,"stargazers_count":32,"open_issues_count":0,"forks_count":1,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-09-10T07:39:18.518Z","etag":null,"topics":["crdt","crdts","offline-first","reason-native","reason-react","reasonml"],"latest_commit_sha":null,"homepage":"","language":"Reason","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/eldh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"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":"2020-04-20T05:12:43.000Z","updated_at":"2025-04-08T10:43:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"f28de182-2be8-4f78-9fd8-901d87cede6a","html_url":"https://github.com/eldh/credt","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/eldh/credt","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldh%2Fcredt","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldh%2Fcredt/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldh%2Fcredt/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldh%2Fcredt/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/eldh","download_url":"https://codeload.github.com/eldh/credt/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/eldh%2Fcredt/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29392846,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-13T00:53:09.511Z","status":"online","status_checked_at":"2026-02-13T02:00:10.076Z","response_time":78,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"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":["crdt","crdts","offline-first","reason-native","reason-react","reasonml"],"created_at":"2024-08-03T01:03:15.435Z","updated_at":"2026-02-13T02:03:10.979Z","avatar_url":"https://github.com/eldh.png","language":"Reason","funding_links":[],"categories":["Reason"],"sub_categories":[],"readme":"# credt\n\nCRDT-ish data structures built for local-first, distributed applications.\n\n## Why would I want this?\n\nCredt makes it easier to build interactive, distributed apps.\n\n- All data changes are applied atomically, which means most conflicts between different clients are avoided.\n- In cases where there are conflicts, they are automatically resolved.\n- Undo/redo is built-in.\n\n### What do you mean CRDT-_ish_?\n\nCredt is similar to operation-based CRDT:s, but it suports some operations that can result in conflicts. The handling of those conflicts is however automatic, so even if an operation failed (for example when attempting to update a record that had been deleted), the result should \"make sense\" to he user.\n\n## How does it work?\n\nCredt is built on top of native data structures. But they don't have any functions that let's you operate on them. Instead, to modify your data you _apply operations_ on it. Operations are serializable, undoable and atomic.\n\n#### Defining a data structure\n\nCurrently, this is what defining a credt list looks like:\n\n```reason\nmodule UserList = {\n  type t = {\n    id: Credt.Util.id,\n    name: string,\n    email: string,\n    age: int,\n  };\n\n  type update =\n    | SetEmail(string)\n    | SetName(string)\n    | SetAge(int);\n\n  let reducer = user =\u003e\n    fun\n    | SetEmail(email) =\u003e ({...user, email}, SetEmail(user.email))\n    | SetName(name) =\u003e ({...user, name}, SetName(user.name))\n    | SetAge(age) =\u003e ({...user, age}, SetAge(user.age));\n\n  include Credt.List.Make({\n    type nonrec t = t;\n    type nonrec update = update;\n    let getId = u =\u003e u.id;\n    let moduleId = \"UserList\" |\u003e Credt.Util.idOfString;\n    let reducer = reducer;\n  });\n};\n```\n\nThis might look like a lot – and there will be a ppx to remove the boilerplate – but it helps to understand what's going on inside credt. Let's go through it:\n\nFirst we create the base type. This should be a record. The `id` field is required, and id:s are required to be unique. Credt has its own id type, and provides a function to generate id:s.\n\n```reason\ntype t = {\n  id: Credt.Util.id,\n  name: string,\n  email: string,\n  age: int,\n};\n```\n\nThen we define actions for the type, ie how the type can be modified. This will be used in the reducer later on.\n\n```reason\ntype update =\n  | SetEmail(string)\n  | SetName(string)\n  | SetAge(int);\n```\n\nThen the reducer, which takes a record of type `t` and an `update`. It returns a tuple `(t, update)` where `t` is a new object with the update applied, and `update` is the _undo_ update, which is used if the operation for some reason needs to be rolled back.\n\n```reason\nlet reducer = user =\u003e\n  fun\n  | SetEmail(email) =\u003e ({...user, email}, SetEmail(user.email))\n  | SetName(name) =\u003e ({...user, name}, SetName(user.name))\n  | SetAge(age) =\u003e ({...user, age}, SetAge(user.age));\n```\n\nFinally, we pass this into `Credt.List.Make` which is a functor that returns a Credt `List`. The `include` keyword means that everything defined in `Credt.List` will be included in our `UserList` module.\n\n```reason\ninclude Credt.List.Make({\n  // Base (t) and update types.\n  type nonrec t = t;\n  type nonrec update = update;\n\n  // A function to get the uniqueid from a record\n  let getId = u =\u003e u.id;\n\n  // A unique id for the module itself.\n  let moduleId = \"UserList\" |\u003e Credt.Util.idOfString;\n\n  // The reducer we defined above\n  let reducer = reducer;\n});\n```\n\n#### Modifying data\n\nTo make changes to the list we just defined, we apply operations on it. For example, adding a user looks like this:\n\n```reason\nlet myUser = { ... };\n\nlet result = UserList.apply([Append(myUser)]);\n```\n\nTo change the user's name:\n\n```reason\nlet result = UserList.apply([Update(myUser.id, SetName(\"Maggie Simpson\"))]);\n```\n\n`Append` and `Update` are variants that belong to `Credt.List`, and `SetName` is the variant we defined in our update type.\n\nThe result of an `apply` call is a `result(unit, list(failedOperations))`, so if some operations failed you can inform the user. This can happen if some other client had removed the record you tried to update for example, or if you yourself batched incompatible updates.\n\n#### Reading data\n\n`UserList.getSnapshot()` will return the current content of `UserList`, and `UserList.get(id)` will return a specific item. To use this in an app, you'd probably listen to updates and use `getSnapshot()` to pass data into your app.\n\n### Manager\n\nAn app will likely consist of a numer of different credt data structures. Some things, like undo/redo and transactions, are inherently global concerns, which is why credt has a \"manager\".\n\n### Transactions\n\nTransactions ensure that dependant operations are handled as one when it comes to undo/redo and that none of the changes are applied if one operation fails.\n\nConsider for example an app where you have a list of issues and a map of labels. If you want to remove a label you have to remove it from the label map, and also remove the reference from all issues that have that label applied.\n\nSome psuedo code of how this would be done with Credt:\n\n```reason\n// Add all operations removing the label from issues\nIssueList.(\n  issues\n    |\u003e List.keep(issueHasLabel(labelId))\n    |\u003e List.map(issue =\u003e Update(RemoveLabel(labelId), issue))\n    |\u003e addToTransaction\n);\n\n// Remove the label from the collection of labels\nLabelMap.(\n  [Remove(labelId)] |\u003e addToTransaction\n);\n\n// Apply the transaction\nlet result = Credt.Manager.applyTransaction() // OK()\n\n```\n\n### Undo \u0026 redo\n\nCredt has global undo \u0026 redo functionality built in. Just call `Credt.Manager.undo()` to revert the latest operation. `Credt.Manager.redo()` will redo the last operation that was undone (if any).\n\n## Developing:\n\n```bash\nnpm install -g esy\ngit clone \u003cthis-repo\u003e\nesy install\nesy build\n```\n\n## Running Tests:\n\nTests currently only run against the native build.\n\n```bash\n# Runs the \"test\" command in `package.json`.\nesy test\n```\n\n## Building\n\nCredt is cross platform and compiles to native (with esy \u0026 dune) and javascript (with bucklescript).\n\n```bash\nesy x TestCredt.exe # Runs the native test build\nyarn bsb -make-world -clean-world # Runs the bucklescript build\n```\n\n## Current status\n\nCredt is under active development. Some parts are missing and the api is very likely to change.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feldh%2Fcredt","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Feldh%2Fcredt","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Feldh%2Fcredt/lists"}