♻️ Use drift instead for floor

This commit is contained in:
2024-09-14 00:30:33 +08:00
parent db808650e3
commit b14e55355f
24 changed files with 886 additions and 692 deletions

View File

@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:get/get.dart';
import 'package:get/get_connect/http/src/request/request.dart';
import 'package:solian/controllers/chat_events_controller.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/exceptions/unauthorized.dart';
import 'package:solian/providers/websocket.dart';
@ -199,11 +198,6 @@ class AuthProvider extends GetConnect {
Get.find<WebSocketProvider>().notifications.clear();
Get.find<WebSocketProvider>().notificationUnread.value = 0;
final chatHistory = ChatEventController();
chatHistory.initialize().then((_) async {
await chatHistory.database.localEvents.wipeLocalEvents();
});
storage.deleteAll();
}

View File

@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/providers/database/tables/messages.dart';
import 'package:solian/models/event.dart';
part 'database.g.dart';
@DriftDatabase(tables: [LocalMessageEventTable])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'solar_network_local_db');
}
}
class DatabaseProvider extends GetxController {
final database = AppDatabase();
}

View File

@ -0,0 +1,429 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class $LocalMessageEventTableTable extends LocalMessageEventTable
with TableInfo<$LocalMessageEventTableTable, LocalMessageEventTableData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
$LocalMessageEventTableTable(this.attachedDatabase, [this._alias]);
static const VerificationMeta _idMeta = const VerificationMeta('id');
@override
late final GeneratedColumn<int> id = GeneratedColumn<int>(
'id', aliasedName, false,
hasAutoIncrement: true,
type: DriftSqlType.int,
requiredDuringInsert: false,
defaultConstraints:
GeneratedColumn.constraintIsAlways('PRIMARY KEY AUTOINCREMENT'));
static const VerificationMeta _channelIdMeta =
const VerificationMeta('channelId');
@override
late final GeneratedColumn<int> channelId = GeneratedColumn<int>(
'channel_id', aliasedName, false,
type: DriftSqlType.int, requiredDuringInsert: true);
static const VerificationMeta _dataMeta = const VerificationMeta('data');
@override
late final GeneratedColumnWithTypeConverter<Event?, String> data =
GeneratedColumn<String>('data', aliasedName, false,
type: DriftSqlType.string, requiredDuringInsert: true)
.withConverter<Event?>($LocalMessageEventTableTable.$converterdata);
static const VerificationMeta _createdAtMeta =
const VerificationMeta('createdAt');
@override
late final GeneratedColumn<DateTime> createdAt = GeneratedColumn<DateTime>(
'created_at', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: false,
defaultValue: Constant(DateTime.now()));
@override
List<GeneratedColumn> get $columns => [id, channelId, data, createdAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'local_message_event_table';
@override
VerificationContext validateIntegrity(
Insertable<LocalMessageEventTableData> instance,
{bool isInserting = false}) {
final context = VerificationContext();
final data = instance.toColumns(true);
if (data.containsKey('id')) {
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
}
if (data.containsKey('channel_id')) {
context.handle(_channelIdMeta,
channelId.isAcceptableOrUnknown(data['channel_id']!, _channelIdMeta));
} else if (isInserting) {
context.missing(_channelIdMeta);
}
context.handle(_dataMeta, const VerificationResult.success());
if (data.containsKey('created_at')) {
context.handle(_createdAtMeta,
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
}
return context;
}
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
LocalMessageEventTableData map(Map<String, dynamic> data,
{String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return LocalMessageEventTableData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}id'])!,
channelId: attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}channel_id'])!,
data: $LocalMessageEventTableTable.$converterdata.fromSql(attachedDatabase
.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}data'])!),
createdAt: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
);
}
@override
$LocalMessageEventTableTable createAlias(String alias) {
return $LocalMessageEventTableTable(attachedDatabase, alias);
}
static TypeConverter<Event?, String> $converterdata =
const MessageEventConverter();
}
class LocalMessageEventTableData extends DataClass
implements Insertable<LocalMessageEventTableData> {
final int id;
final int channelId;
final Event? data;
final DateTime createdAt;
const LocalMessageEventTableData(
{required this.id,
required this.channelId,
this.data,
required this.createdAt});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<int>(id);
map['channel_id'] = Variable<int>(channelId);
if (!nullToAbsent || data != null) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data));
}
map['created_at'] = Variable<DateTime>(createdAt);
return map;
}
LocalMessageEventTableCompanion toCompanion(bool nullToAbsent) {
return LocalMessageEventTableCompanion(
id: Value(id),
channelId: Value(channelId),
data: data == null && nullToAbsent ? const Value.absent() : Value(data),
createdAt: Value(createdAt),
);
}
factory LocalMessageEventTableData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return LocalMessageEventTableData(
id: serializer.fromJson<int>(json['id']),
channelId: serializer.fromJson<int>(json['channelId']),
data: serializer.fromJson<Event?>(json['data']),
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<int>(id),
'channelId': serializer.toJson<int>(channelId),
'data': serializer.toJson<Event?>(data),
'createdAt': serializer.toJson<DateTime>(createdAt),
};
}
LocalMessageEventTableData copyWith(
{int? id,
int? channelId,
Value<Event?> data = const Value.absent(),
DateTime? createdAt}) =>
LocalMessageEventTableData(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data.present ? data.value : this.data,
createdAt: createdAt ?? this.createdAt,
);
LocalMessageEventTableData copyWithCompanion(
LocalMessageEventTableCompanion data) {
return LocalMessageEventTableData(
id: data.id.present ? data.id.value : this.id,
channelId: data.channelId.present ? data.channelId.value : this.channelId,
data: data.data.present ? data.data.value : this.data,
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
);
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableData(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, channelId, data, createdAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is LocalMessageEventTableData &&
other.id == this.id &&
other.channelId == this.channelId &&
other.data == this.data &&
other.createdAt == this.createdAt);
}
class LocalMessageEventTableCompanion
extends UpdateCompanion<LocalMessageEventTableData> {
final Value<int> id;
final Value<int> channelId;
final Value<Event?> data;
final Value<DateTime> createdAt;
const LocalMessageEventTableCompanion({
this.id = const Value.absent(),
this.channelId = const Value.absent(),
this.data = const Value.absent(),
this.createdAt = const Value.absent(),
});
LocalMessageEventTableCompanion.insert({
this.id = const Value.absent(),
required int channelId,
required Event? data,
this.createdAt = const Value.absent(),
}) : channelId = Value(channelId),
data = Value(data);
static Insertable<LocalMessageEventTableData> custom({
Expression<int>? id,
Expression<int>? channelId,
Expression<String>? data,
Expression<DateTime>? createdAt,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (channelId != null) 'channel_id': channelId,
if (data != null) 'data': data,
if (createdAt != null) 'created_at': createdAt,
});
}
LocalMessageEventTableCompanion copyWith(
{Value<int>? id,
Value<int>? channelId,
Value<Event?>? data,
Value<DateTime>? createdAt}) {
return LocalMessageEventTableCompanion(
id: id ?? this.id,
channelId: channelId ?? this.channelId,
data: data ?? this.data,
createdAt: createdAt ?? this.createdAt,
);
}
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
if (id.present) {
map['id'] = Variable<int>(id.value);
}
if (channelId.present) {
map['channel_id'] = Variable<int>(channelId.value);
}
if (data.present) {
map['data'] = Variable<String>(
$LocalMessageEventTableTable.$converterdata.toSql(data.value));
}
if (createdAt.present) {
map['created_at'] = Variable<DateTime>(createdAt.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('LocalMessageEventTableCompanion(')
..write('id: $id, ')
..write('channelId: $channelId, ')
..write('data: $data, ')
..write('createdAt: $createdAt')
..write(')'))
.toString();
}
}
abstract class _$AppDatabase extends GeneratedDatabase {
_$AppDatabase(QueryExecutor e) : super(e);
$AppDatabaseManager get managers => $AppDatabaseManager(this);
late final $LocalMessageEventTableTable localMessageEventTable =
$LocalMessageEventTableTable(this);
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [localMessageEventTable];
}
typedef $$LocalMessageEventTableTableCreateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
required int channelId,
required Event? data,
Value<DateTime> createdAt,
});
typedef $$LocalMessageEventTableTableUpdateCompanionBuilder
= LocalMessageEventTableCompanion Function({
Value<int> id,
Value<int> channelId,
Value<Event?> data,
Value<DateTime> createdAt,
});
class $$LocalMessageEventTableTableFilterComposer
extends FilterComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableFilterComposer(super.$state);
ColumnFilters<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnFilters<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
ColumnWithTypeConverterFilters<Event?, Event, String> get data =>
$state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
column,
joinBuilders: joinBuilders));
ColumnFilters<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnFilters(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableOrderingComposer
extends OrderingComposer<_$AppDatabase, $LocalMessageEventTableTable> {
$$LocalMessageEventTableTableOrderingComposer(super.$state);
ColumnOrderings<int> get id => $state.composableBuilder(
column: $state.table.id,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<int> get channelId => $state.composableBuilder(
column: $state.table.channelId,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<String> get data => $state.composableBuilder(
column: $state.table.data,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
ColumnOrderings<DateTime> get createdAt => $state.composableBuilder(
column: $state.table.createdAt,
builder: (column, joinBuilders) =>
ColumnOrderings(column, joinBuilders: joinBuilders));
}
class $$LocalMessageEventTableTableTableManager extends RootTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()> {
$$LocalMessageEventTableTableTableManager(
_$AppDatabase db, $LocalMessageEventTableTable table)
: super(TableManagerState(
db: db,
table: table,
filteringComposer: $$LocalMessageEventTableTableFilterComposer(
ComposerState(db, table)),
orderingComposer: $$LocalMessageEventTableTableOrderingComposer(
ComposerState(db, table)),
updateCompanionCallback: ({
Value<int> id = const Value.absent(),
Value<int> channelId = const Value.absent(),
Value<Event?> data = const Value.absent(),
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
createCompanionCallback: ({
Value<int> id = const Value.absent(),
required int channelId,
required Event? data,
Value<DateTime> createdAt = const Value.absent(),
}) =>
LocalMessageEventTableCompanion.insert(
id: id,
channelId: channelId,
data: data,
createdAt: createdAt,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
));
}
typedef $$LocalMessageEventTableTableProcessedTableManager
= ProcessedTableManager<
_$AppDatabase,
$LocalMessageEventTableTable,
LocalMessageEventTableData,
$$LocalMessageEventTableTableFilterComposer,
$$LocalMessageEventTableTableOrderingComposer,
$$LocalMessageEventTableTableCreateCompanionBuilder,
$$LocalMessageEventTableTableUpdateCompanionBuilder,
(
LocalMessageEventTableData,
BaseReferences<_$AppDatabase, $LocalMessageEventTableTable,
LocalMessageEventTableData>
),
LocalMessageEventTableData,
PrefetchHooks Function()>;
class $AppDatabaseManager {
final _$AppDatabase _db;
$AppDatabaseManager(this._db);
$$LocalMessageEventTableTableTableManager get localMessageEventTable =>
$$LocalMessageEventTableTableTableManager(
_db, _db.localMessageEventTable);
}

View File

@ -0,0 +1,202 @@
import 'package:drift/drift.dart';
import 'package:get/get.dart' hide Value;
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/database/database.dart';
class MessagesFetchingProvider extends GetxController {
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/whats-new?pivot=$pivot&take=$take',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
return (result, response.count);
}
Future<Event?> fetchRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> fetchRemoteEvents(
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await fetchRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
Future<LocalMessageEventTableData> receiveEvent(Event remote) async {
// Insert record
final database = Get.find<DatabaseProvider>().database;
final entry = await database
.into(database.localMessageEventTable)
.insertReturning(LocalMessageEventTableCompanion.insert(
id: Value(remote.id),
channelId: remote.channelId,
data: remote,
createdAt: Value(remote.createdAt),
));
// Handle side-effect like editing and deleting
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.getSingleOrNull();
if (target != null) {
target.data!.body = remote.body;
target.data!.updatedAt = remote.updatedAt;
await (database.update(database.localMessageEventTable)
..where((x) => x.id.equals(target.id)))
.write(
LocalMessageEventTableCompanion(data: Value(target.data)),
);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await (database.delete(database.localMessageEventTable)
..where((x) => x.id.equals(body.relatedEvent!)))
.go();
}
}
return entry;
}
Future<LocalMessageEventTableData?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final database = Get.find<DatabaseProvider>().database;
final localRecord = await (database.select(database.localMessageEventTable)
..where((x) => x.id.equals(id)))
.getSingleOrNull();
if (localRecord != null) return localRecord;
final remoteRecord = await fetchRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
/// Pull the remote events to local database
Future<(List<Event>, int)?> pullRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final database = Get.find<DatabaseProvider>().database;
final lastOne = await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
final data = await fetchRemoteEvents(
channel,
scope,
remainDepth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await database.batch((batch) {
batch.insertAll(
database.localMessageEventTable,
data.$1.map((x) => LocalMessageEventTableCompanion(
id: Value(x.id),
channelId: Value(x.channelId),
data: Value(x),
createdAt: Value(x.createdAt),
)),
);
});
}
return data;
}
Future<List<LocalMessageEventTableData>> listEvents(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.get();
}
Future<LocalMessageEventTableData?> getLastInChannel(Channel channel) async {
final database = Get.find<DatabaseProvider>().database;
return await (database.select(database.localMessageEventTable)
..where((x) => x.channelId.equals(channel.id))
..orderBy([(t) => OrderingTerm.desc(t.id)]))
.getSingleOrNull();
}
}

View File

@ -0,0 +1,13 @@
import 'dart:convert';
import 'package:drift/drift.dart';
class JsonConverter extends TypeConverter<Object?, String> {
const JsonConverter();
@override
Object? fromSql(String fromDb) => jsonDecode(fromDb);
@override
String toSql(Object? value) => jsonEncode(value);
}

View File

@ -0,0 +1,22 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:solian/models/event.dart';
class LocalMessageEventTable extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get channelId => integer()();
TextColumn get data => text().map(const MessageEventConverter())();
DateTimeColumn get createdAt =>
dateTime().withDefault(Constant(DateTime.now()))();
}
class MessageEventConverter extends TypeConverter<Event?, String> {
const MessageEventConverter();
@override
Event? fromSql(String fromDb) => Event.fromJson(jsonDecode(fromDb));
@override
String toSql(Event? value) => jsonEncode(value?.toJson());
}

View File

@ -1,173 +0,0 @@
import 'package:floor/floor.dart';
import 'package:get/get.dart';
import 'package:solian/exceptions/request.dart';
import 'package:solian/models/channel.dart';
import 'package:solian/models/event.dart';
import 'package:solian/models/pagination.dart';
import 'package:solian/providers/auth.dart';
import 'package:solian/providers/message/events.dart';
Future<MessageHistoryDb> createHistoryDb() async {
final migration1to2 = Migration(1, 2, (database) async {
await database.execute('DROP TABLE IF EXISTS LocalMessage');
});
return await $FloorMessageHistoryDb
.databaseBuilder('messaging_data.dart')
.addMigrations([migration1to2]).build();
}
Future<(List<Event>, int)?> getWhatsNewEvents(int pivot, {take = 10}) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/whats-new?pivot=$pivot&take=$take',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
return (result, response.count);
}
Future<Event?> getRemoteEvent(int id, Channel channel, String scope) async {
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events/$id',
);
if (resp.statusCode == 404) {
return null;
} else if (resp.statusCode != 200) {
throw RequestException(resp);
}
return Event.fromJson(resp.body);
}
Future<(List<Event>, int)?> getRemoteEvents(
Channel channel,
String scope, {
required int remainDepth,
bool Function(List<Event> items)? onBrake,
take = 10,
offset = 0,
}) async {
if (remainDepth <= 0) {
return null;
}
final AuthProvider auth = Get.find();
if (auth.isAuthorized.isFalse) return null;
final client = auth.configureClient('messaging');
final resp = await client.get(
'/channels/$scope/${channel.alias}/events?take=$take&offset=$offset',
);
if (resp.statusCode != 200) {
throw RequestException(resp);
}
final PaginationResult response = PaginationResult.fromJson(resp.body);
final result =
response.data?.map((e) => Event.fromJson(e)).toList() ?? List.empty();
if (onBrake != null && onBrake(result)) {
return (result, response.count);
}
final expandResult = (await getRemoteEvents(
channel,
scope,
remainDepth: remainDepth - 1,
take: take,
offset: offset + result.length,
))
?.$1 ??
List.empty();
return ([...result, ...expandResult], response.count);
}
extension MessageHistoryAdaptor on MessageHistoryDb {
Future<LocalEvent> receiveEvent(Event remote) async {
final entry = LocalEvent(
remote.id,
remote,
remote.channelId,
remote.createdAt,
);
await localEvents.insert(entry);
switch (remote.type) {
case 'messages.edit':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
final target = await localEvents.findById(body.relatedEvent!);
if (target != null) {
target.data.body = remote.body;
target.data.updatedAt = remote.updatedAt;
await localEvents.update(target);
}
}
case 'messages.delete':
final body = EventMessageBody.fromJson(remote.body);
if (body.relatedEvent != null) {
await localEvents.delete(body.relatedEvent!);
}
}
return entry;
}
Future<LocalEvent?> getEvent(int id, Channel channel,
{String scope = 'global'}) async {
final localRecord = await localEvents.findById(id);
if (localRecord != null) return localRecord;
final remoteRecord = await getRemoteEvent(id, channel, scope);
if (remoteRecord == null) return null;
return await receiveEvent(remoteRecord);
}
Future<(List<Event>, int)?> syncRemoteEvents(Channel channel,
{String scope = 'global', depth = 10, offset = 0}) async {
final lastOne = await localEvents.findLastByChannel(channel.id);
final data = await getRemoteEvents(
channel,
scope,
remainDepth: depth,
offset: offset,
onBrake: (items) {
return items.any((x) => x.id == lastOne?.id);
},
);
if (data != null) {
await localEvents.insertBulk(
data.$1
.map((x) => LocalEvent(x.id, x, x.channelId, x.createdAt))
.toList(),
);
}
return data;
}
Future<List<LocalEvent>> listEvents(Channel channel) async {
return await localEvents.findAllByChannel(channel.id);
}
}

View File

@ -1,83 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:floor/floor.dart';
import 'package:solian/models/event.dart';
import 'package:sqflite/sqflite.dart' as sqflite;
part 'events.g.dart';
@entity
class LocalEvent {
@primaryKey
final int id;
final Event data;
final int channelId;
final DateTime createdAt;
LocalEvent(this.id, this.data, this.channelId, this.createdAt);
}
class DateTimeConverter extends TypeConverter<DateTime, int> {
@override
DateTime decode(int databaseValue) {
return DateTime.fromMillisecondsSinceEpoch(databaseValue);
}
@override
int encode(DateTime value) {
return value.millisecondsSinceEpoch;
}
}
class RemoteEventConverter extends TypeConverter<Event, String> {
@override
Event decode(String databaseValue) {
return Event.fromJson(jsonDecode(databaseValue));
}
@override
String encode(Event value) {
return jsonEncode(value.toJson());
}
}
@dao
abstract class LocalEventDao {
@Query('SELECT COUNT(id) FROM LocalEvent WHERE channelId = :channelId')
Future<int?> countByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE id = :id')
Future<LocalEvent?> findById(int id);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC')
Future<List<LocalEvent>> findAllByChannel(int channelId);
@Query('SELECT * FROM LocalEvent WHERE channelId = :channelId ORDER BY createdAt DESC LIMIT 1')
Future<LocalEvent?> findLastByChannel(int channelId);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insert(LocalEvent m);
@Insert(onConflict: OnConflictStrategy.replace)
Future<void> insertBulk(List<LocalEvent> m);
@Update(onConflict: OnConflictStrategy.replace)
Future<void> update(LocalEvent m);
@Query('DELETE FROM LocalEvent WHERE id = :id')
Future<void> delete(int id);
@Query('DELETE FROM LocalEvent WHERE channelId = :channelId')
Future<List<LocalEvent>> deleteByChannel(int channelId);
@Query('DELETE FROM LocalEvent')
Future<void> wipeLocalEvents();
}
@TypeConverters([DateTimeConverter, RemoteEventConverter])
@Database(version: 2, entities: [LocalEvent])
abstract class MessageHistoryDb extends FloorDatabase {
LocalEventDao get localEvents;
}

View File

@ -1,228 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'events.dart';
// **************************************************************************
// FloorGenerator
// **************************************************************************
abstract class $MessageHistoryDbBuilderContract {
/// Adds migrations to the builder.
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations);
/// Adds a database [Callback] to the builder.
$MessageHistoryDbBuilderContract addCallback(Callback callback);
/// Creates the database and initializes it.
Future<MessageHistoryDb> build();
}
// ignore: avoid_classes_with_only_static_members
class $FloorMessageHistoryDb {
/// Creates a database builder for a persistent database.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract databaseBuilder(String name) =>
_$MessageHistoryDbBuilder(name);
/// Creates a database builder for an in memory database.
/// Information stored in an in memory database disappears when the process is killed.
/// Once a database is built, you should keep a reference to it and re-use it.
static $MessageHistoryDbBuilderContract inMemoryDatabaseBuilder() =>
_$MessageHistoryDbBuilder(null);
}
class _$MessageHistoryDbBuilder implements $MessageHistoryDbBuilderContract {
_$MessageHistoryDbBuilder(this.name);
final String? name;
final List<Migration> _migrations = [];
Callback? _callback;
@override
$MessageHistoryDbBuilderContract addMigrations(List<Migration> migrations) {
_migrations.addAll(migrations);
return this;
}
@override
$MessageHistoryDbBuilderContract addCallback(Callback callback) {
_callback = callback;
return this;
}
@override
Future<MessageHistoryDb> build() async {
final path = name != null
? await sqfliteDatabaseFactory.getDatabasePath(name!)
: ':memory:';
final database = _$MessageHistoryDb();
database.database = await database.open(
path,
_migrations,
_callback,
);
return database;
}
}
class _$MessageHistoryDb extends MessageHistoryDb {
_$MessageHistoryDb([StreamController<String>? listener]) {
changeListener = listener ?? StreamController<String>.broadcast();
}
LocalEventDao? _localEventsInstance;
Future<sqflite.Database> open(
String path,
List<Migration> migrations, [
Callback? callback,
]) async {
final databaseOptions = sqflite.OpenDatabaseOptions(
version: 2,
onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
await callback?.onConfigure?.call(database);
},
onOpen: (database) async {
await callback?.onOpen?.call(database);
},
onUpgrade: (database, startVersion, endVersion) async {
await MigrationAdapter.runMigrations(
database, startVersion, endVersion, migrations);
await callback?.onUpgrade?.call(database, startVersion, endVersion);
},
onCreate: (database, version) async {
await database.execute(
'CREATE TABLE IF NOT EXISTS `LocalEvent` (`id` INTEGER NOT NULL, `data` TEXT NOT NULL, `channelId` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, PRIMARY KEY (`id`))');
await callback?.onCreate?.call(database, version);
},
);
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
}
@override
LocalEventDao get localEvents {
return _localEventsInstance ??= _$LocalEventDao(database, changeListener);
}
}
class _$LocalEventDao extends LocalEventDao {
_$LocalEventDao(
this.database,
this.changeListener,
) : _queryAdapter = QueryAdapter(database),
_localEventInsertionAdapter = InsertionAdapter(
database,
'LocalEvent',
(LocalEvent item) => <String, Object?>{
'id': item.id,
'data': _remoteEventConverter.encode(item.data),
'channelId': item.channelId,
'createdAt': _dateTimeConverter.encode(item.createdAt)
}),
_localEventUpdateAdapter = UpdateAdapter(
database,
'LocalEvent',
['id'],
(LocalEvent item) => <String, Object?>{
'id': item.id,
'data': _remoteEventConverter.encode(item.data),
'channelId': item.channelId,
'createdAt': _dateTimeConverter.encode(item.createdAt)
});
final sqflite.DatabaseExecutor database;
final StreamController<String> changeListener;
final QueryAdapter _queryAdapter;
final InsertionAdapter<LocalEvent> _localEventInsertionAdapter;
final UpdateAdapter<LocalEvent> _localEventUpdateAdapter;
@override
Future<int?> countByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT COUNT(id) FROM LocalEvent WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => row.values.first as int,
arguments: [channelId]);
}
@override
Future<LocalEvent?> findById(int id) async {
return _queryAdapter.query('SELECT * FROM LocalEvent WHERE id = ?1',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [id]);
}
@override
Future<List<LocalEvent>> findAllByChannel(int channelId) async {
return _queryAdapter.queryList(
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<LocalEvent?> findLastByChannel(int channelId) async {
return _queryAdapter.query(
'SELECT * FROM LocalEvent WHERE channelId = ?1 ORDER BY createdAt DESC LIMIT 1',
mapper: (Map<String, Object?> row) => LocalEvent(row['id'] as int, _remoteEventConverter.decode(row['data'] as String), row['channelId'] as int, _dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<void> delete(int id) async {
await _queryAdapter
.queryNoReturn('DELETE FROM LocalEvent WHERE id = ?1', arguments: [id]);
}
@override
Future<List<LocalEvent>> deleteByChannel(int channelId) async {
return _queryAdapter.queryList(
'DELETE FROM LocalEvent WHERE channelId = ?1',
mapper: (Map<String, Object?> row) => LocalEvent(
row['id'] as int,
_remoteEventConverter.decode(row['data'] as String),
row['channelId'] as int,
_dateTimeConverter.decode(row['createdAt'] as int)),
arguments: [channelId]);
}
@override
Future<void> wipeLocalEvents() async {
await _queryAdapter.queryNoReturn('DELETE FROM LocalEvent');
}
@override
Future<void> insert(LocalEvent m) async {
await _localEventInsertionAdapter.insert(m, OnConflictStrategy.replace);
}
@override
Future<void> insertBulk(List<LocalEvent> m) async {
await _localEventInsertionAdapter.insertList(m, OnConflictStrategy.replace);
}
@override
Future<void> update(LocalEvent m) async {
await _localEventUpdateAdapter.update(m, OnConflictStrategy.replace);
}
}
// ignore_for_file: unused_element
final _dateTimeConverter = DateTimeConverter();
final _remoteEventConverter = RemoteEventConverter();