@flrande/bak-extension 0.6.8 → 0.6.10
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 +126 -192
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +1949 -1991
- package/src/session-binding.ts +386 -423
package/src/session-binding.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
export const DEFAULT_SESSION_BINDING_LABEL = 'bak agent';
|
|
2
|
-
export const DEFAULT_SESSION_BINDING_COLOR = 'blue';
|
|
3
|
-
export const DEFAULT_SESSION_BINDING_URL = 'about:blank';
|
|
4
|
-
const WINDOW_LOOKUP_TIMEOUT_MS = 1_500;
|
|
5
|
-
const GROUP_LOOKUP_TIMEOUT_MS = 1_000;
|
|
6
|
-
const WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1_500;
|
|
1
|
+
export const DEFAULT_SESSION_BINDING_LABEL = 'bak agent';
|
|
2
|
+
export const DEFAULT_SESSION_BINDING_COLOR = 'blue';
|
|
3
|
+
export const DEFAULT_SESSION_BINDING_URL = 'about:blank';
|
|
4
|
+
const WINDOW_LOOKUP_TIMEOUT_MS = 1_500;
|
|
5
|
+
const GROUP_LOOKUP_TIMEOUT_MS = 1_000;
|
|
6
|
+
const WINDOW_TABS_LOOKUP_TIMEOUT_MS = 1_500;
|
|
7
7
|
|
|
8
8
|
export type SessionBindingColor = 'grey' | 'blue' | 'red' | 'yellow' | 'green' | 'pink' | 'purple' | 'cyan' | 'orange';
|
|
9
9
|
|
|
@@ -16,11 +16,11 @@ export interface SessionBindingTab {
|
|
|
16
16
|
groupId: number | null;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export interface SessionBindingWindow {
|
|
20
|
-
id: number;
|
|
21
|
-
focused: boolean;
|
|
22
|
-
initialTabId?: number | null;
|
|
23
|
-
}
|
|
19
|
+
export interface SessionBindingWindow {
|
|
20
|
+
id: number;
|
|
21
|
+
focused: boolean;
|
|
22
|
+
initialTabId?: number | null;
|
|
23
|
+
}
|
|
24
24
|
|
|
25
25
|
export interface SessionBindingGroup {
|
|
26
26
|
id: number;
|
|
@@ -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,29 +81,23 @@ 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
|
-
sharedBindingTabs: SessionBindingTab[];
|
|
90
|
-
foreignTabs: SessionBindingTab[];
|
|
91
|
-
}
|
|
92
|
-
|
|
84
|
+
updateGroup(groupId: number, options: { title?: string; color?: SessionBindingColor; collapsed?: boolean }): Promise<SessionBindingGroup>;
|
|
85
|
+
}
|
|
86
|
+
|
|
93
87
|
export interface SessionBindingEnsureOptions {
|
|
94
|
-
bindingId?: string;
|
|
95
|
-
focus?: boolean;
|
|
96
|
-
initialUrl?: string;
|
|
97
|
-
label?: string;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export interface SessionBindingOpenTabOptions {
|
|
101
|
-
bindingId?: string;
|
|
102
|
-
url?: string;
|
|
103
|
-
active?: boolean;
|
|
104
|
-
focus?: boolean;
|
|
105
|
-
label?: string;
|
|
106
|
-
}
|
|
88
|
+
bindingId?: string;
|
|
89
|
+
focus?: boolean;
|
|
90
|
+
initialUrl?: string;
|
|
91
|
+
label?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SessionBindingOpenTabOptions {
|
|
95
|
+
bindingId?: string;
|
|
96
|
+
url?: string;
|
|
97
|
+
active?: boolean;
|
|
98
|
+
focus?: boolean;
|
|
99
|
+
label?: string;
|
|
100
|
+
}
|
|
107
101
|
|
|
108
102
|
export interface SessionBindingResolveTargetOptions {
|
|
109
103
|
tabId?: number;
|
|
@@ -124,13 +118,13 @@ class SessionBindingManager {
|
|
|
124
118
|
return this.inspectBinding(bindingId);
|
|
125
119
|
}
|
|
126
120
|
|
|
127
|
-
async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
128
|
-
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
129
|
-
const repairActions: string[] = [];
|
|
130
|
-
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
131
|
-
const persisted = await this.storage.load(bindingId);
|
|
132
|
-
const created = !persisted;
|
|
133
|
-
let state = this.normalizeState(persisted, bindingId, options.label);
|
|
121
|
+
async ensureBinding(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
122
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
123
|
+
const repairActions: string[] = [];
|
|
124
|
+
const initialUrl = options.initialUrl ?? DEFAULT_SESSION_BINDING_URL;
|
|
125
|
+
const persisted = await this.storage.load(bindingId);
|
|
126
|
+
const created = !persisted;
|
|
127
|
+
let state = this.normalizeState(persisted, bindingId, options.label);
|
|
134
128
|
|
|
135
129
|
const originalWindowId = state.windowId;
|
|
136
130
|
let window = state.windowId !== null ? await this.waitForWindow(state.windowId) : null;
|
|
@@ -145,46 +139,53 @@ class SessionBindingManager {
|
|
|
145
139
|
}
|
|
146
140
|
}
|
|
147
141
|
}
|
|
142
|
+
if (!window) {
|
|
143
|
+
const activeWindow = await this.attachBindingToActiveWindow(state);
|
|
144
|
+
if (activeWindow) {
|
|
145
|
+
window = activeWindow;
|
|
146
|
+
repairActions.push('attached-active-window');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
148
149
|
if (!window) {
|
|
149
150
|
const sharedWindow = await this.findSharedBindingWindow(bindingId);
|
|
150
151
|
if (sharedWindow) {
|
|
151
152
|
state.windowId = sharedWindow.id;
|
|
152
153
|
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');
|
|
182
|
-
}
|
|
154
|
+
state.tabIds = [];
|
|
155
|
+
state.activeTabId = null;
|
|
156
|
+
state.primaryTabId = null;
|
|
157
|
+
window = sharedWindow;
|
|
158
|
+
repairActions.push('attached-shared-window');
|
|
159
|
+
} else {
|
|
160
|
+
const createdWindow = await this.browser.createWindow({
|
|
161
|
+
url: initialUrl,
|
|
162
|
+
focused: options.focus === true
|
|
163
|
+
});
|
|
164
|
+
state.windowId = createdWindow.id;
|
|
165
|
+
state.groupId = null;
|
|
166
|
+
state.tabIds = [];
|
|
167
|
+
state.activeTabId = null;
|
|
168
|
+
state.primaryTabId = null;
|
|
169
|
+
window = createdWindow;
|
|
170
|
+
const initialTab =
|
|
171
|
+
typeof createdWindow.initialTabId === 'number'
|
|
172
|
+
? await this.waitForTrackedTab(createdWindow.initialTabId, createdWindow.id)
|
|
173
|
+
: null;
|
|
174
|
+
tabs = initialTab ? [initialTab] : await this.waitForWindowTabs(createdWindow.id);
|
|
175
|
+
state.tabIds = tabs.map((tab) => tab.id);
|
|
176
|
+
if (state.primaryTabId === null) {
|
|
177
|
+
state.primaryTabId = initialTab?.id ?? tabs[0]?.id ?? null;
|
|
178
|
+
}
|
|
179
|
+
if (state.activeTabId === null) {
|
|
180
|
+
state.activeTabId = tabs.find((tab) => tab.active)?.id ?? initialTab?.id ?? tabs[0]?.id ?? null;
|
|
181
|
+
}
|
|
182
|
+
repairActions.push(created ? 'created-window' : 'recreated-window');
|
|
183
|
+
}
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
tabs = tabs.length > 0 ? tabs : await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
186
187
|
const recoveredTabs = await this.recoverBindingTabs(state, tabs);
|
|
187
|
-
if (recoveredTabs.length > tabs.length) {
|
|
188
|
+
if (recoveredTabs.length > tabs.length) {
|
|
188
189
|
tabs = recoveredTabs;
|
|
189
190
|
repairActions.push('recovered-tracked-tabs');
|
|
190
191
|
}
|
|
@@ -193,26 +194,15 @@ class SessionBindingManager {
|
|
|
193
194
|
}
|
|
194
195
|
state.tabIds = tabs.map((tab) => tab.id);
|
|
195
196
|
|
|
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
197
|
if (tabs.length === 0) {
|
|
208
|
-
const primary = await this.createBindingTab({
|
|
209
|
-
windowId: state.windowId,
|
|
210
|
-
url: initialUrl,
|
|
211
|
-
active: true
|
|
212
|
-
});
|
|
213
|
-
tabs = [primary];
|
|
214
|
-
state.tabIds = [primary.id];
|
|
215
|
-
state.primaryTabId = primary.id;
|
|
198
|
+
const primary = await this.createBindingTab({
|
|
199
|
+
windowId: state.windowId,
|
|
200
|
+
url: initialUrl,
|
|
201
|
+
active: options.focus === true
|
|
202
|
+
});
|
|
203
|
+
tabs = [primary];
|
|
204
|
+
state.tabIds = [primary.id];
|
|
205
|
+
state.primaryTabId = primary.id;
|
|
216
206
|
state.activeTabId = primary.id;
|
|
217
207
|
repairActions.push('created-primary-tab');
|
|
218
208
|
}
|
|
@@ -227,7 +217,7 @@ class SessionBindingManager {
|
|
|
227
217
|
repairActions.push('reassigned-active-tab');
|
|
228
218
|
}
|
|
229
219
|
|
|
230
|
-
let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
220
|
+
let group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
231
221
|
if (!group || group.windowId !== state.windowId) {
|
|
232
222
|
const groupId = await this.browser.groupTabs(tabs.map((tab) => tab.id));
|
|
233
223
|
group = await this.browser.updateGroup(groupId, {
|
|
@@ -265,12 +255,12 @@ class SessionBindingManager {
|
|
|
265
255
|
}
|
|
266
256
|
state.tabIds = [...new Set(tabs.map((tab) => tab.id))];
|
|
267
257
|
|
|
268
|
-
if (options.focus === true && state.activeTabId !== null) {
|
|
269
|
-
await this.
|
|
270
|
-
window = await this.
|
|
271
|
-
void window;
|
|
272
|
-
repairActions.push('focused-window');
|
|
273
|
-
}
|
|
258
|
+
if (options.focus === true && state.windowId !== null && state.activeTabId !== null) {
|
|
259
|
+
await this.focusBindingWindow(state.windowId, state.activeTabId);
|
|
260
|
+
window = await this.waitForWindow(state.windowId, 300);
|
|
261
|
+
void window;
|
|
262
|
+
repairActions.push('focused-window');
|
|
263
|
+
}
|
|
274
264
|
|
|
275
265
|
await this.storage.save(state);
|
|
276
266
|
|
|
@@ -285,15 +275,15 @@ class SessionBindingManager {
|
|
|
285
275
|
};
|
|
286
276
|
}
|
|
287
277
|
|
|
288
|
-
async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
|
|
289
|
-
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
290
|
-
const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
|
|
291
|
-
const ensured = await this.ensureBinding({
|
|
292
|
-
bindingId,
|
|
293
|
-
focus: false,
|
|
294
|
-
initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
|
|
295
|
-
label: options.label
|
|
296
|
-
});
|
|
278
|
+
async openTab(options: SessionBindingOpenTabOptions = {}): Promise<{ binding: SessionBindingInfo; tab: SessionBindingTab }> {
|
|
279
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
280
|
+
const hadBinding = (await this.loadBindingRecord(bindingId)) !== null;
|
|
281
|
+
const ensured = await this.ensureBinding({
|
|
282
|
+
bindingId,
|
|
283
|
+
focus: false,
|
|
284
|
+
initialUrl: hadBinding ? options.url ?? DEFAULT_SESSION_BINDING_URL : DEFAULT_SESSION_BINDING_URL,
|
|
285
|
+
label: options.label
|
|
286
|
+
});
|
|
297
287
|
let state = { ...ensured.binding, tabIds: [...ensured.binding.tabIds], tabs: [...ensured.binding.tabs] };
|
|
298
288
|
if (state.windowId !== null && state.tabs.length === 0) {
|
|
299
289
|
const rebound = await this.rebindBindingWindow(state);
|
|
@@ -305,12 +295,12 @@ class SessionBindingManager {
|
|
|
305
295
|
}
|
|
306
296
|
const active = options.active === true;
|
|
307
297
|
const desiredUrl = options.url ?? DEFAULT_SESSION_BINDING_URL;
|
|
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
|
-
);
|
|
298
|
+
let reusablePrimaryTab = await this.resolveReusablePrimaryTab(
|
|
299
|
+
state,
|
|
300
|
+
ensured.created ||
|
|
301
|
+
ensured.repairActions.includes('recreated-window') ||
|
|
302
|
+
ensured.repairActions.includes('created-primary-tab')
|
|
303
|
+
);
|
|
314
304
|
|
|
315
305
|
let createdTab: SessionBindingTab;
|
|
316
306
|
try {
|
|
@@ -328,12 +318,12 @@ class SessionBindingManager {
|
|
|
328
318
|
if (!this.isMissingWindowError(error)) {
|
|
329
319
|
throw error;
|
|
330
320
|
}
|
|
331
|
-
const repaired = await this.ensureBinding({
|
|
332
|
-
bindingId,
|
|
333
|
-
focus: false,
|
|
334
|
-
initialUrl: desiredUrl,
|
|
335
|
-
label: options.label
|
|
336
|
-
});
|
|
321
|
+
const repaired = await this.ensureBinding({
|
|
322
|
+
bindingId,
|
|
323
|
+
focus: false,
|
|
324
|
+
initialUrl: desiredUrl,
|
|
325
|
+
label: options.label
|
|
326
|
+
});
|
|
337
327
|
state = { ...repaired.binding };
|
|
338
328
|
reusablePrimaryTab = await this.resolveReusablePrimaryTab(state, true);
|
|
339
329
|
createdTab = reusablePrimaryTab
|
|
@@ -361,14 +351,13 @@ class SessionBindingManager {
|
|
|
361
351
|
windowId: state.windowId,
|
|
362
352
|
groupId,
|
|
363
353
|
tabIds: nextTabIds,
|
|
364
|
-
activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
|
|
354
|
+
activeTabId: active || options.focus === true ? createdTab.id : state.activeTabId ?? state.primaryTabId ?? createdTab.id,
|
|
365
355
|
primaryTabId: state.primaryTabId ?? createdTab.id
|
|
366
356
|
};
|
|
367
357
|
|
|
368
|
-
if (options.focus === true) {
|
|
369
|
-
await this.
|
|
370
|
-
|
|
371
|
-
}
|
|
358
|
+
if (options.focus === true && state.windowId !== null) {
|
|
359
|
+
await this.focusBindingWindow(state.windowId, createdTab.id);
|
|
360
|
+
}
|
|
372
361
|
|
|
373
362
|
await this.storage.save(nextState);
|
|
374
363
|
const tabs = await this.readTrackedTabs(nextState.tabIds, nextState.windowId);
|
|
@@ -442,96 +431,136 @@ class SessionBindingManager {
|
|
|
442
431
|
}
|
|
443
432
|
|
|
444
433
|
async focus(bindingId: string): Promise<{ ok: true; binding: SessionBindingInfo }> {
|
|
445
|
-
const
|
|
446
|
-
|
|
447
|
-
await this.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
434
|
+
const normalizedBindingId = this.normalizeBindingId(bindingId);
|
|
435
|
+
let binding =
|
|
436
|
+
(await this.inspectBinding(normalizedBindingId)) ??
|
|
437
|
+
(
|
|
438
|
+
await this.ensureBinding({
|
|
439
|
+
bindingId: normalizedBindingId,
|
|
440
|
+
focus: false
|
|
441
|
+
})
|
|
442
|
+
).binding;
|
|
443
|
+
let targetTabId = this.resolveFocusTabId(binding);
|
|
455
444
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
: binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
|
|
465
|
-
if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
|
|
466
|
-
throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
|
|
445
|
+
if (binding.windowId === null || targetTabId === null) {
|
|
446
|
+
binding = (
|
|
447
|
+
await this.ensureBinding({
|
|
448
|
+
bindingId: normalizedBindingId,
|
|
449
|
+
focus: false
|
|
450
|
+
})
|
|
451
|
+
).binding;
|
|
452
|
+
targetTabId = this.resolveFocusTabId(binding);
|
|
467
453
|
}
|
|
468
454
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
455
|
+
if (binding.windowId !== null && targetTabId !== null) {
|
|
456
|
+
try {
|
|
457
|
+
await this.focusBindingWindow(binding.windowId, targetTabId);
|
|
458
|
+
} catch (error) {
|
|
459
|
+
if (!this.isMissingWindowError(error)) {
|
|
460
|
+
throw error;
|
|
461
|
+
}
|
|
462
|
+
binding = (
|
|
463
|
+
await this.ensureBinding({
|
|
464
|
+
bindingId: normalizedBindingId,
|
|
465
|
+
focus: false
|
|
466
|
+
})
|
|
467
|
+
).binding;
|
|
468
|
+
targetTabId = this.resolveFocusTabId(binding);
|
|
469
|
+
if (binding.windowId !== null && targetTabId !== null) {
|
|
470
|
+
await this.focusBindingWindow(binding.windowId, targetTabId);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
478
473
|
}
|
|
479
474
|
|
|
480
|
-
const
|
|
481
|
-
const nextPrimaryTabId =
|
|
482
|
-
binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
483
|
-
const nextActiveTabId =
|
|
484
|
-
binding.activeTabId === resolvedTabId
|
|
485
|
-
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
486
|
-
: binding.activeTabId;
|
|
487
|
-
const nextState: SessionBindingRecord = {
|
|
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,
|
|
493
|
-
tabIds: tabs.map((candidate) => candidate.id),
|
|
494
|
-
activeTabId: nextActiveTabId,
|
|
495
|
-
primaryTabId: nextPrimaryTabId
|
|
496
|
-
};
|
|
497
|
-
await this.storage.save(nextState);
|
|
475
|
+
const refreshed = (await this.inspectBinding(normalizedBindingId)) ?? binding;
|
|
498
476
|
return {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
477
|
+
ok: true,
|
|
478
|
+
binding:
|
|
479
|
+
refreshed.windowId !== null && targetTabId !== null
|
|
480
|
+
? this.withFocusedTab(refreshed, targetTabId)
|
|
481
|
+
: refreshed
|
|
504
482
|
};
|
|
505
483
|
}
|
|
506
|
-
|
|
507
|
-
async
|
|
508
|
-
const
|
|
509
|
-
|
|
484
|
+
|
|
485
|
+
async closeTab(bindingId: string, tabId?: number): Promise<{ binding: SessionBindingInfo | null; closedTabId: number }> {
|
|
486
|
+
const binding = await this.inspectBinding(bindingId);
|
|
487
|
+
if (!binding) {
|
|
488
|
+
throw new Error(`Binding ${bindingId} does not exist`);
|
|
489
|
+
}
|
|
490
|
+
const resolvedTabId =
|
|
491
|
+
typeof tabId === 'number'
|
|
492
|
+
? tabId
|
|
493
|
+
: binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id;
|
|
494
|
+
if (typeof resolvedTabId !== 'number' || !binding.tabIds.includes(resolvedTabId)) {
|
|
495
|
+
throw new Error(`Tab ${tabId ?? 'active'} does not belong to binding ${bindingId}`);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await this.browser.closeTab(resolvedTabId);
|
|
499
|
+
const remainingTabIds = binding.tabIds.filter((candidate) => candidate !== resolvedTabId);
|
|
500
|
+
|
|
501
|
+
if (remainingTabIds.length === 0) {
|
|
502
|
+
await this.storage.delete(binding.id);
|
|
503
|
+
return {
|
|
504
|
+
binding: null,
|
|
505
|
+
closedTabId: resolvedTabId
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const tabs = await this.readLooseTrackedTabs(remainingTabIds);
|
|
510
|
+
const nextPrimaryTabId =
|
|
511
|
+
binding.primaryTabId === resolvedTabId ? tabs[0]?.id ?? null : binding.primaryTabId;
|
|
512
|
+
const nextActiveTabId =
|
|
513
|
+
binding.activeTabId === resolvedTabId
|
|
514
|
+
? tabs.find((candidate) => candidate.active)?.id ?? nextPrimaryTabId ?? tabs[0]?.id ?? null
|
|
515
|
+
: binding.activeTabId;
|
|
516
|
+
const nextState: SessionBindingRecord = {
|
|
517
|
+
id: binding.id,
|
|
518
|
+
label: binding.label,
|
|
519
|
+
color: binding.color,
|
|
520
|
+
windowId: tabs[0]?.windowId ?? binding.windowId,
|
|
521
|
+
groupId: tabs[0]?.groupId ?? binding.groupId,
|
|
522
|
+
tabIds: tabs.map((candidate) => candidate.id),
|
|
523
|
+
activeTabId: nextActiveTabId,
|
|
524
|
+
primaryTabId: nextPrimaryTabId
|
|
525
|
+
};
|
|
526
|
+
await this.storage.save(nextState);
|
|
527
|
+
return {
|
|
528
|
+
binding: {
|
|
529
|
+
...nextState,
|
|
530
|
+
tabs
|
|
531
|
+
},
|
|
532
|
+
closedTabId: resolvedTabId
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async reset(options: SessionBindingEnsureOptions = {}): Promise<SessionBindingEnsureResult> {
|
|
537
|
+
const bindingId = this.normalizeBindingId(options.bindingId);
|
|
538
|
+
await this.close(bindingId);
|
|
510
539
|
return this.ensureBinding({
|
|
511
540
|
...options,
|
|
512
541
|
bindingId
|
|
513
542
|
});
|
|
514
543
|
}
|
|
515
544
|
|
|
516
|
-
async close(bindingId: string): Promise<{ ok: true }> {
|
|
517
|
-
const state = await this.loadBindingRecord(bindingId);
|
|
518
|
-
if (!state) {
|
|
519
|
-
await this.storage.delete(bindingId);
|
|
520
|
-
return { ok: true };
|
|
521
|
-
}
|
|
522
|
-
// Clear persisted state before closing the window so tab/window removal
|
|
523
|
-
// listeners cannot race and resurrect an empty binding record.
|
|
524
|
-
await this.storage.delete(bindingId);
|
|
525
|
-
const trackedTabs = await this.collectBindingTabsForClose(state);
|
|
526
|
-
for (const trackedTab of trackedTabs) {
|
|
527
|
-
try {
|
|
528
|
-
await this.browser.closeTab(trackedTab.id);
|
|
529
|
-
} catch {
|
|
530
|
-
// Ignore tabs that were already removed before explicit close.
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return { ok: true };
|
|
534
|
-
}
|
|
545
|
+
async close(bindingId: string): Promise<{ ok: true }> {
|
|
546
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
547
|
+
if (!state) {
|
|
548
|
+
await this.storage.delete(bindingId);
|
|
549
|
+
return { ok: true };
|
|
550
|
+
}
|
|
551
|
+
// Clear persisted state before closing the window so tab/window removal
|
|
552
|
+
// listeners cannot race and resurrect an empty binding record.
|
|
553
|
+
await this.storage.delete(bindingId);
|
|
554
|
+
const trackedTabs = await this.collectBindingTabsForClose(state);
|
|
555
|
+
for (const trackedTab of trackedTabs) {
|
|
556
|
+
try {
|
|
557
|
+
await this.browser.closeTab(trackedTab.id);
|
|
558
|
+
} catch {
|
|
559
|
+
// Ignore tabs that were already removed before explicit close.
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return { ok: true };
|
|
563
|
+
}
|
|
535
564
|
|
|
536
565
|
async resolveTarget(options: SessionBindingResolveTargetOptions = {}): Promise<SessionBindingTargetResolution> {
|
|
537
566
|
if (typeof options.tabId === 'number') {
|
|
@@ -584,13 +613,13 @@ class SessionBindingManager {
|
|
|
584
613
|
return candidate;
|
|
585
614
|
}
|
|
586
615
|
|
|
587
|
-
private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
|
|
588
|
-
return {
|
|
589
|
-
id: bindingId,
|
|
590
|
-
label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
591
|
-
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
592
|
-
windowId: state?.windowId ?? null,
|
|
593
|
-
groupId: state?.groupId ?? null,
|
|
616
|
+
private normalizeState(state: SessionBindingRecord | null, bindingId: string, label?: string): SessionBindingRecord {
|
|
617
|
+
return {
|
|
618
|
+
id: bindingId,
|
|
619
|
+
label: label?.trim() ? label.trim() : state?.label ?? DEFAULT_SESSION_BINDING_LABEL,
|
|
620
|
+
color: state?.color ?? DEFAULT_SESSION_BINDING_COLOR,
|
|
621
|
+
windowId: state?.windowId ?? null,
|
|
622
|
+
groupId: state?.groupId ?? null,
|
|
594
623
|
tabIds: state?.tabIds ?? [],
|
|
595
624
|
activeTabId: state?.activeTabId ?? null,
|
|
596
625
|
primaryTabId: state?.primaryTabId ?? null
|
|
@@ -687,7 +716,7 @@ class SessionBindingManager {
|
|
|
687
716
|
return [...new Set(state.tabIds.concat([state.activeTabId, state.primaryTabId].filter((value): value is number => typeof value === 'number')))];
|
|
688
717
|
}
|
|
689
718
|
|
|
690
|
-
private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
|
|
719
|
+
private async rebindBindingWindow(state: SessionBindingRecord): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] } | null> {
|
|
691
720
|
const candidateWindowIds: number[] = [];
|
|
692
721
|
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
693
722
|
if (typeof windowId !== 'number') {
|
|
@@ -698,7 +727,7 @@ class SessionBindingManager {
|
|
|
698
727
|
}
|
|
699
728
|
};
|
|
700
729
|
|
|
701
|
-
const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
730
|
+
const group = state.groupId !== null ? await this.waitForGroup(state.groupId) : null;
|
|
702
731
|
pushWindowId(group?.windowId);
|
|
703
732
|
|
|
704
733
|
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
@@ -707,13 +736,13 @@ class SessionBindingManager {
|
|
|
707
736
|
}
|
|
708
737
|
|
|
709
738
|
for (const candidateWindowId of candidateWindowIds) {
|
|
710
|
-
const window = await this.waitForWindow(candidateWindowId);
|
|
739
|
+
const window = await this.waitForWindow(candidateWindowId);
|
|
711
740
|
if (!window) {
|
|
712
741
|
continue;
|
|
713
742
|
}
|
|
714
743
|
let tabs = await this.readTrackedTabs(this.collectCandidateTabIds(state), candidateWindowId);
|
|
715
744
|
if (tabs.length === 0 && group?.id !== null && group?.windowId === candidateWindowId) {
|
|
716
|
-
const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
745
|
+
const windowTabs = await this.waitForWindowTabs(candidateWindowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
717
746
|
tabs = windowTabs.filter((tab) => tab.groupId === group.id);
|
|
718
747
|
}
|
|
719
748
|
if (tabs.length === 0) {
|
|
@@ -735,146 +764,21 @@ class SessionBindingManager {
|
|
|
735
764
|
return null;
|
|
736
765
|
}
|
|
737
766
|
|
|
738
|
-
private async
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
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(
|
|
776
|
-
state: SessionBindingRecord,
|
|
777
|
-
ownership: SessionBindingWindowOwnership,
|
|
778
|
-
initialUrl: string
|
|
779
|
-
): Promise<{ window: SessionBindingWindow; tabs: SessionBindingTab[] }> {
|
|
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
|
-
}));
|
|
789
|
-
const initialTab =
|
|
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);
|
|
767
|
+
private async attachBindingToActiveWindow(state: SessionBindingRecord): Promise<SessionBindingWindow | null> {
|
|
768
|
+
const activeTab = await this.browser.getActiveTab();
|
|
769
|
+
if (!activeTab) {
|
|
770
|
+
return null;
|
|
817
771
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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 });
|
|
772
|
+
const window = await this.waitForWindow(activeTab.windowId, 300);
|
|
773
|
+
if (!window) {
|
|
774
|
+
return null;
|
|
827
775
|
}
|
|
828
|
-
|
|
829
776
|
state.windowId = window.id;
|
|
830
777
|
state.groupId = null;
|
|
831
|
-
state.tabIds =
|
|
832
|
-
state.
|
|
833
|
-
state.
|
|
834
|
-
|
|
835
|
-
...state,
|
|
836
|
-
tabIds: [...state.tabIds]
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
for (const bindingTab of ownership.bindingTabs) {
|
|
840
|
-
try {
|
|
841
|
-
await this.browser.closeTab(bindingTab.id);
|
|
842
|
-
} catch {
|
|
843
|
-
// Ignore tabs that were already removed while evacuating the binding.
|
|
844
|
-
}
|
|
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;
|
|
778
|
+
state.tabIds = [];
|
|
779
|
+
state.activeTabId = null;
|
|
780
|
+
state.primaryTabId = null;
|
|
781
|
+
return window;
|
|
878
782
|
}
|
|
879
783
|
|
|
880
784
|
private async findSharedBindingWindow(bindingId: string, excludedWindowIds: number[] = []): Promise<SessionBindingWindow | null> {
|
|
@@ -886,51 +790,47 @@ class SessionBindingManager {
|
|
|
886
790
|
if (typeof windowId !== 'number') {
|
|
887
791
|
return;
|
|
888
792
|
}
|
|
889
|
-
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
candidateWindowIds.push(windowId);
|
|
893
|
-
};
|
|
793
|
+
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
candidateWindowIds.push(windowId);
|
|
797
|
+
};
|
|
894
798
|
|
|
895
799
|
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
800
|
const trackedTabIds = this.collectCandidateTabIds(peer);
|
|
801
|
+
pushWindowId(peer.windowId);
|
|
903
802
|
for (const trackedTabId of trackedTabIds) {
|
|
904
803
|
peerTabIds.add(trackedTabId);
|
|
905
804
|
}
|
|
805
|
+
if (peer.groupId !== null) {
|
|
806
|
+
const group = await this.waitForGroup(peer.groupId, 300);
|
|
807
|
+
if (group) {
|
|
808
|
+
peerGroupIds.add(group.id);
|
|
809
|
+
pushWindowId(group.windowId);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
906
812
|
const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
|
|
907
813
|
for (const tab of trackedTabs) {
|
|
908
814
|
pushWindowId(tab.windowId);
|
|
909
815
|
}
|
|
910
816
|
}
|
|
911
|
-
|
|
817
|
+
|
|
912
818
|
for (const windowId of candidateWindowIds) {
|
|
913
819
|
const window = await this.waitForWindow(windowId, 300);
|
|
914
820
|
if (window) {
|
|
915
|
-
const windowTabs = await this.
|
|
821
|
+
const windowTabs = await this.readStableWindowTabs(window.id, WINDOW_TABS_LOOKUP_TIMEOUT_MS, 150);
|
|
916
822
|
const ownedTabs = windowTabs.filter(
|
|
917
823
|
(tab) => peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))
|
|
918
824
|
);
|
|
919
825
|
if (ownedTabs.length === 0) {
|
|
920
826
|
continue;
|
|
921
827
|
}
|
|
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
828
|
return window;
|
|
929
829
|
}
|
|
930
830
|
}
|
|
931
|
-
|
|
932
|
-
return null;
|
|
933
|
-
}
|
|
831
|
+
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
934
834
|
|
|
935
835
|
private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
|
|
936
836
|
if (state.windowId === null) {
|
|
@@ -964,19 +864,43 @@ class SessionBindingManager {
|
|
|
964
864
|
return existingTabs;
|
|
965
865
|
}
|
|
966
866
|
|
|
967
|
-
private
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
867
|
+
private resolveFocusTabId(binding: Pick<SessionBindingInfo, 'activeTabId' | 'primaryTabId' | 'tabs'>): number | null {
|
|
868
|
+
return binding.activeTabId ?? binding.primaryTabId ?? binding.tabs[0]?.id ?? null;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
private withFocusedTab(binding: SessionBindingInfo, targetTabId: number): SessionBindingInfo {
|
|
872
|
+
return {
|
|
873
|
+
...binding,
|
|
874
|
+
activeTabId: targetTabId,
|
|
875
|
+
tabs: binding.tabs.map((tab) => ({
|
|
876
|
+
...tab,
|
|
877
|
+
active: tab.id === targetTabId
|
|
878
|
+
}))
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
private async focusBindingWindow(windowId: number, tabId: number): Promise<void> {
|
|
883
|
+
await this.browser.updateWindow(windowId, { focused: true });
|
|
884
|
+
await this.browser.updateTab(tabId, { active: true });
|
|
885
|
+
const focusedTab = await this.waitForTrackedTab(tabId, windowId, 500);
|
|
886
|
+
if (!focusedTab?.active) {
|
|
887
|
+
await this.browser.updateTab(tabId, { active: true });
|
|
977
888
|
}
|
|
978
|
-
return [...merged.values()];
|
|
979
889
|
}
|
|
890
|
+
|
|
891
|
+
private async collectBindingTabsForClose(state: SessionBindingRecord): Promise<SessionBindingTab[]> {
|
|
892
|
+
const trackedTabs = await this.readLooseTrackedTabs(this.collectCandidateTabIds(state));
|
|
893
|
+
if (state.windowId === null || state.groupId === null) {
|
|
894
|
+
return trackedTabs;
|
|
895
|
+
}
|
|
896
|
+
const windowTabs = await this.waitForWindowTabs(state.windowId, 300);
|
|
897
|
+
const groupedTabs = windowTabs.filter((tab) => tab.groupId === state.groupId);
|
|
898
|
+
const merged = new Map<number, SessionBindingTab>();
|
|
899
|
+
for (const tab of [...trackedTabs, ...groupedTabs]) {
|
|
900
|
+
merged.set(tab.id, tab);
|
|
901
|
+
}
|
|
902
|
+
return [...merged.values()];
|
|
903
|
+
}
|
|
980
904
|
|
|
981
905
|
private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
|
|
982
906
|
if (options.windowId === null) {
|
|
@@ -1005,18 +929,18 @@ class SessionBindingManager {
|
|
|
1005
929
|
throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
|
|
1006
930
|
}
|
|
1007
931
|
|
|
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
|
-
}
|
|
932
|
+
private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
|
|
933
|
+
const state = await this.loadBindingRecord(bindingId);
|
|
934
|
+
if (!state) {
|
|
935
|
+
return null;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
939
|
+
tabs = await this.recoverBindingTabs(state, tabs);
|
|
940
|
+
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
941
|
+
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
942
|
+
tabs = [...tabs, activeTab];
|
|
943
|
+
}
|
|
1020
944
|
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
1021
945
|
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
|
|
1022
946
|
if (primaryTab) {
|
|
@@ -1036,12 +960,12 @@ class SessionBindingManager {
|
|
|
1036
960
|
return null;
|
|
1037
961
|
}
|
|
1038
962
|
if (binding.primaryTabId !== null) {
|
|
1039
|
-
const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? (await this.waitForTrackedTab(binding.primaryTabId, binding.windowId));
|
|
963
|
+
const trackedPrimary = binding.tabs.find((tab) => tab.id === binding.primaryTabId) ?? (await this.waitForTrackedTab(binding.primaryTabId, binding.windowId));
|
|
1040
964
|
if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
|
|
1041
965
|
return trackedPrimary;
|
|
1042
966
|
}
|
|
1043
967
|
}
|
|
1044
|
-
const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
968
|
+
const windowTabs = await this.waitForWindowTabs(binding.windowId, WINDOW_TABS_LOOKUP_TIMEOUT_MS);
|
|
1045
969
|
if (windowTabs.length !== 1) {
|
|
1046
970
|
return null;
|
|
1047
971
|
}
|
|
@@ -1060,29 +984,29 @@ class SessionBindingManager {
|
|
|
1060
984
|
return normalizedUrl === '' || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
|
|
1061
985
|
}
|
|
1062
986
|
|
|
1063
|
-
private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
|
|
1064
|
-
const deadline = Date.now() + timeoutMs;
|
|
1065
|
-
while (Date.now() < deadline) {
|
|
1066
|
-
const window = await this.browser.getWindow(windowId);
|
|
1067
|
-
if (window) {
|
|
1068
|
-
return window;
|
|
987
|
+
private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
|
|
988
|
+
const deadline = Date.now() + timeoutMs;
|
|
989
|
+
while (Date.now() < deadline) {
|
|
990
|
+
const window = await this.browser.getWindow(windowId);
|
|
991
|
+
if (window) {
|
|
992
|
+
return window;
|
|
1069
993
|
}
|
|
1070
994
|
await this.delay(50);
|
|
1071
995
|
}
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
|
|
1076
|
-
const deadline = Date.now() + timeoutMs;
|
|
1077
|
-
while (Date.now() < deadline) {
|
|
1078
|
-
const group = await this.browser.getGroup(groupId);
|
|
1079
|
-
if (group) {
|
|
1080
|
-
return group;
|
|
1081
|
-
}
|
|
1082
|
-
await this.delay(50);
|
|
1083
|
-
}
|
|
1084
|
-
return null;
|
|
1085
|
-
}
|
|
996
|
+
return null;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
|
|
1000
|
+
const deadline = Date.now() + timeoutMs;
|
|
1001
|
+
while (Date.now() < deadline) {
|
|
1002
|
+
const group = await this.browser.getGroup(groupId);
|
|
1003
|
+
if (group) {
|
|
1004
|
+
return group;
|
|
1005
|
+
}
|
|
1006
|
+
await this.delay(50);
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1086
1010
|
|
|
1087
1011
|
private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<SessionBindingTab | null> {
|
|
1088
1012
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1097,16 +1021,55 @@ class SessionBindingManager {
|
|
|
1097
1021
|
}
|
|
1098
1022
|
|
|
1099
1023
|
private async waitForWindowTabs(windowId: number, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS): Promise<SessionBindingTab[]> {
|
|
1100
|
-
const deadline = Date.now() + timeoutMs;
|
|
1101
|
-
while (Date.now() < deadline) {
|
|
1102
|
-
const tabs = await this.browser.listTabs({ windowId });
|
|
1103
|
-
if (tabs.length > 0) {
|
|
1024
|
+
const deadline = Date.now() + timeoutMs;
|
|
1025
|
+
while (Date.now() < deadline) {
|
|
1026
|
+
const tabs = await this.browser.listTabs({ windowId });
|
|
1027
|
+
if (tabs.length > 0) {
|
|
1104
1028
|
return tabs;
|
|
1105
1029
|
}
|
|
1106
1030
|
await this.delay(50);
|
|
1107
|
-
}
|
|
1108
|
-
return [];
|
|
1109
|
-
}
|
|
1031
|
+
}
|
|
1032
|
+
return [];
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
private async readStableWindowTabs(
|
|
1036
|
+
windowId: number,
|
|
1037
|
+
timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS,
|
|
1038
|
+
settleMs = 150
|
|
1039
|
+
): Promise<SessionBindingTab[]> {
|
|
1040
|
+
const deadline = Date.now() + timeoutMs;
|
|
1041
|
+
let bestTabs: SessionBindingTab[] = [];
|
|
1042
|
+
let bestSignature = '';
|
|
1043
|
+
let lastSignature = '';
|
|
1044
|
+
let stableSince = 0;
|
|
1045
|
+
|
|
1046
|
+
while (Date.now() < deadline) {
|
|
1047
|
+
const tabs = await this.browser.listTabs({ windowId });
|
|
1048
|
+
if (tabs.length > 0) {
|
|
1049
|
+
const signature = this.windowTabSnapshotSignature(tabs);
|
|
1050
|
+
if (tabs.length > bestTabs.length || (tabs.length === bestTabs.length && signature !== bestSignature)) {
|
|
1051
|
+
bestTabs = tabs;
|
|
1052
|
+
bestSignature = signature;
|
|
1053
|
+
}
|
|
1054
|
+
if (signature !== lastSignature) {
|
|
1055
|
+
lastSignature = signature;
|
|
1056
|
+
stableSince = Date.now();
|
|
1057
|
+
} else if (Date.now() - stableSince >= settleMs) {
|
|
1058
|
+
return bestTabs;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
await this.delay(50);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
return bestTabs;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
private windowTabSnapshotSignature(tabs: SessionBindingTab[]): string {
|
|
1068
|
+
return [...tabs]
|
|
1069
|
+
.map((tab) => `${tab.id}:${tab.groupId ?? 'ungrouped'}`)
|
|
1070
|
+
.sort()
|
|
1071
|
+
.join('|');
|
|
1072
|
+
}
|
|
1110
1073
|
|
|
1111
1074
|
private async delay(ms: number): Promise<void> {
|
|
1112
1075
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1118,4 +1081,4 @@ class SessionBindingManager {
|
|
|
1118
1081
|
}
|
|
1119
1082
|
}
|
|
1120
1083
|
|
|
1121
|
-
export { SessionBindingManager };
|
|
1084
|
+
export { SessionBindingManager };
|