adding packages

This commit is contained in:
2026-01-15 14:38:46 -08:00
parent ef86e3ab6a
commit 6869cf47e5
5253 changed files with 726695 additions and 34 deletions
@@ -0,0 +1,111 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import '../sync_repository.dart';
import 'schema.dart'; // Will export schema.g.dart artifacts via internal generation users
/// A Drift (SQLite) implementation of SyncRepository.
/// Requires a [SyncDatabase] interface which exposes the underlying tables.
class DriftSyncRepository<T> implements SyncRepository<T> {
final SyncDatabase dbWrapper;
final String collectionName;
final Map<String, dynamic> Function(T) toJson;
final T Function(Map<String, dynamic>) fromJson;
DriftSyncRepository({
required this.dbWrapper,
required this.collectionName,
required this.toJson,
required this.fromJson,
});
GeneratedDatabase get db => dbWrapper.db;
@override
Future<List<SyncRecord<T>>> getAll() async {
// Explicitly type the select to ensure 't' is 'SyncEntries'
final query = db.select<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
..where((t) => t.collection.equals(collectionName));
final rows = await query.get();
return rows.map((row) {
return SyncRecord<T>(
id: row.id,
data: fromJson(jsonDecode(row.data)),
baseData:
row.baseData != null ? fromJson(jsonDecode(row.baseData!)) : null,
serverUpdatedAt: row.serverUpdatedAt,
isDirty: row.isDirty,
isDeleted: row.isDeleted,
deletedAt: row.deletedAt,
);
}).toList();
}
@override
Future<SyncRecord<T>?> get(String id) async {
final query = db.select<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
..where((t) => t.collection.equals(collectionName) & t.id.equals(id));
final row = await query.getSingleOrNull();
if (row == null) return null;
return SyncRecord<T>(
id: row.id,
data: fromJson(jsonDecode(row.data)),
baseData:
row.baseData != null ? fromJson(jsonDecode(row.baseData!)) : null,
serverUpdatedAt: row.serverUpdatedAt,
isDirty: row.isDirty,
isDeleted: row.isDeleted,
deletedAt: row.deletedAt,
);
}
@override
Future<void> save(SyncRecord<T> record) async {
// Note: SyncEntriesCompanion is generated in schema.g.dart
await db.into(dbWrapper.syncEntries).insertOnConflictUpdate(
SyncEntriesCompanion(
collection: Value(collectionName),
id: Value(record.id),
data: Value(jsonEncode(toJson(record.data))),
baseData: Value(
record.baseData != null
? jsonEncode(toJson(record.baseData as T))
: null,
),
serverUpdatedAt: Value(record.serverUpdatedAt),
isDirty: Value(record.isDirty),
isDeleted: Value(record.isDeleted),
deletedAt: Value(record.deletedAt),
),
);
}
@override
Future<void> delete(String id) async {
await (db.delete<SyncEntries, SyncEntry>(dbWrapper.syncEntries)
..where((t) => t.collection.equals(collectionName) & t.id.equals(id)))
.go();
}
@override
Future<DateTime> getLastSyncTime() async {
final query = db.select<SyncMeta, SyncMetaData>(dbWrapper.syncMeta)
..where((t) => t.collection.equals(collectionName));
final row = await query.getSingleOrNull();
return row?.lastSync ?? DateTime.fromMillisecondsSinceEpoch(0);
}
@override
Future<void> setLastSyncTime(DateTime time) async {
await db.into(dbWrapper.syncMeta).insertOnConflictUpdate(
SyncMetaCompanion(
collection: Value(collectionName),
lastSync: Value(time),
),
);
}
}
@@ -0,0 +1,55 @@
import 'package:drift/drift.dart';
part 'schema.g.dart';
/// Stores the actual data records + sync metadata
/// We use a composite primary key of (collection, id) to store multiple collections in one table
class SyncEntries extends Table {
TextColumn get collection => text()();
TextColumn get id => text()();
/// serialized JSON of the current local data
TextColumn get data => text()();
/// serialized JSON of the base data (snapshot)
TextColumn get baseData => text().nullable()();
DateTimeColumn get serverUpdatedAt => dateTime().nullable()();
BoolColumn get isDirty => boolean().withDefault(const Constant(false))();
BoolColumn get isDeleted => boolean().withDefault(const Constant(false))();
DateTimeColumn get deletedAt => dateTime().nullable()();
@override
Set<Column> get primaryKey => {collection, id};
}
/// Stores global sync state per collection (e.g. last sync time)
class SyncMeta extends Table {
TextColumn get collection => text()();
DateTimeColumn get lastSync => dateTime().withDefault(
Constant(DateTime.fromMillisecondsSinceEpoch(0)),
)();
@override
Set<Column> get primaryKey => {collection};
}
/// Dummy database class to trigger generation of Companions and Data classes
/// which are used by DriftSyncRepository.
@DriftDatabase(tables: [SyncEntries, SyncMeta])
class PackageDatabase extends _$PackageDatabase implements SyncDatabase {
PackageDatabase(super.e);
@override
int get schemaVersion => 1;
@override
GeneratedDatabase get db => this;
}
/// Interface that your AppDatabase must implement to be used with DriftSyncRepository.
/// This allows the repository to be agnostic of the specific generated database class.
abstract class SyncDatabase {
GeneratedDatabase get db;
TableInfo<SyncEntries, SyncEntry> get syncEntries;
TableInfo<SyncMeta, SyncMetaData> get syncMeta;
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
import 'sync_repository.dart';
/// Default In-Memory Implementation (Good for testing/prototyping)
class InMemoryRepository<T> implements SyncRepository<T> {
final Map<String, SyncRecord<T>> _db = {};
DateTime _lastSync = DateTime.fromMillisecondsSinceEpoch(0);
@override
Future<List<SyncRecord<T>>> getAll() async => _db.values.toList();
@override
Future<SyncRecord<T>?> get(String id) async => _db[id];
@override
Future<void> save(SyncRecord<T> record) async => _db[record.id] = record;
@override
Future<void> delete(String id) async => _db.remove(id);
@override
Future<DateTime> getLastSyncTime() async => _lastSync;
@override
Future<void> setLastSyncTime(DateTime time) async => _lastSync = time;
}
@@ -0,0 +1,61 @@
import 'dart:async';
/// Wraps your data with sync metadata (Dirty flags, tombstones, versions)
class SyncRecord<T> {
final String id;
final T data;
/// Server's timestamp. Null if created locally and never synced.
final DateTime? serverUpdatedAt;
/// Snapshot of data before local edits (Critical for 3-way merge).
final T? baseData;
/// True if local data differs from server data.
final bool isDirty;
/// True if marked for deletion (Soft Delete).
final bool isDeleted;
/// When the soft delete happened (For Retention Policy).
final DateTime? deletedAt;
SyncRecord({
required this.id,
required this.data,
this.serverUpdatedAt,
this.baseData,
this.isDirty = false,
this.isDeleted = false,
this.deletedAt,
});
SyncRecord<T> copyWith({
T? data,
DateTime? serverUpdatedAt,
T? baseData,
bool? isDirty,
bool? isDeleted,
DateTime? deletedAt,
}) {
return SyncRecord<T>(
id: id,
data: data ?? this.data,
serverUpdatedAt: serverUpdatedAt ?? this.serverUpdatedAt,
baseData: baseData ?? this.baseData,
isDirty: isDirty ?? this.isDirty,
isDeleted: isDeleted ?? this.isDeleted,
deletedAt: deletedAt ?? this.deletedAt,
);
}
}
/// Abstract Storage Interface
abstract class SyncRepository<T> {
Future<List<SyncRecord<T>>> getAll();
Future<SyncRecord<T>?> get(String id);
Future<void> save(SyncRecord<T> record);
Future<void> delete(String id); // Hard Delete
Future<DateTime> getLastSyncTime();
Future<void> setLastSyncTime(DateTime time);
}