{"id":22310818,"url":"https://github.com/azarattum/crstore","last_synced_at":"2025-04-04T22:01:40.028Z","repository":{"id":65001314,"uuid":"576422747","full_name":"Azarattum/CRStore","owner":"Azarattum","description":"Conflict-free replicated store.","archived":false,"fork":false,"pushed_at":"2025-02-11T04:48:44.000Z","size":1596,"stargazers_count":100,"open_issues_count":0,"forks_count":5,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-03-28T21:01:34.506Z","etag":null,"topics":["crdt","kysely","local-first","react","reactivity","solid","sqlite","store","superstruct","svelte","trpc"],"latest_commit_sha":null,"homepage":"https://npmjs.com/package/crstore","language":"TypeScript","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/Azarattum.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":"2022-12-09T20:26:50.000Z","updated_at":"2025-02-23T09:03:28.000Z","dependencies_parsed_at":"2025-03-13T21:10:45.528Z","dependency_job_id":"372e56dd-1d46-4641-9b9c-fffb5b42a279","html_url":"https://github.com/Azarattum/CRStore","commit_stats":{"total_commits":114,"total_committers":2,"mean_commits":57.0,"dds":0.01754385964912286,"last_synced_commit":"4dde8314111f725676c22948776ab86c37905b6f"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azarattum%2FCRStore","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azarattum%2FCRStore/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azarattum%2FCRStore/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Azarattum%2FCRStore/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Azarattum","download_url":"https://codeload.github.com/Azarattum/CRStore/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247256093,"owners_count":20909240,"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":["crdt","kysely","local-first","react","reactivity","solid","sqlite","store","superstruct","svelte","trpc"],"created_at":"2024-12-03T21:16:06.201Z","updated_at":"2025-04-04T22:01:39.996Z","avatar_url":"https://github.com/Azarattum.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# CRStore\n\nConflict-free replicated store. \n\n\u003e WARNING: Still in development! Expect breaking changes!\n\u003e\n\u003e BREAKING (v0.20.0): Added support for React \u0026 Solid. For Svelte import `database` from `crstore/svelte`. Renamed `store` to `replicated`.\n\u003e\n\u003e BREAKING (v0.19.0): Updated `cr-sqlite` from v13 to v16. See [changelog](https://github.com/vlcn-io/cr-sqlite/releases)\n\u003e \n\u003e BREAKING (v0.18.0): If you want to support [older browsers](https://caniuse.com/mdn-api_navigator_locks) consider adding [navigator.locks polyfill](https://www.npmjs.com/package/navigator.locks) to your project. CRStore does **not** ship it since `0.18.0`!\n\n- ✨ Elegance of [Svelte](https://svelte.dev/) / [SolidJS](https://www.solidjs.com/) / [React](https://react.dev/)\n- 💪 Power of [SQLite](https://www.sqlite.org/index.html)\n- 🛡️ Safety with [Kysely](https://github.com/koskimas/kysely)\n- ⚡ CRDTs powered by [cr-sqlite](https://github.com/vlcn-io/cr-sqlite)\n- 🔮 Automagical schema using [superstruct](https://github.com/ianstormtaylor/superstruct)\n- 🤝 First class [tRPC](https://github.com/trpc/trpc) support\n- 🐇 Supports [bun:sqlite](https://bun.sh/docs/api/sqlite) (experimental)\n\nInstall `crstore` and `superstruct` (for automatic schema):\n```sh\nnpm install crstore superstruct\n```\n\n## Using CRStore\n\nTo start using `CRStore` first you need to define a schema for your database. This is like a [Kysely schema](https://github.com/koskimas/kysely/blob/master/recipes/schemas.md), but defined with [superstruct](https://github.com/ianstormtaylor/superstruct), so we can have a runtime access to it. \n```ts\nimport { crr, primary } from \"crstore\";\n\n// Struct that represents the table\nconst todos = object({\n  id: string(),\n  title: string(),\n  text: string(),\n  completed: boolean(),\n});\ncrr(todos); // Register table with conflict-free replicated relations\nprimary(todos, \"id\"); // Define a primary key (can be multi-column)\n\nconst schema = object({ todos });\n```\n\nNow you can establish a database connection with your schema:\n```ts\nimport { database } from \"crstore/svelte\";\n\nconst { replicated } = database(schema);\n```\n\u003e Note, that this example uses Svelte version (`replicated`). For React `database` function will return `useReplica` and `createReplica` for SolidJS. Learn more how to use `CRStore` with these frameworks [here](./src/demo/frameworks/).\n\nWith the `replicated` function we can create arbitrary views to our database which are valid svelte stores. For example let's create a store that will have our entire `todos` table:\n```ts\nconst todos = replicated((db) =\u003e db.selectFrom(\"todos\").selectAll());\n```\n\nTo mutate the data we can either call `.update` on the store or add built-in actions upon creation:\n```ts\nconst todos = replicated((db) =\u003e db.selectFrom(\"todos\").selectAll(), {\n  // Define actions for your store\n  toggle(db, id: string) {\n    return db\n      .updateTable(\"todos\")\n      .set({ completed: sql`NOT(completed)` })\n      .where(\"id\", \"=\", id)\n      .execute();\n  },\n  remove(db, id: string) {\n    return db.deleteFrom(\"todos\").where(\"id\", \"=\", id).execute();\n  },\n});\n\n// Call an update manually\ntodos.update((db) =\u003e db.insertInto(\"todos\").values({ ... }).execute());\n// Call an action\ntodos.toggle(\"id\");\n```\n\nWe can simple iterate the store to render the results:\n\u003e Note that the database loads asynchronously, so the store will contain an empty array util it loads.\n```svelte\n{#each $todos as todo}\n  \u003ch2\u003e{todo.title}\u003c/h2\u003e\n  \u003cp\u003e{todo.text}\u003c/p\u003e\n{/each}\n```\n\nThis we dynamically react to all the changes in our database even if we make them from a different store. Each store we create reacts only to changes in tables we have selected from.\n\n## Connecting with tRPC\n\nYou can provide custom handlers for your network layer upon initialization. `push` method is called when you make changes locally that need to be synchronized. `pull` is called when `crstore` wants to subscribe to any changes coming from the network. Let's say you have a `push` [tRPC mutation](https://trpc.io/docs/quickstart) and a `pull` [tRPC subscription](https://trpc.io/docs/subscriptions) then you can use them like so when connection to a database:\n```ts\nconst { replicated } = database(schema, {\n  push: trpc.push.mutate,\n  pull: trpc.pull.subscribe,\n});\n```\n\nThen your server implementation would look something like this:\n```ts\nimport { database } from \"crstore\";\n\nconst { subscribe, merge } = database(schema);\nconst { router, procedure } = initTRPC.create();\n\nconst app = router({\n  push: procedure.input(any()).mutation(({ input }) =\u003e merge(input)),\n  pull: procedure\n    .input(object({ version: number(), client: string() }))\n    .subscription(({ input }) =\u003e\n      observable(({ next }) =\u003e subscribe([\"*\"], next, input))\n    ),\n});\n```\n\n\u003e If you are using `vite-node` to run your server, you should add `define: { \"import.meta.env.SSR\": false }` to your vite config file.\n\n## Advanced Usage\n\n### Depend on other stores\n\nWhen creating a `crstore` you might want it to subscribe to some other stores. For example you can have a writable `query` store and a `search` crstore. Where `search` updates every time `query` updates. To do so you can use `.with(...stores)` syntax when creating a store. All the resolved dependencies will be passed to your SELECT callback.\n```ts\nimport { database } from \"crstore/svelte\";\nimport { writable } from \"svelte/store\";\n\nconst { replicated } = database(schema);\n\nconst query = writable(\"hey\");\nconst search = replicated.with(query)((db, query) =\u003e \n  db.selectFrom(\"todos\").where(\"text\", \"=\", query).selectAll()\n);\n```\n\n### Specify custom paths\n\nIf needed you can specify custom paths to `better-sqlite3` binding, `crsqlite` extension and `crsqlite-wasm` binary. To do so, provide `path` option upon `database` initialization:\n```ts\nimport { database } from \"crstore/svelte\";\n\nconst { replicated } = database(schema, {\n  // These are the default values:\n  paths: {\n    wasm: \"/sqlite.wasm\",\n    extension: \"node_modules/@vlcn.io/crsqlite/build/Release/crsqlite.node\",\n    binding: undefined,\n  }\n});\n```\n\n### Specify database name\n\nIf you need to manage multiple databases you can specify `name` database option. This will be used as a filename on a server or a VFS path on a client.\n```ts\nimport { database } from \"crstore/svelte\";\n\nconst { replicated } = database(schema, {\n  name: \"data/example.db\"\n});\n```\n\n### Specify a custom online checker\n\n`push` and `pull` capabilities rely on checking current online status. When available `navigator.onLine` is used by default. You have an option to override it by providing a custom online function.\n```ts\nimport { database } from \"crstore/svelte\";\n\nconst { replicated } = database(schema, {\n  online: () =\u003e true // Always online\n});\n```\nNote that this is only really needed if you use `pull` and `push` helpers. If your [server implementation](#connecting-with-trpc) uses `subscribe` and `merge` methods instead, the online checker is unnecessary (defaults to `false`).\n\n### Apply updates without creating a store\n\nUse can apply any updates right after you have initialized your database connection by using the `update` function. If there are any stores initialized, they will also be updated if you change any tables they depend on.\n```ts\nimport { database } from \"crstore\";\n\nconst { update } = database(schema);\nupdate((db) =\u003e db.insertInto(\"todos\").values({ ... }));\n```\n\n### Access raw database connection\n\nUse can access the raw database connection. This can sometime be useful for debugging. Note that any mutations you do directly from the connection **will not trigger any reactive updates**! To mutate data safely please use [the `update` function](#apply-updates-without-creating-a-store) instead.\n\n```ts\nimport { database } from \"crstore\";\n\nconst { connection } = database(schema);\nconst db = await connection;\n\nconst data = await db.selectFrom(\"todos\").selectAll().execute()\nconsole.log(data);\n```\n\n### Nested JSON queries\n\n`crstore` provides support for nested JSON queries via it's own [JSON Kysely plugin](src/lib/database/json.ts). You can see how it's used in practice be looking at the [library demo](src/demo/library/library.ts).\n```ts\nimport { groupJSON } from \"crstore\";\n\nconst grouped = replicated((db) =\u003e\n  db\n    .selectFrom(\"tracks\")\n    .leftJoin(\"artists\", \"tracks.artist\", \"artists.id\")\n    .leftJoin(\"albums\", \"tracks.album\", \"albums.id\")\n    .select([\n      \"albums.title as album\",\n      (qb) =\u003e\n        // Here we aggregate all the tracks for the album\n        groupJSON(qb, {\n          id: \"tracks.id\",\n          title: \"tracks.title\",\n          artist: \"artists.title\",\n          album: \"albums.title\",\n        }).as(\"tracks\"),\n    ])\n    // `groupBy` is essential for the aggregation to work\n    .groupBy(\"album\")\n);\n\n$grouped[0] // ↓ The type is inferred from `json`\n// {\n//   album: string | null;\n//   tracks: {\n//     id: string;\n//     title: string;\n//     artist: string | null;\n//     album: string | null;\n//   }[]\n// }\n```\n\n### Specify indexes in the schema\nYou can specify one or more indexes for your tables.\n\n```ts\nimport { index } from \"crstore\";\n\nconst todos = object({\n  id: string(),\n  title: string(),\n  text: string(),\n  completed: boolean(),\n});\nindex(todos, \"title\");\nindex(todos, \"text\", \"completed\"); // Multi-column index\n```\n\n### Define a fractional index for a table\n`cr-sqlite` supports conflict free fractional indexing. To use them in `CRStore` first you should define table as ordered in your schema:\n\n```ts\nimport { ordered } from \"crstore\";\n\nconst todos = object({\n  id: string(),\n  text: string(),\n  completed: boolean(),\n  collection: string(),\n  order: string()\n});\n// Sort by 'order' column in each 'collection'\nordered(todos, \"order\", \"collection\");\n```\n\nThen you can append or prepend items by putting the exported constants as your order value.\n```ts\nimport { APPEND, PREPEND } from \"crstore\";\n\ndb.insertInto(\"todos\")\n  .values({\n    id: \"4321\",\n    text: \"Hello\",\n    completed: false,\n    collection: \"1234\",\n    order: APPEND,\n  })\n  .execute();\n```\n\nTo move an item you should update the `{you_table}_fractindex` virtual table with the `after_id` value.\n```ts\ndb\n  .updateTable(\"todos_fractindex\" as any)\n  .set({ after_id: \"2345\" })\n  .where(\"id\", \"=\", \"4321\")\n  .execute();\n```\n\nCheck out the [sortable example](src/demo/sortable) for more details.\n\n### Setup server side rendering\nWhen defining your database set `ssr` option to `true`:\n```ts\nconst { replicated, merge, subscribe } = database(schema, {\n  ssr: true,\n});\n```\n\nAdd `+page.server.ts` file to preload your data with SvelteKit. You can call `.then` on a store to get a promise with its latest state (the `await` keyword would achieve the same effect). Pass down the value of your store to your page like this:\n```ts\nimport type { PageServerLoad } from \"./$types\";\nimport { items } from \"./stores\";\n\nexport const load: PageServerLoad = async () =\u003e ({ ssr: await items });\n```\n\nIn your `+page.svelte` you render the server-side data until client database is ready.\n```svelte\n\u003cscript lang=\"ts\"\u003e\n  import type { PageData } from \"./$types\";\n  import { items } from \"./stores\";\n  import { ready } from \"$lib\";\n\n  export let data: PageData;\n\u003c/script\u003e\n\n{#each ready($items) ? $items : data.ssr as item}\n  \u003cli\u003e{item.data}\u003c/li\u003e\n{/each}\n```\n\nCheck out the [ssr example](src/demo/ssr/) for complete implementation.\n\n### Error handling\nYou can add an error handler to your database connection.\n```ts\nconst { replicated } = database(schema, {\n  error: (reason) =\u003e console.log(reason),\n});\n```\n\nIt will handle all the errors that happen during subscriber callbacks.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazarattum%2Fcrstore","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fazarattum%2Fcrstore","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fazarattum%2Fcrstore/lists"}