@flrande/bak-extension 0.6.9 → 0.6.11
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 +127 -205
- package/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/src/background.ts +1949 -1991
- package/src/session-binding.ts +387 -437
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> {
|
|
@@ -882,68 +786,51 @@ class SessionBindingManager {
|
|
|
882
786
|
const candidateWindowIds: number[] = [];
|
|
883
787
|
const peerTabIds = new Set<number>();
|
|
884
788
|
const peerGroupIds = new Set<number>();
|
|
885
|
-
const reusablePeerIds = new Set<string>();
|
|
886
789
|
const pushWindowId = (windowId: number | null | undefined): void => {
|
|
887
790
|
if (typeof windowId !== 'number') {
|
|
888
791
|
return;
|
|
889
792
|
}
|
|
890
|
-
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
candidateWindowIds.push(windowId);
|
|
894
|
-
};
|
|
895
|
-
|
|
896
|
-
for (const peer of peers) {
|
|
897
|
-
if (peer.groupId === null) {
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
const group = await this.waitForGroup(peer.groupId, 300);
|
|
901
|
-
if (!group) {
|
|
902
|
-
continue;
|
|
903
|
-
}
|
|
904
|
-
reusablePeerIds.add(peer.id);
|
|
905
|
-
peerGroupIds.add(group.id);
|
|
906
|
-
pushWindowId(group.windowId);
|
|
907
|
-
}
|
|
793
|
+
if (excludedWindowIds.includes(windowId) || candidateWindowIds.includes(windowId)) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
candidateWindowIds.push(windowId);
|
|
797
|
+
};
|
|
908
798
|
|
|
909
799
|
for (const peer of peers) {
|
|
910
|
-
if (!reusablePeerIds.has(peer.id)) {
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
800
|
const trackedTabIds = this.collectCandidateTabIds(peer);
|
|
801
|
+
pushWindowId(peer.windowId);
|
|
914
802
|
for (const trackedTabId of trackedTabIds) {
|
|
915
803
|
peerTabIds.add(trackedTabId);
|
|
916
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
|
+
}
|
|
917
812
|
const trackedTabs = await this.readLooseTrackedTabs(trackedTabIds);
|
|
918
813
|
for (const tab of trackedTabs) {
|
|
919
|
-
|
|
920
|
-
pushWindowId(tab.windowId);
|
|
921
|
-
}
|
|
814
|
+
pushWindowId(tab.windowId);
|
|
922
815
|
}
|
|
923
816
|
}
|
|
924
|
-
|
|
817
|
+
|
|
925
818
|
for (const windowId of candidateWindowIds) {
|
|
926
819
|
const window = await this.waitForWindow(windowId, 300);
|
|
927
820
|
if (window) {
|
|
928
|
-
const windowTabs = await this.
|
|
821
|
+
const windowTabs = await this.readStableWindowTabs(window.id, WINDOW_TABS_LOOKUP_TIMEOUT_MS, 150);
|
|
929
822
|
const ownedTabs = windowTabs.filter(
|
|
930
823
|
(tab) => peerTabIds.has(tab.id) || (tab.groupId !== null && peerGroupIds.has(tab.groupId))
|
|
931
824
|
);
|
|
932
825
|
if (ownedTabs.length === 0) {
|
|
933
826
|
continue;
|
|
934
827
|
}
|
|
935
|
-
const foreignTabs = windowTabs.filter(
|
|
936
|
-
(tab) => !peerTabIds.has(tab.id) && (tab.groupId === null || !peerGroupIds.has(tab.groupId))
|
|
937
|
-
);
|
|
938
|
-
if (foreignTabs.length > 0) {
|
|
939
|
-
continue;
|
|
940
|
-
}
|
|
941
828
|
return window;
|
|
942
829
|
}
|
|
943
830
|
}
|
|
944
|
-
|
|
945
|
-
return null;
|
|
946
|
-
}
|
|
831
|
+
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
947
834
|
|
|
948
835
|
private async recoverBindingTabs(state: SessionBindingRecord, existingTabs: SessionBindingTab[]): Promise<SessionBindingTab[]> {
|
|
949
836
|
if (state.windowId === null) {
|
|
@@ -977,19 +864,43 @@ class SessionBindingManager {
|
|
|
977
864
|
return existingTabs;
|
|
978
865
|
}
|
|
979
866
|
|
|
980
|
-
private
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
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 });
|
|
990
888
|
}
|
|
991
|
-
return [...merged.values()];
|
|
992
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
|
+
}
|
|
993
904
|
|
|
994
905
|
private async createBindingTab(options: { windowId: number | null; url: string; active: boolean }): Promise<SessionBindingTab> {
|
|
995
906
|
if (options.windowId === null) {
|
|
@@ -1018,18 +929,18 @@ class SessionBindingManager {
|
|
|
1018
929
|
throw lastError ?? new Error(`No window with id: ${options.windowId}.`);
|
|
1019
930
|
}
|
|
1020
931
|
|
|
1021
|
-
private async inspectBinding(bindingId: string): Promise<SessionBindingInfo | null> {
|
|
1022
|
-
const state = await this.loadBindingRecord(bindingId);
|
|
1023
|
-
if (!state) {
|
|
1024
|
-
return null;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
let tabs = await this.readTrackedTabs(state.tabIds, state.windowId);
|
|
1028
|
-
tabs = await this.recoverBindingTabs(state, tabs);
|
|
1029
|
-
const activeTab = state.activeTabId !== null ? await this.waitForTrackedTab(state.activeTabId, state.windowId, 300) : null;
|
|
1030
|
-
if (activeTab && !tabs.some((tab) => tab.id === activeTab.id)) {
|
|
1031
|
-
tabs = [...tabs, activeTab];
|
|
1032
|
-
}
|
|
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
|
+
}
|
|
1033
944
|
if (tabs.length === 0 && state.primaryTabId !== null) {
|
|
1034
945
|
const primaryTab = await this.waitForTrackedTab(state.primaryTabId, state.windowId, 300);
|
|
1035
946
|
if (primaryTab) {
|
|
@@ -1049,12 +960,12 @@ class SessionBindingManager {
|
|
|
1049
960
|
return null;
|
|
1050
961
|
}
|
|
1051
962
|
if (binding.primaryTabId !== null) {
|
|
1052
|
-
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));
|
|
1053
964
|
if (trackedPrimary && (allowReuse || this.isReusableBlankSessionBindingTab(trackedPrimary, binding))) {
|
|
1054
965
|
return trackedPrimary;
|
|
1055
966
|
}
|
|
1056
967
|
}
|
|
1057
|
-
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);
|
|
1058
969
|
if (windowTabs.length !== 1) {
|
|
1059
970
|
return null;
|
|
1060
971
|
}
|
|
@@ -1073,29 +984,29 @@ class SessionBindingManager {
|
|
|
1073
984
|
return normalizedUrl === '' || normalizedUrl === DEFAULT_SESSION_BINDING_URL;
|
|
1074
985
|
}
|
|
1075
986
|
|
|
1076
|
-
private async waitForWindow(windowId: number, timeoutMs = WINDOW_LOOKUP_TIMEOUT_MS): Promise<SessionBindingWindow | null> {
|
|
1077
|
-
const deadline = Date.now() + timeoutMs;
|
|
1078
|
-
while (Date.now() < deadline) {
|
|
1079
|
-
const window = await this.browser.getWindow(windowId);
|
|
1080
|
-
if (window) {
|
|
1081
|
-
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;
|
|
1082
993
|
}
|
|
1083
994
|
await this.delay(50);
|
|
1084
995
|
}
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
private async waitForGroup(groupId: number, timeoutMs = GROUP_LOOKUP_TIMEOUT_MS): Promise<SessionBindingGroup | null> {
|
|
1089
|
-
const deadline = Date.now() + timeoutMs;
|
|
1090
|
-
while (Date.now() < deadline) {
|
|
1091
|
-
const group = await this.browser.getGroup(groupId);
|
|
1092
|
-
if (group) {
|
|
1093
|
-
return group;
|
|
1094
|
-
}
|
|
1095
|
-
await this.delay(50);
|
|
1096
|
-
}
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
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
|
+
}
|
|
1099
1010
|
|
|
1100
1011
|
private async waitForTrackedTab(tabId: number, windowId: number | null, timeoutMs = 1_000): Promise<SessionBindingTab | null> {
|
|
1101
1012
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1110,16 +1021,55 @@ class SessionBindingManager {
|
|
|
1110
1021
|
}
|
|
1111
1022
|
|
|
1112
1023
|
private async waitForWindowTabs(windowId: number, timeoutMs = WINDOW_TABS_LOOKUP_TIMEOUT_MS): Promise<SessionBindingTab[]> {
|
|
1113
|
-
const deadline = Date.now() + timeoutMs;
|
|
1114
|
-
while (Date.now() < deadline) {
|
|
1115
|
-
const tabs = await this.browser.listTabs({ windowId });
|
|
1116
|
-
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) {
|
|
1117
1028
|
return tabs;
|
|
1118
1029
|
}
|
|
1119
1030
|
await this.delay(50);
|
|
1120
|
-
}
|
|
1121
|
-
return [];
|
|
1122
|
-
}
|
|
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
|
+
}
|
|
1123
1073
|
|
|
1124
1074
|
private async delay(ms: number): Promise<void> {
|
|
1125
1075
|
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1131,4 +1081,4 @@ class SessionBindingManager {
|
|
|
1131
1081
|
}
|
|
1132
1082
|
}
|
|
1133
1083
|
|
|
1134
|
-
export { SessionBindingManager };
|
|
1084
|
+
export { SessionBindingManager };
|