make tab restore more resilient

This commit is contained in:
Fabian Freund
2026-05-02 05:58:38 +02:00
parent 69f21bb7d8
commit 063decd451
11 changed files with 2861 additions and 29 deletions
@@ -52,6 +52,10 @@ part 'tab.g.dart';
@Riverpod(keepAlive: true)
class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService();
final _sessionStartedAt = DateTime.now();
bool _didPruneTombstones = false;
bool _reclosing = false;
bool _suppressNextReclose = false;
final _tabFromIntent = <String>{};
final _closeLock = Lock();
@@ -550,38 +554,28 @@ class TabRepository extends _$TabRepository {
}
}
Future<void> closeTab(String tabId) {
Future<void> _closeTabsInternal(
List<String> tabIds, {
required bool recordTombstones,
}) {
return _closeLock.synchronized(() async {
// Collect isolation context before close
final isolationContextId = ref
.read(tabStatesProvider)[tabId]
?.isolationContextId;
if (ref.read(selectedTabProvider) == tabId) {
await _selectNextTab(tabId);
if (tabIds.isEmpty) {
return;
}
await _preservePromotedChildOrderOnClose([tabId]);
await _tabsService.removeTab(tabId: tabId);
// Queue isolation cleanup — actual cleanup runs after syncTabs
// deletes the DB row, so the count check is accurate.
if (isolationContextId != null) {
_pendingIsolationCleanup.add(isolationContextId);
if (recordTombstones) {
await ref
.read(tabDatabaseProvider)
.tabDao
.addClosedTabTombstones(tabIds);
}
});
}
Future<void> closeTabs(List<String> tabIds) {
return _closeLock.synchronized(() async {
// Collect isolation contexts from tabs being closed
for (final tabId in tabIds) {
final contextId = ref
final isolationContextId = ref
.read(tabStatesProvider)[tabId]
?.isolationContextId;
if (contextId != null) {
_pendingIsolationCleanup.add(contextId);
if (isolationContextId != null) {
_pendingIsolationCleanup.add(isolationContextId);
}
}
@@ -592,10 +586,29 @@ class TabRepository extends _$TabRepository {
await _preservePromotedChildOrderOnClose(tabIds);
await _tabsService.removeTabs(ids: tabIds);
if (tabIds.length == 1) {
await _tabsService.removeTab(tabId: tabIds.single);
} else {
await _tabsService.removeTabs(ids: tabIds);
}
});
}
Future<void> closeTab(String tabId) {
return _closeTabsInternal([tabId], recordTombstones: true);
}
Future<void> closeTabs(List<String> tabIds) {
return _closeTabsInternal(tabIds, recordTombstones: true);
}
Future<void> _clearTombstonesForCurrentTabs(List<String> tabIds) {
return ref
.read(tabDatabaseProvider)
.tabDao
.deleteClosedTabTombstones(tabIds);
}
Future<void> _preservePromotedChildOrderOnClose(List<String> tabIds) {
return ref
.read(tabDatabaseProvider)
@@ -662,9 +675,49 @@ class TabRepository extends _$TabRepository {
}
Future<void> undoClose() {
// Suppress the next reclose pass: undo can resurrect a tab whose
// tombstone is still on disk (from a previous session); without this
// flag the listener would immediately re-close it.
_suppressNextReclose = true;
return _tabsService.undo();
}
Future<bool> _recloseRestoredClosedTabs(List<String> tabIds) async {
if (_reclosing) {
return false;
}
_reclosing = true;
try {
final tabDao = ref.read(tabDatabaseProvider).tabDao;
if (!_didPruneTombstones) {
await tabDao.pruneExpiredClosedTabTombstones();
_didPruneTombstones = true;
}
final restoredClosedTabIds = await tabDao.getStartupRestoredClosedTabIds(
tabIds,
sessionStartedAt: _sessionStartedAt,
);
if (restoredClosedTabIds.isEmpty) {
return false;
}
// recordTombstones: false — the tombstone already exists from the
// original close; rewriting it would bump closed_at into this session
// and disable the resurrection check on the next emission.
await _closeTabsInternal(
restoredClosedTabIds.toList(growable: false),
recordTombstones: false,
);
return true;
} finally {
_reclosing = false;
}
}
/// Cleans up isolation contexts from previous crashed sessions.
/// Called once after tab list stabilizes on startup.
// Future<void> _cleanupOrphanedIsolationContexts() async {
@@ -845,6 +898,15 @@ class TabRepository extends _$TabRepository {
ref.listen(
tabListProvider,
(previous, next) async {
if (_suppressNextReclose) {
_suppressNextReclose = false;
// Drop tombstones for the tabs that just came back via undo so
// future emissions don't treat them as resurrections.
await _clearTombstonesForCurrentTabs(next.value);
} else if (await _recloseRestoredClosedTabs(next.value)) {
return;
}
//Only sync tabs if there has been a previous value or is not empty
final shouldSyncTabs =
next.value.isNotEmpty || (previous?.value.isNotEmpty ?? false);
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
}
}
String _$tabRepositoryHash() => r'4c5bda4d0ddcc66cfa420d3c2ca97db42b73b878';
String _$tabRepositoryHash() => r'84ecfed72f17c367fc7bf0100f6367d4e7609596';
abstract class _$TabRepository extends $Notifier<void> {
void build();
@@ -47,6 +47,8 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
final _undoHistory = <String, TabData>{};
Timer? _clearHistoryTimer;
static const closedTabTombstoneTtl = Duration(hours: 24);
TabDao(super.db);
UpdateStatement<Tab, TabData> _updateByIdStatement(String id) =>
@@ -317,6 +319,103 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
return statement.write(TabCompanion(containerId: Value(containerId)));
}
Future<void> addClosedTabTombstones(
Iterable<String> tabIds, {
DateTime? closedAt,
}) async {
final uniqueIds = tabIds.toSet();
if (uniqueIds.isEmpty) {
return;
}
final effectiveClosedAt = closedAt ?? DateTime.now();
await batch((batch) {
for (final tabId in uniqueIds) {
batch.insert(
db.closedTabTombstone,
ClosedTabTombstoneCompanion.insert(
tabId: tabId,
closedAt: effectiveClosedAt,
),
onConflict: DoUpdate(
(_) =>
ClosedTabTombstoneCompanion(closedAt: Value(effectiveClosedAt)),
),
);
}
});
}
Future<void> deleteClosedTabTombstones(Iterable<String> tabIds) {
final uniqueIds = tabIds.toSet();
if (uniqueIds.isEmpty) {
return Future.value();
}
return (db.closedTabTombstone.delete()
..where((t) => t.tabId.isIn(uniqueIds)))
.go();
}
Future<void> pruneExpiredClosedTabTombstones({
Duration ttl = closedTabTombstoneTtl,
DateTime? now,
}) {
final cutoff = (now ?? DateTime.now()).subtract(ttl);
return (db.closedTabTombstone.delete()
..where((t) => t.closedAt.isSmallerOrEqualValue(cutoff)))
.go();
}
Future<Set<String>> getStartupRestoredClosedTabIds(
Iterable<String> tabIds, {
required DateTime sessionStartedAt,
Duration ttl = closedTabTombstoneTtl,
DateTime? now,
}) async {
final uniqueIds = tabIds.toSet();
if (uniqueIds.isEmpty) {
return const <String>{};
}
final freshnessCutoff = (now ?? DateTime.now()).subtract(ttl);
final query = selectOnly(db.closedTabTombstone)
..addColumns([db.closedTabTombstone.tabId])
..where(
db.closedTabTombstone.tabId.isIn(uniqueIds) &
db.closedTabTombstone.closedAt.isSmallerThanValue(
sessionStartedAt,
) &
db.closedTabTombstone.closedAt.isBiggerOrEqualValue(
freshnessCutoff,
),
);
return query
.map((row) => row.read(db.closedTabTombstone.tabId)!)
.get()
.then((rows) => rows.toSet());
}
Future<Map<String, String>> _containerIdsByContextualIdentity(
Iterable<String> contextIds,
) async {
final uniqueContextIds = contextIds.toSet();
if (uniqueContextIds.isEmpty) {
return const <String, String>{};
}
final rows = await db.definitionsDrift
.containerIdsByContextualIdentities(
contextIds: uniqueContextIds.toList(growable: false),
)
.get();
return {for (final row in rows) row.contextualIdentity: row.id};
}
Future<void> reorderTabs({
required List<String> movingTabIds,
required String? previousTabId,
@@ -633,6 +732,32 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
parentIdsToValidate,
).get().then((ids) => ids.toSet());
final containerRepairCandidates = {
for (final state in next.values)
if (state.contextId != null &&
(previous?[state.id]?.contextId != state.contextId ||
previous?[state.id] == null))
state.id: state.contextId!,
};
final currentContainerIds = containerRepairCandidates.isEmpty
? const <String, String?>{}
: await getTabsContainerId(
containerRepairCandidates.keys,
).get().then(Map.fromEntries);
final repairableContexts = containerRepairCandidates.entries
.where((entry) => currentContainerIds[entry.key] == null)
.map((entry) => entry.value)
.toSet();
final containerIdsByContext = await _containerIdsByContextualIdentity(
repairableContexts,
);
final repairedContainerIds = <String, String>{
for (final entry in containerRepairCandidates.entries)
if (currentContainerIds[entry.key] == null)
if (containerIdsByContext[entry.value] case final containerId?)
entry.key: containerId,
};
// Complete validation map
for (final state in next.values) {
final previousState = previous?[state.id];
@@ -658,13 +783,17 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
previousState.url != state.url ||
previousState.title != state.title ||
previousState.parentId != state.parentId ||
previousState.tabMode != state.tabMode) {
previousState.tabMode != state.tabMode ||
repairedContainerIds.containsKey(state.id)) {
batch.update(
db.tab,
TabCompanion(
parentId: validatedParentIds.containsKey(state.id)
? Value(validatedParentIds[state.id])
: const Value.absent(),
containerId:
repairedContainerIds[state.id].mapNotNull(Value.new) ??
const Value.absent(),
url: (previousState?.url != state.url)
? Value(state.url)
: const Value.absent(),
@@ -31,7 +31,7 @@ import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
@DriftDatabase(include: {'definitions.drift'}, daos: [ContainerDao, TabDao])
class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
@override
final int schemaVersion = 8;
final int schemaVersion = 9;
@override
final int ftsTokenLimit = 10;
@@ -137,5 +137,8 @@ class TabDatabase extends $TabDatabase with TrigramQueryBuilderMixin {
await m.createIndex(schema.idxTabParentContainer);
},
from8To9: (m, schema) async {
await m.create(schema.closedTabTombstone);
},
);
}
@@ -17,6 +17,9 @@ abstract class $TabDatabase extends i0.GeneratedDatabase {
$TabDatabaseManager get managers => $TabDatabaseManager(this);
late final i1.Container container = i1.Container(this);
late final i1.Tab tab = i1.Tab(this);
late final i1.ClosedTabTombstone closedTabTombstone = i1.ClosedTabTombstone(
this,
);
late final i1.TabFts tabFts = i1.TabFts(this);
late final i2.ContainerDao containerDao = i2.ContainerDao(
this as i3.TabDatabase,
@@ -32,6 +35,7 @@ abstract class $TabDatabase extends i0.GeneratedDatabase {
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
container,
tab,
closedTabTombstone,
i1.idxTabParentContainer,
tabFts,
i1.tabMaintainParentChainOnDelete,
@@ -86,6 +90,8 @@ class $TabDatabaseManager {
i1.$ContainerTableManager get container =>
i1.$ContainerTableManager(_db, _db.container);
i1.$TabTableManager get tab => i1.$TabTableManager(_db, _db.tab);
i1.$ClosedTabTombstoneTableManager get closedTabTombstone =>
i1.$ClosedTabTombstoneTableManager(_db, _db.closedTabTombstone);
i1.$TabFtsTableManager get tabFts => i1.$TabFtsTableManager(_db, _db.tabFts);
}
@@ -809,6 +809,128 @@ final class Schema8 extends i0.VersionedSchema {
);
}
final class Schema9 extends i0.VersionedSchema {
Schema9({required super.database}) : super(version: 9);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
container,
tab,
closedTabTombstone,
idxTabParentContainer,
tabFts,
tabMaintainParentChainOnDelete,
tabAfterInsert,
tabAfterDelete,
tabAfterUpdate,
];
late final Shape0 container = Shape0(
source: i0.VersionedTable(
entityName: 'container',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_0, _column_1, _column_2, _column_3],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 tab = Shape5(
source: i0.VersionedTable(
entityName: 'tab',
withoutRowId: false,
isStrict: false,
tableConstraints: [
'CHECK((tab_mode = 2 AND isolation_context_id IS NOT NULL)OR(tab_mode != 2 AND isolation_context_id IS NULL))',
],
columns: [
_column_0,
_column_16,
_column_4,
_column_5,
_column_6,
_column_7,
_column_8,
_column_17,
_column_18,
_column_19,
_column_10,
_column_11,
_column_12,
_column_13,
_column_14,
_column_15,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape6 closedTabTombstone = Shape6(
source: i0.VersionedTable(
entityName: 'closed_tab_tombstone',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [_column_20, _column_21],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxTabParentContainer = i1.Index(
'idx_tab_parent_container',
'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)',
);
late final Shape2 tabFts = Shape2(
source: i0.VersionedVirtualTable(
entityName: 'tab_fts',
moduleAndArgs:
'fts5(title, url, extracted_content_plain, full_content_plain, content=tab, tokenize="trigram")',
columns: [_column_8, _column_7, _column_12, _column_14],
attachedDatabase: database,
),
alias: null,
);
final i1.Trigger tabMaintainParentChainOnDelete = i1.Trigger(
'CREATE TRIGGER tab_maintain_parent_chain_on_delete BEFORE DELETE ON tab BEGIN UPDATE tab SET parent_id = CASE WHEN OLD.parent_id IS NOT NULL AND EXISTS (SELECT 1 FROM tab WHERE id = OLD.parent_id) THEN OLD.parent_id ELSE NULL END WHERE parent_id = OLD.id;END',
'tab_maintain_parent_chain_on_delete',
);
final i1.Trigger tabAfterInsert = i1.Trigger(
'CREATE TRIGGER tab_after_insert AFTER INSERT ON tab BEGIN INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END',
'tab_after_insert',
);
final i1.Trigger tabAfterDelete = i1.Trigger(
'CREATE TRIGGER tab_after_delete AFTER DELETE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);END',
'tab_after_delete',
);
final i1.Trigger tabAfterUpdate = i1.Trigger(
'CREATE TRIGGER tab_after_update AFTER UPDATE ON tab BEGIN INSERT INTO tab_fts (tab_fts, "rowid", title, url, extracted_content_plain, full_content_plain) VALUES (\'delete\', old."rowid", old.title, old.url, old.extracted_content_plain, old.full_content_plain);INSERT INTO tab_fts ("rowid", title, url, extracted_content_plain, full_content_plain) VALUES (new."rowid", new.title, new.url, new.extracted_content_plain, new.full_content_plain);END',
'tab_after_update',
);
}
class Shape6 extends i0.VersionedTable {
Shape6({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get tabId =>
columnsByName['tab_id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get closedAt =>
columnsByName['closed_at']! as i1.GeneratedColumn<int>;
}
i1.GeneratedColumn<String> _column_20(String aliasedName) =>
i1.GeneratedColumn<String>(
'tab_id',
aliasedName,
false,
type: i1.DriftSqlType.string,
$customConstraints: 'PRIMARY KEY NOT NULL',
);
i1.GeneratedColumn<int> _column_21(String aliasedName) =>
i1.GeneratedColumn<int>(
'closed_at',
aliasedName,
false,
type: i1.DriftSqlType.int,
$customConstraints: 'NOT NULL',
);
i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
required Future<void> Function(i1.Migrator m, Schema4 schema) from3To4,
@@ -816,6 +938,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -849,6 +972,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from7To8(migrator, schema);
return 8;
case 8:
final schema = Schema9(database: database);
final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema);
return 9;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -862,6 +990,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema6 schema) from5To6,
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from2To3: from2To3,
@@ -870,5 +999,6 @@ i1.OnUpgrade stepByStep({
from5To6: from5To6,
from6To7: from6To7,
from7To8: from7To8,
from8To9: from8To9,
),
);
@@ -37,6 +37,11 @@ CREATE TABLE tab(
)
);
CREATE TABLE closed_tab_tombstone(
tab_id TEXT PRIMARY KEY NOT NULL,
closed_at DATETIME NOT NULL
);
-- Composite index used by `tabsWithRootAndDepth` (the seed checks
-- `parent_id IS NULL OR NOT EXISTS(... WHERE p.id = parent_id AND
-- p.container_id IS :container_id)`) and by `lastChildTabId`
@@ -390,6 +395,13 @@ containerByContextualIdentity:
WHERE container.metadata ->> '$.contextualIdentity' = :contextId
LIMIT 1;
containerIdsByContextualIdentities:
SELECT
id,
CAST(container.metadata ->> '$.contextualIdentity' AS TEXT) AS contextual_identity
FROM container
WHERE CAST(container.metadata ->> '$.contextualIdentity' AS TEXT) IN :contextIds;
containersToClearOnExit:
SELECT container.metadata ->> '$.contextualIdentity' AS contextual_identity
FROM container
@@ -882,6 +882,159 @@ typedef $TabProcessedTableManager =
i3.TabData,
i0.PrefetchHooks Function({bool containerId})
>;
typedef $ClosedTabTombstoneCreateCompanionBuilder =
i3.ClosedTabTombstoneCompanion Function({
required String tabId,
required DateTime closedAt,
i0.Value<int> rowid,
});
typedef $ClosedTabTombstoneUpdateCompanionBuilder =
i3.ClosedTabTombstoneCompanion Function({
i0.Value<String> tabId,
i0.Value<DateTime> closedAt,
i0.Value<int> rowid,
});
class $ClosedTabTombstoneFilterComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnFilters<String> get tabId => $composableBuilder(
column: $table.tabId,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<DateTime> get closedAt => $composableBuilder(
column: $table.closedAt,
builder: (column) => i0.ColumnFilters(column),
);
}
class $ClosedTabTombstoneOrderingComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.ColumnOrderings<String> get tabId => $composableBuilder(
column: $table.tabId,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<DateTime> get closedAt => $composableBuilder(
column: $table.closedAt,
builder: (column) => i0.ColumnOrderings(column),
);
}
class $ClosedTabTombstoneAnnotationComposer
extends i0.Composer<i0.GeneratedDatabase, i3.ClosedTabTombstone> {
$ClosedTabTombstoneAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
i0.GeneratedColumn<String> get tabId =>
$composableBuilder(column: $table.tabId, builder: (column) => column);
i0.GeneratedColumn<DateTime> get closedAt =>
$composableBuilder(column: $table.closedAt, builder: (column) => column);
}
class $ClosedTabTombstoneTableManager
extends
i0.RootTableManager<
i0.GeneratedDatabase,
i3.ClosedTabTombstone,
i3.ClosedTabTombstoneData,
i3.$ClosedTabTombstoneFilterComposer,
i3.$ClosedTabTombstoneOrderingComposer,
i3.$ClosedTabTombstoneAnnotationComposer,
$ClosedTabTombstoneCreateCompanionBuilder,
$ClosedTabTombstoneUpdateCompanionBuilder,
(
i3.ClosedTabTombstoneData,
i0.BaseReferences<
i0.GeneratedDatabase,
i3.ClosedTabTombstone,
i3.ClosedTabTombstoneData
>,
),
i3.ClosedTabTombstoneData,
i0.PrefetchHooks Function()
> {
$ClosedTabTombstoneTableManager(
i0.GeneratedDatabase db,
i3.ClosedTabTombstone table,
) : super(
i0.TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
i3.$ClosedTabTombstoneFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
i3.$ClosedTabTombstoneOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
i3.$ClosedTabTombstoneAnnotationComposer($db: db, $table: table),
updateCompanionCallback:
({
i0.Value<String> tabId = const i0.Value.absent(),
i0.Value<DateTime> closedAt = const i0.Value.absent(),
i0.Value<int> rowid = const i0.Value.absent(),
}) => i3.ClosedTabTombstoneCompanion(
tabId: tabId,
closedAt: closedAt,
rowid: rowid,
),
createCompanionCallback:
({
required String tabId,
required DateTime closedAt,
i0.Value<int> rowid = const i0.Value.absent(),
}) => i3.ClosedTabTombstoneCompanion.insert(
tabId: tabId,
closedAt: closedAt,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
.toList(),
prefetchHooksCallback: null,
),
);
}
typedef $ClosedTabTombstoneProcessedTableManager =
i0.ProcessedTableManager<
i0.GeneratedDatabase,
i3.ClosedTabTombstone,
i3.ClosedTabTombstoneData,
i3.$ClosedTabTombstoneFilterComposer,
i3.$ClosedTabTombstoneOrderingComposer,
i3.$ClosedTabTombstoneAnnotationComposer,
$ClosedTabTombstoneCreateCompanionBuilder,
$ClosedTabTombstoneUpdateCompanionBuilder,
(
i3.ClosedTabTombstoneData,
i0.BaseReferences<
i0.GeneratedDatabase,
i3.ClosedTabTombstone,
i3.ClosedTabTombstoneData
>,
),
i3.ClosedTabTombstoneData,
i0.PrefetchHooks Function()
>;
typedef $TabFtsCreateCompanionBuilder =
i3.TabFtsCompanion Function({
required String title,
@@ -2060,6 +2213,196 @@ class TabCompanion extends i0.UpdateCompanion<i3.TabData> {
}
}
class ClosedTabTombstone extends i0.Table
with i0.TableInfo<ClosedTabTombstone, i3.ClosedTabTombstoneData> {
@override
final i0.GeneratedDatabase attachedDatabase;
final String? _alias;
ClosedTabTombstone(this.attachedDatabase, [this._alias]);
late final i0.GeneratedColumn<String> tabId = i0.GeneratedColumn<String>(
'tab_id',
aliasedName,
false,
type: i0.DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL',
);
late final i0.GeneratedColumn<DateTime> closedAt =
i0.GeneratedColumn<DateTime>(
'closed_at',
aliasedName,
false,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL',
);
@override
List<i0.GeneratedColumn> get $columns => [tabId, closedAt];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'closed_tab_tombstone';
@override
Set<i0.GeneratedColumn> get $primaryKey => {tabId};
@override
i3.ClosedTabTombstoneData map(
Map<String, dynamic> data, {
String? tablePrefix,
}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return i3.ClosedTabTombstoneData(
tabId: attachedDatabase.typeMapping.read(
i0.DriftSqlType.string,
data['${effectivePrefix}tab_id'],
)!,
closedAt: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}closed_at'],
)!,
);
}
@override
ClosedTabTombstone createAlias(String alias) {
return ClosedTabTombstone(attachedDatabase, alias);
}
@override
bool get dontWriteConstraints => true;
}
class ClosedTabTombstoneData extends i0.DataClass
implements i0.Insertable<i3.ClosedTabTombstoneData> {
final String tabId;
final DateTime closedAt;
const ClosedTabTombstoneData({required this.tabId, required this.closedAt});
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
map['tab_id'] = i0.Variable<String>(tabId);
map['closed_at'] = i0.Variable<DateTime>(closedAt);
return map;
}
factory ClosedTabTombstoneData.fromJson(
Map<String, dynamic> json, {
i0.ValueSerializer? serializer,
}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return ClosedTabTombstoneData(
tabId: serializer.fromJson<String>(json['tab_id']),
closedAt: serializer.fromJson<DateTime>(json['closed_at']),
);
}
@override
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'tab_id': serializer.toJson<String>(tabId),
'closed_at': serializer.toJson<DateTime>(closedAt),
};
}
i3.ClosedTabTombstoneData copyWith({String? tabId, DateTime? closedAt}) =>
i3.ClosedTabTombstoneData(
tabId: tabId ?? this.tabId,
closedAt: closedAt ?? this.closedAt,
);
ClosedTabTombstoneData copyWithCompanion(
i3.ClosedTabTombstoneCompanion data,
) {
return ClosedTabTombstoneData(
tabId: data.tabId.present ? data.tabId.value : this.tabId,
closedAt: data.closedAt.present ? data.closedAt.value : this.closedAt,
);
}
@override
String toString() {
return (StringBuffer('ClosedTabTombstoneData(')
..write('tabId: $tabId, ')
..write('closedAt: $closedAt')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(tabId, closedAt);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is i3.ClosedTabTombstoneData &&
other.tabId == this.tabId &&
other.closedAt == this.closedAt);
}
class ClosedTabTombstoneCompanion
extends i0.UpdateCompanion<i3.ClosedTabTombstoneData> {
final i0.Value<String> tabId;
final i0.Value<DateTime> closedAt;
final i0.Value<int> rowid;
const ClosedTabTombstoneCompanion({
this.tabId = const i0.Value.absent(),
this.closedAt = const i0.Value.absent(),
this.rowid = const i0.Value.absent(),
});
ClosedTabTombstoneCompanion.insert({
required String tabId,
required DateTime closedAt,
this.rowid = const i0.Value.absent(),
}) : tabId = i0.Value(tabId),
closedAt = i0.Value(closedAt);
static i0.Insertable<i3.ClosedTabTombstoneData> custom({
i0.Expression<String>? tabId,
i0.Expression<DateTime>? closedAt,
i0.Expression<int>? rowid,
}) {
return i0.RawValuesInsertable({
if (tabId != null) 'tab_id': tabId,
if (closedAt != null) 'closed_at': closedAt,
if (rowid != null) 'rowid': rowid,
});
}
i3.ClosedTabTombstoneCompanion copyWith({
i0.Value<String>? tabId,
i0.Value<DateTime>? closedAt,
i0.Value<int>? rowid,
}) {
return i3.ClosedTabTombstoneCompanion(
tabId: tabId ?? this.tabId,
closedAt: closedAt ?? this.closedAt,
rowid: rowid ?? this.rowid,
);
}
@override
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
final map = <String, i0.Expression>{};
if (tabId.present) {
map['tab_id'] = i0.Variable<String>(tabId.value);
}
if (closedAt.present) {
map['closed_at'] = i0.Variable<DateTime>(closedAt.value);
}
if (rowid.present) {
map['rowid'] = i0.Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ClosedTabTombstoneCompanion(')
..write('tabId: $tabId, ')
..write('closedAt: $closedAt, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
i0.Index get idxTabParentContainer => i0.Index(
'idx_tab_parent_container',
'CREATE INDEX idx_tab_parent_container ON tab (parent_id, container_id)',
@@ -2643,6 +2986,23 @@ class DefinitionsDrift extends i9.ModularAccessor {
).asyncMap(container.mapFromRow);
}
i0.Selectable<ContainerIdsByContextualIdentitiesResult>
containerIdsByContextualIdentities({required List<String> contextIds}) {
var $arrayStartIndex = 1;
final expandedcontextIds = $expandVar($arrayStartIndex, contextIds.length);
$arrayStartIndex += contextIds.length;
return customSelect(
'SELECT id, CAST(container.metadata ->> \'\$.contextualIdentity\' AS TEXT) AS contextual_identity FROM container WHERE CAST(container.metadata ->> \'\$.contextualIdentity\' AS TEXT) IN ($expandedcontextIds)',
variables: [for (var $ in contextIds) i0.Variable<String>($)],
readsFrom: {container},
).map(
(i0.QueryRow row) => ContainerIdsByContextualIdentitiesResult(
id: row.read<String>('id'),
contextualIdentity: row.read<String>('contextual_identity'),
),
);
}
i0.Selectable<String?> containersToClearOnExit() {
return customSelect(
'SELECT container.metadata ->> \'\$.contextualIdentity\' AS contextual_identity FROM container WHERE json_extract(container.metadata, \'\$.clearDataOnExit\') = 1 AND container.metadata ->> \'\$.contextualIdentity\' IS NOT NULL',
@@ -2727,6 +3087,15 @@ class UnorderedTabDescendantsResult {
UnorderedTabDescendantsResult({required this.id, this.parentId});
}
class ContainerIdsByContextualIdentitiesResult {
final String id;
final String contextualIdentity;
ContainerIdsByContextualIdentitiesResult({
required this.id,
required this.contextualIdentity,
});
}
class IsolatedContextContainerPairsResult {
final String? isolationContextId;
final String? containerId;