%% generate tags start %%
#software-engineering #software-engineering/local-first
%% generate tags end %%
#software-engineering/local-first/crdt
## What is CRDT?
Conflict-free Replicated Data Types (CRDTs) are data structures that enable multiple copies of the same data to be synchronized seamlessly across different computers in a network, even in the presence of network delays or partitions. They achieve this by allowing concurrent updates on different replicas and then deterministically resolving conflicts, ensuring strong eventual consistency without requiring a central server.

## Simplest Implementation of CRDT in Typescript
A simple implementation of a CRDT (Conflict-Free Replicated Data Type) in TypeScript can be illustrated using a basic "Counter" CRDT. This CRDT will allow increments and decrements to be made on different nodes (or replicas) and then merged together without conflict.
We'll implement a G-Counter (Grow-only Counter), which is a CRDT that supports only increments. Each node tracks its own count, and the total count is the sum of the counts from all nodes.
Here's a basic implementation:
```typescript
interface GCounter {
id: string;
counts: { [nodeId: string]: number };
}
class GCounterCRDT {
private counter: GCounter;
constructor(nodeId: string) {
this.counter = { id: nodeId, counts: { [nodeId]: 0 } };
}
// Increment the counter
increment(): void {
this.counter.counts[this.counter.id] += 1;
}
// Get the total count
getCount(): number {
return Object.values(this.counter.counts).reduce((a, b) => a + b, 0);
}
// Merge another counter into this one
merge(otherCounter: GCounter): void {
for (const nodeId in otherCounter.counts) {
this.counter.counts[nodeId] = Math.max(
this.counter.counts[nodeId] || 0,
otherCounter.counts[nodeId]
);
}
}
// Get the internal state of the counter
getState(): GCounter {
return this.counter;
}
}
// Usage
const counter1 = new GCounterCRDT('node1');
const counter2 = new GCounterCRDT('node2');
counter1.increment();
counter1.increment();
counter2.increment();
console.log(counter1.getCount()); // Outputs: 2
console.log(counter2.getCount()); // Outputs: 1
counter2.merge(counter1.getState());
console.log(counter2.getCount()); // Outputs: 3 (after merging)
```
In this implementation:
- Each `GCounterCRDT` instance represents a node.
- The `increment` method increases the count for the current node.
- The `getCount` method calculates the total count across all nodes.
- The `merge` method is used to merge the state of another counter into the current one, taking the maximum count for each node. This is the CRDT's convergence mechanism ensuring that all replicas eventually reach the same state if they all receive each other's updates.
This is a straightforward example and serves to illustrate the basic principles of a CRDT. Real-world implementations can be more complex, especially when dealing with more complex data types and ensuring efficient synchronization and conflict resolution across a distributed system.
## Simulate This Example Using Honojs as Server
Simulating a CRDT (Conflict-Free Replicated Data Type) example using Hono, a server framework for Cloudflare Workers, involves setting up a basic server that handles increments from different clients (nodes) and merges these increments using a CRDT. For simplicity, we'll continue with the G-Counter CRDT from the previous example.
Here's a step-by-step approach:
### 1. Set Up a Basic Hono Server
First, install Hono:
```bash
npm install hono
```
Then, create a simple server:
```typescript
// Import Hono
import { Hono } from 'hono';
// Create a new Hono app
const app = new Hono();
// Define your routes and handlers here
// Start the server
app.listen({ port: 8080 });
```
### 2. Implement the G-Counter CRDT
You can use the G-Counter CRDT implementation from the previous example. However, in a real-world scenario, you would store the state of the CRDT in a distributed database or a data store that all instances of your server can access.
### 3. Create Routes to Handle Increments and Merges
You will need to add routes to handle increments and merges. For example:
```typescript
// Increment the counter for a given node
app.post('/increment/:nodeId', (c) => {
const nodeId = c.req.param('nodeId');
// Logic to increment the counter for this node
// ...
return c.text('Incremented');
});
// Merge counters
app.post('/merge', async (c) => {
const otherCounter = await c.req.json();
// Logic to merge this counter with the received one
// ...
return c.text('Merged');
});
```
### 4. Integrate the CRDT Logic
Integrate the CRDT logic into these routes. Since this is a simulation, you might keep the state in-memory (not suitable for production). In a real application, you would use a persistent and possibly distributed data store.
### 5. Example Server Code
Here's a complete example of what the server might look like:
```typescript
import { Hono } from 'hono';
// CRDT Implementation
// (Include the GCounterCRDT class here)
const app = new Hono();
// In-memory storage for the counter (for simulation purposes)
const counter = new GCounterCRDT('serverNode');
// Increment route
app.post('/increment/:nodeId', (c) => {
const nodeId = c.req.param('nodeId');
counter.increment(nodeId);
return c.text('Incremented');
});
// Merge route
app.post('/merge', async (c) => {
const otherCounter = await c.req.json() as GCounter;
counter.merge(otherCounter);
return c.text('Merged');
});
// Get count route
app.get('/count', (c) => {
return c.json({ count: counter.getCount() });
});
app.listen({ port: 8080 });
```
This server allows clients to increment their counter, send their counter state to be merged, and get the total count. Note that this is a simplified example and may need adjustments for actual distributed environments, especially regarding persistent and synchronized state management.
> [!info] Different primitive types will have different CRDT?
> In terms of primitive data types, CRDTs can be implemented for various basic types like counters, sets, maps, and lists. Each primitive type may have its own corresponding CRDT implementations, each tailored to handle conflicts in a way that's appropriate for that type of data. For example:
>
> 1. **Counters**: A common CRDT for counters is the G-Counter (Grow-only Counter) which only allows increments. Each node can independently increment its local copy of the counter. Merging simply involves taking the maximum of the counters from all nodes.
> 2. **Sets**: For sets, there are CRDTs like the OR-Set (Observed-Removed Set) which can handle additions and removals. In an OR-Set, elements are tagged with unique identifiers, allowing the system to keep track of which elements have been added or removed, even if the same element is added after being removed.
> 3. **Maps and Registers**: CRDTs for maps and registers (key-value stores) can be more complex. For example, a map could be a collection of registers, each of which is a CRDT that can handle concurrent updates.
> 4. **Lists/Sequences**: Implementing CRDTs for ordered lists or sequences can be particularly challenging due to the need to preserve order while resolving conflicts. Algorithms like the RGA (Replicated Growable Array) or LSEQ address these challenges.