@flrande/bak-extension 0.6.7 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.bak-e2e-build-stamp +1 -1
- package/dist/background.global.js +166 -49
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/session-binding.ts +298 -169
|
@@ -1 +1 @@
|
|
|
1
|
-
2026-03-
|
|
1
|
+
2026-03-13T08:51:15.417Z
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
// package.json
|
|
63
63
|
var package_default = {
|
|
64
64
|
name: "@flrande/bak-extension",
|
|
65
|
-
version: "0.6.
|
|
65
|
+
version: "0.6.8",
|
|
66
66
|
type: "module",
|
|
67
67
|
scripts: {
|
|
68
68
|
build: "tsup src/background.ts src/content.ts src/popup.ts --format iife --out-dir dist --clean && node scripts/copy-assets.mjs",
|
|
@@ -563,26 +563,37 @@
|
|
|
563
563
|
}
|
|
564
564
|
}
|
|
565
565
|
if (!window2) {
|
|
566
|
-
const
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
state.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
state.activeTabId =
|
|
584
|
-
|
|
585
|
-
|
|
566
|
+
const sharedWindow = await this.findSharedBindingWindow(bindingId);
|
|
567
|
+
if (sharedWindow) {
|
|
568
|
+
state.windowId = sharedWindow.id;
|
|
569
|
+
state.groupId = null;
|
|
570
|
+
state.tabIds = [];
|
|
571
|
+
state.activeTabId = null;
|
|
572
|
+
state.primaryTabId = null;
|
|
573
|
+
window2 = sharedWindow;
|
|
574
|
+
repairActions.push("attached-shared-window");
|
|
575
|
+
} else {
|
|
576
|
+
const createdWindow = await this.browser.createWindow({
|
|
577
|
+
url: initialUrl,
|
|
578
|
+
focused: options.focus === true
|
|
579
|
+
});
|
|
580
|
+
state.windowId = createdWindow.id;
|
|
581
|
+
state.groupId = null;
|
|
582
|
+
state.tabIds = [];
|
|
583
|
+
state.activeTabId = null;
|
|
584
|
+
state.primaryTabId = null;
|
|
585
|
+
window2 = createdWindow;
|
|
586
|
+
const initialTab = typeof createdWindow.initialTabId === "number" ? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id) : null;
|
|
587
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
588
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
589
|
+
if (state.primaryTabId === null) {
|
|
590
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
591
|
+
}
|
|
592
|
+
if (state.activeTabId === null) {
|
|
593
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
594
|
+
}
|
|
595
|
+
repairActions.push(created ? "created-window" : "recreated-window");
|
|
596
|
+
}
|
|
586
597
|
}
|
|
587
598
|
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
588
599
|
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
@@ -596,12 +607,12 @@
|
|
|
596
607
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
597
608
|
if (state.windowId !== null) {
|
|
598
609
|
const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
|
|
599
|
-
if (ownership.foreignTabs.length > 0) {
|
|
600
|
-
const migrated = await this.
|
|
610
|
+
if (ownership.foreignTabs.length > 0 && ownership.bindingTabs.length > 0) {
|
|
611
|
+
const migrated = await this.moveBindingIntoBakWindow(state, ownership, initialUrl);
|
|
601
612
|
window2 = migrated.window;
|
|
602
613
|
tabs = migrated.tabs;
|
|
603
614
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
604
|
-
repairActions.push("
|
|
615
|
+
repairActions.push("evacuated-foreign-window");
|
|
605
616
|
}
|
|
606
617
|
}
|
|
607
618
|
if (tabs.length === 0) {
|
|
@@ -698,7 +709,7 @@
|
|
|
698
709
|
const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
|
|
699
710
|
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
700
711
|
state,
|
|
701
|
-
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
712
|
+
ensured.created || ensured.repairActions.includes("recreated-window") || ensured.repairActions.includes("created-primary-tab")
|
|
702
713
|
);
|
|
703
714
|
let createdTab;
|
|
704
715
|
try {
|
|
@@ -831,29 +842,32 @@
|
|
|
831
842
|
return { ok: true, binding: refreshed.binding };
|
|
832
843
|
}
|
|
833
844
|
async closeTab(bindingId, tabId) {
|
|
834
|
-
const
|
|
835
|
-
|
|
836
|
-
|
|
845
|
+
const binding = await this.inspectBinding(bindingId);
|
|
846
|
+
if (!binding) {
|
|
847
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
848
|
+
}
|
|
849
|
+
const resolvedTabId = typeof tabId === "number" ? tabId : binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
|
|
850
|
+
if (typeof resolvedTabId !== "number" || !binding.tabIds.includes(resolvedTabId)) {
|
|
837
851
|
throw new Error(`Tab ${tabId ?? "active"} does not belong to binding ${bindingId}`);
|
|
838
852
|
}
|
|
839
853
|
await this.browser.closeTab(resolvedTabId);
|
|
840
|
-
const remainingTabIds =
|
|
854
|
+
const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
841
855
|
if (remainingTabIds.length === 0) {
|
|
842
|
-
await this.storage.delete(
|
|
856
|
+
await this.storage.delete(binding.id);
|
|
843
857
|
return {
|
|
844
858
|
binding: null,
|
|
845
859
|
closedTabId: resolvedTabId
|
|
846
860
|
};
|
|
847
861
|
}
|
|
848
862
|
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
849
|
-
const nextPrimaryTabId =
|
|
850
|
-
const nextActiveTabId =
|
|
863
|
+
const nextPrimaryTabId = binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
864
|
+
const nextActiveTabId = binding.activeTabId === resolvedTabId ? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null : binding.activeTabId;
|
|
851
865
|
const nextState = {
|
|
852
|
-
id:
|
|
853
|
-
label:
|
|
854
|
-
color:
|
|
855
|
-
windowId: tabs[0]?.windowId ??
|
|
856
|
-
groupId: tabs[0]?.groupId ??
|
|
866
|
+
id: binding.id,
|
|
867
|
+
label: binding.label,
|
|
868
|
+
color: binding.color,
|
|
869
|
+
windowId: tabs[0]?.windowId ?? binding.windowId,
|
|
870
|
+
groupId: tabs[0]?.groupId ?? binding.groupId,
|
|
857
871
|
tabIds: tabs.map((candidate) => candidate.id),
|
|
858
872
|
activeTabId: nextActiveTabId,
|
|
859
873
|
primaryTabId: nextPrimaryTabId
|
|
@@ -882,7 +896,7 @@
|
|
|
882
896
|
return { ok: true };
|
|
883
897
|
}
|
|
884
898
|
await this.storage.delete(bindingId);
|
|
885
|
-
const trackedTabs = await this.
|
|
899
|
+
const trackedTabs = await this.collectBindingTabsForClose(state);
|
|
886
900
|
for (const trackedTab of trackedTabs) {
|
|
887
901
|
try {
|
|
888
902
|
await this.browser.closeTab(trackedTab.id);
|
|
@@ -1069,24 +1083,59 @@
|
|
|
1069
1083
|
}
|
|
1070
1084
|
async inspectBindingWindowOwnership(state, windowId) {
|
|
1071
1085
|
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
1072
|
-
const
|
|
1086
|
+
const bindingTabIds = new Set(this.collectCandidateTabIds(state));
|
|
1087
|
+
const peerBindings = (await this.storage.list()).filter((candidate) => candidate.id !== state.id);
|
|
1088
|
+
const peerTabIds = /* @__PURE__ */ new Set();
|
|
1089
|
+
const peerGroupIds = /* @__PURE__ */ new Set();
|
|
1090
|
+
for (const peer of peerBindings) {
|
|
1091
|
+
for (const tabId of this.collectCandidateTabIds(peer)) {
|
|
1092
|
+
peerTabIds.add(tabId);
|
|
1093
|
+
}
|
|
1094
|
+
if (peer.groupId !== null) {
|
|
1095
|
+
peerGroupIds.add(peer.groupId);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
const bindingTabs = [];
|
|
1099
|
+
const sharedBindingTabs = [];
|
|
1100
|
+
const foreignTabs = [];
|
|
1101
|
+
for (const tab of windowTabs) {
|
|
1102
|
+
if (bindingTabIds.has(tab.id) || state.groupId !== null && tab.groupId === state.groupId) {
|
|
1103
|
+
bindingTabs.push(tab);
|
|
1104
|
+
continue;
|
|
1105
|
+
}
|
|
1106
|
+
if (peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)) {
|
|
1107
|
+
sharedBindingTabs.push(tab);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
foreignTabs.push(tab);
|
|
1111
|
+
}
|
|
1073
1112
|
return {
|
|
1074
|
-
bindingTabs
|
|
1075
|
-
|
|
1113
|
+
bindingTabs,
|
|
1114
|
+
sharedBindingTabs,
|
|
1115
|
+
foreignTabs
|
|
1076
1116
|
};
|
|
1077
1117
|
}
|
|
1078
|
-
async
|
|
1118
|
+
async moveBindingIntoBakWindow(state, ownership, initialUrl) {
|
|
1079
1119
|
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
1080
1120
|
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
1081
|
-
const
|
|
1121
|
+
const sharedWindow = await this.findSharedBindingWindow(state.id, state.windowId === null ? [] : [state.windowId]);
|
|
1122
|
+
const window2 = sharedWindow ?? await this.browser.createWindow({
|
|
1082
1123
|
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
1083
1124
|
focused: false
|
|
1084
1125
|
});
|
|
1085
|
-
const initialTab = typeof window2.initialTabId
|
|
1086
|
-
const recreatedTabs =
|
|
1087
|
-
const firstTab = recreatedTabs[0] ?? null;
|
|
1126
|
+
const initialTab = sharedWindow || typeof window2.initialTabId !== "number" ? null : await this.waitForTrackedTab(window2.initialTabId, window2.id);
|
|
1127
|
+
const recreatedTabs = [];
|
|
1088
1128
|
const tabIdMap = /* @__PURE__ */ new Map();
|
|
1089
|
-
if (sourceTabs[0]
|
|
1129
|
+
if (sourceTabs[0]) {
|
|
1130
|
+
const firstTab = initialTab ? await this.browser.updateTab(initialTab.id, {
|
|
1131
|
+
url: sourceTabs[0].url,
|
|
1132
|
+
active: false
|
|
1133
|
+
}) : await this.createBindingTab({
|
|
1134
|
+
windowId: window2.id,
|
|
1135
|
+
url: sourceTabs[0].url,
|
|
1136
|
+
active: false
|
|
1137
|
+
});
|
|
1138
|
+
recreatedTabs.push(firstTab);
|
|
1090
1139
|
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
1091
1140
|
}
|
|
1092
1141
|
for (const sourceTab of sourceTabs.slice(1)) {
|
|
@@ -1098,7 +1147,7 @@
|
|
|
1098
1147
|
recreatedTabs.push(recreated);
|
|
1099
1148
|
tabIdMap.set(sourceTab.id, recreated.id);
|
|
1100
1149
|
}
|
|
1101
|
-
const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ??
|
|
1150
|
+
const nextPrimaryTabId = (state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : void 0) ?? recreatedTabs[0]?.id ?? null;
|
|
1102
1151
|
const nextActiveTabId = (state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : void 0) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
1103
1152
|
if (nextActiveTabId !== null) {
|
|
1104
1153
|
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
@@ -1113,7 +1162,10 @@
|
|
|
1113
1162
|
tabIds: [...state.tabIds]
|
|
1114
1163
|
});
|
|
1115
1164
|
for (const bindingTab of ownership.bindingTabs) {
|
|
1116
|
-
|
|
1165
|
+
try {
|
|
1166
|
+
await this.browser.closeTab(bindingTab.id);
|
|
1167
|
+
} catch {
|
|
1168
|
+
}
|
|
1117
1169
|
}
|
|
1118
1170
|
return {
|
|
1119
1171
|
window: window2,
|
|
@@ -1145,6 +1197,57 @@
|
|
|
1145
1197
|
}
|
|
1146
1198
|
return ordered;
|
|
1147
1199
|
}
|
|
1200
|
+
async findSharedBindingWindow(bindingId, excludedWindowIds = []) {
|
|
1201
|
+
const peers = (await this.storage.list()).filter((candidate) => candidate.id !== bindingId);
|
|
1202
|
+
const candidateWindowIds = [];
|
|
1203
|
+
const peerTabIds = /* @__PURE__ */ new Set();
|
|
1204
|
+
const peerGroupIds = /* @__PURE__ */ new Set();
|
|
1205
|
+
const pushWindowId = (windowId) => {
|
|
1206
|
+
if (typeof windowId !== "number") {
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
candidateWindowIds.push(windowId);
|
|
1213
|
+
};
|
|
1214
|
+
for (const peer of peers) {
|
|
1215
|
+
pushWindowId(peer.windowId);
|
|
1216
|
+
if (peer.groupId !== null) {
|
|
1217
|
+
peerGroupIds.add(peer.groupId);
|
|
1218
|
+
const group = await this.waitForGroup(peer.groupId, 300);
|
|
1219
|
+
pushWindowId(group?.windowId);
|
|
1220
|
+
}
|
|
1221
|
+
const trackedTabIds = this.collectCandidateTabIds(peer);
|
|
1222
|
+
for (const trackedTabId of trackedTabIds) {
|
|
1223
|
+
peerTabIds.add(trackedTabId);
|
|
1224
|
+
}
|
|
1225
|
+
const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
|
|
1226
|
+
for (const tab of trackedTabs) {
|
|
1227
|
+
pushWindowId(tab.windowId);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
for (const windowId of candidateWindowIds) {
|
|
1231
|
+
const window2 = await this.waitForWindow(windowId, 300);
|
|
1232
|
+
if (window2) {
|
|
1233
|
+
const windowTabs = await this.waitForWindowTabs(window2.id, 300);
|
|
1234
|
+
const ownedTabs = windowTabs.filter(
|
|
1235
|
+
(tab) => peerTabIds.has(tab.id) || tab.groupId !== null && peerGroupIds.has(tab.groupId)
|
|
1236
|
+
);
|
|
1237
|
+
if (ownedTabs.length === 0) {
|
|
1238
|
+
continue;
|
|
1239
|
+
}
|
|
1240
|
+
const foreignTabs = windowTabs.filter(
|
|
1241
|
+
(tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
|
|
1242
|
+
);
|
|
1243
|
+
if (foreignTabs.length > 0) {
|
|
1244
|
+
continue;
|
|
1245
|
+
}
|
|
1246
|
+
return window2;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1148
1251
|
async recoverBindingTabs(state, existingTabs) {
|
|
1149
1252
|
if (state.windowId === null) {
|
|
1150
1253
|
return existingTabs;
|
|
@@ -1171,6 +1274,19 @@
|
|
|
1171
1274
|
}
|
|
1172
1275
|
return existingTabs;
|
|
1173
1276
|
}
|
|
1277
|
+
async collectBindingTabsForClose(state) {
|
|
1278
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
1279
|
+
if (state.windowId === null || state.groupId === null) {
|
|
1280
|
+
return trackedTabs;
|
|
1281
|
+
}
|
|
1282
|
+
const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
|
|
1283
|
+
const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
|
|
1284
|
+
const merged = /* @__PURE__ */ new Map();
|
|
1285
|
+
for (const tab of [...trackedTabs, ...groupedTabs]) {
|
|
1286
|
+
merged.set(tab.id, tab);
|
|
1287
|
+
}
|
|
1288
|
+
return [...merged.values()];
|
|
1289
|
+
}
|
|
1174
1290
|
async createBindingTab(options) {
|
|
1175
1291
|
if (options.windowId === null) {
|
|
1176
1292
|
throw new Error("Binding window is unavailable");
|
|
@@ -1200,6 +1316,7 @@
|
|
|
1200
1316
|
return null;
|
|
1201
1317
|
}
|
|
1202
1318
|
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
1319
|
+
tabs = await this.recoverBindingTabs(state, tabs);
|
|
1203
1320
|
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
1204
1321
|
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
1205
1322
|
tabs = [...tabs, activeTab];
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
package/src/session-binding.ts
CHANGED
|
@@ -68,8 +68,8 @@ export interface SessionBindingStorage {
|
|
|
68
68
|
list(): Promise<SessionBindingRecord[]>;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
-
export interface SessionBindingBrowser {
|
|
72
|
-
getTab(tabId: number): Promise<SessionBindingTab | null>;
|
|
71
|
+
export interface SessionBindingBrowser {
|
|
72
|
+
getTab(tabId: number): Promise<SessionBindingTab | null>;
|
|
73
73
|
getActiveTab(): Promise<SessionBindingTab | null>;
|
|
74
74
|
listTabs(filter?: { windowId?: number }): Promise<SessionBindingTab[]>;
|
|
75
75
|
createTab(options: { windowId?: number; url?: string; active?: boolean }): Promise<SessionBindingTab>;
|
|
@@ -81,14 +81,15 @@ export interface SessionBindingBrowser {
|
|
|
81
81
|
closeWindow(windowId: number): Promise<void>;
|
|
82
82
|
getGroup(groupId: number): Promise<SessionBindingGroup | null>;
|
|
83
83
|
groupTabs(tabIds: number[], groupId?: number): Promise<number>;
|
|
84
|
-
updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
interface SessionBindingWindowOwnership {
|
|
88
|
-
bindingTabs: SessionBindingTab[];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface SessionBindingWindowOwnership {
|
|
88
|
+
bindingTabs: SessionBindingTab[];
|
|
89
|
+
sharedBindingTabs: SessionBindingTab[];
|
|
90
|
+
foreignTabs: SessionBindingTab[];
|
|
91
|
+
}
|
|
92
|
+
|
|
92
93
|
export interface SessionBindingEnsureOptions {
|
|
93
94
|
bindingId?: string;
|
|
94
95
|
focus?: boolean;
|
|
@@ -145,54 +146,65 @@ class SessionBindingManager {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
if (!window) {
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
state.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
149
|
+
const sharedWindow = await this.findSharedBindingWindow(bindingId);
|
|
150
|
+
if (sharedWindow) {
|
|
151
|
+
state.windowId = sharedWindow.id;
|
|
152
|
+
state.groupId = null;
|
|
153
|
+
state.tabIds = [];
|
|
154
|
+
state.activeTabId = null;
|
|
155
|
+
state.primaryTabId = null;
|
|
156
|
+
window = sharedWindow;
|
|
157
|
+
repairActions.push('attached-shared-window');
|
|
158
|
+
} else {
|
|
159
|
+
const createdWindow = await this.browser.createWindow({
|
|
160
|
+
url: initialUrl,
|
|
161
|
+
focused: options.focus === true
|
|
162
|
+
});
|
|
163
|
+
state.windowId = createdWindow.id;
|
|
164
|
+
state.groupId = null;
|
|
165
|
+
state.tabIds = [];
|
|
166
|
+
state.activeTabId = null;
|
|
167
|
+
state.primaryTabId = null;
|
|
168
|
+
window = createdWindow;
|
|
169
|
+
const initialTab =
|
|
170
|
+
typeof createdWindow.initialTabId === 'number'
|
|
171
|
+
? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
|
|
172
|
+
: null;
|
|
173
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
174
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
175
|
+
if (state.primaryTabId === null) {
|
|
176
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
177
|
+
}
|
|
178
|
+
if (state.activeTabId === null) {
|
|
179
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
180
|
+
}
|
|
181
|
+
repairActions.push(created ? 'created-window' : 'recreated-window');
|
|
169
182
|
}
|
|
170
|
-
repairActions.push(created ? 'created-window' : 'recreated-window');
|
|
171
183
|
}
|
|
172
|
-
|
|
173
|
-
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
174
|
-
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
175
|
-
if (recoveredTabs.length > tabs.length) {
|
|
184
|
+
|
|
185
|
+
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
186
|
+
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
187
|
+
if (recoveredTabs.length > tabs.length) {
|
|
176
188
|
tabs = recoveredTabs;
|
|
177
189
|
repairActions.push('recovered-tracked-tabs');
|
|
178
190
|
}
|
|
179
191
|
if (tabs.length !== state.tabIds.length) {
|
|
180
192
|
repairActions.push('pruned-missing-tabs');
|
|
181
|
-
}
|
|
182
|
-
state.tabIds = tabs.map((tab) => tab.id);
|
|
183
|
-
|
|
184
|
-
if (state.windowId !== null) {
|
|
185
|
-
const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
|
|
186
|
-
if (ownership.foreignTabs.length > 0) {
|
|
187
|
-
const migrated = await this.
|
|
188
|
-
window = migrated.window;
|
|
189
|
-
tabs = migrated.tabs;
|
|
190
|
-
state.tabIds = tabs.map((tab) => tab.id);
|
|
191
|
-
repairActions.push('
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (tabs.length === 0) {
|
|
193
|
+
}
|
|
194
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
195
|
+
|
|
196
|
+
if (state.windowId !== null) {
|
|
197
|
+
const ownership = await this.inspectBindingWindowOwnership(state, state.windowId);
|
|
198
|
+
if (ownership.foreignTabs.length > 0 && ownership.bindingTabs.length > 0) {
|
|
199
|
+
const migrated = await this.moveBindingIntoBakWindow(state, ownership, initialUrl);
|
|
200
|
+
window = migrated.window;
|
|
201
|
+
tabs = migrated.tabs;
|
|
202
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
203
|
+
repairActions.push('evacuated-foreign-window');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (tabs.length === 0) {
|
|
196
208
|
const primary = await this.createBindingTab({
|
|
197
209
|
windowId: state.windowId,
|
|
198
210
|
url: initialUrl,
|
|
@@ -293,13 +305,12 @@ class SessionBindingManager {
|
|
|
293
305
|
}
|
|
294
306
|
const active = options.active === true;
|
|
295
307
|
const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
|
|
296
|
-
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
297
|
-
state,
|
|
298
|
-
ensured.created ||
|
|
299
|
-
ensured.repairActions.includes('recreated-window') ||
|
|
300
|
-
ensured.repairActions.includes('created-primary-tab')
|
|
301
|
-
|
|
302
|
-
);
|
|
308
|
+
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
309
|
+
state,
|
|
310
|
+
ensured.created ||
|
|
311
|
+
ensured.repairActions.includes('recreated-window') ||
|
|
312
|
+
ensured.repairActions.includes('created-primary-tab')
|
|
313
|
+
);
|
|
303
314
|
|
|
304
315
|
let createdTab: SessionBindingTab;
|
|
305
316
|
try {
|
|
@@ -443,20 +454,23 @@ class SessionBindingManager {
|
|
|
443
454
|
}
|
|
444
455
|
|
|
445
456
|
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
446
|
-
const
|
|
457
|
+
const binding = await this.inspectBinding(bindingId);
|
|
458
|
+
if (!binding) {
|
|
459
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
460
|
+
}
|
|
447
461
|
const resolvedTabId =
|
|
448
462
|
typeof tabId === 'number'
|
|
449
463
|
? tabId
|
|
450
|
-
:
|
|
451
|
-
if (typeof resolvedTabId !== 'number' || !
|
|
464
|
+
: binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
|
|
465
|
+
if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
|
|
452
466
|
throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
|
|
453
467
|
}
|
|
454
468
|
|
|
455
469
|
await this.browser.closeTab(resolvedTabId);
|
|
456
|
-
const remainingTabIds =
|
|
470
|
+
const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
457
471
|
|
|
458
472
|
if (remainingTabIds.length === 0) {
|
|
459
|
-
await this.storage.delete(
|
|
473
|
+
await this.storage.delete(binding.id);
|
|
460
474
|
return {
|
|
461
475
|
binding: null,
|
|
462
476
|
closedTabId: resolvedTabId
|
|
@@ -465,17 +479,17 @@ class SessionBindingManager {
|
|
|
465
479
|
|
|
466
480
|
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
467
481
|
const nextPrimaryTabId =
|
|
468
|
-
|
|
482
|
+
binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
469
483
|
const nextActiveTabId =
|
|
470
|
-
|
|
484
|
+
binding.activeTabId === resolvedTabId
|
|
471
485
|
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
472
|
-
:
|
|
486
|
+
: binding.activeTabId;
|
|
473
487
|
const nextState: SessionBindingRecord = {
|
|
474
|
-
id:
|
|
475
|
-
label:
|
|
476
|
-
color:
|
|
477
|
-
windowId: tabs[0]?.windowId ??
|
|
478
|
-
groupId: tabs[0]?.groupId ??
|
|
488
|
+
id: binding.id,
|
|
489
|
+
label: binding.label,
|
|
490
|
+
color: binding.color,
|
|
491
|
+
windowId: tabs[0]?.windowId ?? binding.windowId,
|
|
492
|
+
groupId: tabs[0]?.groupId ?? binding.groupId,
|
|
479
493
|
tabIds: tabs.map((candidate) => candidate.id),
|
|
480
494
|
activeTabId: nextActiveTabId,
|
|
481
495
|
primaryTabId: nextPrimaryTabId
|
|
@@ -508,7 +522,7 @@ class SessionBindingManager {
|
|
|
508
522
|
// Clear persisted state before closing the window so tab/window removal
|
|
509
523
|
// listeners cannot race and resurrect an empty binding record.
|
|
510
524
|
await this.storage.delete(bindingId);
|
|
511
|
-
const trackedTabs = await this.
|
|
525
|
+
const trackedTabs = await this.collectBindingTabsForClose(state);
|
|
512
526
|
for (const trackedTab of trackedTabs) {
|
|
513
527
|
try {
|
|
514
528
|
await this.browser.closeTab(trackedTab.id);
|
|
@@ -673,7 +687,7 @@ class SessionBindingManager {
|
|
|
673
687
|
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
|
|
674
688
|
}
|
|
675
689
|
|
|
676
|
-
private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
|
|
690
|
+
private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
|
|
677
691
|
const candidateWindowIds: number[] = [];
|
|
678
692
|
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
679
693
|
if (typeof windowId !== 'number') {
|
|
@@ -718,59 +732,100 @@ class SessionBindingManager {
|
|
|
718
732
|
return { window, tabs };
|
|
719
733
|
}
|
|
720
734
|
|
|
721
|
-
return null;
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
private async inspectBindingWindowOwnership(state: SessionBindingRecord, windowId: number): Promise<SessionBindingWindowOwnership> {
|
|
725
|
-
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
726
|
-
const
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
735
|
+
return null;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
private async inspectBindingWindowOwnership(state: SessionBindingRecord, windowId: number): Promise<SessionBindingWindowOwnership> {
|
|
739
|
+
const windowTabs = await this.waitForWindowTabs(windowId, 500);
|
|
740
|
+
const bindingTabIds = new Set(this.collectCandidateTabIds(state));
|
|
741
|
+
const peerBindings = (await this.storage.list()).filter((candidate) => candidate.id !== state.id);
|
|
742
|
+
const peerTabIds = new Set<number>();
|
|
743
|
+
const peerGroupIds = new Set<number>();
|
|
744
|
+
for (const peer of peerBindings) {
|
|
745
|
+
for (const tabId of this.collectCandidateTabIds(peer)) {
|
|
746
|
+
peerTabIds.add(tabId);
|
|
747
|
+
}
|
|
748
|
+
if (peer.groupId !== null) {
|
|
749
|
+
peerGroupIds.add(peer.groupId);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const bindingTabs: SessionBindingTab[] = [];
|
|
754
|
+
const sharedBindingTabs: SessionBindingTab[] = [];
|
|
755
|
+
const foreignTabs: SessionBindingTab[] = [];
|
|
756
|
+
for (const tab of windowTabs) {
|
|
757
|
+
if (bindingTabIds.has(tab.id) || (state.groupId !== null && tab.groupId === state.groupId)) {
|
|
758
|
+
bindingTabs.push(tab);
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))) {
|
|
762
|
+
sharedBindingTabs.push(tab);
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
foreignTabs.push(tab);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
bindingTabs,
|
|
770
|
+
sharedBindingTabs,
|
|
771
|
+
foreignTabs
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
private async moveBindingIntoBakWindow(
|
|
734
776
|
state: SessionBindingRecord,
|
|
735
777
|
ownership: SessionBindingWindowOwnership,
|
|
736
778
|
initialUrl: string
|
|
737
779
|
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
738
|
-
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
739
|
-
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
740
|
-
const
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
780
|
+
const sourceTabs = this.orderSessionBindingTabsForMigration(state, ownership.bindingTabs);
|
|
781
|
+
const seedUrl = sourceTabs[0]?.url ?? initialUrl;
|
|
782
|
+
const sharedWindow = await this.findSharedBindingWindow(state.id, state.windowId === null ? [] : [state.windowId]);
|
|
783
|
+
const window =
|
|
784
|
+
sharedWindow ??
|
|
785
|
+
(await this.browser.createWindow({
|
|
786
|
+
url: seedUrl || DEFAULT_SESSION_BINDING_URL,
|
|
787
|
+
focused: false
|
|
788
|
+
}));
|
|
744
789
|
const initialTab =
|
|
745
|
-
typeof window.initialTabId
|
|
746
|
-
const recreatedTabs =
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
if (sourceTabs[0]
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
}
|
|
773
|
-
|
|
790
|
+
sharedWindow || typeof window.initialTabId !== 'number' ? null : await this.waitForTrackedTab(window.initialTabId, window.id);
|
|
791
|
+
const recreatedTabs: SessionBindingTab[] = [];
|
|
792
|
+
const tabIdMap = new Map<number, number>();
|
|
793
|
+
|
|
794
|
+
if (sourceTabs[0]) {
|
|
795
|
+
const firstTab = initialTab
|
|
796
|
+
? await this.browser.updateTab(initialTab.id, {
|
|
797
|
+
url: sourceTabs[0].url,
|
|
798
|
+
active: false
|
|
799
|
+
})
|
|
800
|
+
: await this.createBindingTab({
|
|
801
|
+
windowId: window.id,
|
|
802
|
+
url: sourceTabs[0].url,
|
|
803
|
+
active: false
|
|
804
|
+
});
|
|
805
|
+
recreatedTabs.push(firstTab);
|
|
806
|
+
tabIdMap.set(sourceTabs[0].id, firstTab.id);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const sourceTab of sourceTabs.slice(1)) {
|
|
810
|
+
const recreated = await this.createBindingTab({
|
|
811
|
+
windowId: window.id,
|
|
812
|
+
url: sourceTab.url,
|
|
813
|
+
active: false
|
|
814
|
+
});
|
|
815
|
+
recreatedTabs.push(recreated);
|
|
816
|
+
tabIdMap.set(sourceTab.id, recreated.id);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
const nextPrimaryTabId =
|
|
820
|
+
(state.primaryTabId !== null ? tabIdMap.get(state.primaryTabId) : undefined) ??
|
|
821
|
+
recreatedTabs[0]?.id ??
|
|
822
|
+
null;
|
|
823
|
+
const nextActiveTabId =
|
|
824
|
+
(state.activeTabId !== null ? tabIdMap.get(state.activeTabId) : undefined) ?? nextPrimaryTabId ?? recreatedTabs[0]?.id ?? null;
|
|
825
|
+
if (nextActiveTabId !== null) {
|
|
826
|
+
await this.browser.updateTab(nextActiveTabId, { active: true });
|
|
827
|
+
}
|
|
828
|
+
|
|
774
829
|
state.windowId = window.id;
|
|
775
830
|
state.groupId = null;
|
|
776
831
|
state.tabIds = recreatedTabs.map((tab) => tab.id);
|
|
@@ -782,43 +837,102 @@ class SessionBindingManager {
|
|
|
782
837
|
});
|
|
783
838
|
|
|
784
839
|
for (const bindingTab of ownership.bindingTabs) {
|
|
785
|
-
|
|
840
|
+
try {
|
|
841
|
+
await this.browser.closeTab(bindingTab.id);
|
|
842
|
+
} catch {
|
|
843
|
+
// Ignore tabs that were already removed while evacuating the binding.
|
|
844
|
+
}
|
|
786
845
|
}
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
window,
|
|
849
|
+
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
private orderSessionBindingTabsForMigration(state: SessionBindingRecord, tabs: SessionBindingTab[]): SessionBindingTab[] {
|
|
854
|
+
const ordered: SessionBindingTab[] = [];
|
|
855
|
+
const seen = new Set<number>();
|
|
856
|
+
const pushById = (tabId: number | null): void => {
|
|
857
|
+
if (typeof tabId !== 'number') {
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
const tab = tabs.find((candidate) => candidate.id === tabId);
|
|
861
|
+
if (!tab || seen.has(tab.id)) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
ordered.push(tab);
|
|
865
|
+
seen.add(tab.id);
|
|
866
|
+
};
|
|
867
|
+
|
|
868
|
+
pushById(state.primaryTabId);
|
|
869
|
+
pushById(state.activeTabId);
|
|
870
|
+
for (const tab of tabs) {
|
|
871
|
+
if (seen.has(tab.id)) {
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
ordered.push(tab);
|
|
875
|
+
seen.add(tab.id);
|
|
876
|
+
}
|
|
877
|
+
return ordered;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
private async findSharedBindingWindow(bindingId: string, excludedWindowIds: number[] = []): Promise<SessionBindingWindow | null> {
|
|
881
|
+
const peers = (await this.storage.list()).filter((candidate) => candidate.id !== bindingId);
|
|
882
|
+
const candidateWindowIds: number[] = [];
|
|
883
|
+
const peerTabIds = new Set<number>();
|
|
884
|
+
const peerGroupIds = new Set<number>();
|
|
885
|
+
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
886
|
+
if (typeof windowId !== 'number') {
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
candidateWindowIds.push(windowId);
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
for (const peer of peers) {
|
|
896
|
+
pushWindowId(peer.windowId);
|
|
897
|
+
if (peer.groupId !== null) {
|
|
898
|
+
peerGroupIds.add(peer.groupId);
|
|
899
|
+
const group = await this.waitForGroup(peer.groupId, 300);
|
|
900
|
+
pushWindowId(group?.windowId);
|
|
901
|
+
}
|
|
902
|
+
const trackedTabIds = this.collectCandidateTabIds(peer);
|
|
903
|
+
for (const trackedTabId of trackedTabIds) {
|
|
904
|
+
peerTabIds.add(trackedTabId);
|
|
905
|
+
}
|
|
906
|
+
const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
|
|
907
|
+
for (const tab of trackedTabs) {
|
|
908
|
+
pushWindowId(tab.windowId);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
for (const windowId of candidateWindowIds) {
|
|
913
|
+
const window = await this.waitForWindow(windowId, 300);
|
|
914
|
+
if (window) {
|
|
915
|
+
const windowTabs = await this.waitForWindowTabs(window.id, 300);
|
|
916
|
+
const ownedTabs = windowTabs.filter(
|
|
917
|
+
(tab) => peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))
|
|
918
|
+
);
|
|
919
|
+
if (ownedTabs.length === 0) {
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
const foreignTabs = windowTabs.filter(
|
|
923
|
+
(tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
|
|
924
|
+
);
|
|
925
|
+
if (foreignTabs.length > 0) {
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
return window;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
return null;
|
|
933
|
+
}
|
|
787
934
|
|
|
788
|
-
|
|
789
|
-
window,
|
|
790
|
-
tabs: await this.readTrackedTabs(state.tabIds, state.windowId)
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
private orderSessionBindingTabsForMigration(state: SessionBindingRecord, tabs: SessionBindingTab[]): SessionBindingTab[] {
|
|
795
|
-
const ordered: SessionBindingTab[] = [];
|
|
796
|
-
const seen = new Set<number>();
|
|
797
|
-
const pushById = (tabId: number | null): void => {
|
|
798
|
-
if (typeof tabId !== 'number') {
|
|
799
|
-
return;
|
|
800
|
-
}
|
|
801
|
-
const tab = tabs.find((candidate) => candidate.id === tabId);
|
|
802
|
-
if (!tab || seen.has(tab.id)) {
|
|
803
|
-
return;
|
|
804
|
-
}
|
|
805
|
-
ordered.push(tab);
|
|
806
|
-
seen.add(tab.id);
|
|
807
|
-
};
|
|
808
|
-
|
|
809
|
-
pushById(state.primaryTabId);
|
|
810
|
-
pushById(state.activeTabId);
|
|
811
|
-
for (const tab of tabs) {
|
|
812
|
-
if (seen.has(tab.id)) {
|
|
813
|
-
continue;
|
|
814
|
-
}
|
|
815
|
-
ordered.push(tab);
|
|
816
|
-
seen.add(tab.id);
|
|
817
|
-
}
|
|
818
|
-
return ordered;
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
|
|
935
|
+
private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
|
|
822
936
|
if (state.windowId === null) {
|
|
823
937
|
return existingTabs;
|
|
824
938
|
}
|
|
@@ -847,8 +961,22 @@ class SessionBindingManager {
|
|
|
847
961
|
return preferredTabs;
|
|
848
962
|
}
|
|
849
963
|
|
|
850
|
-
return existingTabs;
|
|
851
|
-
}
|
|
964
|
+
return existingTabs;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private async collectBindingTabsForClose(state: SessionBindingRecord): Promise<SessionBindingTab[]> {
|
|
968
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
969
|
+
if (state.windowId === null || state.groupId === null) {
|
|
970
|
+
return trackedTabs;
|
|
971
|
+
}
|
|
972
|
+
const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
|
|
973
|
+
const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
|
|
974
|
+
const merged = new Map<number, SessionBindingTab>();
|
|
975
|
+
for (const tab of [...trackedTabs, ...groupedTabs]) {
|
|
976
|
+
merged.set(tab.id, tab);
|
|
977
|
+
}
|
|
978
|
+
return [...merged.values()];
|
|
979
|
+
}
|
|
852
980
|
|
|
853
981
|
private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
|
|
854
982
|
if (options.windowId === null) {
|
|
@@ -877,17 +1005,18 @@ class SessionBindingManager {
|
|
|
877
1005
|
throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
|
|
878
1006
|
}
|
|
879
1007
|
|
|
880
|
-
private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
|
|
881
|
-
const state = await this.loadBindingRecord(bindingId);
|
|
882
|
-
if (!state) {
|
|
883
|
-
return null;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
1008
|
+
private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
|
|
1009
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
1010
|
+
if (!state) {
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
1015
|
+
tabs = await this.recoverBindingTabs(state, tabs);
|
|
1016
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
1017
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
1018
|
+
tabs = [...tabs, activeTab];
|
|
1019
|
+
}
|
|
891
1020
|
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
892
1021
|
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
|
|
893
1022
|
if (primaryTab) {
|