{"id":48150137,"url":"https://github.com/sovereignbase/convergent-replicated-list","last_synced_at":"2026-05-23T12:01:52.090Z","repository":{"id":348921152,"uuid":"1199412437","full_name":"sovereignbase/convergent-replicated-list","owner":"sovereignbase","description":"Convergent Replicated List (CR-List), a delta CRDT for an ordered sequence of entries.","archived":false,"fork":false,"pushed_at":"2026-05-23T09:27:22.000Z","size":1191,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-05-23T09:29:39.103Z","etag":null,"topics":["convergent","crdt","delta-sync","distributed-systems","gossip","list","local-first","replicated","sovereignbase","typescript"],"latest_commit_sha":null,"homepage":"https://sovereignbase.dev/convergent-replicated-list/","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sovereignbase.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-02T10:22:03.000Z","updated_at":"2026-05-23T09:27:26.000Z","dependencies_parsed_at":null,"dependency_job_id":"a5c3871e-57b2-450c-ac3d-d3a40be67baf","html_url":"https://github.com/sovereignbase/convergent-replicated-list","commit_stats":null,"previous_names":["sovereignbase/convergent-replicated-list"],"tags_count":13,"template":false,"template_full_name":"jortsupetterson/package-template","purl":"pkg:github/sovereignbase/convergent-replicated-list","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sovereignbase%2Fconvergent-replicated-list","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sovereignbase%2Fconvergent-replicated-list/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sovereignbase%2Fconvergent-replicated-list/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sovereignbase%2Fconvergent-replicated-list/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sovereignbase","download_url":"https://codeload.github.com/sovereignbase/convergent-replicated-list/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sovereignbase%2Fconvergent-replicated-list/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33394672,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-23T04:15:53.637Z","status":"ssl_error","status_checked_at":"2026-05-23T04:15:53.242Z","response_time":53,"last_error":"SSL_read: 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":["convergent","crdt","delta-sync","distributed-systems","gossip","list","local-first","replicated","sovereignbase","typescript"],"created_at":"2026-04-04T17:10:09.873Z","updated_at":"2026-05-23T12:01:52.084Z","avatar_url":"https://github.com/sovereignbase.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![npm version](https://img.shields.io/npm/v/@sovereignbase/convergent-replicated-list)](https://www.npmjs.com/package/@sovereignbase/convergent-replicated-list)\n[![CI](https://github.com/sovereignbase/convergent-replicated-list/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/sovereignbase/convergent-replicated-list/actions/workflows/ci.yaml)\n[![codecov](https://codecov.io/gh/sovereignbase/convergent-replicated-list/branch/master/graph/badge.svg)](https://codecov.io/gh/sovereignbase/convergent-replicated-list)\n[![license](https://img.shields.io/npm/l/@sovereignbase/convergent-replicated-list)](LICENSE)\n\n# convergent-replicated-list\n\nConvergent Replicated List (CR-List), a delta CRDT for an ordered sequence of entries.\n\n- [Check the docs](https://sovereignbase.dev/convergent-replicated-list/docs/)\n- [Read the specification](https://sovereignbase.dev/convergent-replicated-list/)\n\n## Compatibility\n\n- Runtimes: Node \u003e= 22, modern browsers, Bun, Deno, Cloudflare Workers, Edge Runtime.\n- Module format: ESM + CommonJS.\n- Required globals / APIs: `EventTarget`, `CustomEvent`.\n- TypeScript: bundled types.\n\n## Goals\n\n- Deterministic convergence of the live list projection under asynchronous gossip delivery.\n- Consistent behavior across Node, browsers, worker, and edge runtimes.\n- Garbage collection possibility without breaking live-view convergence.\n- Event-driven API\n\n## Installation\n\n```sh\nnpm install @sovereignbase/convergent-replicated-list\n# or\npnpm add @sovereignbase/convergent-replicated-list\n# or\nyarn add @sovereignbase/convergent-replicated-list\n# or\nbun add @sovereignbase/convergent-replicated-list\n# or\ndeno add jsr:@sovereignbase/convergent-replicated-list\n# or\nvlt install jsr:@sovereignbase/convergent-replicated-list\n```\n\n## Usage\n\n### Copy-paste example\n\n```ts\nimport { CRList } from '@sovereignbase/convergent-replicated-list'\n\nconst alice = new CRList\u003cstring\u003e()\nconst bob = new CRList\u003cstring\u003e()\n\nalice.addEventListener('delta', (event) =\u003e {\n  bob.merge(event.detail)\n})\n\nalice.append('hello')\nalice.append('world')\nalice.prepend('first')\n\nconsole.log([...alice]) // ['first', 'hello', 'world']\nconsole.log([...bob]) // ['first', 'hello', 'world']\nconsole.log(alice[1]) // 'hello'\n```\n\n### Hydrating from a snapshot\n\n```ts\nimport {\n  CRList,\n  type CRListSnapshot,\n} from '@sovereignbase/convergent-replicated-list'\n\nconst source = new CRList\u003cstring\u003e()\nlet snapshot!: CRListSnapshot\u003cstring\u003e\n\nsource.addEventListener(\n  'snapshot',\n  (event) =\u003e {\n    snapshot = event.detail\n  },\n  { once: true }\n)\n\nsource.append('draft')\nsource.append('ready')\nsource.snapshot()\n\nconst restored = new CRList\u003cstring\u003e(snapshot)\n\nconsole.log([...restored]) // ['draft', 'ready']\n```\n\n### Event channels\n\n```ts\nimport { CRList } from '@sovereignbase/convergent-replicated-list'\n\nconst list = new CRList\u003cstring\u003e()\n\nlist.addEventListener('delta', (event) =\u003e {\n  console.log('delta', event.detail)\n})\n\nlist.addEventListener('change', (event) =\u003e {\n  console.log('change', event.detail)\n})\n\nlist.addEventListener('snapshot', (event) =\u003e {\n  console.log('snapshot', event.detail)\n})\n\nlist.addEventListener('ack', (event) =\u003e {\n  console.log('ack', event.detail)\n})\n\nlist.append('a')\nlist[0] = 'b'\ndelete list[0]\n```\n\n### Iteration and JSON serialization\n\n```ts\nimport { CRList } from '@sovereignbase/convergent-replicated-list'\n\nconst list = new CRList\u003cstring\u003e()\n\nlist[0] = 'up'\nlist.append('dude!')\nlist.prepend('What is')\n\nconst snapshotJson = JSON.stringify(list)\nconst restored = new CRList\u003cstring\u003e(JSON.parse(snapshotJson))\n\nfor (const value of list) {\n  console.log(value)\n}\n\nfor (const index in list) {\n  console.log(index)\n}\n\nlist.forEach((value, index, target) =\u003e {\n  console.log(index, value, target.size)\n})\n\nconst found = list.find((value, index, target) =\u003e {\n  return index === 1 \u0026\u0026 target.size === 3 \u0026\u0026 value === 'up'\n})\n\nconsole.log(found) // 'up'\nconsole.log([...restored]) // ['What is', 'up', 'dude!']\n```\n\nThis example assumes your list values are JSON-compatible. For general\n`structuredClone`-compatible values such as `Date`, `Map`, or `BigInt`, persist\nsnapshots with a structured-clone-capable store or an application-level codec\ninstead of plain `JSON.stringify` / `JSON.parse`.\n\nNumeric reads, `for...of`, `find()`, and `forEach()` return detached copies of\nvisible values. Mutating those returned values does not mutate the underlying\nreplica state.\n\n### Acknowledgements and garbage collection\n\n```ts\nimport { CRList } from '@sovereignbase/convergent-replicated-list'\n\nconst alice = new CRList\u003cstring\u003e()\nconst bob = new CRList\u003cstring\u003e()\nconst frontiers = new Map\u003cstring, string\u003e()\n\nalice.addEventListener('delta', (event) =\u003e bob.merge(event.detail))\nbob.addEventListener('delta', (event) =\u003e alice.merge(event.detail))\n\nalice.addEventListener('ack', (event) =\u003e {\n  frontiers.set('alice', event.detail)\n})\n\nbob.addEventListener('ack', (event) =\u003e {\n  frontiers.set('bob', event.detail)\n})\n\nalice.append('x')\nalice[0] = 'y'\ndelete alice[0]\n\nalice.acknowledge()\nbob.acknowledge()\n\nalice.garbageCollect([...frontiers.values()])\nbob.garbageCollect([...frontiers.values()])\n```\n\n### Advanced exports\n\nIf you need to build your own ordered-sequence CRDT binding instead of using the\nhigh-level `CRList` class, the package also exports the core CRUD and MAGS\nfunctions together with the replica and payload types.\n\nThose low-level exports let you build custom list abstractions, protocol\nwrappers, or framework-specific bindings while preserving the same convergence\nrules as the default `CRList` binding.\n\n```ts\nimport {\n  __create,\n  __update,\n  __merge,\n  __snapshot,\n  type CRListDelta,\n  type CRListSnapshot,\n} from '@sovereignbase/convergent-replicated-list'\n\nconst source = __create\u003cstring\u003e()\nconst target = __create\u003cstring\u003e()\nconst local = __update(0, ['hello', 'world'], source, 'after')\n\nif (local) {\n  const outgoing: CRListDelta\u003cstring\u003e = local.delta\n  const remoteChange = __merge(target, outgoing)\n\n  console.log(remoteChange)\n}\n\nconst snapshot: CRListSnapshot\u003cstring\u003e = __snapshot(target)\nconsole.log(snapshot)\n```\n\nThe intended split is:\n\n- `__create`, `__read`, `__update`, `__delete` for local replica mutations.\n- `__merge`, `__acknowledge`, `__garbageCollect`, `__snapshot` for gossip,\n  compaction, and serialization.\n- `CRList` when you want the default event-driven class API.\n\n## Runtime behavior\n\n### Validation and errors\n\nLow-level exports can throw `CRListError` with stable error codes:\n\n- `VALUE_NOT_CLONEABLE`\n- `INDEX_OUT_OF_BOUNDS`\n- `LIST_EMPTY`\n- `LIST_INTEGRITY_VIOLATION`\n- `UPDATE_EXPECTED_AN_ARRAY`\n\nIngress stays tolerant:\n\n- malformed top-level merge payloads are ignored\n- malformed snapshot values are dropped during hydration\n- invalid UUIDs are ignored\n- duplicate insert and delete deltas are idempotent\n- stale or malicious deltas do not break convergence of the live view\n\n### Safety and copying semantics\n\n- Snapshots are detached structured-clone full-state payloads.\n- Deltas are detached structured-clone gossip payloads intended to be forwarded\n  as-is.\n- `change` is a minimal index-keyed local patch.\n- `toJSON()` returns a detached structured-clone snapshot.\n- `JSON.stringify()` and `toString()` are only reliable when list values are\n  JSON-compatible.\n- Numeric reads, `for...of`, `find()`, and `forEach()` expose detached copies of visible values rather than mutable references into replica state.\n- `for...of`, `find()`, `forEach()`, numeric indexing, `append()`, `prepend()`, `remove()`, `merge()`, `snapshot()`, `acknowledge()`, and `garbageCollect()` all operate on the live list projection.\n\n### Convergence and compaction\n\n- The convergence target is the live list projection, not internal cursor placement.\n- Stable `predecessor` anchors determine deterministic ordering together with UUIDv7 sorting when placement cannot be resolved from a live predecessor chain.\n- Tombstones remain until acknowledgement frontiers make them safe to collect.\n- Garbage collection does not change the converged live projection for replicas that later catch up from delta or snapshot state.\n\n## Tests\n\n```sh\nnpm run test\n```\n\nWhat the current test suite covers:\n\n- Coverage on built `dist/**/*.js`: `100%` statements, `100%` branches, `100%` functions, and `100%` lines, together with focused source-coverage tests for helper edge paths.\n- Public `CRList` surface: indexing, iteration, `find`, `forEach`, proxy traps, events, JSON/inspect behavior.\n- Core edge paths and malicious ingress handling for `__create`, `__read`, `__update`, `__delete`, `__merge`, `__snapshot`, `__acknowledge`, and `__garbageCollect`.\n- Internal defensive branches under intentionally corrupt in-memory replica state.\n- Integration convergence stress for:\n  - local CRUD live-view semantics\n  - snapshot hydration independent of value order\n  - merge idempotency for duplicate insert/delete deltas\n  - stale-peer acknowledgement and garbage collection recovery\n  - shuffled asynchronous gossip delivery\n  - shuffled delivery with replica restarts\n  - concurrent insert after concurrently deleted predecessor\n  - `100` aggressive deterministic convergence scenarios\n- End-to-end runtime matrix for:\n  - Node ESM\n  - Node CJS\n  - Bun ESM\n  - Bun CJS\n  - Deno ESM\n  - Cloudflare Workers ESM\n  - Edge Runtime ESM\n  - Browsers via Playwright: Chromium, Firefox, WebKit, mobile Chrome, mobile Safari\n\n## Benchmarks\n\n```sh\nnpm run bench\n```\n\nLast measured on Node `v22.14.0` (`win32 x64`):\n\n| group   | scenario                                           |     n | ops | crlist ms | crlist ms/op | crlist ops/sec | yjs ms/op | yjs ops/sec | json-joy ms/op | json-joy ops/sec | automerge ms/op | automerge ops/sec | winner    |\n| ------- | -------------------------------------------------- | ----: | --: | --------: | -----------: | -------------: | --------: | ----------: | -------------: | ---------------: | --------------: | ----------------: | --------- |\n| crud    | create / hydrate snapshot                          | 5,000 | 250 |  1,196.96 |         4.79 |         208.86 |      9.27 |      107.88 |          12.86 |            77.78 |          161.27 |               6.2 | crlist    |\n| crud    | read / random indexed reads                        | 5,000 | 250 |      0.69 |            0 |     359,919.38 |         0 |  250,375.56 |           0.01 |       109,938.43 |               0 |       1,445,922.5 | automerge |\n| crud    | update / append after tail                         | 5,000 | 250 |       1.8 |         0.01 |     138,557.89 |      0.02 |    40,340.8 |           0.02 |        44,789.22 |               2 |            498.99 | crlist    |\n| crud    | update / insert before middle                      | 5,000 | 250 |      3.07 |         0.01 |      81,425.27 |      0.02 |   55,321.97 |           0.01 |        79,961.62 |            1.92 |             521.5 | crlist    |\n| crud    | update / insert at head                            | 5,000 | 250 |      2.86 |         0.01 |      87,305.74 |      0.01 |  104,672.58 |           0.02 |        41,149.55 |            1.98 |            504.33 | yjs       |\n| crud    | update / overwrite random                          | 5,000 | 250 |      5.63 |         0.02 |      44,397.09 |      0.05 |   20,391.02 |           0.03 |        34,526.57 |            2.23 |            448.79 | crlist    |\n| crud    | delete / single deletes from middle                | 5,000 | 250 |      1.77 |         0.01 |     140,924.46 |      0.02 |    56,326.6 |           0.02 |         41,923.1 |            0.33 |          3,034.57 | crlist    |\n| crud    | delete / range deletes                             | 5,000 | 250 |      6.06 |         0.02 |      41,262.98 |      0.02 |   40,741.83 |           0.07 |        13,344.01 |            0.42 |          2,365.95 | crlist    |\n| mags    | snapshot                                           | 5,000 | 250 |     61.55 |         0.25 |       4,061.61 |      5.16 |      193.92 |           10.3 |            97.13 |           15.68 |             63.78 | crlist    |\n| mags    | acknowledge                                        | 5,000 | 250 |     44.79 |         0.18 |        5,582.1 |       n/a |         n/a |            n/a |              n/a |             n/a |               n/a | n/a       |\n| mags    | garbage collect                                    | 5,000 | 250 |    147.13 |         0.59 |       1,699.21 |       n/a |         n/a |            n/a |              n/a |             n/a |               n/a | n/a       |\n| mags    | merge ordered deltas                               | 5,000 | 250 |      1.69 |         0.01 |     147,658.14 |      0.04 |   27,173.62 |           0.01 |        82,464.71 |            3.68 |            271.46 | crlist    |\n| mags    | merge shuffled gossip                              | 5,000 | 250 |       n/a |          n/a |            n/a |      0.59 |    1,685.81 |            n/a |              n/a |            0.33 |          3,043.22 | automerge |\n| class   | constructor / hydrate snapshot                     | 5,000 | 250 |  1,518.88 |         6.08 |          164.6 |      9.63 |       103.8 |          11.07 |            90.32 |             205 |              4.88 | crlist    |\n| class   | append after tail                                  | 5,000 | 250 |      2.99 |         0.01 |      83,701.62 |      0.01 |   69,074.13 |           0.01 |       162,834.63 |            2.38 |            419.43 | json-joy  |\n| class   | prepend before middle                              | 5,000 | 250 |      2.86 |         0.01 |      87,284.41 |      0.01 |  129,125.56 |           0.01 |       128,218.28 |            2.08 |            481.22 | yjs       |\n| class   | remove from middle                                 | 5,000 | 250 |      1.99 |         0.01 |     125,514.61 |      0.01 |   76,115.09 |           0.01 |        99,407.53 |            0.37 |          2,673.64 | crlist    |\n| class   | find near tail                                     | 5,000 | 250 |    119.12 |         0.48 |       2,098.66 |      0.21 |    4,715.35 |           1.71 |           585.14 |            0.02 |         40,475.34 | automerge |\n| class   | snapshot                                           | 5,000 | 250 |     62.59 |         0.25 |       3,994.02 |      4.88 |      205.07 |           8.26 |           121.09 |           19.84 |              50.4 | crlist    |\n| class   | acknowledge                                        | 5,000 | 250 |     38.72 |         0.15 |       6,456.58 |       n/a |         n/a |            n/a |              n/a |             n/a |               n/a | n/a       |\n| class   | garbage collect                                    | 5,000 | 250 |    108.65 |         0.43 |       2,301.01 |       n/a |         n/a |            n/a |              n/a |             n/a |               n/a | n/a       |\n| class   | merge ordered deltas                               | 5,000 | 250 |      1.29 |         0.01 |     193,963.85 |      0.03 |   34,196.92 |              0 |       258,371.23 |            4.92 |            203.07 | json-joy  |\n| class   | merge shuffled gossip                              | 5,000 | 250 |       n/a |          n/a |            n/a |      0.45 |    2,221.81 |            n/a |              n/a |            0.71 |          1,400.93 | yjs       |\n| latency | append write to remote visible                     | 5,000 | 250 |      3.61 |         0.01 |      69,202.24 |      0.07 |   14,698.45 |           0.02 |        54,034.19 |            8.78 |            113.89 | crlist    |\n| latency | middle insert write to remote visible              | 5,000 | 250 |      8.29 |         0.03 |      30,168.46 |      0.05 |   19,072.32 |           0.02 |        61,106.77 |            8.26 |            121.13 | json-joy  |\n| latency | head insert write to remote visible                | 5,000 | 250 |     22.31 |         0.09 |       11,207.6 |      0.05 |   19,615.07 |           0.02 |           43,743 |            8.65 |             115.6 | json-joy  |\n| latency | head delete to remote hidden                       | 5,000 | 250 |    294.31 |         1.18 |         849.46 |      0.05 |   20,731.06 |           0.06 |        16,273.29 |            3.73 |            267.95 | yjs       |\n| latency | middle delete to remote hidden                     | 5,000 | 250 |    258.47 |         1.03 |         967.23 |      0.05 |   21,973.39 |           0.06 |        18,026.59 |            3.06 |            326.98 | yjs       |\n| latency | tail delete to remote hidden                       | 5,000 | 250 |    260.32 |         1.04 |         960.35 |      0.07 |    14,636.4 |           0.04 |        25,885.28 |            2.83 |            353.71 | json-joy  |\n| latency | out-of-order write delivery to remote visible      | 5,000 | 250 |     36.86 |         0.15 |        6,781.7 |    207.42 |        4.82 |            n/a |              n/a |          224.72 |              4.45 | crlist    |\n| latency | out-of-order delete delivery to remote convergence | 5,000 | 250 |    327.06 |         1.31 |         764.38 |      0.03 |   30,974.71 |           0.12 |         8,192.85 |            1.03 |            968.05 | yjs       |\n\n## License\n\nApache-2.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsovereignbase%2Fconvergent-replicated-list","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsovereignbase%2Fconvergent-replicated-list","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsovereignbase%2Fconvergent-replicated-list/lists"}