feat: Implement the sync protocol (#54)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Marcelo Mendonça Soares 2026-05-29 01:54:23 -03:00 • committed by GitHub
commit f85239c3a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 3031 additions and 306 deletions

View file

@ -1,7 +1,9 @@
export 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
export 'src/business/crdt_sync.dart';
export 'src/endpoints/crdt_sync_endpoint.dart';
export 'src/generated/endpoints.dart';
// The models are exported by the client package to ensure the same class as the
// one expected by the functions.
// one expected by the functions. Never export the server-side protocol.
// export 'src/generated/protocol.dart';

View file

@ -0,0 +1,28 @@
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
/// Extension methods for [Serverpod] to configure the CRDT sync on the server.
extension CrdtSyncInitialize on Serverpod {
/// Configures the endpoint with the given sync tables.
///
/// Must be called during server startup before any sync requests are made.
/// Will override any previous initialization.
///
/// [syncBatchSize] controls the maximum number of merge changes carried by
/// each sync stream chunk.
///
/// [continuousSyncInterval] controls how long a continuous sync session waits
/// after completing one sync round before checking for local changes again.
void initializeCrdtSync({
required List<Table> syncTables,
int syncBatchSize = CrdtSync.defaultSyncBatchSize,
Duration continuousSyncInterval = CrdtSync.defaultContinuousSyncInterval,
}) {
CrdtSync.initialize(
syncTables: syncTables,
serializationManager: serializationManager,
syncBatchSize: syncBatchSize,
continuousSyncInterval: continuousSyncInterval,
);
}
}

View file

@ -0,0 +1,24 @@
import 'dart:async';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
/// Endpoint for CRDT-based offline-first synchronization.
class CrdtSyncEndpoint extends Endpoint {
@override
bool get requireLogin => true;
/// Opens a bidirectional CRDT sync session with the authenticated client.
Stream<CrdtSyncStreamEvent> sync(
Session session, {
required Stream<CrdtSyncStreamEvent> changes,
bool once = false,
}) async* {
yield* CrdtSync.instance.sync(
session,
userId: UuidValue.withValidation(session.authenticated!.userIdentifier),
inbound: changes,
once: once,
);
}
}

View file

@ -1,40 +0,0 @@
// import 'package:serverpod/serverpod.dart';
// import 'package:serverpod_offline_sync_shared/serverpod_offline_sync_shared.dart';
// // import '../generated/protocol.dart';
// /// TODO: Create the classes.
// abstract class CrdtDataEntry {}
// /// Endpoint for the CRDT sync.
// class SyncOfflineChangesEndpoint extends Endpoint {
// @override
// bool get requireLogin => true;
// /// Syncs the offline changes between server and client.
// ///
// /// This endpoint is used to sync the offline changes between server and
// /// client. After a handshake on the [migrationVersion] to ensure that the
// /// merge can happen, the client will send all [offlineChanges] since the
// /// [lastSyncHlc] HLC and the server will send all online changes through
// /// the returning stream.
// ///
// /// The connection will be kept open until one end disconnects, forwarding
// /// the changes to the other end.
// Stream<CrdtDataEntry> syncOfflineChanges(
// Session session, {
// required String migrationVersion,
// required Hlc lastSyncHlc,
// /// TODO: Create the classes.
// /// CrdtDataEntry will be a sealed class with the changes.
// ///
// /// - CrdtDataRow: New rows inserted.
// /// - CrdtDataField: Fields updated.
// /// - CrdtDataDeleted: Rows deleted.
// required Stream<CrdtDataEntry> offlineChanges,
// }) async* {
// // TODO: Implement the business layer for the CRDT sync.
// // yield* CrdtSync.syncOfflineChanges(session);
// }
// }

View file

@ -0,0 +1,8 @@
analyzer:
errors:
dead_code: ignore
duplicate_ignore: ignore
unnecessary_type_check: ignore
formatter:
trailing_commas: preserve

View file

@ -1,11 +0,0 @@
### A set of remote CRDT changes to merge into the local database.
class: CrdtMergeSet
fields:
### Remote row insertions.
inserts: List<CrdtMergeInsert>
### Remote field updates.
updates: List<CrdtMergeUpdate>
### Remote tombstone updates.
deletes: List<CrdtMergeDelete>

View file

@ -11,9 +11,5 @@ fields:
### Unique identifier for the node.
uuidNodeId: UuidValue, default=random_v7, unique
# TODO: Make this non-nullable once the migration version is implemented.
### The migration version of the CRDT data that the node has seen.
lastSeenMigrationVersion: String?
### The last HLC timestamp when the node has synchronized.
lastReceivedHlc: Hlc?

View file

@ -0,0 +1,7 @@
### Marks a graceful end of a one-shot sync session.
###
### Both peers send [CrdtSyncClose] after merging the final batch in a
### one-shot session so each side can shut down without closing the transport
### early.
class: CrdtSyncClose
extends: CrdtSyncStreamEvent

View file

@ -0,0 +1,12 @@
### Session handshake sent once by each peer before the first sync batch.
###
### [localNodeId] identifies the sending peer. [syncTablesHash] is the
### deterministic schema hash both sides must agree on before merging data.
class: CrdtSyncConnect
extends: CrdtSyncStreamEvent
fields:
### This peer's node identifier.
localNodeId: UuidValue
### The hash of the synchronized schema configured on this peer.
syncTablesHash: String

View file

@ -0,0 +1,3 @@
### Marks the end of a framed sync batch.
class: CrdtSyncEndOfBatch
extends: CrdtSyncStreamEvent

View file

@ -0,0 +1,6 @@
### A chunk of merge changes carried inside a framed sync batch.
class: CrdtSyncMergeChunk
extends: CrdtSyncStreamEvent
fields:
### The merge changes to apply.
changes: List<CrdtMergeChange>

View file

@ -0,0 +1,7 @@
### One-time checkpoint sent by each peer after connect to exchange required
### information for the sync session.
class: CrdtSyncSinceHlc
extends: CrdtSyncStreamEvent
fields:
### The last HLC timestamp that the sender has received from the other peer.
lastSyncFromPeer: Hlc

View file

@ -0,0 +1,5 @@
### A CRDT sync event sent over a bidirectional stream.
###
### This is the base class of the CRDT sync protocol.
class: CrdtSyncStreamEvent
sealed: true

View file

@ -0,0 +1,7 @@
### Marks a timeout of an idle sync session.
###
### This is a synthetic event that will be injected in the inbound stream
### being awaited if it does not emit any events for a given timeout. It is
### what allows both sides to stay silent if they have nothing to send.
class: CrdtSyncIdleTimeout
extends: CrdtSyncStreamEvent