improve sync logic
This commit is contained in:
@@ -46,6 +46,7 @@ class SyncTabsResult {
|
|||||||
@DriftAccessor()
|
@DriftAccessor()
|
||||||
class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
||||||
final _undoHistory = <String, TabData>{};
|
final _undoHistory = <String, TabData>{};
|
||||||
|
final _pendingParentIds = <String, String>{};
|
||||||
Timer? _clearHistoryTimer;
|
Timer? _clearHistoryTimer;
|
||||||
|
|
||||||
static const closedTabTombstoneTtl = Duration(hours: 24);
|
static const closedTabTombstoneTtl = Duration(hours: 24);
|
||||||
@@ -167,6 +168,54 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _resolvePendingParents() async {
|
||||||
|
if (_pendingParentIds.isEmpty) return;
|
||||||
|
|
||||||
|
final pendingChildren = selectOnly(db.tab)
|
||||||
|
..addColumns([db.tab.id])
|
||||||
|
..where(
|
||||||
|
db.tab.id.isIn(_pendingParentIds.keys) &
|
||||||
|
db.tab.parentId.isNull() &
|
||||||
|
db.tab.source.isNotValue(TabSource.manual.index),
|
||||||
|
);
|
||||||
|
final pendingChildIds = (await pendingChildren.get())
|
||||||
|
.map((row) => row.read(db.tab.id)!)
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
if (pendingChildIds.isEmpty) {
|
||||||
|
_pendingParentIds.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvableParents = await getExistingTabIds(
|
||||||
|
pendingChildIds.map((childId) => _pendingParentIds[childId]!).toSet(),
|
||||||
|
).get().then((ids) => ids.toSet());
|
||||||
|
|
||||||
|
await batch((batch) {
|
||||||
|
for (final childId in pendingChildIds) {
|
||||||
|
final parentId = _pendingParentIds[childId];
|
||||||
|
if (parentId == null || !resolvableParents.contains(parentId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
batch.update(
|
||||||
|
db.tab,
|
||||||
|
TabCompanion(
|
||||||
|
parentId: Value(parentId),
|
||||||
|
source: const Value(TabSource.manual),
|
||||||
|
),
|
||||||
|
where: (t) => t.id.equals(childId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
_pendingParentIds.removeWhere(
|
||||||
|
(childId, parentId) =>
|
||||||
|
!pendingChildIds.contains(childId) ||
|
||||||
|
resolvableParents.contains(parentId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> _generateOrderKey({
|
Future<String> _generateOrderKey({
|
||||||
required Value<String?> parentId,
|
required Value<String?> parentId,
|
||||||
required Value<String?> containerId,
|
required Value<String?> containerId,
|
||||||
@@ -1081,35 +1130,33 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
// change from Gecko.
|
// change from Gecko.
|
||||||
final parentSyncEligibleIds = next.isEmpty
|
final parentSyncEligibleIds = next.isEmpty
|
||||||
? const <String>{}
|
? const <String>{}
|
||||||
: {
|
: await (() async {
|
||||||
for (final tab in await (select(
|
final query = selectOnly(db.tab)
|
||||||
db.tab,
|
..addColumns([db.tab.id, db.tab.source])
|
||||||
)..where((t) => t.id.isIn(next.keys))).get())
|
..where(db.tab.id.isIn(next.keys) & db.tab.parentId.isNull());
|
||||||
if (tab.source != TabSource.manual && tab.parentId == null)
|
return {
|
||||||
tab.id,
|
for (final row in await query.get())
|
||||||
};
|
if (row.readWithConverter(db.tab.source) != TabSource.manual)
|
||||||
|
row.read(db.tab.id)!,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
// Collect parent IDs that need database validation
|
// Validate every candidate parent against the database — a parentId
|
||||||
final parentIdsToValidate = <String>{};
|
// present in `next` is not proof the row has been persisted yet (e.g.
|
||||||
final validatedParentIds = <String, String?>{};
|
// engine state snapshot arriving before the matching tab-list insert).
|
||||||
|
// Without the DB check we could write a parent_id pointing at a row
|
||||||
for (final state in next.values) {
|
// that does not exist, triggering the self-referential FK violation
|
||||||
if (parentSyncEligibleIds.contains(state.id) &&
|
// and aborting the whole batch.
|
||||||
state.parentId != null) {
|
final parentIdsToValidate = <String>{
|
||||||
if (next.containsKey(state.parentId)) {
|
for (final state in next.values)
|
||||||
// Parent exists in current state
|
if (parentSyncEligibleIds.contains(state.id) &&
|
||||||
validatedParentIds[state.id] = state.parentId;
|
state.parentId != null)
|
||||||
} else {
|
state.parentId!,
|
||||||
// Need to validate against database
|
};
|
||||||
parentIdsToValidate.add(state.parentId!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch validate parent IDs that aren't in the current state
|
|
||||||
final existingParentIds = await getExistingTabIds(
|
final existingParentIds = await getExistingTabIds(
|
||||||
parentIdsToValidate,
|
parentIdsToValidate,
|
||||||
).get().then((ids) => ids.toSet());
|
).get().then((ids) => ids.toSet());
|
||||||
|
final validatedParentIds = <String, String?>{};
|
||||||
|
|
||||||
final containerRepairCandidates = {
|
final containerRepairCandidates = {
|
||||||
for (final state in next.values)
|
for (final state in next.values)
|
||||||
@@ -1137,17 +1184,22 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
entry.key: containerId,
|
entry.key: containerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Complete validation map
|
|
||||||
for (final state in next.values) {
|
for (final state in next.values) {
|
||||||
if (!parentSyncEligibleIds.contains(state.id) ||
|
if (!parentSyncEligibleIds.contains(state.id)) {
|
||||||
validatedParentIds.containsKey(state.id) ||
|
_pendingParentIds.remove(state.id);
|
||||||
state.parentId == null) {
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.parentId == null) {
|
||||||
|
_pendingParentIds.remove(state.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This parent ID needed database validation
|
|
||||||
if (existingParentIds.contains(state.parentId)) {
|
if (existingParentIds.contains(state.parentId)) {
|
||||||
validatedParentIds[state.id] = state.parentId;
|
validatedParentIds[state.id] = state.parentId;
|
||||||
|
_pendingParentIds.remove(state.id);
|
||||||
|
} else {
|
||||||
|
_pendingParentIds[state.id] = state.parentId!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1220,6 +1272,13 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final retainedTabIds = retainTabIds.toSet();
|
||||||
|
_pendingParentIds.removeWhere(
|
||||||
|
(childId, parentId) =>
|
||||||
|
!retainedTabIds.contains(childId) ||
|
||||||
|
!retainedTabIds.contains(parentId),
|
||||||
|
);
|
||||||
|
|
||||||
var currentOrderKey = await db.containerDao
|
var currentOrderKey = await db.containerDao
|
||||||
.generateLeadingOrderKey(null)
|
.generateLeadingOrderKey(null)
|
||||||
.getSingle();
|
.getSingle();
|
||||||
@@ -1242,6 +1301,8 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
onConflict: DoNothing(),
|
onConflict: DoNothing(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await _resolvePendingParents();
|
||||||
|
|
||||||
return SyncTabsResult(
|
return SyncTabsResult(
|
||||||
deletedIsolationContextIds: deletedIsolationContextIds,
|
deletedIsolationContextIds: deletedIsolationContextIds,
|
||||||
deletedCount: deleted.length,
|
deletedCount: deleted.length,
|
||||||
|
|||||||
@@ -150,6 +150,35 @@ void main() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
'tab-list sync resolves an unresolved engine parent after inserting the parent row',
|
||||||
|
() async {
|
||||||
|
await _insertTabs(db, const [
|
||||||
|
_TabFixture('child', source: TabSource.addedEvent),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await db.tabDao.updateTabs(null, {
|
||||||
|
'child': _tabState('child', parentId: 'late-parent'),
|
||||||
|
});
|
||||||
|
|
||||||
|
final unresolvedChild = await db.tabDao
|
||||||
|
.getTabDataById('child')
|
||||||
|
.getSingleOrNull();
|
||||||
|
expect(unresolvedChild, isNotNull);
|
||||||
|
expect(unresolvedChild!.parentId, isNull);
|
||||||
|
expect(unresolvedChild.source, TabSource.addedEvent);
|
||||||
|
|
||||||
|
await db.tabDao.syncTabs(retainTabIds: const ['late-parent', 'child']);
|
||||||
|
|
||||||
|
final resolvedChild = await db.tabDao
|
||||||
|
.getTabDataById('child')
|
||||||
|
.getSingleOrNull();
|
||||||
|
expect(resolvedChild, isNotNull);
|
||||||
|
expect(resolvedChild!.parentId, 'late-parent');
|
||||||
|
expect(resolvedChild.source, TabSource.manual);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
test(
|
test(
|
||||||
'content-state sync ignores parent-only changes with unresolved parents',
|
'content-state sync ignores parent-only changes with unresolved parents',
|
||||||
() async {
|
() async {
|
||||||
|
|||||||
Reference in New Issue
Block a user