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.
34 lines
1.2 KiB
Dart
34 lines
1.2 KiB
Dart
import 'dart:math';
|
|
|
|
import '../security/secret_store.dart';
|
|
|
|
/// A stable per-INSTALL device id, kept in the keystore. Distinct from the
|
|
/// seed-derived CRDT node id (`rootSeedHex.substring(0,16)`), which is IDENTICAL
|
|
/// across a user's devices — so it can't tell them apart. Device-to-device sync
|
|
/// needs each device to own a separate record, hence a random per-install id.
|
|
class DeviceIdStore {
|
|
DeviceIdStore(this._store, {Random? random}) : _random = random ?? Random.secure();
|
|
|
|
final SecretStore _store;
|
|
final Random _random;
|
|
|
|
static const _key = 'tane.device.id';
|
|
|
|
Future<String>? _cached;
|
|
|
|
/// This install's device id, generated and persisted on first use.
|
|
Future<String> deviceId() => _cached ??= _readOrCreate();
|
|
|
|
Future<String> _readOrCreate() async {
|
|
final existing = await _store.read(_key);
|
|
if (existing != null && existing.isNotEmpty) return existing;
|
|
final id = _randomHex(8); // 64 bits — plenty to avoid device collisions
|
|
await _store.write(_key, id);
|
|
return id;
|
|
}
|
|
|
|
String _randomHex(int bytes) => [
|
|
for (var i = 0; i < bytes; i++)
|
|
_random.nextInt(256).toRadixString(16).padLeft(2, '0'),
|
|
].join();
|
|
}
|