@flrande/bak-extension 0.6.6 → 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/dist/popup.global.js +141 -23
- package/dist/popup.html +49 -7
- package/package.json +2 -2
- package/public/popup.html +49 -7
- package/src/popup.ts +160 -23
- package/src/session-binding.ts +298 -169
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) {
|