feat(sync): device-to-device inventory sync, end to end
Wires the encrypted sync transport into a working replication between one identity's own devices, reusing the CRDT-ready model (HLC + tombstones + LWW) the data model was built for. - SocialSession gains a 5th interface, sync, over the shared connection, built with a per-install deviceId + the app's inventory namespace. - DeviceIdStore: a stable random per-install id in the keystore — distinct from the seed-derived CRDT node id, which is IDENTICAL across a user's devices (so it couldn't tell them apart / they'd clobber each other's replaceable record). - InventorySnapshotIO (implemented by ExportImportService): build/apply the inventory as the SAME interchange JSON as backups but UNSEALED — the sync transport does the encryption, so it must not double-seal. Apply = the existing idempotent LWW importInventory. - SyncService: on (re)connect publishes this device's snapshot + subscribes to the others'; a local edit (debounced, off tableUpdates) republishes; incoming snapshots merge idempotently; skips its own echo; a byte check skips re-publishing an unchanged snapshot so the ping-pong converges. Foreground only, like the inbox. - DI/Bootstrap/switchSocialAccount wire and lifecycle it per identity. Tests: DeviceIdStore, ExportImportService snapshot round-trip (unsealed + idempotent), SyncService.handleRemote (applies others', skips own). Follow-ups: deltas vs full-snapshot pushes; background sync; conflict UI.
This commit is contained in:
parent
182c334883
commit
225880fc64
10 changed files with 363 additions and 12 deletions
|
|
@ -19,6 +19,7 @@ import '../security/secret_store.dart';
|
|||
import '../security/secure_key_store.dart';
|
||||
import '../services/auto_backup_service.dart';
|
||||
import '../services/auto_backup_store.dart';
|
||||
import '../services/device_id_store.dart';
|
||||
import '../services/export_import_service.dart';
|
||||
import '../services/file_picker_file_service.dart';
|
||||
import '../services/file_service.dart';
|
||||
|
|
@ -40,6 +41,7 @@ import '../services/social_account_store.dart';
|
|||
import '../services/social_connection.dart';
|
||||
import '../services/social_service.dart';
|
||||
import '../services/social_settings.dart';
|
||||
import '../services/sync_service.dart';
|
||||
import '../services/trust_referents.dart';
|
||||
import '../services/unread_service.dart';
|
||||
import '../services/wot_settings.dart';
|
||||
|
|
@ -88,6 +90,10 @@ Future<void> configureDependencies() async {
|
|||
final activeAccount = await accounts.active();
|
||||
final scope = socialAccountScope(activeAccount);
|
||||
|
||||
// Stable per-install id for device-to-device sync (distinct from the
|
||||
// seed-derived CRDT node id, which is shared across a user's devices).
|
||||
final deviceId = await DeviceIdStore(secretStore).deviceId();
|
||||
|
||||
final database = AppDatabase(
|
||||
openEncryptedExecutor(await _databaseFile(), dbKeyHex),
|
||||
);
|
||||
|
|
@ -104,8 +110,11 @@ Future<void> configureDependencies() async {
|
|||
// profile stay hidden) rather than blanking the whole app.
|
||||
SocialService? socialService;
|
||||
try {
|
||||
socialService =
|
||||
await SocialService.fromRootSeedHex(rootSeedHex, account: activeAccount);
|
||||
socialService = await SocialService.fromRootSeedHex(
|
||||
rootSeedHex,
|
||||
account: activeAccount,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
} catch (e, s) {
|
||||
debugPrint('Social identity derivation failed; inventory-only mode: $e\n$s');
|
||||
}
|
||||
|
|
@ -224,6 +233,16 @@ Future<void> configureDependencies() async {
|
|||
unread: getIt<UnreadService>(),
|
||||
notifications: getIt<NotificationService>(),
|
||||
),
|
||||
)
|
||||
// Replicates the inventory across this identity's own devices over the
|
||||
// shared connection. Started in `Bootstrap`.
|
||||
..registerSingleton<SyncService>(
|
||||
SyncService(
|
||||
connection: connection,
|
||||
io: getIt<ExportImportService>(),
|
||||
localChanges: database.tableUpdates().map<void>((_) {}),
|
||||
selfDeviceId: deviceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -244,8 +263,14 @@ Future<void> switchSocialAccount(int account) async {
|
|||
await getIt<SocialAccountStore>().setActive(account);
|
||||
final scope = socialAccountScope(account);
|
||||
final rootSeedHex = await getIt<SecureKeyStore>().rootSeedHex();
|
||||
// Per-install, so it survives an identity switch.
|
||||
final deviceId = await DeviceIdStore(secretStore).deviceId();
|
||||
|
||||
// Tear down the old identity's listener + shared connection before replacing.
|
||||
// Tear down the old identity's listener + sync + shared connection first.
|
||||
if (getIt.isRegistered<SyncService>()) {
|
||||
await getIt<SyncService>().stop();
|
||||
await getIt.unregister<SyncService>();
|
||||
}
|
||||
if (getIt.isRegistered<InboxService>()) {
|
||||
await getIt<InboxService>().stop();
|
||||
await getIt.unregister<InboxService>();
|
||||
|
|
@ -275,8 +300,11 @@ Future<void> switchSocialAccount(int account) async {
|
|||
..registerSingleton<UnreadService>(
|
||||
UnreadService(getIt<MessageStore>(), secretStore));
|
||||
|
||||
final social =
|
||||
await SocialService.fromRootSeedHex(rootSeedHex, account: account);
|
||||
final social = await SocialService.fromRootSeedHex(
|
||||
rootSeedHex,
|
||||
account: account,
|
||||
deviceId: deviceId,
|
||||
);
|
||||
final connection =
|
||||
SocialConnection(social: social, settings: getIt<SocialSettings>());
|
||||
final inbox = InboxService(
|
||||
|
|
@ -287,12 +315,20 @@ Future<void> switchSocialAccount(int account) async {
|
|||
unread: getIt<UnreadService>(),
|
||||
notifications: getIt<NotificationService>(),
|
||||
);
|
||||
final sync = SyncService(
|
||||
connection: connection,
|
||||
io: getIt<ExportImportService>(),
|
||||
localChanges: getIt<AppDatabase>().tableUpdates().map<void>((_) {}),
|
||||
selfDeviceId: deviceId,
|
||||
);
|
||||
getIt
|
||||
..registerSingleton<SocialService>(social)
|
||||
..registerSingleton<SocialConnection>(connection)
|
||||
..registerSingleton<InboxService>(inbox);
|
||||
// Subscribe the listener BEFORE the connection starts connecting.
|
||||
..registerSingleton<InboxService>(inbox)
|
||||
..registerSingleton<SyncService>(sync);
|
||||
// Subscribe the listeners BEFORE the connection starts connecting.
|
||||
inbox.start();
|
||||
sync.start();
|
||||
connection.start();
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue