Skip to content

Commit e095662

Browse files
committed
Major refactor
This version introduces a major refactor which results in multiple breaking changes. This was done with the intention to make this package the basis for a family of CRDT libraries. Another motivation was to make this package compatible with [crdt_sync](https://github.com/cachapa/crdt_sync), thereby abstracting the communication protocol and network management for real-time remote synchronization. Changes: - Simplified API - Removed insert and get operations to make package more storage-agnostic - Made most methods optionally async - Re-implemented CrdtMap as a zero-effort ephemeral implementation
1 parent dda170e commit e095662

15 files changed

+717
-664
lines changed

CHANGELOG.md

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## 5.0.0
2+
This version introduces a major refactor which results in multiple breaking changes. This was done with the intention to make this package the basis for a family of CRDT libraries.
3+
4+
Another motivation was to make this package compatible with [crdt_sync](https://github.com/cachapa/crdt_sync), thereby abstracting the communication protocol and network management for real-time remote synchronization.
5+
6+
Changes:
7+
- Simplified API
8+
- Removed insert and get operations to make package more storage-agnostic
9+
- Made most methods optionally async
10+
- Reimplemented CrdtMap as a zero-dependency implementation
11+
112
## 4.0.3
213
- Update to Dart 3
314

README.md

+36-28
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,56 @@
11
Dart implementation of Conflict-free Replicated Data Types (CRDTs).
22

3-
This project is heavily influenced by James Long's talk [CRTDs for Mortals](https://www.dotconferences.com/2019/12/james-long-crdts-for-mortals) and includes a Dart-native implementation of Hybrid Local Clocks (HLC) based on the paper [Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases](https://cse.buffalo.edu/tech-reports/2014-04.pdf).
3+
This project is heavily influenced by James Long's talk [CRTDs for Mortals](https://www.dotconferences.com/2019/12/james-long-crdts-for-mortals) and includes a Dart-native implementation of Hybrid Local Clocks (HLC) based on the paper [Logical Physical Clocks and Consistent Snapshots in Globally Distributed Databases](https://cse.buffalo.edu/tech-reports/2014-04.pdf) (pdf).
44

5-
It has [zero external dependencies](https://github.com/cachapa/crdt/blob/master/pubspec.yaml), so it should run everywhere where Dart runs.
5+
It has [minimal external dependencies](https://github.com/cachapa/crdt/blob/master/pubspec.yaml), so it should run anywhere where Dart runs, which is pretty much everywhere.
66

7-
See [sql_crdt](https://github.com/cachapa/sql_crdt) for an implementation of CRDTs backed by an SQL database.
7+
The `Crdt` class implements CRDT conflict resolution and serves as a storage-agnostic interface for specific implementations. Als included with this package is `MapCrdt`, an ephemeral implementation using Dart HashMaps.
8+
9+
Other implementations include (so far):
10+
- [hive_crdt](https://github.com/cachapa/hive_crdt), a no-sql implementation using [Hive](https://pub.dev/packages/hive) as persistent storage.
11+
- [sql_crdt](https://github.com/cachapa/sql_crdt), an abstract implementation for using relational databases as a data storage backend.
12+
- [sqlite_crdt](https://github.com/cachapa/sqlite_crdt), an implementation using Sqlite for storage, useful for mobile or small projects.
13+
- [postgres_crdt](https://github.com/cachapa/postgres_crdt), a `sql_crdt` that benefits from PostgreSQL's performance and scalability intended for backend applications.
14+
15+
See also [crdt_sync](https://github.com/cachapa/crdt_sync), a turnkey approach for real-time network synchronization of `Crdt` nodes.
816

917
## Usage
1018

11-
The `Crdt` class works as a layer on top of a map. The simplest way to experiment is to initialise it with an empty map:
19+
The simplest way to experiment with this package is to use the provided `MapCrdt` implementation:
1220

1321
```dart
14-
import 'package:crdt/crdt.dart';
22+
import 'package:crdt/map_crdt.dart';
1523
1624
void main() {
17-
var crdt = MapCrdt('node_id');
18-
19-
// Insert a record
20-
crdt.put('a', 1);
21-
// Read the record
22-
print('Record: ${crdt.get('a')}');
23-
24-
// Export the CRDT as Json
25-
final json = crdt.toJson();
26-
// Send to remote node
27-
final remoteJson = sendToRemote(json);
28-
// Merge remote CRDT with local
29-
crdt.mergeJson(remoteJson);
30-
// Verify updated record
31-
print('Record after merging: ${crdt.get('a')}');
32-
}
25+
var crdt1 = MapCrdt(['table']);
26+
var crdt2 = MapCrdt(['table']);
27+
28+
print('Inserting 2 records in crdt1…');
29+
crdt1.put('table', 'a', 1);
30+
crdt1.put('table', 'b', 1);
31+
32+
print('crdt1: ${crdt1.get('table')}');
3333
34-
// Mock sending the CRDT to a remote node and getting an updated one back
35-
String sendToRemote(String json) {
36-
final hlc = Hlc.now('another_nodeId');
37-
return '{"a":{"hlc":"$hlc","value":2}}';
34+
print('\nInserting a conflicting record in crdt2…');
35+
crdt2.put('table', 'a', 2);
36+
37+
print('crdt2: ${crdt2.get('table')}');
38+
39+
print('\nMerging crdt2 into crdt1…');
40+
crdt1.merge(crdt2.getChangeset());
41+
42+
print('crdt1: ${crdt1.get('table')}');
3843
}
3944
```
4045

41-
You'll probably want to implement some sort of persistent storage by subclassing the `Crdt` class. An example using [Hive](https://pub.dev/packages/hive) is provided in [hive_crdt](https://github.com/cachapa/hive_crdt).
46+
## Implementations
47+
48+
`crdt` is currently helping build local-first experiences for:
4249

43-
## Example
50+
- [Libra](https://libra-app.eu) a weigh management app with 1M+ installs.
51+
- [tudo](https://github.com/cachapa/tudo) an open-source simple to-do app + backend.
4452

45-
A [simple example](https://github.com/cachapa/crdt/blob/master/example/crdt_example.dart) is provided with this project.
53+
Are you using this package in your project? Let me know!
4654

4755
## Features and bugs
4856

example/crdt_example.dart

+16-19
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
1-
import 'package:crdt/crdt.dart';
1+
import 'package:crdt/src/map_crdt/map_crdt.dart';
22

33
void main() {
4-
var crdt = MapCrdt('node_id');
4+
var crdt1 = MapCrdt(['table']);
5+
var crdt2 = MapCrdt(['table']);
56

6-
// Insert a record
7-
crdt.put('a', 1);
8-
// Read the record
9-
print('Record: ${crdt.get('a')}');
7+
print('Inserting 2 records in crdt1…');
8+
crdt1.put('table', 'a', 1);
9+
crdt1.put('table', 'b', 1);
1010

11-
// Export the CRDT as Json
12-
final json = crdt.toJson();
13-
// Send to remote node
14-
final remoteJson = sendToRemote(json);
15-
// Merge remote CRDT with local
16-
crdt.mergeJson(remoteJson);
17-
// Verify updated record
18-
print('Record after merging: ${crdt.get('a')}');
19-
}
11+
print('crdt1: ${crdt1.getMap('table')}');
12+
13+
print('\nInserting a conflicting record in crdt2…');
14+
crdt2.put('table', 'a', 2);
15+
16+
print('crdt2: ${crdt2.getMap('table')}');
17+
18+
print('\nMerging crdt2 into crdt1…');
19+
crdt1.merge(crdt2.getChangeset());
2020

21-
// Mock sending the CRDT to a remote node and getting an updated one back
22-
String sendToRemote(String json) {
23-
final hlc = Hlc.now('another_nodeId');
24-
return '{"a":{"hlc":"$hlc","value":2}}';
21+
print('crdt1: ${crdt1.getMap('table')}');
2522
}

lib/crdt.dart

+1-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
library crdt;
22

33
export 'src/crdt.dart';
4-
export 'src/crdt_json.dart';
54
export 'src/hlc.dart';
6-
export 'src/map_crdt.dart';
7-
export 'src/record.dart';
5+
export 'src/types.dart';

lib/map_crdt.dart

+4-52
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,5 @@
1-
import 'dart:async';
1+
library map_crdt;
22

3-
import 'crdt.dart';
4-
import 'hlc.dart';
5-
import 'record.dart';
6-
7-
/// A CRDT backed by a in-memory map.
8-
/// Useful for testing, or for applications which only require temporary datasets.
9-
class MapCrdt<K, V> extends Crdt<K, V> {
10-
final _map = <K, Record<V>>{};
11-
final _controller = StreamController<MapEntry<K, V?>>.broadcast();
12-
13-
@override
14-
final String nodeId;
15-
16-
MapCrdt(this.nodeId, [Map<K, Record<V>> seed = const {}]) {
17-
_map.addAll(seed);
18-
}
19-
20-
@override
21-
bool containsKey(K key) => _map.containsKey(key);
22-
23-
@override
24-
Record<V>? getRecord(K key) => _map[key];
25-
26-
@override
27-
void putRecord(K key, Record<V> value) {
28-
_map[key] = value;
29-
_controller.add(MapEntry(key, value.value));
30-
}
31-
32-
@override
33-
void putRecords(Map<K, Record<V>> recordMap) {
34-
_map.addAll(recordMap);
35-
recordMap
36-
.map((key, value) => MapEntry(key, value.value))
37-
.entries
38-
.forEach(_controller.add);
39-
}
40-
41-
@override
42-
Map<K, Record<V>> recordMap({Hlc? modifiedSince}) =>
43-
Map<K, Record<V>>.from(_map)
44-
..removeWhere((_, record) =>
45-
record.modified.logicalTime < (modifiedSince?.logicalTime ?? 0));
46-
47-
@override
48-
Stream<MapEntry<K, V?>> watch({K? key}) =>
49-
_controller.stream.where((event) => key == null || key == event.key);
50-
51-
@override
52-
void purge() => _map.clear();
53-
}
3+
export 'src/map_crdt/map_crdt.dart';
4+
export 'src/map_crdt/map_crdt_base.dart';
5+
export 'src/map_crdt/record.dart';

0 commit comments

Comments
 (0)