@jobshimo/browser-link 0.40.0 → 0.42.0
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/README.md +27 -12
- package/dist/admin/actions.d.ts +57 -0
- package/dist/admin/actions.js +163 -0
- package/dist/admin/actions.js.map +1 -0
- package/dist/admin/descriptor.d.ts +65 -0
- package/dist/admin/descriptor.js +205 -0
- package/dist/admin/descriptor.js.map +1 -0
- package/dist/admin/i18n.d.ts +90 -0
- package/dist/admin/i18n.js +203 -0
- package/dist/admin/i18n.js.map +1 -0
- package/dist/admin/primary.d.ts +21 -0
- package/dist/admin/primary.js +56 -0
- package/dist/admin/primary.js.map +1 -0
- package/dist/admin/sources.d.ts +22 -0
- package/dist/admin/sources.js +82 -0
- package/dist/admin/sources.js.map +1 -0
- package/dist/bridge/dispatch.js +17 -7
- package/dist/bridge/dispatch.js.map +1 -1
- package/dist/bridge/ws-bridge.d.ts +65 -3
- package/dist/bridge/ws-bridge.js +165 -7
- package/dist/bridge/ws-bridge.js.map +1 -1
- package/dist/cli.js +6 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/doctor.d.ts +24 -0
- package/dist/commands/doctor.js +62 -1
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/free-port.d.ts +40 -1
- package/dist/commands/free-port.js +72 -7
- package/dist/commands/free-port.js.map +1 -1
- package/dist/extension/admin-view.d.ts +95 -0
- package/dist/extension/admin-view.js +164 -0
- package/dist/extension/admin-view.js.map +1 -0
- package/dist/extension/background.js +176 -8
- package/dist/extension/background.js.map +1 -1
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/popup.html +113 -0
- package/dist/extension/popup.js +282 -1
- package/dist/extension/popup.js.map +1 -1
- package/dist/extension/repair-policy.d.ts +60 -0
- package/dist/extension/repair-policy.js +79 -0
- package/dist/extension/repair-policy.js.map +1 -0
- package/dist/extension/version-status.d.ts +44 -0
- package/dist/extension/version-status.js +60 -0
- package/dist/extension/version-status.js.map +1 -0
- package/dist/lifecycle/election.d.ts +98 -0
- package/dist/lifecycle/election.js +187 -0
- package/dist/lifecycle/election.js.map +1 -0
- package/dist/lifecycle/processes.d.ts +67 -0
- package/dist/lifecycle/processes.js +239 -0
- package/dist/lifecycle/processes.js.map +1 -0
- package/dist/lifecycle/repair.d.ts +48 -0
- package/dist/lifecycle/repair.js +67 -0
- package/dist/lifecycle/repair.js.map +1 -0
- package/dist/lifecycle/version-endpoint.d.ts +16 -0
- package/dist/lifecycle/version-endpoint.js +50 -0
- package/dist/lifecycle/version-endpoint.js.map +1 -0
- package/dist/manual/topics.js +1 -1
- package/dist/manual/topics.js.map +1 -1
- package/dist/messages.d.ts +115 -2
- package/dist/server.js +104 -1
- package/dist/server.js.map +1 -1
- package/dist/tools/browser-dispatch.d.ts +8 -0
- package/dist/tools/browser-dispatch.js +15 -1
- package/dist/tools/browser-dispatch.js.map +1 -1
- package/dist/ui/app.d.ts +4 -1
- package/dist/ui/app.js +55 -42
- package/dist/ui/app.js.map +1 -1
- package/dist/ui/screens/free-port.js +4 -2
- package/dist/ui/screens/free-port.js.map +1 -1
- package/dist/ui/screens/index.d.ts +1 -0
- package/dist/ui/screens/index.js +1 -0
- package/dist/ui/screens/index.js.map +1 -1
- package/dist/ui/screens/menu.d.ts +11 -1
- package/dist/ui/screens/menu.js +12 -4
- package/dist/ui/screens/menu.js.map +1 -1
- package/dist/ui/screens/status.d.ts +19 -0
- package/dist/ui/screens/status.js +67 -0
- package/dist/ui/screens/status.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.js +16 -0
- package/dist/version.js.map +1 -1
- package/package.json +1 -1
|
@@ -13,6 +13,8 @@ import { DEFAULT_FLOW_RECORDING_ENABLED, FLOW_RECORDING_STORAGE_KEY, FLOW_RECORD
|
|
|
13
13
|
import { buildRecorderJs, buildStopRecorderJs } from './inpage/recorder.js';
|
|
14
14
|
import { DISCARDED_WHILE_SAVING_MESSAGE, appendRecordingStep, buildAmbiguousNote, buildNavigationWaitForStep, generateRecordingSession, isNavigationForRecording, isSameRecordingSession, parseRecordedPayload, toFlowStep, } from './recording.js';
|
|
15
15
|
import { ALARM_MIN_DELAY_MS, CONNECT_IN_PROGRESS_ERROR, createReconnectScheduler, isInvoluntaryClose, planResume, reconnectStorageKey, tabIdFromReconnectKey, } from './reconnect-policy.js';
|
|
16
|
+
import { REPAIR_TIMEOUT_MS, describeReconnectResults, describeRepairReply, planRepair, prevTabIdKey, rememberedTabIds, } from './repair-policy.js';
|
|
17
|
+
import { computeVersionCheck } from './version-status.js';
|
|
16
18
|
const WS_URL = 'ws://127.0.0.1:17529';
|
|
17
19
|
const CDP_VERSION = '1.3';
|
|
18
20
|
const CONSOLE_BUFFER_MAX = 200;
|
|
@@ -148,12 +150,21 @@ function isRuntimeMessage(msg) {
|
|
|
148
150
|
const m = msg;
|
|
149
151
|
if (m.action === 'versionCheck')
|
|
150
152
|
return true;
|
|
153
|
+
if (m.action === 'repairConnection')
|
|
154
|
+
return true;
|
|
151
155
|
if (m.action === 'openActivityWindow')
|
|
152
156
|
return true;
|
|
153
157
|
if (m.action === 'activityQuery')
|
|
154
158
|
return true;
|
|
155
159
|
if (m.action === 'activityPurge')
|
|
156
160
|
return true;
|
|
161
|
+
if (m.action === 'adminDescribe')
|
|
162
|
+
return true;
|
|
163
|
+
if (m.action === 'adminAction') {
|
|
164
|
+
const v = m.value;
|
|
165
|
+
return (typeof m.id === 'string' &&
|
|
166
|
+
(v === undefined || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean'));
|
|
167
|
+
}
|
|
157
168
|
if (m.action === 'saveRecording') {
|
|
158
169
|
return typeof m.tabId === 'number' && typeof m.name === 'string';
|
|
159
170
|
}
|
|
@@ -250,6 +261,61 @@ function purgeActivity(input) {
|
|
|
250
261
|
// A dry run only counts — no VACUUM, so it gets the short budget.
|
|
251
262
|
input.dryRun === true ? ACTIVITY_QUERY_TIMEOUT_MS : ACTIVITY_PURGE_TIMEOUT_MS);
|
|
252
263
|
}
|
|
264
|
+
const pendingAdminRequests = new Map();
|
|
265
|
+
/** Settle one admin reply; an unknown or timed-out id is dropped (see
|
|
266
|
+
* `settleActivityRequest` for why). */
|
|
267
|
+
function settleAdminRequest(requestId, reply) {
|
|
268
|
+
const pending = pendingAdminRequests.get(requestId);
|
|
269
|
+
if (typeof pending !== 'function')
|
|
270
|
+
return;
|
|
271
|
+
pendingAdminRequests.delete(requestId);
|
|
272
|
+
pending(reply);
|
|
273
|
+
}
|
|
274
|
+
/** Describing and most actions are local config reads and writes. */
|
|
275
|
+
const ADMIN_TIMEOUT_MS = 5_000;
|
|
276
|
+
/** `doctor` probes ports and processes server-side and is slow by nature. */
|
|
277
|
+
const ADMIN_DOCTOR_TIMEOUT_MS = 20_000;
|
|
278
|
+
/**
|
|
279
|
+
* One correlated admin round trip over any connected tab's socket — the same
|
|
280
|
+
* shape as `activityRoundTrip`. `onFailure` builds the reply kind the caller
|
|
281
|
+
* expects, so `not-connected` and `timeout` reach the popup in the same
|
|
282
|
+
* envelope a real answer would.
|
|
283
|
+
*/
|
|
284
|
+
async function adminRoundTrip(frame, timeoutMs, onFailure) {
|
|
285
|
+
const socket = [...tabStates.values()]
|
|
286
|
+
.map((s) => s.ws)
|
|
287
|
+
.find((ws) => ws !== undefined && ws.readyState === WebSocket.OPEN);
|
|
288
|
+
if (!socket)
|
|
289
|
+
return onFailure('', 'not-connected');
|
|
290
|
+
const requestId = crypto.randomUUID();
|
|
291
|
+
return new Promise((resolve) => {
|
|
292
|
+
const timer = setTimeout(() => {
|
|
293
|
+
pendingAdminRequests.delete(requestId);
|
|
294
|
+
resolve(onFailure(requestId, 'timeout'));
|
|
295
|
+
}, timeoutMs);
|
|
296
|
+
pendingAdminRequests.set(requestId, (reply) => {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
resolve(reply);
|
|
299
|
+
});
|
|
300
|
+
send(socket, frame(requestId));
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function adminDescribe() {
|
|
304
|
+
return adminRoundTrip((request_id) => ({ kind: 'admin.describe', request_id }), ADMIN_TIMEOUT_MS, (request_id, error) => ({ kind: 'admin.state', request_id, ok: false, error }));
|
|
305
|
+
}
|
|
306
|
+
function adminAction(input) {
|
|
307
|
+
return adminRoundTrip((request_id) => ({
|
|
308
|
+
kind: 'admin.action',
|
|
309
|
+
request_id,
|
|
310
|
+
id: input.id,
|
|
311
|
+
...(input.value !== undefined ? { value: input.value } : {}),
|
|
312
|
+
...(input.confirm === true ? { confirm: true } : {}),
|
|
313
|
+
}), input.id === 'doctor' ? ADMIN_DOCTOR_TIMEOUT_MS : ADMIN_TIMEOUT_MS, (request_id, error) => ({
|
|
314
|
+
kind: 'admin.action.result',
|
|
315
|
+
request_id,
|
|
316
|
+
result: { ok: false, error },
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
253
319
|
/** Chrome window geometry for the Activity panel. Wide enough for the full
|
|
254
320
|
* row — time, agent, tool, target and outcome — without wrapping, which is
|
|
255
321
|
* the entire reason this is not the 320px popup. */
|
|
@@ -2486,7 +2552,7 @@ chrome.debugger.onEvent.addListener((source, method, params) => {
|
|
|
2486
2552
|
* stale tab_ids after a re-election.
|
|
2487
2553
|
*/
|
|
2488
2554
|
async function readPreviousTabId(chromeTabId) {
|
|
2489
|
-
const key =
|
|
2555
|
+
const key = prevTabIdKey(chromeTabId);
|
|
2490
2556
|
try {
|
|
2491
2557
|
const data = await chrome.storage.session.get(key);
|
|
2492
2558
|
const v = data[key];
|
|
@@ -2497,7 +2563,7 @@ async function readPreviousTabId(chromeTabId) {
|
|
|
2497
2563
|
}
|
|
2498
2564
|
}
|
|
2499
2565
|
async function storeTabId(chromeTabId, serverTabId) {
|
|
2500
|
-
const key =
|
|
2566
|
+
const key = prevTabIdKey(chromeTabId);
|
|
2501
2567
|
try {
|
|
2502
2568
|
await chrome.storage.session.set({ [key]: serverTabId });
|
|
2503
2569
|
}
|
|
@@ -2506,7 +2572,7 @@ async function storeTabId(chromeTabId, serverTabId) {
|
|
|
2506
2572
|
}
|
|
2507
2573
|
}
|
|
2508
2574
|
async function forgetTabId(chromeTabId) {
|
|
2509
|
-
const key =
|
|
2575
|
+
const key = prevTabIdKey(chromeTabId);
|
|
2510
2576
|
try {
|
|
2511
2577
|
await chrome.storage.session.remove(key);
|
|
2512
2578
|
}
|
|
@@ -2770,6 +2836,13 @@ async function doConnectTab(tabId) {
|
|
|
2770
2836
|
if (typeof msg.payload.serverVersion === 'string') {
|
|
2771
2837
|
state.serverVersion = msg.payload.serverVersion;
|
|
2772
2838
|
}
|
|
2839
|
+
// Same narrowing for the 0.41.0 fields.
|
|
2840
|
+
if (typeof msg.payload.installedVersion === 'string') {
|
|
2841
|
+
state.installedVersion = msg.payload.installedVersion;
|
|
2842
|
+
}
|
|
2843
|
+
if (typeof msg.payload.serverPid === 'number') {
|
|
2844
|
+
state.serverPid = msg.payload.serverPid;
|
|
2845
|
+
}
|
|
2773
2846
|
// Remember this id so the next reconnect (after a primary swap)
|
|
2774
2847
|
// asks the new primary to honour it. The primary emits
|
|
2775
2848
|
// `tab-renamed` in the bridge event log if it can't.
|
|
@@ -2845,6 +2918,17 @@ async function doConnectTab(tabId) {
|
|
|
2845
2918
|
settleActivityRequest(msg.request_id, msg);
|
|
2846
2919
|
return;
|
|
2847
2920
|
}
|
|
2921
|
+
// Not tab activity either: only the popup's Admin section waits on it.
|
|
2922
|
+
case 'admin.state':
|
|
2923
|
+
case 'admin.action.result': {
|
|
2924
|
+
settleAdminRequest(msg.request_id, msg);
|
|
2925
|
+
return;
|
|
2926
|
+
}
|
|
2927
|
+
case 'bridge.repair.result': {
|
|
2928
|
+
// Not tab activity: the popup's Repair button is waiting on it.
|
|
2929
|
+
settleRepairRequest(msg);
|
|
2930
|
+
return;
|
|
2931
|
+
}
|
|
2848
2932
|
default: {
|
|
2849
2933
|
// Compile-time exhaustiveness check; at runtime an unknown kind
|
|
2850
2934
|
// (newer server, older extension) is ignored defensively.
|
|
@@ -2927,17 +3011,87 @@ function getTabStatus(tabId) {
|
|
|
2927
3011
|
function getVersionCheck() {
|
|
2928
3012
|
const extension = chrome.runtime.getManifest().version;
|
|
2929
3013
|
let server = null;
|
|
3014
|
+
let installed = null;
|
|
2930
3015
|
for (const s of tabStates.values()) {
|
|
2931
3016
|
if (typeof s.serverVersion === 'string' && s.serverVersion.length > 0) {
|
|
2932
3017
|
server = s.serverVersion;
|
|
3018
|
+
installed =
|
|
3019
|
+
typeof s.installedVersion === 'string' && s.installedVersion.length > 0
|
|
3020
|
+
? s.installedVersion
|
|
3021
|
+
: null;
|
|
2933
3022
|
break;
|
|
2934
3023
|
}
|
|
2935
3024
|
}
|
|
2936
|
-
return {
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
3025
|
+
return computeVersionCheck({ extension, server, installed });
|
|
3026
|
+
}
|
|
3027
|
+
/* "Repair connection" (popup button). The decision lives in repair-policy.ts;
|
|
3028
|
+
* this is the thin wiring that gathers its inputs and carries out the step. */
|
|
3029
|
+
/** The one in-flight `bridge.repair`, keyed by request id. A late or unknown
|
|
3030
|
+
* reply is dropped, same rule as `settleActivityRequest`. */
|
|
3031
|
+
const pendingRepairs = new Map();
|
|
3032
|
+
function settleRepairRequest(reply) {
|
|
3033
|
+
const pending = pendingRepairs.get(reply.request_id);
|
|
3034
|
+
if (typeof pending !== 'function')
|
|
3035
|
+
return;
|
|
3036
|
+
pendingRepairs.delete(reply.request_id);
|
|
3037
|
+
pending(reply);
|
|
3038
|
+
}
|
|
3039
|
+
async function readRememberedTabIds() {
|
|
3040
|
+
try {
|
|
3041
|
+
return rememberedTabIds(await chrome.storage.session.get(null));
|
|
3042
|
+
}
|
|
3043
|
+
catch {
|
|
3044
|
+
return [];
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
/** Send `bridge.repair` on one open socket and wait for the answer. On
|
|
3048
|
+
* success the server drops every socket uncleanly and the existing
|
|
3049
|
+
* auto-reconnect scheduler brings the tabs back; nothing else to do here. */
|
|
3050
|
+
function askServerRestart(socket) {
|
|
3051
|
+
const requestId = crypto.randomUUID();
|
|
3052
|
+
return new Promise((resolve) => {
|
|
3053
|
+
const timer = setTimeout(() => {
|
|
3054
|
+
pendingRepairs.delete(requestId);
|
|
3055
|
+
resolve(describeRepairReply('timeout'));
|
|
3056
|
+
}, REPAIR_TIMEOUT_MS);
|
|
3057
|
+
pendingRepairs.set(requestId, (reply) => {
|
|
3058
|
+
clearTimeout(timer);
|
|
3059
|
+
resolve(describeRepairReply(reply));
|
|
3060
|
+
});
|
|
3061
|
+
send(socket, { kind: 'bridge.repair', request_id: requestId });
|
|
3062
|
+
});
|
|
3063
|
+
}
|
|
3064
|
+
async function repairConnection() {
|
|
3065
|
+
const socket = [...tabStates.values()]
|
|
3066
|
+
.map((s) => s.ws)
|
|
3067
|
+
.find((ws) => ws !== undefined && ws.readyState === WebSocket.OPEN);
|
|
3068
|
+
const plan = planRepair({
|
|
3069
|
+
connected: socket !== undefined,
|
|
3070
|
+
serverVersion: getVersionCheck().server,
|
|
3071
|
+
rememberedTabIds: await readRememberedTabIds(),
|
|
3072
|
+
});
|
|
3073
|
+
switch (plan.step) {
|
|
3074
|
+
case 'ask-server-restart':
|
|
3075
|
+
// `planRepair` only picks this step when a socket is open.
|
|
3076
|
+
return socket ? askServerRestart(socket) : { ok: false, message: 'Not connected' };
|
|
3077
|
+
case 'reconnect-only': {
|
|
3078
|
+
// Same path as the popup's Connect: an explicit attempt replaces any
|
|
3079
|
+
// pending auto-reconnect for the tab, and connectTab's per-tab lock
|
|
3080
|
+
// turns a race with an in-flight retry into a benign result.
|
|
3081
|
+
const results = await Promise.all(plan.tabIds.map(async (tabId) => {
|
|
3082
|
+
reconnectScheduler.cancel(tabId);
|
|
3083
|
+
try {
|
|
3084
|
+
return await connectTab(tabId);
|
|
3085
|
+
}
|
|
3086
|
+
catch {
|
|
3087
|
+
return { ok: false };
|
|
3088
|
+
}
|
|
3089
|
+
}));
|
|
3090
|
+
return describeReconnectResults(results);
|
|
3091
|
+
}
|
|
3092
|
+
case 'manual':
|
|
3093
|
+
return { ok: false, message: plan.message };
|
|
3094
|
+
}
|
|
2941
3095
|
}
|
|
2942
3096
|
// === Flow recording by demonstration ===================================
|
|
2943
3097
|
//
|
|
@@ -3304,6 +3458,12 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
3304
3458
|
sendResponse(getVersionCheck());
|
|
3305
3459
|
return false;
|
|
3306
3460
|
}
|
|
3461
|
+
if (msg.action === 'repairConnection') {
|
|
3462
|
+
void repairConnection().then(sendResponse, (err) => {
|
|
3463
|
+
sendResponse({ ok: false, message: err instanceof Error ? err.message : String(err) });
|
|
3464
|
+
});
|
|
3465
|
+
return true;
|
|
3466
|
+
}
|
|
3307
3467
|
if (msg.action === 'openActivityWindow') {
|
|
3308
3468
|
void openActivityWindow().then(sendResponse);
|
|
3309
3469
|
return true;
|
|
@@ -3316,6 +3476,14 @@ chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
|
|
|
3316
3476
|
void purgeActivity(msg).then(sendResponse);
|
|
3317
3477
|
return true;
|
|
3318
3478
|
}
|
|
3479
|
+
if (msg.action === 'adminDescribe') {
|
|
3480
|
+
void adminDescribe().then(sendResponse);
|
|
3481
|
+
return true;
|
|
3482
|
+
}
|
|
3483
|
+
if (msg.action === 'adminAction') {
|
|
3484
|
+
void adminAction(msg).then(sendResponse);
|
|
3485
|
+
return true;
|
|
3486
|
+
}
|
|
3319
3487
|
if (msg.action === 'recordingStatus') {
|
|
3320
3488
|
sendResponse(getRecordingStatus(msg.tabId));
|
|
3321
3489
|
return false;
|