fix: Close generated sync sessions and expose the shared demo (#135)

This commit is contained in:
Marcelo Mendonça Soares 2026-09-16 11:40:42 -03:00 • committed by GitHub
commit 0f4b8116f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 266 additions and 35 deletions

View file

@ -9,23 +9,25 @@ them converge.
If you are here to learn the package, read **`lib/src/offline_replica.dart`**
first — it is the entire integration in ~60 lines, deliberately free of demo and
UI code. Every replica follows the same four steps:
UI code. Every replica follows the same three steps:
1. **Open a local database.** `client.createSession(path)` opens a Serverpod
client database (a local SQLite file) and runs the client migrations.
2. **Wrap it for CRDT sync.** `OfflineSyncDatabaseSession.wraps(raw, syncTables: …,
persistentUserId: …)`, then `offlineSyncSession.db.initialize()` to establish this
device's CRDT node so it can take part in sync.
3. **Read and write generated models** against that session, e.g.
1. **Open a sync session.** The generated
`client.createSyncSession(path, isDebugMode: kDebugMode, persistentUserId: …)`
opens the local SQLite database, runs client migrations, wraps the generated
sync tables, and initializes CRDT tracking.
2. **Read and write generated models** against that session, e.g.
`Person.db.insertRow(session, person)` or `session.db.find<Person>()`. (The
demo also routes these through a generic `TableOps` registry so its
metadata-driven UI can touch any table without a per-type switch — that part
is demo-only; the `seed*` methods in `lib/src/demo_controller.dart` show the
plain form.)
4. **Synchronize through a Serverpod client.**
3. **Synchronize through a Serverpod client.**
`client.offlineSync.syncOnce(session, onMergeSuccess: …)` for a one-shot push/pull,
or `client.offlineSync.syncContinuously(session, …)` to stream until cancelled.
When disposing a replica, cancel its sync subscription and call `session.close()`
to release the local database connection.
Connectivity is just *which client you sync through*: the demo swaps
`Client.httpClientOverride` between a real transport and a failing one to
simulate going offline — the local database is untouched either way.

View file

@ -191,10 +191,7 @@ class DemoController extends ChangeNotifier {
void dispose() {
_disposed = true;
stopScenarioAutoPlay();
unawaited(_stopAllStreams());
for (final replica in _sessions.values) {
unawaited(replica.close());
}
unawaited(_closeSessions());
for (final state in replicas.values) {
state.dispose();
}
@ -202,6 +199,14 @@ class DemoController extends ChangeNotifier {
super.dispose();
}
Future<void> _closeSessions() async {
try {
await _stopAllStreams();
} finally {
await Future.wait(_sessions.values.map((replica) => replica.close()));
}
}
@override
void notifyListeners() {
if (_disposed) return;

View file

@ -2,8 +2,6 @@ import 'dart:io';
import 'package:flutter/foundation.dart' show debugPrint, kDebugMode;
import 'package:path/path.dart' as p;
import 'package:serverpod_database/serverpod_database.dart'
show ClientDatabaseSession;
import 'package:serverpod_offline_sync_client/serverpod_offline_sync_client.dart';
import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_client.dart';
@ -13,8 +11,8 @@ import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_c
/// This class is the entire surface a real integration needs — read it first to
/// understand the package. The four steps are:
///
/// 1. [open] opens a local SQLite database ([Client.createSession])
/// and wraps it in a [OfflineSyncDatabaseSession]. Use
/// 1. [open] uses [Client.createSyncSession] to open and initialize a local
/// SQLite database as an [OfflineSyncDatabaseSession]. Use
/// [openOrReset] when the file may predate the shipped
/// schema. The rest of the app reads and writes generated models through
/// [session].
@ -23,16 +21,13 @@ import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_c
/// e.g. `Person.db.insertRow(offlineSyncSession, person)`).
/// 3. [syncOnce] / [syncContinuously] push local changes and merge remote
/// ones through a Serverpod [Client].
/// 4. [reset] / [close] tear the replica down.
/// 4. [reset] clears local data; [close] releases the database connection.
///
/// It deliberately holds **no UI state**: the demo's `DemoController` layers
/// busy/error/projection bookkeeping on top of these calls so that this file
/// stays a clean reference for the package itself.
class OfflineReplica {
OfflineReplica._(this._rawSession, this.session, this.persistentUserId);
/// Owns the underlying SQLite connection; closed by [close].
final ClientDatabaseSession _rawSession;
OfflineReplica._(this.session, this.persistentUserId);
/// CRDT-aware session used for every model read and write. By default it only
/// exposes visible (non-tombstoned) rows; queries opt into hidden rows with
@ -44,26 +39,19 @@ class OfflineReplica {
/// Opens [databasePath] through [client] and wraps it for CRDT sync.
///
/// [Client.createSession] opens the local client database (running
/// client migrations); [OfflineSyncDatabaseSession.wraps] layers CRDT
/// tracking over it; and `db.initialize()` establishes this device's CRDT
/// node so it can take part in sync.
/// [Client.createSyncSession] runs client migrations, wraps the database for
/// the generated sync tables, and initializes CRDT tracking for this user.
static Future<OfflineReplica> open({
required Client client,
required String databasePath,
required UuidValue persistentUserId,
}) async {
final rawSession = await client.createSession(
final session = await client.createSyncSession(
databasePath,
isDebugMode: kDebugMode,
);
final offlineSyncSession = OfflineSyncDatabaseSession.wraps(
rawSession,
syncTables: syncTables,
persistentUserId: persistentUserId,
);
await offlineSyncSession.db.initialize();
return OfflineReplica._(rawSession, offlineSyncSession, persistentUserId);
return OfflineReplica._(session, persistentUserId);
}
/// Like [open], but deletes [databasePath] and retries once when opening
@ -160,5 +148,5 @@ class OfflineReplica {
}
/// Closes the underlying database connection.
Future<void> close() => _rawSession.close();
Future<void> close() => session.close();
}