refactor!: Rename sync APIs and ownership spaces (#128)

This commit is contained in:
Marcelo Mendonça Soares 2026-09-14 13:39:07 -03:00 • committed by GitHub
commit b89725f6ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 43650 additions and 9417 deletions

View file

@ -52,7 +52,7 @@ class ReplicaState extends ChangeNotifier {
SyncPhase phase = SyncPhase.idle;
String? lastSyncedLabel;
String? error;
offline.CrdtSyncSession? stream;
offline.OfflineSyncSubscription? stream;
bool busy = false;
bool _disposed = false;
@ -141,7 +141,7 @@ class DemoController extends ChangeNotifier {
final user = selectedUser?.username ?? '<user>';
return 'Replica A and B are independent local SQLite stores '
'($user-a.db, $user-b.db), each with its own CRDT node id but the '
'same sync scope. They share nothing locally and only exchange data '
'same sync space. They share nothing locally and only exchange data '
'by syncing through the server — exactly like two phones on one '
'account.';
}
@ -286,8 +286,8 @@ class DemoController extends ChangeNotifier {
try {
await session.syncOnce(
client,
onMergeSuccess: (scopeUuid, hlc) =>
_handleReplicaMerge(slot, scopeUuid, hlc),
onMergeSuccess: (spaceUuid, hlc) =>
_handleReplicaMerge(slot, spaceUuid, hlc),
);
state.phase = SyncPhase.idle;
state.lastSyncedLabel = 'synced ${_timeLabel()}';
@ -313,8 +313,8 @@ class DemoController extends ChangeNotifier {
() async {
final stream = session.syncContinuously(
client,
onMergeSuccess: (scopeUuid, hlc) =>
unawaited(_handleReplicaMerge(slot, scopeUuid, hlc)),
onMergeSuccess: (spaceUuid, hlc) =>
unawaited(_handleReplicaMerge(slot, spaceUuid, hlc)),
);
state.stream = stream;
state.phase = SyncPhase.streaming;
@ -383,7 +383,7 @@ class DemoController extends ChangeNotifier {
server.loading = true;
if (notify) server.changed();
try {
final rows = await client.demoDebug.fetchScopeSnapshot(
final rows = await client.demoDebug.fetchSpaceSnapshot(
includeHidden: showHidden,
);
// The server returns a flat list of row models. To flag CRDT-hidden rows
@ -391,7 +391,7 @@ class DemoController extends ChangeNotifier {
// used to do internally); skipped entirely when hidden rows aren't shown.
final hiddenIds = <String>{};
if (showHidden) {
final visible = await client.demoDebug.fetchScopeSnapshot(
final visible = await client.demoDebug.fetchSpaceSnapshot(
includeHidden: false,
);
final visibleIds = {
@ -422,10 +422,10 @@ class DemoController extends ChangeNotifier {
Future<void> _handleReplicaMerge(
ReplicaSlot slot,
offline.UuidValue scopeUuid,
offline.UuidValue spaceUuid,
Object hlc,
) async {
replicas[slot]!.lastSyncedLabel = 'merged $scopeUuid at $hlc';
replicas[slot]!.lastSyncedLabel = 'merged $spaceUuid at $hlc';
await _refreshReplica(slot, notify: false);
await _fetchServer(notify: false);
replicas[slot]!.changed();
@ -469,7 +469,7 @@ class DemoController extends ChangeNotifier {
);
}
/// Wipes both replicas and the server scope at once, concurrently.
/// Wipes both replicas and the server space at once, concurrently.
Future<void> resetAll() async {
await _stopAllStreams();
await Future.wait([
@ -479,15 +479,15 @@ class DemoController extends ChangeNotifier {
]);
}
/// Hard-clears the caller's server scope, including synced rows and CRDT
/// Hard-clears the caller's server space, including synced rows and CRDT
/// metadata.
Future<void> resetServer() async {
if (!online) return;
await _runServer('Reset the server scope.', () async {
await _runServer('Reset the server space.', () async {
await _stopAllStreams();
_notifyReplicas();
try {
await client.demoDebug.resetScope();
await client.demoDebug.resetSpace();
} catch (error) {
server.error = 'reset failed: $error';
rethrow;
@ -795,9 +795,9 @@ class DemoController extends ChangeNotifier {
notifyListeners();
return;
}
await _runServer('Seeded the server scope ($kind).', () async {
await _runServer('Seeded the server space ($kind).', () async {
try {
await client.demoDebug.seedScope(kind, text);
await client.demoDebug.seedSpace(kind, text);
} catch (error) {
server.error = 'seed failed: $error';
rethrow;
@ -912,7 +912,7 @@ class DemoController extends ChangeNotifier {
/// since a plain findById only returns visible rows.
Future<TableRow<protocol.UuidValue?>?> _findHiddenRow(
TableOps ops,
offline.CrdtDatabaseSession crdt,
offline.OfflineSyncDatabaseSession crdt,
protocol.UuidValue id,
) async {
final all = await ops.findAll(crdt, includeHidden: true);
@ -1249,8 +1249,8 @@ class RowDetail {
final Map<String, dynamic> fields;
final List<EditableField> editable;
/// The CRDT scope id stored on the row, if any.
String? get scopeId => fields['scopeId']?.toString();
/// The CRDT space id stored on the row, if any.
String? get spaceId => fields['spaceId']?.toString();
}
final _tableDefinitionsByName = <String, TableDefinition>{
@ -1309,7 +1309,7 @@ bool _isPublicField(String key) {
}
bool _isEditableField(String key) {
return _isPublicField(key) && key != 'id' && key != 'scopeId';
return _isPublicField(key) && key != 'id' && key != 'spaceId';
}
bool _isEditableColumn(ColumnDefinition column) {

View file

@ -115,7 +115,7 @@ class _Toolbar extends StatelessWidget {
),
Tooltip(
tooltip: (context) => const TooltipContainer(
child: Text('Wipe both replicas and the server scope at once'),
child: Text('Wipe both replicas and the server space at once'),
),
child: OutlineButton(
onPressed: controller.anyBusy
@ -643,7 +643,7 @@ class _ServerPanel extends StatelessWidget {
const Gap(4),
Tooltip(
tooltip: (context) => const TooltipContainer(
child: Text('Reset server scope: clear all rows'),
child: Text('Reset server space: clear all rows'),
),
child: IconButton.ghost(
icon: const Icon(Icons.delete_outline, size: 16),
@ -706,7 +706,7 @@ class _ServerSeedButton extends StatelessWidget {
Widget build(BuildContext context) {
return Tooltip(
tooltip: (context) =>
const TooltipContainer(child: Text('Seed the server scope directly')),
const TooltipContainer(child: Text('Seed the server space directly')),
child: IconButton.ghost(
icon: const Icon(Icons.add, size: 16),
onPressed: enabled ? () => _open(context) : null,

View file

@ -76,7 +76,7 @@ class ScenarioCounter {
}
}
/// Normalizes a demo username so the same person maps to one scope.
/// Normalizes a demo username so the same person maps to one space.
String normalizeUsername(String username) {
return username.trim().toLowerCase();
}

View file

@ -14,13 +14,13 @@ import 'package:serverpod_offline_sync_test_client/serverpod_offline_sync_test_c
/// understand the package. The four steps are:
///
/// 1. [open] opens a local SQLite database ([Client.createSession])
/// and wraps it in a [CrdtDatabaseSession]. Use
/// and wraps it in a [OfflineSyncDatabaseSession]. Use
/// [openOrReset] when the file may predate the shipped
/// schema. The rest of the app reads and writes generated models through
/// [session].
/// 2. CRUD happens against [session] with the generated model APIs (see
/// the `seed*` methods in `DemoController` for the plain, recommended form,
/// e.g. `Person.db.insertRow(crdtSession, person)`).
/// 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.
@ -37,7 +37,7 @@ class OfflineReplica {
/// 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
/// the `includeHiddenRows` where-clause.
final CrdtDatabaseSession session;
final OfflineSyncDatabaseSession session;
/// The signed-in user every local write on this replica is attributed to.
final UuidValue persistentUserId;
@ -45,7 +45,7 @@ class OfflineReplica {
/// Opens [databasePath] through [client] and wraps it for CRDT sync.
///
/// [Client.createSession] opens the local client database (running
/// client migrations); [CrdtDatabaseSession.wraps] layers CRDT
/// 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.
static Future<OfflineReplica> open({
@ -57,13 +57,13 @@ class OfflineReplica {
databasePath,
isDebugMode: kDebugMode,
);
final crdtSession = CrdtDatabaseSession.wraps(
final offlineSyncSession = OfflineSyncDatabaseSession.wraps(
rawSession,
syncTables: syncTables,
persistentUserId: persistentUserId,
);
await crdtSession.db.initialize();
return OfflineReplica._(rawSession, crdtSession, persistentUserId);
await offlineSyncSession.db.initialize();
return OfflineReplica._(rawSession, offlineSyncSession, persistentUserId);
}
/// Like [open], but deletes [databasePath] and retries once when opening
@ -108,50 +108,50 @@ class OfflineReplica {
/// network. The replica's local database is the same either way.
Future<void> syncOnce(
Client client, {
CrdtSyncOnMergeSuccess? onMergeSuccess,
OfflineSyncOnMergeSuccess? onMergeSuccess,
}) {
return client.crdt.syncOnce(session, onMergeSuccess: onMergeSuccess);
return client.offlineSync.syncOnce(session, onMergeSuccess: onMergeSuccess);
}
/// Streams changes through [client] until the returned session is cancelled
/// (via [CrdtSyncSession.cancel]) or the remote stream closes.
CrdtSyncSession syncContinuously(
/// (via [OfflineSyncSubscription.cancel]) or the remote stream closes.
OfflineSyncSubscription syncContinuously(
Client client, {
CrdtSyncOnMergeSuccess? onMergeSuccess,
OfflineSyncOnMergeSuccess? onMergeSuccess,
}) {
return client.crdt.syncContinuously(
return client.offlineSync.syncContinuously(
session,
onMergeSuccess: onMergeSuccess,
);
}
/// Wipes this replica back to an empty device by deleting its local CRDT
/// scope row. Every synced table cascades on `scopeId` -> `crdt_scopes`, so
/// space row. Every synced table cascades on `spaceId` -> `offline_sync_spaces`, so
/// the domain rows and CRDT metadata go with it — no need to drop and recreate
/// the database file. A fresh scope/node is established lazily on the next
/// the database file. A fresh space/node is established lazily on the next
/// write.
///
/// The delete runs with `defer_foreign_keys` on: the scopeId cascade fans out
/// The delete runs with `defer_foreign_keys` on: the spaceId cascade fans out
/// across the CRDT metadata diamond (`crdt_data_rows`/`crdt_data_fields`/
/// `crdt_data_tombstone` reference `crdt_nodes` with NO ACTION while both
/// sides cascade off `crdt_scopes`), and SQLite's cascade order can
/// sides cascade off `offline_sync_spaces`), and SQLite's cascade order can
/// transiently violate those immediate checks. Deferring them to commit lets
/// the whole cascade complete first.
Future<void> reset() async {
final scope = await CrdtScope.db.findFirstRow(
final space = await OfflineSyncSpace.db.findFirstRow(
session,
where: (t) => t.uuidScopeId.equals(persistentUserId),
where: (t) => t.uuidSpaceId.equals(persistentUserId),
);
final scopeId = scope?.id;
if (scopeId != null) {
final spaceId = space?.id;
if (spaceId != null) {
await session.db.transaction((transaction) async {
await session.db.unsafeExecute(
'PRAGMA defer_foreign_keys = ON',
transaction: transaction,
);
await CrdtScope.db.deleteWhere(
await OfflineSyncSpace.db.deleteWhere(
session,
where: (t) => t.id.equals(scopeId),
where: (t) => t.id.equals(spaceId),
transaction: transaction,
);
});

View file

@ -184,5 +184,5 @@ class RelationshipCatalog {
}
static bool _isSystemColumn(String name) =>
name == 'id' || name == 'scopeId' || name.startsWith('_');
name == 'id' || name == 'spaceId' || name.startsWith('_');
}

View file

@ -225,7 +225,7 @@ class _RowDetailSheetState extends State<_RowDetailSheet> {
Text(detail.slot.label).small().muted(),
const Gap(12),
_kv(context, 'Row UUID', detail.uuid),
_kv(context, 'Scope id', detail.scopeId ?? '—'),
_kv(context, 'Space id', detail.spaceId ?? '—'),
_kv(
context,
'Visibility',

View file

@ -125,7 +125,7 @@ class DemoSnapshot {
/// classifies each row's visibility.
static Future<DemoSnapshot> load(
RelationshipCatalog catalog,
offline.CrdtDatabaseSession crdt,
offline.OfflineSyncDatabaseSession crdt,
) async {
final rows = <RowView>[];
for (final table in catalog.tables) {
@ -153,7 +153,7 @@ class DemoSnapshot {
}
/// Builds a snapshot from the server's merged truth. [serverRows] is the flat
/// `List<dynamic>` returned by `fetchScopeSnapshot`: each element is a typed
/// `List<dynamic>` returned by `fetchSpaceSnapshot`: each element is a typed
/// domain model, deserialized by Serverpod from its `__className__` tag, so
/// the table name and JSON come straight off the model. Rows whose id is in
/// [hiddenIds] are flagged hidden; there is no local CRDT metadata to show.