@xnng/browser-relay 1.6.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/LICENSE +22 -0
- package/README.md +445 -0
- package/docs/README.zh-CN.md +421 -0
- package/docs/benchmarks/browser-gaps-codex-ax.json +418 -0
- package/docs/benchmarks/browser-gaps-relay-after.json +655 -0
- package/docs/benchmarks/browser-gaps-relay-baseline.json +679 -0
- package/docs/benchmarks/browser-readiness-cost.json +106 -0
- package/docs/benchmarks/browser-runtime-balanced-headed.json +412 -0
- package/docs/benchmarks/browser-runtime-balanced-rtt50.json +412 -0
- package/docs/benchmarks/browser-runtime-balanced.json +412 -0
- package/docs/benchmarks/browser-runtime-rtt50.json +194 -0
- package/docs/benchmarks/browser-runtime.json +254 -0
- package/docs/benchmarks/browser-use-parity.json +54 -0
- package/docs/benchmarks/codex-extension-audit.json +131 -0
- package/docs/benchmarks/codex-native-protocol.md +69 -0
- package/docs/benchmarks/codex-native-replay.js +116 -0
- package/docs/benchmarks/codex-native-status.json +81 -0
- package/docs/benchmarks/codex-native.json +1003 -0
- package/docs/benchmarks/extension-sessions.png +0 -0
- package/docs/benchmarks/extension-tasks.png +0 -0
- package/docs/benchmarks/iframe-routing-regression.json +37 -0
- package/docs/browser-use-comparison.md +331 -0
- package/docs/browser-use-gap-audit.md +141 -0
- package/docs/browser-use-parity.md +105 -0
- package/docs/demo/intranet.html +103 -0
- package/docs/releases/v1.5.0.md +59 -0
- package/docs/releases/v1.5.1.md +16 -0
- package/docs/releases/v1.5.2.md +42 -0
- package/docs/releases/v1.5.3.md +33 -0
- package/docs/releases/v1.5.4.md +42 -0
- package/docs/releases/v1.6.0.md +16 -0
- package/docs/remote-control-hub.md +523 -0
- package/extension/activity.js +328 -0
- package/extension/automation.js +1839 -0
- package/extension/background.js +1854 -0
- package/extension/i18n.js +149 -0
- package/extension/icons/icon128.png +0 -0
- package/extension/icons/icon16.png +0 -0
- package/extension/icons/icon32.png +0 -0
- package/extension/icons/icon48.png +0 -0
- package/extension/manifest.json +47 -0
- package/extension/observations.js +109 -0
- package/extension/options.html +289 -0
- package/extension/options.js +269 -0
- package/extension/popup.html +74 -0
- package/extension/popup.js +105 -0
- package/extension/protocol.js +45 -0
- package/extension/remote-auth.js +18 -0
- package/extension/sessions.js +134 -0
- package/extension/snapshot.js +161 -0
- package/extension/task-groups.js +108 -0
- package/extension/tasks.js +186 -0
- package/extension/wait.js +89 -0
- package/hub/README.md +42 -0
- package/hub/package-lock.json +1544 -0
- package/hub/package.json +13 -0
- package/hub/src/rpc.js +41 -0
- package/hub/src/worker.js +322 -0
- package/hub/wrangler.example.toml +19 -0
- package/package.json +83 -0
- package/server/cdp-bridge.js +200 -0
- package/server/cli.js +1798 -0
- package/server/hub-server.js +258 -0
- package/server/install.js +250 -0
- package/server/mcp-server.js +504 -0
- package/server/npx-runner.js +96 -0
- package/server/relay-server.js +1356 -0
- package/server/remote-protocol.js +76 -0
- package/server/runtime-worker.js +166 -0
- package/server/script-runtime.js +189 -0
- package/server/sdk.js +307 -0
- package/server/service-state.js +103 -0
- package/server/snapshot.js +161 -0
- package/server/uninstall.js +61 -0
- package/server/windows-service-entry.js +58 -0
- package/server/windows-service.js +360 -0
- package/skills/browser-relay/SKILL.md +192 -0
- package/skills/browser-relay/references/legacy-api.md +163 -0
- package/skills/browser-relay/references/runtime.md +240 -0
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
// Activity belongs to a user operation, not to a debugger connection. In
|
|
2
|
+
// particular, automatic console/network subscriptions must stay visually idle.
|
|
3
|
+
export function isUserCdpCommand(method) {
|
|
4
|
+
return /^(Input\.|Accessibility\.(get|query)|DOMSnapshot\.captureSnapshot$|DOM\.(getDocument|getOuterHTML|querySelector|performSearch|getSearchResults|getBoxModel|getContentQuads|scrollIntoViewIfNeeded|focus|set)|Runtime\.(evaluate|callFunctionOn)$|Page\.(navigate|reload|captureScreenshot|printToPDF|handleJavaScriptDialog)$|Network\.getResponseBody$)/.test(method);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// A read or click often finishes in well under a second. Keep its indication
|
|
8
|
+
// long enough to show a full favicon cycle and visible movement in the glow;
|
|
9
|
+
// subsequent operations renew this window without restarting the animation.
|
|
10
|
+
export function createActivityTracker({ show, hide, settleMs = 4000 }) {
|
|
11
|
+
const entries = new Map();
|
|
12
|
+
function clear(tabId) {
|
|
13
|
+
const entry = entries.get(tabId);
|
|
14
|
+
if (!entry) return;
|
|
15
|
+
clearTimeout(entry.timer);
|
|
16
|
+
entries.delete(tabId);
|
|
17
|
+
hide(tabId);
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
begin(tabId) {
|
|
21
|
+
let entry = entries.get(tabId);
|
|
22
|
+
if (!entry) {
|
|
23
|
+
entry = { count: 0, timer: null };
|
|
24
|
+
entries.set(tabId, entry);
|
|
25
|
+
show(tabId);
|
|
26
|
+
}
|
|
27
|
+
entry.count++;
|
|
28
|
+
clearTimeout(entry.timer);
|
|
29
|
+
let ended = false;
|
|
30
|
+
return (immediate = false) => {
|
|
31
|
+
if (ended || entries.get(tabId) !== entry) return;
|
|
32
|
+
ended = true;
|
|
33
|
+
if (--entry.count) return;
|
|
34
|
+
if (immediate) clear(tabId);
|
|
35
|
+
else entry.timer = setTimeout(() => clear(tabId), settleMs);
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
clear,
|
|
39
|
+
active: (tabId) => entries.has(tabId),
|
|
40
|
+
running: (tabId) => (entries.get(tabId)?.count || 0) > 0,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Runs only in our isolated world, in the top document. Chrome may retain its
|
|
45
|
+
// chosen favicon when a new candidate is appended, so decorate existing icon
|
|
46
|
+
// links too. Restore their attributes, including site updates during the task.
|
|
47
|
+
// The page title is never edited. The edge light lives in a closed shadow root
|
|
48
|
+
// and cannot intercept pointer input or enter the accessibility tree.
|
|
49
|
+
export function updateActivityIndicator({ token, frames, leaseMs = 4000, stop = false }) {
|
|
50
|
+
const key = '__browserRelayActivityIndicator';
|
|
51
|
+
let state = globalThis[key];
|
|
52
|
+
if (stop) {
|
|
53
|
+
if (state?.token === token) state.dispose();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (state && (state.token !== token || state.disposed)) {
|
|
57
|
+
state.dispose(true);
|
|
58
|
+
state = null;
|
|
59
|
+
}
|
|
60
|
+
if (state) {
|
|
61
|
+
state.expiresAt = Date.now() + leaseMs;
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (!frames?.length || !document.head) return;
|
|
65
|
+
const link = document.createElement('link');
|
|
66
|
+
link.rel = 'icon';
|
|
67
|
+
link.type = 'image/png';
|
|
68
|
+
link.sizes = '32x32';
|
|
69
|
+
link.dataset.browserRelayActivity = 'true';
|
|
70
|
+
const originals = new Map();
|
|
71
|
+
const attributes = ['href', 'type', 'sizes'];
|
|
72
|
+
function rememberSiteChanges(node, record) {
|
|
73
|
+
for (const name of attributes) {
|
|
74
|
+
const value = node.getAttribute(name);
|
|
75
|
+
if (value !== record.written[name]) record.original[name] = value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function restore(node, record) {
|
|
79
|
+
rememberSiteChanges(node, record);
|
|
80
|
+
for (const [name, value] of Object.entries(record.original)) {
|
|
81
|
+
if (value === null) node.removeAttribute(name);
|
|
82
|
+
else node.setAttribute(name, value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)');
|
|
86
|
+
const overlay = document.createElement('div');
|
|
87
|
+
overlay.dataset.browserRelayOverlay = 'true';
|
|
88
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
89
|
+
overlay.inert = true;
|
|
90
|
+
// Inline important rules keep broad website styles away from the host.
|
|
91
|
+
overlay.style.cssText = 'all:initial!important;position:fixed!important;inset:0!important;width:auto!important;height:auto!important;margin:0!important;padding:0!important;border:0!important;display:block!important;z-index:2147483647!important;pointer-events:none!important;overflow:hidden!important;contain:strict!important;visibility:visible!important;opacity:1!important;transform:none!important;';
|
|
92
|
+
const shadow = overlay.attachShadow({ mode: 'closed' });
|
|
93
|
+
shadow.innerHTML = `<style>
|
|
94
|
+
* { box-sizing: border-box; pointer-events: none !important; }
|
|
95
|
+
.surface { position: absolute; inset: 0; opacity: 0;
|
|
96
|
+
transition: opacity 360ms cubic-bezier(.2,.7,.2,1); }
|
|
97
|
+
.surface.visible { opacity: 1; }
|
|
98
|
+
.aura { position: absolute; inset: 0; }
|
|
99
|
+
.layer { position: absolute; inset: 0; }
|
|
100
|
+
.band { position: absolute; inset: 0; border-radius: 0;
|
|
101
|
+
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
|
102
|
+
mask-composite: exclude; }
|
|
103
|
+
.spectrum { position: absolute; inset: 0;
|
|
104
|
+
background: conic-gradient(from 215deg at 50% 50%,
|
|
105
|
+
#ffae8018, #f06db518 42deg, #9568f918 82deg, #557dff18 120deg,
|
|
106
|
+
#55dded18 160deg, #8c9eff18 195deg, #c279f818 230deg,
|
|
107
|
+
#f485bd18 270deg, #ffd09b18 310deg, #ffae8018); }
|
|
108
|
+
.spectrum::before, .spectrum::after { content: ''; position: absolute;
|
|
109
|
+
inset: -28%; transform-origin: center;
|
|
110
|
+
mask: radial-gradient(ellipse, #fff 48%, #fff9 70%, #fff0 100%); }
|
|
111
|
+
.spectrum::before {
|
|
112
|
+
background:
|
|
113
|
+
radial-gradient(ellipse at 23% 24%, #69e5ff 0%, #677dffcc 12%, #8178ed00 36%),
|
|
114
|
+
radial-gradient(ellipse at 81% 80%, #a478ff 0%, #6c99ffb3 12%, #6c99ff00 38%);
|
|
115
|
+
animation: aura-cool 3.8s cubic-bezier(.4,0,.6,1) -.9s infinite alternate; }
|
|
116
|
+
.spectrum::after {
|
|
117
|
+
background:
|
|
118
|
+
radial-gradient(ellipse at 76% 22%, #f69fce 0%, #d779ffb3 12%, #d779ff00 36%),
|
|
119
|
+
radial-gradient(ellipse at 24% 82%, #ffbf87 0%, #fc849dbf 14%, #fc849d00 40%);
|
|
120
|
+
animation: aura-warm 5.1s cubic-bezier(.4,0,.6,1) -2.6s infinite alternate; }
|
|
121
|
+
.diffuse { filter: blur(20px); opacity: .28; }
|
|
122
|
+
.diffuse .band { inset: -14px; padding: 26px; }
|
|
123
|
+
.bloom { filter: blur(9px); opacity: .52; }
|
|
124
|
+
.bloom .band { inset: -7px; padding: 14px; }
|
|
125
|
+
@keyframes aura-cool {
|
|
126
|
+
0% { transform: translate(-8%, 5%) rotate(-28deg) scale(1.05); opacity: .64; }
|
|
127
|
+
28% { transform: translate(12%, -9%) rotate(48deg) scale(1.1); opacity: .96; }
|
|
128
|
+
64% { transform: translate(-4%, 12%) rotate(5deg) scale(.96); opacity: .72; }
|
|
129
|
+
100% { transform: translate(8%, -4%) rotate(88deg) scale(1.06); opacity: .88; }
|
|
130
|
+
}
|
|
131
|
+
@keyframes aura-warm {
|
|
132
|
+
0% { transform: translate(6%, -7%) rotate(62deg) scale(1.04); opacity: .84; }
|
|
133
|
+
39% { transform: translate(-12%, 8%) rotate(-36deg) scale(1.1); opacity: .68; }
|
|
134
|
+
72% { transform: translate(9%, 5%) rotate(29deg) scale(.96); opacity: .94; }
|
|
135
|
+
100% { transform: translate(-5%, -11%) rotate(-78deg) scale(1.08); opacity: .74; }
|
|
136
|
+
}
|
|
137
|
+
@media (prefers-reduced-motion: reduce) {
|
|
138
|
+
.surface { transition: none; }
|
|
139
|
+
.spectrum::before, .spectrum::after { animation: none; }
|
|
140
|
+
}
|
|
141
|
+
@media print { .surface { display: none; } }
|
|
142
|
+
</style><div class="surface"><div class="aura">
|
|
143
|
+
<div class="layer diffuse"><div class="band"><div class="spectrum"></div></div></div>
|
|
144
|
+
<div class="layer bloom"><div class="band"><div class="spectrum"></div></div></div>
|
|
145
|
+
</div></div>`;
|
|
146
|
+
const surface = shadow.querySelector('.surface');
|
|
147
|
+
document.documentElement.append(overlay);
|
|
148
|
+
const entrance = requestAnimationFrame(() => {
|
|
149
|
+
// Establish the initial opacity before enabling the transition.
|
|
150
|
+
surface.getBoundingClientRect();
|
|
151
|
+
surface.classList.add('visible');
|
|
152
|
+
});
|
|
153
|
+
state = {
|
|
154
|
+
token,
|
|
155
|
+
expiresAt: Date.now() + leaseMs,
|
|
156
|
+
frame: 0,
|
|
157
|
+
timer: null,
|
|
158
|
+
disposed: false,
|
|
159
|
+
fadeTimer: null,
|
|
160
|
+
dispose(immediate = false) {
|
|
161
|
+
if (!state.disposed) {
|
|
162
|
+
state.disposed = true;
|
|
163
|
+
cancelAnimationFrame(entrance);
|
|
164
|
+
clearInterval(state.timer);
|
|
165
|
+
link.remove();
|
|
166
|
+
for (const [node, record] of originals) restore(node, record);
|
|
167
|
+
window.removeEventListener('pagehide', state.dispose);
|
|
168
|
+
surface.classList.remove('visible');
|
|
169
|
+
}
|
|
170
|
+
const remove = () => {
|
|
171
|
+
clearTimeout(state.fadeTimer);
|
|
172
|
+
overlay.remove();
|
|
173
|
+
if (globalThis[key] === state) delete globalThis[key];
|
|
174
|
+
};
|
|
175
|
+
if (immediate || reducedMotion.matches) remove();
|
|
176
|
+
else if (!state.fadeTimer) state.fadeTimer = setTimeout(remove, 360);
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const tick = () => {
|
|
180
|
+
if (state.disposed) return;
|
|
181
|
+
if (Date.now() >= state.expiresAt) return state.dispose();
|
|
182
|
+
const frame = reducedMotion.matches ? 0 : state.frame++ % frames.length;
|
|
183
|
+
if (link.href !== frames[frame]) link.href = frames[frame];
|
|
184
|
+
const currentIcons = new Set(document.querySelectorAll('link[rel~="icon"]'));
|
|
185
|
+
currentIcons.delete(link);
|
|
186
|
+
for (const [node, record] of originals) {
|
|
187
|
+
if (!currentIcons.has(node)) {
|
|
188
|
+
restore(node, record);
|
|
189
|
+
originals.delete(node);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const node of currentIcons) {
|
|
193
|
+
let record = originals.get(node);
|
|
194
|
+
if (!record) {
|
|
195
|
+
const original = Object.fromEntries(attributes.map(name => [name, node.getAttribute(name)]));
|
|
196
|
+
record = { original, written: { ...original } };
|
|
197
|
+
originals.set(node, record);
|
|
198
|
+
}
|
|
199
|
+
rememberSiteChanges(node, record);
|
|
200
|
+
record.written = { href: frames[frame], type: 'image/png', sizes: '32x32' };
|
|
201
|
+
for (const [name, value] of Object.entries(record.written))
|
|
202
|
+
if (node.getAttribute(name) !== value) node.setAttribute(name, value);
|
|
203
|
+
}
|
|
204
|
+
if (link.parentNode !== document.head) document.head.append(link);
|
|
205
|
+
if (!overlay.isConnected) document.documentElement.append(overlay);
|
|
206
|
+
};
|
|
207
|
+
globalThis[key] = state;
|
|
208
|
+
tick();
|
|
209
|
+
state.timer = setInterval(tick, 160);
|
|
210
|
+
window.addEventListener('pagehide', state.dispose, { once: true });
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export async function activityIconFrames(bitmap) {
|
|
215
|
+
const canvas = new OffscreenCanvas(32, 32);
|
|
216
|
+
const ctx = canvas.getContext('2d');
|
|
217
|
+
const frames = [];
|
|
218
|
+
for (let i = 0; i < 12; i++) {
|
|
219
|
+
const breath = (1 - Math.cos(i / 12 * Math.PI * 2)) / 2;
|
|
220
|
+
ctx.clearRect(0, 0, 32, 32);
|
|
221
|
+
// Keep the site's icon legible at Chrome's normal 16px size. A 6px status
|
|
222
|
+
// dot has a dark rim and a light outer keyline for light and dark tab bars.
|
|
223
|
+
if (bitmap) ctx.drawImage(bitmap, 1, 1, 28, 28);
|
|
224
|
+
else {
|
|
225
|
+
ctx.fillStyle = '#4f6580';
|
|
226
|
+
ctx.beginPath();
|
|
227
|
+
ctx.arc(14, 14, 11, 0, Math.PI * 2);
|
|
228
|
+
ctx.fill();
|
|
229
|
+
}
|
|
230
|
+
ctx.save();
|
|
231
|
+
ctx.shadowColor = `rgba(45, 241, 193, ${0.35 + breath * 0.4})`;
|
|
232
|
+
ctx.shadowBlur = 1 + breath * 2;
|
|
233
|
+
ctx.fillStyle = '#effffb';
|
|
234
|
+
ctx.beginPath();
|
|
235
|
+
ctx.arc(24, 24, 7.6, 0, Math.PI * 2);
|
|
236
|
+
ctx.fill();
|
|
237
|
+
ctx.restore();
|
|
238
|
+
ctx.fillStyle = '#103d36';
|
|
239
|
+
ctx.beginPath();
|
|
240
|
+
ctx.arc(24, 24, 6.6, 0, Math.PI * 2);
|
|
241
|
+
ctx.fill();
|
|
242
|
+
ctx.fillStyle = `rgb(${24 + Math.round(breath * 50)}, ${202 + Math.round(breath * 42)}, ${157 + Math.round(breath * 38)})`;
|
|
243
|
+
ctx.beginPath();
|
|
244
|
+
ctx.arc(24, 24, 5.5, 0, Math.PI * 2);
|
|
245
|
+
ctx.fill();
|
|
246
|
+
const bytes = new Uint8Array(await (await canvas.convertToBlob({ type: 'image/png' })).arrayBuffer());
|
|
247
|
+
frames.push(`data:image/png;base64,${btoa(String.fromCharCode(...bytes))}`);
|
|
248
|
+
}
|
|
249
|
+
return frames;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function createTabActivityRenderer(chromeApi) {
|
|
253
|
+
const entries = new Map();
|
|
254
|
+
const evaluate = (tabId, contextId, payload) => chromeApi.debugger.sendCommand(
|
|
255
|
+
{ tabId }, 'Runtime.evaluate', {
|
|
256
|
+
contextId,
|
|
257
|
+
expression: `(${updateActivityIndicator.toString()})(${JSON.stringify(payload)})`,
|
|
258
|
+
returnByValue: true,
|
|
259
|
+
},
|
|
260
|
+
);
|
|
261
|
+
async function refresh(tabId, entry) {
|
|
262
|
+
if (entry.busy || entries.get(tabId) !== entry) return;
|
|
263
|
+
entry.busy = true;
|
|
264
|
+
try {
|
|
265
|
+
if (!entry.contextId) {
|
|
266
|
+
const [{ frameTree }, tab] = await Promise.all([
|
|
267
|
+
chromeApi.debugger.sendCommand({ tabId }, 'Page.getFrameTree'),
|
|
268
|
+
chromeApi.tabs.get(tabId),
|
|
269
|
+
]);
|
|
270
|
+
const { executionContextId } = await chromeApi.debugger.sendCommand(
|
|
271
|
+
{ tabId }, 'Page.createIsolatedWorld', {
|
|
272
|
+
frameId: frameTree.frame.id, worldName: 'browser-relay-activity',
|
|
273
|
+
},
|
|
274
|
+
);
|
|
275
|
+
let bitmap;
|
|
276
|
+
try {
|
|
277
|
+
// Chrome's local favicon cache: no third-party favicon service and
|
|
278
|
+
// no new requests to the user's websites to draw the indicator.
|
|
279
|
+
const url = new URL(chromeApi.runtime.getURL('/_favicon/'));
|
|
280
|
+
url.searchParams.set('pageUrl', tab.url);
|
|
281
|
+
url.searchParams.set('size', '32');
|
|
282
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
|
|
283
|
+
bitmap = await createImageBitmap(await response.blob());
|
|
284
|
+
} catch { /* An iconless page still gets a small, consistent indicator. */ }
|
|
285
|
+
let frames;
|
|
286
|
+
try { frames = await activityIconFrames(bitmap); }
|
|
287
|
+
finally { bitmap?.close(); }
|
|
288
|
+
if (entries.get(tabId) !== entry) return;
|
|
289
|
+
entry.contextId = executionContextId;
|
|
290
|
+
await evaluate(tabId, entry.contextId, { token: entry.token, frames });
|
|
291
|
+
} else {
|
|
292
|
+
const result = await evaluate(tabId, entry.contextId, { token: entry.token });
|
|
293
|
+
if (result.result?.value !== true) entry.contextId = null;
|
|
294
|
+
}
|
|
295
|
+
} catch {
|
|
296
|
+
entry.contextId = null;
|
|
297
|
+
// Cosmetic failure must never fail or replay a browser action. A later
|
|
298
|
+
// refresh can recover after navigation or a debugger wake-up.
|
|
299
|
+
} finally {
|
|
300
|
+
entry.busy = false;
|
|
301
|
+
if (entries.get(tabId) === entry)
|
|
302
|
+
entry.timer = setTimeout(() => void refresh(tabId, entry), 1000);
|
|
303
|
+
else if (entry.contextId)
|
|
304
|
+
void evaluate(tabId, entry.contextId, { token: entry.token, stop: true }).catch(() => {});
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
function stop(tabId) {
|
|
308
|
+
const entry = entries.get(tabId);
|
|
309
|
+
if (!entry) return;
|
|
310
|
+
entries.delete(tabId);
|
|
311
|
+
clearTimeout(entry.timer);
|
|
312
|
+
if (entry.contextId)
|
|
313
|
+
void evaluate(tabId, entry.contextId, { token: entry.token, stop: true }).catch(() => {});
|
|
314
|
+
}
|
|
315
|
+
return {
|
|
316
|
+
start(tabId) {
|
|
317
|
+
if (entries.has(tabId)) return;
|
|
318
|
+
const entry = { contextId: null, timer: null, busy: false, token: crypto.randomUUID() };
|
|
319
|
+
entries.set(tabId, entry);
|
|
320
|
+
void refresh(tabId, entry);
|
|
321
|
+
},
|
|
322
|
+
stop,
|
|
323
|
+
navigated(tabId, running) {
|
|
324
|
+
stop(tabId);
|
|
325
|
+
if (running) this.start(tabId);
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
}
|