@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.
Files changed (82) hide show
  1. package/README.md +27 -12
  2. package/dist/admin/actions.d.ts +57 -0
  3. package/dist/admin/actions.js +163 -0
  4. package/dist/admin/actions.js.map +1 -0
  5. package/dist/admin/descriptor.d.ts +65 -0
  6. package/dist/admin/descriptor.js +205 -0
  7. package/dist/admin/descriptor.js.map +1 -0
  8. package/dist/admin/i18n.d.ts +90 -0
  9. package/dist/admin/i18n.js +203 -0
  10. package/dist/admin/i18n.js.map +1 -0
  11. package/dist/admin/primary.d.ts +21 -0
  12. package/dist/admin/primary.js +56 -0
  13. package/dist/admin/primary.js.map +1 -0
  14. package/dist/admin/sources.d.ts +22 -0
  15. package/dist/admin/sources.js +82 -0
  16. package/dist/admin/sources.js.map +1 -0
  17. package/dist/bridge/dispatch.js +17 -7
  18. package/dist/bridge/dispatch.js.map +1 -1
  19. package/dist/bridge/ws-bridge.d.ts +65 -3
  20. package/dist/bridge/ws-bridge.js +165 -7
  21. package/dist/bridge/ws-bridge.js.map +1 -1
  22. package/dist/cli.js +6 -2
  23. package/dist/cli.js.map +1 -1
  24. package/dist/commands/doctor.d.ts +24 -0
  25. package/dist/commands/doctor.js +62 -1
  26. package/dist/commands/doctor.js.map +1 -1
  27. package/dist/commands/free-port.d.ts +40 -1
  28. package/dist/commands/free-port.js +72 -7
  29. package/dist/commands/free-port.js.map +1 -1
  30. package/dist/extension/admin-view.d.ts +95 -0
  31. package/dist/extension/admin-view.js +164 -0
  32. package/dist/extension/admin-view.js.map +1 -0
  33. package/dist/extension/background.js +176 -8
  34. package/dist/extension/background.js.map +1 -1
  35. package/dist/extension/manifest.json +1 -1
  36. package/dist/extension/popup.html +113 -0
  37. package/dist/extension/popup.js +282 -1
  38. package/dist/extension/popup.js.map +1 -1
  39. package/dist/extension/repair-policy.d.ts +60 -0
  40. package/dist/extension/repair-policy.js +79 -0
  41. package/dist/extension/repair-policy.js.map +1 -0
  42. package/dist/extension/version-status.d.ts +44 -0
  43. package/dist/extension/version-status.js +60 -0
  44. package/dist/extension/version-status.js.map +1 -0
  45. package/dist/lifecycle/election.d.ts +98 -0
  46. package/dist/lifecycle/election.js +187 -0
  47. package/dist/lifecycle/election.js.map +1 -0
  48. package/dist/lifecycle/processes.d.ts +67 -0
  49. package/dist/lifecycle/processes.js +239 -0
  50. package/dist/lifecycle/processes.js.map +1 -0
  51. package/dist/lifecycle/repair.d.ts +48 -0
  52. package/dist/lifecycle/repair.js +67 -0
  53. package/dist/lifecycle/repair.js.map +1 -0
  54. package/dist/lifecycle/version-endpoint.d.ts +16 -0
  55. package/dist/lifecycle/version-endpoint.js +50 -0
  56. package/dist/lifecycle/version-endpoint.js.map +1 -0
  57. package/dist/manual/topics.js +1 -1
  58. package/dist/manual/topics.js.map +1 -1
  59. package/dist/messages.d.ts +115 -2
  60. package/dist/server.js +104 -1
  61. package/dist/server.js.map +1 -1
  62. package/dist/tools/browser-dispatch.d.ts +8 -0
  63. package/dist/tools/browser-dispatch.js +15 -1
  64. package/dist/tools/browser-dispatch.js.map +1 -1
  65. package/dist/ui/app.d.ts +4 -1
  66. package/dist/ui/app.js +55 -42
  67. package/dist/ui/app.js.map +1 -1
  68. package/dist/ui/screens/free-port.js +4 -2
  69. package/dist/ui/screens/free-port.js.map +1 -1
  70. package/dist/ui/screens/index.d.ts +1 -0
  71. package/dist/ui/screens/index.js +1 -0
  72. package/dist/ui/screens/index.js.map +1 -1
  73. package/dist/ui/screens/menu.d.ts +11 -1
  74. package/dist/ui/screens/menu.js +12 -4
  75. package/dist/ui/screens/menu.js.map +1 -1
  76. package/dist/ui/screens/status.d.ts +19 -0
  77. package/dist/ui/screens/status.js +67 -0
  78. package/dist/ui/screens/status.js.map +1 -0
  79. package/dist/version.d.ts +8 -0
  80. package/dist/version.js +16 -0
  81. package/dist/version.js.map +1 -1
  82. 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 = `prevTabId:${chromeTabId}`;
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 = `prevTabId:${chromeTabId}`;
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 = `prevTabId:${chromeTabId}`;
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
- extension,
2938
- server,
2939
- aligned: server === null ? null : server === extension,
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;