@flrande/bak-extension 0.6.3 → 0.6.5
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 +267 -55
- package/dist/manifest.json +1 -1
- package/dist/popup.global.js +92 -2
- package/dist/popup.html +72 -1
- package/package.json +2 -2
- package/public/popup.html +72 -1
- package/src/background.ts +355 -134
- package/src/popup.ts +135 -11
- package/src/session-binding.ts +132 -74
package/src/background.ts
CHANGED
|
@@ -59,11 +59,47 @@ interface ExtensionConfig {
|
|
|
59
59
|
debugRichText: boolean;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
interface RuntimeErrorDetails {
|
|
63
|
-
message: string;
|
|
64
|
-
context: 'config' | 'socket' | 'request' | 'parse';
|
|
65
|
-
at: number;
|
|
66
|
-
}
|
|
62
|
+
interface RuntimeErrorDetails {
|
|
63
|
+
message: string;
|
|
64
|
+
context: 'config' | 'socket' | 'request' | 'parse';
|
|
65
|
+
at: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PopupSessionBindingSummary {
|
|
69
|
+
id: string;
|
|
70
|
+
label: string;
|
|
71
|
+
tabCount: number;
|
|
72
|
+
activeTabId: number | null;
|
|
73
|
+
windowId: number | null;
|
|
74
|
+
groupId: number | null;
|
|
75
|
+
detached: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface PopupState {
|
|
79
|
+
ok: true;
|
|
80
|
+
connected: boolean;
|
|
81
|
+
connectionState: 'connected' | 'connecting' | 'reconnecting' | 'disconnected' | 'manual' | 'missing-token';
|
|
82
|
+
hasToken: boolean;
|
|
83
|
+
port: number;
|
|
84
|
+
wsUrl: string;
|
|
85
|
+
debugRichText: boolean;
|
|
86
|
+
lastError: string | null;
|
|
87
|
+
lastErrorAt: number | null;
|
|
88
|
+
lastErrorContext: RuntimeErrorDetails['context'] | null;
|
|
89
|
+
reconnectAttempt: number;
|
|
90
|
+
nextReconnectInMs: number | null;
|
|
91
|
+
manualDisconnect: boolean;
|
|
92
|
+
extensionVersion: string;
|
|
93
|
+
lastBindingUpdateAt: number | null;
|
|
94
|
+
lastBindingUpdateReason: string | null;
|
|
95
|
+
sessionBindings: {
|
|
96
|
+
count: number;
|
|
97
|
+
attachedCount: number;
|
|
98
|
+
detachedCount: number;
|
|
99
|
+
tabCount: number;
|
|
100
|
+
items: PopupSessionBindingSummary[];
|
|
101
|
+
};
|
|
102
|
+
}
|
|
67
103
|
|
|
68
104
|
const DEFAULT_PORT = 17373;
|
|
69
105
|
const STORAGE_KEY_TOKEN = 'pairToken';
|
|
@@ -112,10 +148,14 @@ const REPLAY_FORBIDDEN_HEADER_NAMES = new Set([
|
|
|
112
148
|
let ws: WebSocket | null = null;
|
|
113
149
|
let reconnectTimer: number | null = null;
|
|
114
150
|
let nextReconnectInMs: number | null = null;
|
|
151
|
+
let nextReconnectAt: number | null = null;
|
|
115
152
|
let reconnectAttempt = 0;
|
|
116
153
|
let lastError: RuntimeErrorDetails | null = null;
|
|
117
154
|
let manualDisconnect = false;
|
|
118
155
|
let sessionBindingStateMutationQueue: Promise<void> = Promise.resolve();
|
|
156
|
+
let preserveHumanFocusDepth = 0;
|
|
157
|
+
let lastBindingUpdateAt: number | null = null;
|
|
158
|
+
let lastBindingUpdateReason: string | null = null;
|
|
119
159
|
|
|
120
160
|
async function getConfig(): Promise<ExtensionConfig> {
|
|
121
161
|
const stored = await chrome.storage.local.get([STORAGE_KEY_TOKEN, STORAGE_KEY_PORT, STORAGE_KEY_DEBUG_RICH_TEXT]);
|
|
@@ -150,19 +190,33 @@ function setRuntimeError(message: string, context: RuntimeErrorDetails['context'
|
|
|
150
190
|
};
|
|
151
191
|
}
|
|
152
192
|
|
|
153
|
-
function clearReconnectTimer(): void {
|
|
154
|
-
if (reconnectTimer !== null) {
|
|
155
|
-
clearTimeout(reconnectTimer);
|
|
156
|
-
reconnectTimer = null;
|
|
157
|
-
}
|
|
158
|
-
nextReconnectInMs = null;
|
|
159
|
-
|
|
193
|
+
function clearReconnectTimer(): void {
|
|
194
|
+
if (reconnectTimer !== null) {
|
|
195
|
+
clearTimeout(reconnectTimer);
|
|
196
|
+
reconnectTimer = null;
|
|
197
|
+
}
|
|
198
|
+
nextReconnectInMs = null;
|
|
199
|
+
nextReconnectAt = null;
|
|
200
|
+
}
|
|
160
201
|
|
|
161
|
-
function sendResponse(payload: CliResponse): void {
|
|
162
|
-
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
163
|
-
ws.send(JSON.stringify(payload));
|
|
164
|
-
}
|
|
165
|
-
}
|
|
202
|
+
function sendResponse(payload: CliResponse): void {
|
|
203
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
204
|
+
ws.send(JSON.stringify(payload));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sendEvent(event: string, data: Record<string, unknown>): void {
|
|
209
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
210
|
+
ws.send(
|
|
211
|
+
JSON.stringify({
|
|
212
|
+
type: 'event',
|
|
213
|
+
event,
|
|
214
|
+
data,
|
|
215
|
+
ts: Date.now()
|
|
216
|
+
})
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
166
220
|
|
|
167
221
|
function toError(code: string, message: string, data?: Record<string, unknown>): CliResponse['error'] {
|
|
168
222
|
return { code, message, data };
|
|
@@ -250,9 +304,71 @@ async function loadSessionBindingState(bindingId: string): Promise<SessionBindin
|
|
|
250
304
|
return stateMap[bindingId] ?? null;
|
|
251
305
|
}
|
|
252
306
|
|
|
253
|
-
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
254
|
-
return Object.values(await loadSessionBindingStateMap());
|
|
255
|
-
}
|
|
307
|
+
async function listSessionBindingStates(): Promise<SessionBindingRecord[]> {
|
|
308
|
+
return Object.values(await loadSessionBindingStateMap());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function summarizeSessionBindings(states: SessionBindingRecord[]): PopupState['sessionBindings'] {
|
|
312
|
+
const items = states.map((state) => {
|
|
313
|
+
const detached = state.windowId === null || state.tabIds.length === 0;
|
|
314
|
+
return {
|
|
315
|
+
id: state.id,
|
|
316
|
+
label: state.label,
|
|
317
|
+
tabCount: state.tabIds.length,
|
|
318
|
+
activeTabId: state.activeTabId,
|
|
319
|
+
windowId: state.windowId,
|
|
320
|
+
groupId: state.groupId,
|
|
321
|
+
detached
|
|
322
|
+
} satisfies PopupSessionBindingSummary;
|
|
323
|
+
});
|
|
324
|
+
return {
|
|
325
|
+
count: items.length,
|
|
326
|
+
attachedCount: items.filter((item) => !item.detached).length,
|
|
327
|
+
detachedCount: items.filter((item) => item.detached).length,
|
|
328
|
+
tabCount: items.reduce((sum, item) => sum + item.tabCount, 0),
|
|
329
|
+
items
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function buildPopupState(): Promise<PopupState> {
|
|
334
|
+
const config = await getConfig();
|
|
335
|
+
const sessionBindings = summarizeSessionBindings(await listSessionBindingStates());
|
|
336
|
+
const reconnectRemainingMs = nextReconnectAt === null ? null : Math.max(0, nextReconnectAt - Date.now());
|
|
337
|
+
let connectionState: PopupState['connectionState'];
|
|
338
|
+
if (!config.token) {
|
|
339
|
+
connectionState = 'missing-token';
|
|
340
|
+
} else if (ws?.readyState === WebSocket.OPEN) {
|
|
341
|
+
connectionState = 'connected';
|
|
342
|
+
} else if (ws?.readyState === WebSocket.CONNECTING) {
|
|
343
|
+
connectionState = 'connecting';
|
|
344
|
+
} else if (manualDisconnect) {
|
|
345
|
+
connectionState = 'manual';
|
|
346
|
+
} else if (nextReconnectInMs !== null) {
|
|
347
|
+
connectionState = 'reconnecting';
|
|
348
|
+
} else {
|
|
349
|
+
connectionState = 'disconnected';
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
connected: ws?.readyState === WebSocket.OPEN,
|
|
355
|
+
connectionState,
|
|
356
|
+
hasToken: Boolean(config.token),
|
|
357
|
+
port: config.port,
|
|
358
|
+
wsUrl: `ws://127.0.0.1:${config.port}/extension`,
|
|
359
|
+
debugRichText: config.debugRichText,
|
|
360
|
+
lastError: lastError?.message ?? null,
|
|
361
|
+
lastErrorAt: lastError?.at ?? null,
|
|
362
|
+
lastErrorContext: lastError?.context ?? null,
|
|
363
|
+
reconnectAttempt,
|
|
364
|
+
nextReconnectInMs: reconnectRemainingMs,
|
|
365
|
+
manualDisconnect,
|
|
366
|
+
extensionVersion: EXTENSION_VERSION,
|
|
367
|
+
lastBindingUpdateAt,
|
|
368
|
+
lastBindingUpdateReason,
|
|
369
|
+
sessionBindings
|
|
370
|
+
};
|
|
371
|
+
}
|
|
256
372
|
|
|
257
373
|
async function saveSessionBindingState(state: SessionBindingRecord): Promise<void> {
|
|
258
374
|
await mutateSessionBindingStateMap((stateMap) => {
|
|
@@ -265,6 +381,35 @@ async function deleteSessionBindingState(bindingId: string): Promise<void> {
|
|
|
265
381
|
delete stateMap[bindingId];
|
|
266
382
|
});
|
|
267
383
|
}
|
|
384
|
+
|
|
385
|
+
function toSessionBindingEventBrowser(state: SessionBindingRecord | null): Record<string, unknown> | null {
|
|
386
|
+
if (!state) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
windowId: state.windowId,
|
|
391
|
+
groupId: state.groupId,
|
|
392
|
+
tabIds: [...state.tabIds],
|
|
393
|
+
activeTabId: state.activeTabId,
|
|
394
|
+
primaryTabId: state.primaryTabId
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function emitSessionBindingUpdated(
|
|
399
|
+
bindingId: string,
|
|
400
|
+
reason: string,
|
|
401
|
+
state: SessionBindingRecord | null,
|
|
402
|
+
extras: Record<string, unknown> = {}
|
|
403
|
+
): void {
|
|
404
|
+
lastBindingUpdateAt = Date.now();
|
|
405
|
+
lastBindingUpdateReason = reason;
|
|
406
|
+
sendEvent('sessionBinding.updated', {
|
|
407
|
+
bindingId,
|
|
408
|
+
reason,
|
|
409
|
+
browser: toSessionBindingEventBrowser(state),
|
|
410
|
+
...extras
|
|
411
|
+
});
|
|
412
|
+
}
|
|
268
413
|
|
|
269
414
|
const sessionBindingBrowser: SessionBindingBrowser = {
|
|
270
415
|
async getTab(tabId) {
|
|
@@ -673,18 +818,23 @@ async function restoreFocusContext(context: FocusContext): Promise<void> {
|
|
|
673
818
|
}
|
|
674
819
|
}
|
|
675
820
|
|
|
676
|
-
async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
|
|
677
|
-
if (!enabled) {
|
|
678
|
-
return action();
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const focusContext = await captureFocusContext();
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
821
|
+
async function preserveHumanFocus<T>(enabled: boolean, action: () => Promise<T>): Promise<T> {
|
|
822
|
+
if (!enabled) {
|
|
823
|
+
return action();
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
const focusContext = await captureFocusContext();
|
|
827
|
+
preserveHumanFocusDepth += 1;
|
|
828
|
+
try {
|
|
829
|
+
return await action();
|
|
830
|
+
} finally {
|
|
831
|
+
try {
|
|
832
|
+
await restoreFocusContext(focusContext);
|
|
833
|
+
} finally {
|
|
834
|
+
preserveHumanFocusDepth = Math.max(0, preserveHumanFocusDepth - 1);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
688
838
|
|
|
689
839
|
function requireRpcEnvelope(
|
|
690
840
|
method: string,
|
|
@@ -1686,11 +1836,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1686
1836
|
const result = await bindingManager.ensureBinding({
|
|
1687
1837
|
bindingId: String(params.bindingId ?? ''),
|
|
1688
1838
|
focus: params.focus === true,
|
|
1689
|
-
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
1839
|
+
initialUrl: typeof params.url === 'string' ? params.url : undefined,
|
|
1840
|
+
label: typeof params.label === 'string' ? params.label : undefined
|
|
1690
1841
|
});
|
|
1691
1842
|
for (const tab of result.binding.tabs) {
|
|
1692
1843
|
void ensureNetworkDebugger(tab.id).catch(() => undefined);
|
|
1693
1844
|
}
|
|
1845
|
+
emitSessionBindingUpdated(result.binding.id, 'ensure', result.binding);
|
|
1694
1846
|
return {
|
|
1695
1847
|
browser: result.binding,
|
|
1696
1848
|
created: result.created,
|
|
@@ -1711,11 +1863,13 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1711
1863
|
bindingId: String(params.bindingId ?? ''),
|
|
1712
1864
|
url: expectedUrl,
|
|
1713
1865
|
active: params.active === true,
|
|
1714
|
-
focus: params.focus === true
|
|
1866
|
+
focus: params.focus === true,
|
|
1867
|
+
label: typeof params.label === 'string' ? params.label : undefined
|
|
1715
1868
|
});
|
|
1716
1869
|
});
|
|
1717
1870
|
const finalized = await finalizeOpenedSessionBindingTab(opened, expectedUrl);
|
|
1718
1871
|
void ensureNetworkDebugger(finalized.tab.id).catch(() => undefined);
|
|
1872
|
+
emitSessionBindingUpdated(finalized.binding.id, 'open-tab', finalized.binding);
|
|
1719
1873
|
return {
|
|
1720
1874
|
browser: finalized.binding,
|
|
1721
1875
|
tab: finalized.tab
|
|
@@ -1738,6 +1892,7 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1738
1892
|
case 'sessionBinding.setActiveTab': {
|
|
1739
1893
|
const result = await bindingManager.setActiveTab(Number(params.tabId), String(params.bindingId ?? ''));
|
|
1740
1894
|
void ensureNetworkDebugger(result.tab.id).catch(() => undefined);
|
|
1895
|
+
emitSessionBindingUpdated(result.binding.id, 'set-active-tab', result.binding);
|
|
1741
1896
|
return {
|
|
1742
1897
|
browser: result.binding,
|
|
1743
1898
|
tab: result.tab
|
|
@@ -1755,8 +1910,10 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1755
1910
|
const result = await bindingManager.reset({
|
|
1756
1911
|
bindingId: String(params.bindingId ?? ''),
|
|
1757
1912
|
focus: params.focus === true,
|
|
1758
|
-
initialUrl: typeof params.url === 'string' ? params.url : undefined
|
|
1913
|
+
initialUrl: typeof params.url === 'string' ? params.url : undefined,
|
|
1914
|
+
label: typeof params.label === 'string' ? params.label : undefined
|
|
1759
1915
|
});
|
|
1916
|
+
emitSessionBindingUpdated(result.binding.id, 'reset', result.binding);
|
|
1760
1917
|
return {
|
|
1761
1918
|
browser: result.binding,
|
|
1762
1919
|
created: result.created,
|
|
@@ -1765,8 +1922,22 @@ async function handleRequest(request: CliRequest): Promise<unknown> {
|
|
|
1765
1922
|
};
|
|
1766
1923
|
});
|
|
1767
1924
|
}
|
|
1925
|
+
case 'sessionBinding.closeTab': {
|
|
1926
|
+
const bindingId = String(params.bindingId ?? '');
|
|
1927
|
+
const result = await bindingManager.closeTab(bindingId, typeof params.tabId === 'number' ? params.tabId : undefined);
|
|
1928
|
+
emitSessionBindingUpdated(bindingId, 'close-tab', result.binding, {
|
|
1929
|
+
closedTabId: result.closedTabId
|
|
1930
|
+
});
|
|
1931
|
+
return {
|
|
1932
|
+
browser: result.binding,
|
|
1933
|
+
closedTabId: result.closedTabId
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1768
1936
|
case 'sessionBinding.close': {
|
|
1769
|
-
|
|
1937
|
+
const bindingId = String(params.bindingId ?? '');
|
|
1938
|
+
const result = await bindingManager.close(bindingId);
|
|
1939
|
+
emitSessionBindingUpdated(bindingId, 'close', null);
|
|
1940
|
+
return result;
|
|
1770
1941
|
}
|
|
1771
1942
|
case 'page.goto': {
|
|
1772
1943
|
return await preserveHumanFocus(typeof target.tabId !== 'number', async () => {
|
|
@@ -2269,14 +2440,16 @@ function scheduleReconnect(reason: string): void {
|
|
|
2269
2440
|
return;
|
|
2270
2441
|
}
|
|
2271
2442
|
|
|
2272
|
-
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
2273
|
-
reconnectAttempt += 1;
|
|
2274
|
-
nextReconnectInMs = delayMs;
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2443
|
+
const delayMs = computeReconnectDelayMs(reconnectAttempt);
|
|
2444
|
+
reconnectAttempt += 1;
|
|
2445
|
+
nextReconnectInMs = delayMs;
|
|
2446
|
+
nextReconnectAt = Date.now() + delayMs;
|
|
2447
|
+
reconnectTimer = setTimeout(() => {
|
|
2448
|
+
reconnectTimer = null;
|
|
2449
|
+
nextReconnectInMs = null;
|
|
2450
|
+
nextReconnectAt = null;
|
|
2451
|
+
void connectWebSocket();
|
|
2452
|
+
}, delayMs) as unknown as number;
|
|
2280
2453
|
|
|
2281
2454
|
if (!lastError) {
|
|
2282
2455
|
setRuntimeError(`Reconnect scheduled: ${reason}`, 'socket');
|
|
@@ -2299,26 +2472,30 @@ async function connectWebSocket(): Promise<void> {
|
|
|
2299
2472
|
return;
|
|
2300
2473
|
}
|
|
2301
2474
|
|
|
2302
|
-
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2475
|
+
const url = `ws://127.0.0.1:${config.port}/extension?token=${encodeURIComponent(config.token)}`;
|
|
2476
|
+
const socket = new WebSocket(url);
|
|
2477
|
+
ws = socket;
|
|
2478
|
+
|
|
2479
|
+
socket.addEventListener('open', () => {
|
|
2480
|
+
if (ws !== socket) {
|
|
2481
|
+
return;
|
|
2482
|
+
}
|
|
2483
|
+
manualDisconnect = false;
|
|
2484
|
+
reconnectAttempt = 0;
|
|
2485
|
+
lastError = null;
|
|
2486
|
+
socket.send(JSON.stringify({
|
|
2310
2487
|
type: 'hello',
|
|
2311
2488
|
role: 'extension',
|
|
2312
2489
|
version: EXTENSION_VERSION,
|
|
2313
2490
|
ts: Date.now()
|
|
2314
2491
|
}));
|
|
2315
2492
|
});
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
try {
|
|
2319
|
-
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
2320
|
-
if (!request.id || !request.method) {
|
|
2321
|
-
return;
|
|
2493
|
+
|
|
2494
|
+
socket.addEventListener('message', (event) => {
|
|
2495
|
+
try {
|
|
2496
|
+
const request = JSON.parse(String(event.data)) as CliRequest;
|
|
2497
|
+
if (!request.id || !request.method) {
|
|
2498
|
+
return;
|
|
2322
2499
|
}
|
|
2323
2500
|
void handleRequest(request)
|
|
2324
2501
|
.then((result) => {
|
|
@@ -2335,66 +2512,109 @@ async function connectWebSocket(): Promise<void> {
|
|
|
2335
2512
|
ok: false,
|
|
2336
2513
|
error: toError('E_INTERNAL', error instanceof Error ? error.message : String(error))
|
|
2337
2514
|
});
|
|
2338
|
-
}
|
|
2339
|
-
});
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
ws
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2515
|
+
}
|
|
2516
|
+
});
|
|
2517
|
+
|
|
2518
|
+
socket.addEventListener('close', () => {
|
|
2519
|
+
if (ws !== socket) {
|
|
2520
|
+
return;
|
|
2521
|
+
}
|
|
2522
|
+
ws = null;
|
|
2523
|
+
scheduleReconnect('socket-closed');
|
|
2524
|
+
});
|
|
2525
|
+
|
|
2526
|
+
socket.addEventListener('error', () => {
|
|
2527
|
+
if (ws !== socket) {
|
|
2528
|
+
return;
|
|
2529
|
+
}
|
|
2530
|
+
setRuntimeError('Cannot connect to bak cli', 'socket');
|
|
2531
|
+
socket.close();
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2351
2534
|
|
|
2352
2535
|
chrome.tabs.onRemoved.addListener((tabId) => {
|
|
2353
2536
|
dropNetworkCapture(tabId);
|
|
2354
2537
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2538
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
|
|
2355
2539
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2356
2540
|
if (!state.tabIds.includes(tabId)) {
|
|
2357
2541
|
continue;
|
|
2358
2542
|
}
|
|
2359
2543
|
const nextTabIds = state.tabIds.filter((id) => id !== tabId);
|
|
2360
|
-
|
|
2544
|
+
if (nextTabIds.length === 0) {
|
|
2545
|
+
delete stateMap[bindingId];
|
|
2546
|
+
updates.push({ bindingId, state: null });
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
const fallbackTabId = nextTabIds[0] ?? null;
|
|
2550
|
+
const nextState: SessionBindingRecord = {
|
|
2361
2551
|
...state,
|
|
2362
2552
|
tabIds: nextTabIds,
|
|
2363
|
-
activeTabId: state.activeTabId === tabId ?
|
|
2364
|
-
primaryTabId: state.primaryTabId === tabId ?
|
|
2553
|
+
activeTabId: state.activeTabId === tabId ? fallbackTabId : state.activeTabId,
|
|
2554
|
+
primaryTabId: state.primaryTabId === tabId ? fallbackTabId : state.primaryTabId
|
|
2365
2555
|
};
|
|
2556
|
+
stateMap[bindingId] = nextState;
|
|
2557
|
+
updates.push({ bindingId, state: nextState });
|
|
2558
|
+
}
|
|
2559
|
+
return updates;
|
|
2560
|
+
}).then((updates) => {
|
|
2561
|
+
for (const update of updates) {
|
|
2562
|
+
emitSessionBindingUpdated(update.bindingId, 'tab-removed', update.state, {
|
|
2563
|
+
closedTabId: tabId
|
|
2564
|
+
});
|
|
2366
2565
|
}
|
|
2367
2566
|
});
|
|
2368
2567
|
});
|
|
2369
2568
|
|
|
2370
2569
|
chrome.tabs.onActivated.addListener((activeInfo) => {
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2570
|
+
if (preserveHumanFocusDepth > 0) {
|
|
2571
|
+
return;
|
|
2572
|
+
}
|
|
2573
|
+
void chrome.windows
|
|
2574
|
+
.get(activeInfo.windowId)
|
|
2575
|
+
.then((window) => window.focused === true)
|
|
2576
|
+
.catch(() => false)
|
|
2577
|
+
.then((windowFocused) => {
|
|
2578
|
+
if (!windowFocused) {
|
|
2579
|
+
return [] as Array<{ bindingId: string; state: SessionBindingRecord }>;
|
|
2375
2580
|
}
|
|
2376
|
-
stateMap
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2581
|
+
return mutateSessionBindingStateMap((stateMap) => {
|
|
2582
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord }> = [];
|
|
2583
|
+
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2584
|
+
if (state.windowId !== activeInfo.windowId || !state.tabIds.includes(activeInfo.tabId)) {
|
|
2585
|
+
continue;
|
|
2586
|
+
}
|
|
2587
|
+
const nextState: SessionBindingRecord = {
|
|
2588
|
+
...state,
|
|
2589
|
+
activeTabId: activeInfo.tabId
|
|
2590
|
+
};
|
|
2591
|
+
stateMap[bindingId] = nextState;
|
|
2592
|
+
updates.push({ bindingId, state: nextState });
|
|
2593
|
+
}
|
|
2594
|
+
return updates;
|
|
2595
|
+
});
|
|
2596
|
+
})
|
|
2597
|
+
.then((updates) => {
|
|
2598
|
+
for (const update of updates) {
|
|
2599
|
+
emitSessionBindingUpdated(update.bindingId, 'tab-activated', update.state);
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2382
2602
|
});
|
|
2383
2603
|
|
|
2384
2604
|
chrome.windows.onRemoved.addListener((windowId) => {
|
|
2385
2605
|
void mutateSessionBindingStateMap((stateMap) => {
|
|
2606
|
+
const updates: Array<{ bindingId: string; state: SessionBindingRecord | null }> = [];
|
|
2386
2607
|
for (const [bindingId, state] of Object.entries(stateMap)) {
|
|
2387
2608
|
if (state.windowId !== windowId) {
|
|
2388
2609
|
continue;
|
|
2389
2610
|
}
|
|
2390
|
-
stateMap[bindingId]
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
};
|
|
2611
|
+
delete stateMap[bindingId];
|
|
2612
|
+
updates.push({ bindingId, state: null });
|
|
2613
|
+
}
|
|
2614
|
+
return updates;
|
|
2615
|
+
}).then((updates) => {
|
|
2616
|
+
for (const update of updates) {
|
|
2617
|
+
emitSessionBindingUpdated(update.bindingId, 'window-removed', update.state);
|
|
2398
2618
|
}
|
|
2399
2619
|
});
|
|
2400
2620
|
});
|
|
@@ -2410,48 +2630,49 @@ chrome.runtime.onStartup.addListener(() => {
|
|
|
2410
2630
|
void connectWebSocket();
|
|
2411
2631
|
|
|
2412
2632
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
2413
|
-
if (message?.type === 'bak.updateConfig') {
|
|
2414
|
-
manualDisconnect = false;
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
if (message?.type === 'bak.getState') {
|
|
2427
|
-
void getConfig().then((config) => {
|
|
2428
|
-
sendResponse({
|
|
2429
|
-
ok: true,
|
|
2430
|
-
connected: ws?.readyState === WebSocket.OPEN,
|
|
2431
|
-
hasToken: Boolean(config.token),
|
|
2432
|
-
port: config.port,
|
|
2433
|
-
debugRichText: config.debugRichText,
|
|
2434
|
-
lastError: lastError?.message ?? null,
|
|
2435
|
-
lastErrorAt: lastError?.at ?? null,
|
|
2436
|
-
lastErrorContext: lastError?.context ?? null,
|
|
2437
|
-
reconnectAttempt,
|
|
2438
|
-
nextReconnectInMs
|
|
2439
|
-
});
|
|
2440
|
-
});
|
|
2441
|
-
return true;
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
if (message?.type === 'bak.disconnect') {
|
|
2445
|
-
manualDisconnect = true;
|
|
2446
|
-
clearReconnectTimer();
|
|
2447
|
-
reconnectAttempt = 0;
|
|
2448
|
-
ws?.close();
|
|
2449
|
-
ws = null;
|
|
2450
|
-
sendResponse({ ok: true });
|
|
2451
|
-
return false;
|
|
2452
|
-
}
|
|
2633
|
+
if (message?.type === 'bak.updateConfig') {
|
|
2634
|
+
manualDisconnect = false;
|
|
2635
|
+
const token = typeof message.token === 'string' ? message.token.trim() : '';
|
|
2636
|
+
void setConfig({
|
|
2637
|
+
...(token ? { token } : {}),
|
|
2638
|
+
port: Number(message.port ?? DEFAULT_PORT),
|
|
2639
|
+
debugRichText: message.debugRichText === true
|
|
2640
|
+
}).then(() => {
|
|
2641
|
+
ws?.close();
|
|
2642
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
2643
|
+
});
|
|
2644
|
+
return true;
|
|
2645
|
+
}
|
|
2453
2646
|
|
|
2454
|
-
|
|
2455
|
-
|
|
2647
|
+
if (message?.type === 'bak.getState') {
|
|
2648
|
+
void buildPopupState().then((state) => {
|
|
2649
|
+
sendResponse(state);
|
|
2650
|
+
});
|
|
2651
|
+
return true;
|
|
2652
|
+
}
|
|
2653
|
+
|
|
2654
|
+
if (message?.type === 'bak.disconnect') {
|
|
2655
|
+
manualDisconnect = true;
|
|
2656
|
+
clearReconnectTimer();
|
|
2657
|
+
reconnectAttempt = 0;
|
|
2658
|
+
lastError = null;
|
|
2659
|
+
ws?.close();
|
|
2660
|
+
ws = null;
|
|
2661
|
+
sendResponse({ ok: true });
|
|
2662
|
+
return false;
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
if (message?.type === 'bak.reconnectNow') {
|
|
2666
|
+
manualDisconnect = false;
|
|
2667
|
+
clearReconnectTimer();
|
|
2668
|
+
reconnectAttempt = 0;
|
|
2669
|
+
ws?.close();
|
|
2670
|
+
ws = null;
|
|
2671
|
+
void connectWebSocket().then(() => sendResponse({ ok: true }));
|
|
2672
|
+
return true;
|
|
2673
|
+
}
|
|
2674
|
+
|
|
2675
|
+
return false;
|
|
2676
|
+
});
|
|
2456
2677
|
|
|
2457
2678
|
|