fix: Make the CrdtDatabase class ephemeral (#82)

This commit is contained in:
Marcelo Mendonça Soares 2026-06-24 01:18:23 -03:00 • committed by GitHub
commit 517abde4a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 362 additions and 151 deletions

View file

@ -1,12 +1,34 @@
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
/// The CRDT sync configured per [Serverpod] instance.
///
/// Keyed by the [Serverpod] instance so each pod owns its own [CrdtSync] (and
/// the [CrdtDatabaseContext] it carries) instead of sharing a single
/// process-wide singleton.
final _crdtSyncByServerpod = Expando<CrdtSync>('crdtSync');
/// Intercepts each Serverpod session database with a CRDT-aware database once
/// [CrdtSyncInitialize.initializeCrdtSync] has configured sync.
///
/// When sync has not been configured for the session's [Serverpod], the
/// original [inner] database is returned unchanged.
Database crdtDatabaseInterceptor(Session session, Database inner) {
final crdtSync = _crdtSyncByServerpod[session.server.serverpod];
return crdtSync?.wrapDatabase(inner) ?? inner;
}
/// Extension methods for [Serverpod] to configure the CRDT sync on the server.
extension CrdtSyncInitialize on Serverpod {
/// Configures the endpoint with the given sync tables.
/// Configures the CRDT sync with the given sync tables.
///
/// Must be called during server startup before any sync requests are made.
/// Will override any previous initialization.
/// Will override any previous initialization for this [Serverpod] instance.
///
/// The [Serverpod] instance must be constructed with [crdtDatabaseInterceptor]
/// as its `databaseInterceptor`. Otherwise each session's [Session.db] stays a
/// plain database and server-side ORM mutations on synced tables are not
/// CRDT-tracked.
///
/// [syncBatchSize] controls the maximum number of merge changes carried by
/// each sync stream chunk.
@ -18,7 +40,7 @@ extension CrdtSyncInitialize on Serverpod {
int syncBatchSize = CrdtSync.defaultSyncBatchSize,
Duration continuousSyncInterval = CrdtSync.defaultContinuousSyncInterval,
}) {
CrdtSync.initialize(
_crdtSyncByServerpod[this] = CrdtSync(
syncTables: syncTables,
serializationManager: serializationManager,
syncBatchSize: syncBatchSize,
@ -26,3 +48,15 @@ extension CrdtSyncInitialize on Serverpod {
);
}
}
/// Extension to access the CRDT sync for [Session] from the [Serverpod] instance.
extension CrdtSessionExtension on Session {
/// Returns the CRDT sync configured for this session.
CrdtSync get crdt =>
_crdtSyncByServerpod[server.serverpod] ??
(throw StateError(
'The CrdtSync has not been initialized for this Serverpod instance. '
'Call pod.initializeCrdtSync(...) during server startup to configure '
'the CRDT sync.',
));
}

View file

@ -3,6 +3,8 @@ import 'dart:async';
import 'package:serverpod/serverpod.dart';
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
import '../business/crdt_sync.dart';
/// Endpoint for CRDT-based offline-first synchronization.
class CrdtSyncEndpoint extends Endpoint {
@override
@ -14,7 +16,7 @@ class CrdtSyncEndpoint extends Endpoint {
required Stream<CrdtSyncStreamEvent> changes,
bool once = false,
}) async* {
yield* CrdtSync.instance.sync(
yield* session.crdt.sync(
session,
userId: UuidValue.withValidation(session.authenticated!.userIdentifier),
inbound: changes,