crdt_lf 3.2.1
crdt_lf: ^3.2.1 copied to clipboard
Conflict-free replicated data type (CRDT) - Local-first implementation provided in dart
3.2.1 #
Date: 2026-06-27
Fixed #
- Nested handlers failed to reconstruct (and therefore to sync) in dart2js-minified builds (e.g. Flutter web
--release), because handler type identity relied onruntimeType.toString()(an opaque"minified:..."token there). Introduced the stableHandler.handlerTypetag used for routing, snapshot manifest,HandlerRefs and factory keys — defaults toruntimeType.toString()(non-breaking), overridden with a constant by built-in handlers, with an optionalhandlerTypeconstructor argument for generic handlers.
3.2.0 #
Date: 2026-06-25
Added #
CRDTFugueListHandler<T>— a list handler that uses the Fugue algorithm to minimize interleaving of concurrent edits. LikeCRDTListHandler<T>it is generic overTwith an optionalValueCodec<T>. 72- Added
CRDTFugueMovableListHandler<T>, a list CRDT that combines Fugue's interleaving-minimizing insertion with an explicitmove(from, to)operation that preserves the moved element's identity across concurrent reorderings (implements the algorithm from Kleppmann, Moving Elements in List CRDTs, PaPoC 2020). 26 - It is possible to recursively nest “handlers,” which allow for real-world modeling. The following handlers have been added:
CRDTListRefHandler,CRDTMapRefHandler, andCRDTMovableListRefHandler, which, instead of handling values, allow for the nesting of additional handlers. 74 CRDTRegisterHandler<T>— a single-value last-writer-wins register (the scalar counterpart of the collection handlers). Use it for a standalone mergeable value (flag, number, non-collaborative string), e.g. as a scalar field of a nested node.
Changed #
- Performance:
FugueTreenow answers position↔node queries through a square-root-decomposition positional index (sqrt_decomposition) instead of a full in-order traversal, so locating, inserting, deleting and updating a node by position costs O(√n) instead of O(n) (Performing 1,000 operations in theFugueListHandlerreduces the execution time from ~121ms to ~34ms). 71 - Performance: requesting the operations of a handler now scales linearly with that handler's own operations instead of with the whole oplog.
Handler.operations()reads from the new per-handler index inChangeStorerather than scanning every change, so resolving a handler's state — and therefore reading a nested tree of handlers — no longer degrades quadratically as the number of handlers grows (resolving an 800-node nested document drops from ~1.3s to ~32ms). - Performance:
CRDTDocument.importChangesupdates the handler caches once per batch instead of once per applied change, removing the O(handlers × changes) cost on large imports (importing and resolving an 800-node nested document drops from ~2.0s to ~120ms). - chore: improved documentation highlighting how data can be modelled. The differences between the handlers and the available options are emphasized through concrete examples.
3.1.0 #
Date: 2026-06-13
Performance-focused release: handler caches are now updated in place instead of being deep-copied on every operation, and several core algorithms were rewritten to remove quadratic behavior. 70
Added #
DAG.getAncestorsOfAll(Iterable<OperationId>)— single traversal with a shared visited set over multiple sources;exportChanges(from:)now uses it instead of one walk per frontier head.Frontiers.reset(Iterable<OperationId>)— replaces the frontier content directly.
Changed #
- Performance:
CRDTORSetHandler,CRDTORMapHandler,CRDTListHandlerandCRDTMapHandlerno longer deep-copy their cached state on every operation; the cache is mutated in place (the OR-Set handler benchmark drops from ~180ms to ~27ms for 1000 operations on a fresh document). - Performance:
CRDTFugueTextHandlerresolves its nodes and text lazily on read instead of re-traversing the whole tree after every operation. - Performance:
CRDTTextHandlerreplays history on a mutable list of code units and applies incremental updates with a single string concatenation instead of multipleStringBufferround-trips. - Performance:
ChangeStore.exportChangesNewerThananswers from a lazily-built per-peer index sorted by clock (binary search) instead of scanning every stored change — this is the sync-server hot path (~500x faster on a 50k-change store). - Performance:
ChangeStore.pruneonly rebuilds the changes whose dependencies were actually pruned, preserving object identity for untouched changes. - Performance:
CRDTDocumenttopological sort uses aListQueue(removeFirst) instead ofList.removeAt(0), removing quadratic behavior on large imports. - Performance:
DAG.getLCAfilters the lowest common ancestors through the children sets instead of re-running full ancestor walks. CRDTListHandler.valueandCRDTMapHandler.valuenow consistently return the handler's internal collection on both the cached and the recomputed path (previously the recomputed path returned a copy). Treat the returned collection as read-only.CacheableStateProvider.cachedStatemay now be mutated in place between reads (live view rather than per-operation snapshot).
Fixed #
Frontiers.mergecompared operations with a total-order HLC comparison instead of causal dominance, so concurrent heads of different peers collapsed to the single operation with the highest clock after everyDAG.prune(i.e. after every pruning snapshot or garbage collection). Frontiers now keep one head per peer (operations of the same peer are totally ordered; operations of different peers are concurrent), andDAG.prune/DAG.mergerecompute the frontier from the structure of the graph. The documentversionno longer under-reports after a snapshot with concurrent peers.DAGconstructor mis-usedMap.fromIterablewhen building the version vector from a non-empty node map, throwing a runtime type error. The vector is now built explicitly taking the maximum clock per peer.
3.0.0 #
Date: 2026-06-11
Breaking changes
The internal data model has migrated from JSON to a compact binary encoding. Changes, operations, peer IDs, and clock values are now stored and transmitted as raw bytes. Views over the binary data are created lazily on demand rather than eagerly decoded into Dart objects. This results in measurably better throughput and reduced memory fragmentation. 64
Change.fromJsonandChange.toJsonremoved. UseChange.fromBytes(Uint8List)andChange.toBytes()instead.Change.fromPayload({..., payload: Map<String, dynamic>})renamed toChange.fromPayloadBytes({..., payloadBytes: Uint8List}). The payload is now an opaque binary blob.Change.payload(Map<String, dynamic>) removed. UseChange.payloadBytes()returningUint8List.CRDTDocument.binaryExportChangesreturn type changed fromList<int>toUint8List.CRDTDocument.binaryImportChangesparameter type changed fromList<int>toUint8List.Operation.handlerIdFrom(payload: Map)removed. Operation identity is now derived from the binary envelope viaOperationEnvelopeCodec.Snapshot.datatype changed fromMap<String, dynamic>toMap<String, Uint8List>. Each entry is the opaque binary blob produced by the corresponding handler'sgetSnapshotState()and is owned by that handler.Snapshot.toJson/Snapshot.fromJsonremoved. UseSnapshot.toBytes()/Snapshot.fromBytes(Uint8List)instead.SnapshotProvider.getSnapshotState()return type changed fromdynamictoUint8List. Each handler is now responsible for encoding its own state to bytes (typically by reusing itsValueCodec<T>) and for decoding it back fromlastSnapshot().SnapshotProvider.lastSnapshot()return type changed fromdynamictoUint8List?.VersionVector.toJson/VersionVector.fromJsonremoved. UseVersionVector.toBytes()/VersionVector.fromBytes(Uint8List)instead.
Added #
Change.toBytes()andChange.fromBytes(Uint8List)— binary serialization for a single change, replacing the removedtoJson/fromJson.VersionVector.toBytes()andVersionVector.fromBytes(Uint8List)— compact binary encoding for version vectors.Snapshot.toBytes()andSnapshot.fromBytes(Uint8List)— binary serialization for snapshots.CRDTDocument.registeredHandlers— read-only map of handlers currently registered on the document, intended for introspection and tooling.
Changed #
ChangeStorenow indexes changes byOpIdKeyinstead ofOperationId, eliminating redundant object allocation on lookup.- Several hot-path performance improvements:
HybridLogicalClock.toUint8Listnow uses integer arithmetic instead of floating-point,PeerId.fromUint8Listavoids regex validation and intermediate string allocation,DAG.getAncestorswas converted from O(n²) BFS to O(n) DFS, and frequently-usedOperationTypeinstances are now cached lazily on each handler.
Fixed #
- Fixed
CRDTFugueTextHandlerthrowingCrdtException: Node already existsafter restoring document state viabinaryImportChanges,importChanges, orimportSnapshot. 65 (thx to @coltrane)
2.5.0 #
Date: 2026-01-03
Added #
- Added
garbageCollecttoCRDTDocumentto prune the document history. It prunes the document history up to the given version vector.VersionVector.intersectioncan be used to compute the minimum common version vector that contains the minimum clock for each peer. 61 - Added
fromVersionVectortoCRDTDocument.exportChangesto export changes that are newer than a given version vector.
Changed #
- Implemented hashCode memoization for
PeerId,FugueElementId,Change,ORHandlerTag,ORMapEntry,OperationId,OperationType. Constructors are no longer const, resulting in faster equality checks and reduced CPU usage during heavy parsing or collection lookups. - chore: improved example. Can now time travel and garbage collect the document history.
2.4.0 #
Date: 2025-12-29
Added #
- Added
HistorySessiontoCRDTDocumentto navigate the history of the document. It allows "Time travel" functionality by moving a temporal cursor back and forth through the changes. Can be called usingdocument.toTimeTravel()55
Changed #
- CRDTDocument now extends
BaseCRDTDocumentinstead of implementing it directly.Handlers now useBaseCRDTDocumentinstead ofCRDTDocument. - Improved
CRDTDocumentdisposal management. After disposal, all operations on the document will throwDocumentDisposedException57 - Reuse tag creation logic in
CRDTORMapHandlerandCRDTORSetHandlerto avoid code duplication 54 - chore: improved documentation
2.3.0 #
2.2.0 #
2.1.0 #
2.0.0 #
Date: 2025-09-16
Breaking changes
- Changed
CRDTFugueTextHandleroperations payload
Added #
- Created a set of mixins to be used by handlers to optimize performance during operation insertions.
- Thrown
HandlerAlreadyRegisteredExceptionwhen a handler is registered twice - Added
TransactionManagerto manage transactional batching of notifications and local changes emission 43 - Added
compound"system" to compact consecutive operations during transaction 45 - Added
CRDTORSetHandler42
Changed #
- On
importChangeslisteners ofupdatesare notified only one times at import end Handlersnow not invalidate cache when an operation is applied due to the new mixins system. This greatly improves the computation of the handler value as it is persisted much more often.- chore: improved handlers benchmark system
Fixed #
CRDTMapHandlerupdating an absent key is ignored
1.0.0 #
Date: 2025-08-18
Breaking changes
Create a set of exception classes to be used across the library. Replace StateError with CrdtException and its subclasses.
applyChange: throwsCausallyNotReadyExceptioninstead ofStateErrorwhen a change's dependencies are not met;- On import when a cycle is detected among changes throws
ChangesCycleExceptioninstead ofStateError; - On add node when a node already exists throws
DuplicateNodeExceptioninstead ofStateError; - On add node when a dependency is missing throws
MissingDependencyExceptioninstead ofStateError; - On Fugue tree insertion when a node already exists throws
DuplicateNodeExceptioninstead ofException.
Removed redundant hlc from Change. change.hlc is also available as getter 37
Hlc in version vector is now serialized as string instead of int64. This avoids precision loss when serialized as JSON for web interoperability.
Added #
documentIdtoCRDTDocument, specified document identity to remove ambiguity between peer and document 38 (thx to @Jei-sKappa)toStringtoSnapshotandVersionVector- added a stream to
CRDTDocumentto be notified of every change (changes, snapshots, merges, ...) - added
mutableand method toVersionVectorto create mutable copies - added a export changes method to
CRDTDocumentto export changes that are newer than a given version vector
Changed #
- chore: setup .github/workflows and update coverage links 33
- chore: update readme with recommended approach for complex handler types
- chore: update topological sort implementation 3
- chore: added benchmarks
Fixed #
- Fix
CRDTFugueTextHandlerto ensure state is synchronized before performing operations 39 - Fix readme reference links
- Fix double hlc increment on
CRDTDocument.createChange - Fix snapshot initialization for handlers that return a non primitive value
0.8.0 #
0.7.1 #
0.7.0+1 #
0.7.0 #
Date: 2025-06-14
Added #
CRDTDocument.mergeSnapshotto merge a snapshot with the current snapshotCRDTDocument.importto import changes and snapshots with a single method and different strategies
Changed #
- On changes pruning, if a change has a dependency on a pruned change, the dependency is removed to preserve integrity
0.6.1 #
0.6.0 #
0.5.1 #
0.5.0 #
0.4.0 #
0.3.0 #
Date: 2025-04-21
Added #
CRDTDocumentexposelocalChangesstream to listen to local changes 18- flutter_example contains a routing with a basic example for each use case (currently only todo list)16
- Split Fugue algorithm from text handler 4