@mind-harness/mh-browser 0.2.4 → 0.2.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/extension/manifest.json +1 -1
- package/lib/client.cjs +1 -1
- package/lib/index.js +495 -267
- package/lib/typert.host.js +1 -1
- package/lib/typert.remote-client.js +1 -1
- package/lib/types/service/abortable.d.ts +24 -0
- package/lib/types/service/extension.d.ts +7 -6
- package/lib/types/service/windows.d.ts +10 -3
- package/lib/types/tools/given.d.ts +5 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -33,6 +33,146 @@ function defaultProfileDir(profile, home = resolveDshHome()) {
|
|
|
33
33
|
return profile === void 0 ? join(home, "mh", "browser") : join(home, "mh", "profiles", profile.name, "browser");
|
|
34
34
|
}
|
|
35
35
|
//#endregion
|
|
36
|
+
//#region lib/types/types.js
|
|
37
|
+
var MhBrowserError = class extends Error {
|
|
38
|
+
code;
|
|
39
|
+
page;
|
|
40
|
+
constructor(code, message, page) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = "MhBrowserError";
|
|
43
|
+
this.code = code;
|
|
44
|
+
if (page !== void 0) this.page = page;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** Owner of the person's own window. It belongs to no Session. */
|
|
48
|
+
const PERSON_OWNER = "person";
|
|
49
|
+
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
50
|
+
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
51
|
+
/**
|
|
52
|
+
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
53
|
+
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
54
|
+
*/
|
|
55
|
+
const INTERRUPTED = "INTERRUPTED";
|
|
56
|
+
/** The browser did not start or could not be restarted. */
|
|
57
|
+
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
58
|
+
function unknownTab() {
|
|
59
|
+
return {
|
|
60
|
+
url: "",
|
|
61
|
+
reason: UNKNOWN_TAB_REASON
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region lib/types/service/abortable.js
|
|
66
|
+
const WAITING_STOPPED$1 = "stopped while waiting";
|
|
67
|
+
/** A wait ended because its signal aborted, including a deadline synthesized here. */
|
|
68
|
+
function stopped$1() {
|
|
69
|
+
return new MhBrowserError(INTERRUPTED, WAITING_STOPPED$1);
|
|
70
|
+
}
|
|
71
|
+
function isInterrupted(error) {
|
|
72
|
+
return error instanceof MhBrowserError && error.code === "INTERRUPTED";
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* `promise`, or a rejection when `signal` aborts. The original promise is
|
|
76
|
+
* always observed, so abandoning it leaves no unhandled rejection.
|
|
77
|
+
*/
|
|
78
|
+
function untilAborted(promise, signal) {
|
|
79
|
+
const settled = promise.then((value) => ({
|
|
80
|
+
ok: true,
|
|
81
|
+
value
|
|
82
|
+
}), (error) => ({
|
|
83
|
+
ok: false,
|
|
84
|
+
error
|
|
85
|
+
}));
|
|
86
|
+
if (signal === void 0) return unwrap(settled);
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
let done = false;
|
|
89
|
+
const finish = (run) => {
|
|
90
|
+
if (done) return;
|
|
91
|
+
done = true;
|
|
92
|
+
signal.removeEventListener("abort", onAbort);
|
|
93
|
+
run();
|
|
94
|
+
};
|
|
95
|
+
const onAbort = () => {
|
|
96
|
+
finish(() => reject(stopped$1()));
|
|
97
|
+
};
|
|
98
|
+
if (signal.aborted) {
|
|
99
|
+
finish(() => reject(stopped$1()));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
settled.then((result) => {
|
|
104
|
+
finish(() => {
|
|
105
|
+
if (result.ok) resolve(result.value);
|
|
106
|
+
else reject(result.error);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** `signal`, a deadline of `ms`, or whichever of the two aborts first. */
|
|
112
|
+
function capped(signal, ms) {
|
|
113
|
+
const limit = AbortSignal.timeout(ms);
|
|
114
|
+
if (signal === void 0) return limit;
|
|
115
|
+
return AbortSignal.any([signal, limit]);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Run `op` once `previous` settles. Aborting `signal` while waiting rejects
|
|
119
|
+
* `run` and does not call `op`. `tail` still follows `previous`, and `op`
|
|
120
|
+
* when `op` has started, so a later caller cannot overlap the work.
|
|
121
|
+
*/
|
|
122
|
+
function enqueue(previous, op, signal) {
|
|
123
|
+
let started = Promise.resolve();
|
|
124
|
+
let began = false;
|
|
125
|
+
return {
|
|
126
|
+
run: new Promise((resolve, reject) => {
|
|
127
|
+
let settled = false;
|
|
128
|
+
const fail = () => {
|
|
129
|
+
if (settled) return;
|
|
130
|
+
settled = true;
|
|
131
|
+
reject(stopped$1());
|
|
132
|
+
};
|
|
133
|
+
const onAbort = () => {
|
|
134
|
+
if (began) return;
|
|
135
|
+
fail();
|
|
136
|
+
};
|
|
137
|
+
const start = () => {
|
|
138
|
+
signal?.removeEventListener("abort", onAbort);
|
|
139
|
+
if (settled || signal?.aborted === true) {
|
|
140
|
+
fail();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
let work;
|
|
144
|
+
try {
|
|
145
|
+
began = true;
|
|
146
|
+
work = op();
|
|
147
|
+
} catch (error) {
|
|
148
|
+
started = Promise.reject(error);
|
|
149
|
+
started.catch(() => void 0);
|
|
150
|
+
if (!settled) {
|
|
151
|
+
settled = true;
|
|
152
|
+
reject(error);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
started = work;
|
|
157
|
+
resolve(work);
|
|
158
|
+
};
|
|
159
|
+
if (signal?.aborted === true) {
|
|
160
|
+
fail();
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (signal !== void 0) signal.addEventListener("abort", onAbort, { once: true });
|
|
164
|
+
previous.then(start, start);
|
|
165
|
+
}),
|
|
166
|
+
tail: previous.then(() => started, () => started).then(() => void 0, () => void 0)
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function unwrap(settled) {
|
|
170
|
+
return settled.then((result) => {
|
|
171
|
+
if (result.ok) return result.value;
|
|
172
|
+
throw result.error;
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
//#endregion
|
|
36
176
|
//#region lib/types/representation/agent-view.js
|
|
37
177
|
const AGENT_VIEW_VERSION = "0.1";
|
|
38
178
|
const AGENT_VIEW_ACCEPT = "text/markdown; variant=agent-view";
|
|
@@ -446,35 +586,6 @@ function formatSeconds(seconds) {
|
|
|
446
586
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
447
587
|
}
|
|
448
588
|
//#endregion
|
|
449
|
-
//#region lib/types/types.js
|
|
450
|
-
var MhBrowserError = class extends Error {
|
|
451
|
-
code;
|
|
452
|
-
page;
|
|
453
|
-
constructor(code, message, page) {
|
|
454
|
-
super(message);
|
|
455
|
-
this.name = "MhBrowserError";
|
|
456
|
-
this.code = code;
|
|
457
|
-
if (page !== void 0) this.page = page;
|
|
458
|
-
}
|
|
459
|
-
};
|
|
460
|
-
/** Owner of the person's own window. It belongs to no Session. */
|
|
461
|
-
const PERSON_OWNER = "person";
|
|
462
|
-
const UNKNOWN_TAB = "UNKNOWN_TAB";
|
|
463
|
-
const UNKNOWN_TAB_REASON = "no such tab in this session";
|
|
464
|
-
/**
|
|
465
|
-
* Internal: a wait ended because its signal aborted. A tool never reports it:
|
|
466
|
-
* a stopped call ends as the tool runtime's abort, a takeover as `TAKEN_OVER`.
|
|
467
|
-
*/
|
|
468
|
-
const INTERRUPTED = "INTERRUPTED";
|
|
469
|
-
/** The browser did not start or could not be restarted. */
|
|
470
|
-
const BROWSER_UNAVAILABLE = "BROWSER_UNAVAILABLE";
|
|
471
|
-
function unknownTab() {
|
|
472
|
-
return {
|
|
473
|
-
url: "",
|
|
474
|
-
reason: UNKNOWN_TAB_REASON
|
|
475
|
-
};
|
|
476
|
-
}
|
|
477
|
-
//#endregion
|
|
478
589
|
//#region lib/types/tools/session.js
|
|
479
590
|
/** dsh shows the model only the message, so the message starts with the code. */
|
|
480
591
|
var BrowserToolError = class extends HarnessError {
|
|
@@ -694,6 +805,18 @@ function visit(value, depth, found) {
|
|
|
694
805
|
for (const item of Object.values(row)) visit(item, depth + 1, found);
|
|
695
806
|
}
|
|
696
807
|
//#endregion
|
|
808
|
+
//#region lib/types/tools/given.js
|
|
809
|
+
/** A string the caller gave. Blank is the same as omitting it; the text itself is kept. */
|
|
810
|
+
function givenString(value) {
|
|
811
|
+
if (typeof value !== "string" || value.trim() === "") return void 0;
|
|
812
|
+
return value;
|
|
813
|
+
}
|
|
814
|
+
/** Seconds to wait. Zero is the same as omitting it; any other number is kept. */
|
|
815
|
+
function givenTime(value) {
|
|
816
|
+
if (typeof value !== "number" || value === 0) return void 0;
|
|
817
|
+
return value;
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
697
820
|
//#region lib/types/tools/delivery.js
|
|
698
821
|
/** Every mh-browser tool result's `meta.by`; a result without it is not a delivery. */
|
|
699
822
|
const META_BY = "mh-browser";
|
|
@@ -1370,9 +1493,9 @@ const OPS = [
|
|
|
1370
1493
|
},
|
|
1371
1494
|
build: async (args) => ({
|
|
1372
1495
|
kind: "wait_for",
|
|
1373
|
-
...
|
|
1374
|
-
...
|
|
1375
|
-
...
|
|
1496
|
+
...stringField("text", args.text),
|
|
1497
|
+
...stringField("textGone", args.textGone),
|
|
1498
|
+
...timeField(args.time)
|
|
1376
1499
|
})
|
|
1377
1500
|
},
|
|
1378
1501
|
{
|
|
@@ -1399,7 +1522,7 @@ const OPS = [
|
|
|
1399
1522
|
filename: true,
|
|
1400
1523
|
build: async (args) => ({
|
|
1401
1524
|
kind: "snapshot",
|
|
1402
|
-
...
|
|
1525
|
+
...stringField("target", args.target),
|
|
1403
1526
|
...typeof args.depth === "number" ? { depth: args.depth } : {},
|
|
1404
1527
|
...args.boxes === true ? { boxes: true } : {}
|
|
1405
1528
|
})
|
|
@@ -1428,8 +1551,8 @@ const OPS = [
|
|
|
1428
1551
|
filename: true,
|
|
1429
1552
|
build: async (args) => ({
|
|
1430
1553
|
kind: "find",
|
|
1431
|
-
...
|
|
1432
|
-
...
|
|
1554
|
+
...stringField("text", args.text),
|
|
1555
|
+
...stringField("regex", args.regex),
|
|
1433
1556
|
...typeof args.maxResults === "number" ? { maxResults: args.maxResults } : {}
|
|
1434
1557
|
})
|
|
1435
1558
|
},
|
|
@@ -1473,7 +1596,7 @@ const OPS = [
|
|
|
1473
1596
|
build: async (args) => ({
|
|
1474
1597
|
kind: "take_screenshot",
|
|
1475
1598
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1476
|
-
...
|
|
1599
|
+
...stringField("target", args.target),
|
|
1477
1600
|
...args.type === "png" || args.type === "jpeg" || args.type === "webp" ? { type: args.type } : {},
|
|
1478
1601
|
...args.fullPage === true ? { fullPage: true } : {},
|
|
1479
1602
|
...args.scale === "css" || args.scale === "device" ? { scale: args.scale } : {}
|
|
@@ -1507,7 +1630,7 @@ const OPS = [
|
|
|
1507
1630
|
function: string(args.function),
|
|
1508
1631
|
callId: callIdOf(exec),
|
|
1509
1632
|
...typeof args.element === "string" ? { element: args.element } : {},
|
|
1510
|
-
...
|
|
1633
|
+
...stringField("target", args.target)
|
|
1511
1634
|
})
|
|
1512
1635
|
}
|
|
1513
1636
|
];
|
|
@@ -1542,7 +1665,7 @@ function defineOperation(ctx, reader, spec) {
|
|
|
1542
1665
|
const target = writeTarget(ctx, session);
|
|
1543
1666
|
ctx.mhBrowser.noteWorkspace(session.id, target);
|
|
1544
1667
|
const input = args;
|
|
1545
|
-
const filename = spec.filename === true
|
|
1668
|
+
const filename = spec.filename === true ? givenString(input.filename) : void 0;
|
|
1546
1669
|
if (filename !== void 0) await assertInside(target, filename);
|
|
1547
1670
|
try {
|
|
1548
1671
|
const tab = tabOf(reader, session, args.tab);
|
|
@@ -1600,6 +1723,18 @@ function string(value) {
|
|
|
1600
1723
|
function stringList$1(value) {
|
|
1601
1724
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1602
1725
|
}
|
|
1726
|
+
function stringField(key, value) {
|
|
1727
|
+
const given = givenString(value);
|
|
1728
|
+
if (given === void 0) return {};
|
|
1729
|
+
if (key === "target") return { target: given };
|
|
1730
|
+
if (key === "text") return { text: given };
|
|
1731
|
+
if (key === "textGone") return { textGone: given };
|
|
1732
|
+
return { regex: given };
|
|
1733
|
+
}
|
|
1734
|
+
function timeField(value) {
|
|
1735
|
+
const given = givenTime(value);
|
|
1736
|
+
return given === void 0 ? {} : { time: given };
|
|
1737
|
+
}
|
|
1603
1738
|
function optionalString(args, key) {
|
|
1604
1739
|
return typeof args[key] === "string" ? { element: args[key] } : {};
|
|
1605
1740
|
}
|
|
@@ -1911,6 +2046,8 @@ function takeoverText(taken) {
|
|
|
1911
2046
|
}
|
|
1912
2047
|
//#endregion
|
|
1913
2048
|
//#region lib/types/presence/register.js
|
|
2049
|
+
/** A presence read that stops answering. One stuck page must not hold the next step. */
|
|
2050
|
+
const CHECK_MS = 6e4;
|
|
1914
2051
|
const BROWSER_TOOLS = /* @__PURE__ */ new Set([
|
|
1915
2052
|
"browser_open",
|
|
1916
2053
|
"browser_invoke",
|
|
@@ -1931,14 +2068,17 @@ function registerPresence(ctx, reader) {
|
|
|
1931
2068
|
ctx.effect(() => () => {
|
|
1932
2069
|
lifetime.abort();
|
|
1933
2070
|
});
|
|
1934
|
-
const waitForChecks = async (agent) => {
|
|
2071
|
+
const waitForChecks = async (agent, signal) => {
|
|
1935
2072
|
let pending;
|
|
1936
|
-
while ((pending = tails.get(agent)) !== void 0)
|
|
2073
|
+
while ((pending = tails.get(agent)) !== void 0) {
|
|
2074
|
+
await untilAborted(pending, signal).catch(() => void 0);
|
|
2075
|
+
if (signal.aborted) return;
|
|
2076
|
+
}
|
|
1937
2077
|
};
|
|
1938
2078
|
const queue = (agent, urls) => {
|
|
1939
2079
|
const current = (tails.get(agent) ?? Promise.resolve()).then(async () => {
|
|
1940
2080
|
if (lifetime.signal.aborted) return;
|
|
1941
|
-
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: lifetime.signal });
|
|
2081
|
+
const reminder = await ctx.mhBrowser.presenceCheck(agent.session.id, urls, { signal: AbortSignal.any([lifetime.signal, AbortSignal.timeout(CHECK_MS)]) });
|
|
1942
2082
|
if (reminder === void 0 || lifetime.signal.aborted) return;
|
|
1943
2083
|
syncInbox(agent, reader.presence(agent.session), [], reminderMessage(reminder));
|
|
1944
2084
|
}).catch((error) => {
|
|
@@ -2015,7 +2155,7 @@ function registerPresence(ctx, reader) {
|
|
|
2015
2155
|
agents.set(agent.session.id, agent);
|
|
2016
2156
|
const handed = messages.some((message) => message.source.kind === "user") ? ctx.mhBrowser.takePersonHandback(agent.session.id) : void 0;
|
|
2017
2157
|
if (handed !== void 0 && handed.urls.length > 0) queue(agent, handed.urls);
|
|
2018
|
-
await waitForChecks(agent);
|
|
2158
|
+
await waitForChecks(agent, signal);
|
|
2019
2159
|
signal.throwIfAborted();
|
|
2020
2160
|
const desired = await compose(ctx.mhBrowser, agent, reader.presence(agent.session), messages, signal);
|
|
2021
2161
|
signal.throwIfAborted();
|
|
@@ -2159,11 +2299,10 @@ function record(value) {
|
|
|
2159
2299
|
return {};
|
|
2160
2300
|
}
|
|
2161
2301
|
function checkOptions(signal, extra) {
|
|
2162
|
-
const options = {};
|
|
2302
|
+
const options = { signal: capped(signal, CHECK_MS) };
|
|
2163
2303
|
if (extra?.baseline === true) options.baseline = true;
|
|
2164
2304
|
if (extra?.replaces === true) options.replaces = true;
|
|
2165
2305
|
if (extra?.force === true) options.force = true;
|
|
2166
|
-
if (signal !== void 0) options.signal = signal;
|
|
2167
2306
|
return options;
|
|
2168
2307
|
}
|
|
2169
2308
|
function canonical$1(url) {
|
|
@@ -3684,6 +3823,8 @@ async function dispatch(page, op, snapshot, signal) {
|
|
|
3684
3823
|
//#region lib/types/service/evaluate.js
|
|
3685
3824
|
/** How long requests the function started are still collected after it returns. */
|
|
3686
3825
|
const SETTLE_MS$1 = 50;
|
|
3826
|
+
/** A debugger call or a detach the page no longer answers. */
|
|
3827
|
+
const CALL_MS$2 = 15e3;
|
|
3687
3828
|
/**
|
|
3688
3829
|
* Run the expression in the page and collect write requests.
|
|
3689
3830
|
* A request is from this call when its initiator stack, including parents,
|
|
@@ -3693,7 +3834,8 @@ const SETTLE_MS$1 = 50;
|
|
|
3693
3834
|
*/
|
|
3694
3835
|
async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
3695
3836
|
const marker = `mh-evaluate-${callId}-${randomUUID().slice(0, 8)}`;
|
|
3696
|
-
const
|
|
3837
|
+
const limit = capped(signal, CALL_MS$2);
|
|
3838
|
+
const client = await untilAborted(page.context().newCDPSession(page), limit);
|
|
3697
3839
|
const notes = /* @__PURE__ */ new Map();
|
|
3698
3840
|
const onRequest = (event) => {
|
|
3699
3841
|
const method = event.request?.method?.toUpperCase();
|
|
@@ -3713,10 +3855,10 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3713
3855
|
const stop = whenAborted(signal);
|
|
3714
3856
|
let dialogOpen = false;
|
|
3715
3857
|
try {
|
|
3716
|
-
await client.send("Network.enable");
|
|
3717
|
-
await client.send("Debugger.enable");
|
|
3718
|
-
await client.send("Debugger.setSkipAllPauses", { skip: true });
|
|
3719
|
-
await client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 });
|
|
3858
|
+
await untilAborted(client.send("Network.enable"), limit);
|
|
3859
|
+
await untilAborted(client.send("Debugger.enable"), limit);
|
|
3860
|
+
await untilAborted(client.send("Debugger.setSkipAllPauses", { skip: true }), limit);
|
|
3861
|
+
await untilAborted(client.send("Debugger.setAsyncCallStackDepth", { maxDepth: 32 }), limit);
|
|
3720
3862
|
const evaluation = (target === void 0 ? page.evaluate(runSource, {
|
|
3721
3863
|
expr: source,
|
|
3722
3864
|
marker
|
|
@@ -3768,7 +3910,7 @@ async function evaluateMarked(page, source, target, callId, signal, dialog) {
|
|
|
3768
3910
|
stop.dispose();
|
|
3769
3911
|
client.off("Network.requestWillBeSent", onRequest);
|
|
3770
3912
|
const detached = client.detach().catch(() => void 0);
|
|
3771
|
-
if (!dialogOpen) await detached;
|
|
3913
|
+
if (!dialogOpen) await untilAborted(detached, capped(signal, CALL_MS$2)).catch(() => void 0);
|
|
3772
3914
|
}
|
|
3773
3915
|
}
|
|
3774
3916
|
/** One line per group: `POST https://site × 2 (this call)`. */
|
|
@@ -4166,6 +4308,8 @@ const REPLIED = "继续";
|
|
|
4166
4308
|
//#region lib/types/service/extension.js
|
|
4167
4309
|
const PACKAGE = "@mind-harness/mh-browser";
|
|
4168
4310
|
const WORKER_MS = 15e3;
|
|
4311
|
+
/** A service-worker call that does not answer. The caller's signal can end it sooner. */
|
|
4312
|
+
const CALL_MS$1 = 15e3;
|
|
4169
4313
|
/**
|
|
4170
4314
|
* Directory of the unpacked extension shipped with this package.
|
|
4171
4315
|
* Walks up from this module: sources live in `src/`, the bundle in `lib/`.
|
|
@@ -4205,8 +4349,9 @@ var Extension = class Extension {
|
|
|
4205
4349
|
return `chrome-extension://${this.id}/${name}`;
|
|
4206
4350
|
}
|
|
4207
4351
|
/** `chrome.tabs.create` in the named window. Returns the extension's tab id. */
|
|
4208
|
-
async createTab(windowId, url) {
|
|
4209
|
-
|
|
4352
|
+
async createTab(windowId, url, signal) {
|
|
4353
|
+
const worker = await this.worker(signal);
|
|
4354
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4210
4355
|
const tab = await globalThis.chrome.tabs.create({
|
|
4211
4356
|
windowId: arg.windowId,
|
|
4212
4357
|
url: arg.url,
|
|
@@ -4217,23 +4362,25 @@ var Extension = class Extension {
|
|
|
4217
4362
|
}, {
|
|
4218
4363
|
windowId,
|
|
4219
4364
|
url
|
|
4220
|
-
});
|
|
4365
|
+
}), signal);
|
|
4221
4366
|
}
|
|
4222
|
-
async tabIdsIn(windowId) {
|
|
4223
|
-
|
|
4367
|
+
async tabIdsIn(windowId, signal) {
|
|
4368
|
+
const worker = await this.worker(signal);
|
|
4369
|
+
return this.answer(worker.evaluate(async (target) => {
|
|
4224
4370
|
return (await globalThis.chrome.tabs.query({ windowId: target })).flatMap((tab) => tab.id === void 0 ? [] : [tab.id]);
|
|
4225
|
-
}, windowId);
|
|
4371
|
+
}, windowId), signal);
|
|
4226
4372
|
}
|
|
4227
|
-
async activate(tabId) {
|
|
4228
|
-
|
|
4373
|
+
async activate(tabId, signal) {
|
|
4374
|
+
const worker = await this.worker(signal);
|
|
4375
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4229
4376
|
await globalThis.chrome.tabs.update(id, { active: true });
|
|
4230
|
-
}, tabId);
|
|
4377
|
+
}, tabId), signal);
|
|
4231
4378
|
}
|
|
4232
|
-
async group(tabId, target) {
|
|
4233
|
-
const worker = await this.worker();
|
|
4379
|
+
async group(tabId, target, signal) {
|
|
4380
|
+
const worker = await this.worker(signal);
|
|
4234
4381
|
if ("groupId" in target) {
|
|
4235
4382
|
const groupId = target.groupId;
|
|
4236
|
-
return worker.evaluate(async (arg) => {
|
|
4383
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4237
4384
|
return globalThis.chrome.tabs.group({
|
|
4238
4385
|
tabIds: [arg.tabId],
|
|
4239
4386
|
groupId: arg.groupId
|
|
@@ -4241,10 +4388,10 @@ var Extension = class Extension {
|
|
|
4241
4388
|
}, {
|
|
4242
4389
|
tabId,
|
|
4243
4390
|
groupId
|
|
4244
|
-
});
|
|
4391
|
+
}), signal);
|
|
4245
4392
|
}
|
|
4246
4393
|
const windowId = target.windowId;
|
|
4247
|
-
return worker.evaluate(async (arg) => {
|
|
4394
|
+
return this.answer(worker.evaluate(async (arg) => {
|
|
4248
4395
|
return globalThis.chrome.tabs.group({
|
|
4249
4396
|
tabIds: [arg.tabId],
|
|
4250
4397
|
createProperties: { windowId: arg.windowId }
|
|
@@ -4252,10 +4399,11 @@ var Extension = class Extension {
|
|
|
4252
4399
|
}, {
|
|
4253
4400
|
tabId,
|
|
4254
4401
|
windowId
|
|
4255
|
-
});
|
|
4402
|
+
}), signal);
|
|
4256
4403
|
}
|
|
4257
|
-
async styleGroup(groupId, title, color) {
|
|
4258
|
-
|
|
4404
|
+
async styleGroup(groupId, title, color, signal) {
|
|
4405
|
+
const worker = await this.worker(signal);
|
|
4406
|
+
await this.answer(worker.evaluate(async (arg) => {
|
|
4259
4407
|
await globalThis.chrome.tabGroups.update(arg.groupId, {
|
|
4260
4408
|
title: arg.title,
|
|
4261
4409
|
color: arg.color
|
|
@@ -4264,17 +4412,21 @@ var Extension = class Extension {
|
|
|
4264
4412
|
groupId,
|
|
4265
4413
|
title,
|
|
4266
4414
|
color
|
|
4267
|
-
});
|
|
4415
|
+
}), signal);
|
|
4268
4416
|
}
|
|
4269
|
-
async ungroup(tabId) {
|
|
4270
|
-
|
|
4417
|
+
async ungroup(tabId, signal) {
|
|
4418
|
+
const worker = await this.worker(signal);
|
|
4419
|
+
await this.answer(worker.evaluate(async (id) => {
|
|
4271
4420
|
await globalThis.chrome.tabs.ungroup(id);
|
|
4272
|
-
}, tabId);
|
|
4421
|
+
}, tabId), signal);
|
|
4422
|
+
}
|
|
4423
|
+
answer(pending, signal) {
|
|
4424
|
+
return untilAborted(pending, capped(signal, CALL_MS$1));
|
|
4273
4425
|
}
|
|
4274
|
-
async worker() {
|
|
4426
|
+
async worker(signal) {
|
|
4275
4427
|
const current = this.workerPage;
|
|
4276
4428
|
if (current !== void 0 && this.context.serviceWorkers().includes(current)) return current;
|
|
4277
|
-
const worker = await findWorker(this.context, this.id);
|
|
4429
|
+
const worker = await untilAborted(findWorker(this.context, this.id), capped(signal, CALL_MS$1));
|
|
4278
4430
|
this.workerPage = worker;
|
|
4279
4431
|
return worker;
|
|
4280
4432
|
}
|
|
@@ -4300,6 +4452,10 @@ async function findWorker(context, id, timeoutMs = WORKER_MS) {
|
|
|
4300
4452
|
const TAB_CAP = 8;
|
|
4301
4453
|
/** How long a new window or tab may take to appear. */
|
|
4302
4454
|
const TARGET_MS = 5e3;
|
|
4455
|
+
/** A CDP or extension call that does not answer. The caller's signal can end it sooner. */
|
|
4456
|
+
const CALL_MS = 15e3;
|
|
4457
|
+
/** Closing a page the renderer no longer answers. */
|
|
4458
|
+
const CLOSE_MS = 5e3;
|
|
4303
4459
|
const WORLD = "mh-browser";
|
|
4304
4460
|
const BINDING = "mhBrowserInput";
|
|
4305
4461
|
const LISTENER = `(() => {
|
|
@@ -4521,10 +4677,10 @@ var Windows = class {
|
|
|
4521
4677
|
return open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4522
4678
|
}
|
|
4523
4679
|
/** Make this tab the current one in its window. Does not raise the browser. */
|
|
4524
|
-
async activate(tab) {
|
|
4680
|
+
async activate(tab, signal) {
|
|
4525
4681
|
const extension = this.extension;
|
|
4526
4682
|
if (extension === void 0 || tab.chromeTabId === void 0) return;
|
|
4527
|
-
await extension.activate(tab.chromeTabId);
|
|
4683
|
+
await extension.activate(tab.chromeTabId, signal);
|
|
4528
4684
|
}
|
|
4529
4685
|
/** Group title for the person. Empty until a title is known. */
|
|
4530
4686
|
setTitle(owner, title) {
|
|
@@ -4551,54 +4707,70 @@ var Windows = class {
|
|
|
4551
4707
|
* use the extension. Without a window, later pages use `window.open` from
|
|
4552
4708
|
* an isolated world so they stay in that window.
|
|
4553
4709
|
*/
|
|
4554
|
-
openBlank(owner) {
|
|
4710
|
+
openBlank(owner, signal) {
|
|
4555
4711
|
this.pendingCreates.set(owner, (this.pendingCreates.get(owner) ?? 0) + 1);
|
|
4556
|
-
const
|
|
4557
|
-
const run =
|
|
4712
|
+
const queued = enqueue(this.creating, () => this.createTab(owner, signal), signal);
|
|
4713
|
+
const run = queued.run.finally(() => {
|
|
4558
4714
|
const left = (this.pendingCreates.get(owner) ?? 1) - 1;
|
|
4559
4715
|
if (left <= 0) this.pendingCreates.delete(owner);
|
|
4560
4716
|
else this.pendingCreates.set(owner, left);
|
|
4561
4717
|
});
|
|
4562
|
-
this.creating =
|
|
4718
|
+
this.creating = queued.tail;
|
|
4563
4719
|
return run;
|
|
4564
4720
|
}
|
|
4565
|
-
async createTab(owner) {
|
|
4721
|
+
async createTab(owner, signal) {
|
|
4566
4722
|
const extension = this.extension;
|
|
4567
4723
|
if (extension === void 0) {
|
|
4568
|
-
const opener = await this.openerFor(owner);
|
|
4569
|
-
const page = await this.openTabIn(opener);
|
|
4570
|
-
return this.keep(owner, page, void 0);
|
|
4724
|
+
const opener = await this.openerFor(owner, signal);
|
|
4725
|
+
const page = await this.openTabIn(opener, signal);
|
|
4726
|
+
return this.keep(owner, page, void 0, void 0, signal);
|
|
4571
4727
|
}
|
|
4572
4728
|
const open = this.owned(owner);
|
|
4573
4729
|
if (open.length === 0) {
|
|
4574
|
-
const page = await this.openWindow();
|
|
4575
|
-
const windowId = await this.windowIdOf(page);
|
|
4576
|
-
const chromeTabId = await this.extensionTabIn(extension, windowId);
|
|
4577
|
-
const tab = await this.keep(owner, page, chromeTabId);
|
|
4578
|
-
this.groupTab(tab);
|
|
4730
|
+
const page = await this.openWindow(signal);
|
|
4731
|
+
const windowId = await this.windowIdOf(page, signal);
|
|
4732
|
+
const chromeTabId = await this.extensionTabIn(extension, windowId, signal);
|
|
4733
|
+
const tab = await this.keep(owner, page, chromeTabId, void 0, signal);
|
|
4734
|
+
this.groupTab(tab, signal);
|
|
4579
4735
|
return tab;
|
|
4580
4736
|
}
|
|
4581
4737
|
const anchor = open.reduce((best, tab) => tab.touchedAt >= best.touchedAt ? tab : best);
|
|
4582
|
-
const opened = await this.openTabViaExtension(anchor.windowId);
|
|
4583
|
-
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId);
|
|
4584
|
-
this.groupTab(tab);
|
|
4738
|
+
const opened = await this.openTabViaExtension(anchor.windowId, signal);
|
|
4739
|
+
const tab = await this.keep(owner, opened.page, opened.chromeTabId, anchor.windowId, signal);
|
|
4740
|
+
this.groupTab(tab, signal);
|
|
4585
4741
|
return tab;
|
|
4586
4742
|
}
|
|
4587
|
-
async keep(owner, page, chromeTabId, windowId) {
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4743
|
+
async keep(owner, page, chromeTabId, windowId, signal) {
|
|
4744
|
+
let tab;
|
|
4745
|
+
try {
|
|
4746
|
+
tab = {
|
|
4747
|
+
id: await this.allocate(),
|
|
4748
|
+
owner,
|
|
4749
|
+
page,
|
|
4750
|
+
targetId: await this.targetIdOf(page, signal),
|
|
4751
|
+
windowId: windowId ?? await this.windowIdOf(page, signal),
|
|
4752
|
+
chromeTabId,
|
|
4753
|
+
url: page.url(),
|
|
4754
|
+
touchedAt: Date.now(),
|
|
4755
|
+
...idleTab()
|
|
4756
|
+
};
|
|
4757
|
+
this.remember(tab);
|
|
4758
|
+
await this.arm(page, owner, tab.id, signal);
|
|
4759
|
+
return tab;
|
|
4760
|
+
} catch (error) {
|
|
4761
|
+
if (tab !== void 0 && this.byPage.get(page) === tab) await this.closePage(tab);
|
|
4762
|
+
else await this.shutdown(page);
|
|
4763
|
+
throw error;
|
|
4764
|
+
}
|
|
4765
|
+
}
|
|
4766
|
+
/** The page from a create, or close it when this wait is abandoned. */
|
|
4767
|
+
async claimedPage(pending, signal) {
|
|
4768
|
+
try {
|
|
4769
|
+
return await untilAborted(pending, capped(signal, TARGET_MS));
|
|
4770
|
+
} catch (error) {
|
|
4771
|
+
pending.then((page) => this.shutdown(page)).catch(() => void 0);
|
|
4772
|
+
throw error;
|
|
4773
|
+
}
|
|
4602
4774
|
}
|
|
4603
4775
|
/**
|
|
4604
4776
|
* Close one tab. `note` is listed in the next `<tabs>` block. A tab the
|
|
@@ -4612,9 +4784,12 @@ var Windows = class {
|
|
|
4612
4784
|
}
|
|
4613
4785
|
await this.closePage(tab);
|
|
4614
4786
|
}
|
|
4615
|
-
async closeOwner(owner) {
|
|
4616
|
-
for (const tab of this.owned(owner))
|
|
4617
|
-
|
|
4787
|
+
async closeOwner(owner, signal) {
|
|
4788
|
+
for (const tab of this.owned(owner)) {
|
|
4789
|
+
if (signal?.aborted === true) break;
|
|
4790
|
+
await this.closePage(tab);
|
|
4791
|
+
}
|
|
4792
|
+
if (signal?.aborted !== true) await this.closeOpener(owner);
|
|
4618
4793
|
this.closedNotes.delete(owner);
|
|
4619
4794
|
for (const [id, last] of this.lastUrls) if (last.owner === owner) this.lastUrls.delete(id);
|
|
4620
4795
|
this.groups.delete(owner);
|
|
@@ -4632,7 +4807,7 @@ var Windows = class {
|
|
|
4632
4807
|
}
|
|
4633
4808
|
this.serviceClose.add(tab.page);
|
|
4634
4809
|
this.forget(tab);
|
|
4635
|
-
if (!tab.page.isClosed()) await tab.page
|
|
4810
|
+
if (!tab.page.isClosed()) await this.shutdown(tab.page);
|
|
4636
4811
|
}
|
|
4637
4812
|
/** A tab leaves the registry. The window closes with its last tab. */
|
|
4638
4813
|
forget(tab) {
|
|
@@ -4660,7 +4835,7 @@ var Windows = class {
|
|
|
4660
4835
|
session?.detach().catch(() => void 0);
|
|
4661
4836
|
if (opener.isClosed()) return;
|
|
4662
4837
|
this.serviceClose.add(opener);
|
|
4663
|
-
await
|
|
4838
|
+
await this.shutdown(opener);
|
|
4664
4839
|
}
|
|
4665
4840
|
/** Close least-recently touched idle tabs until the owner is within the cap. */
|
|
4666
4841
|
async enforceCap(owner) {
|
|
@@ -4707,57 +4882,58 @@ var Windows = class {
|
|
|
4707
4882
|
await writeCounter(this.profileDir, this.nextId);
|
|
4708
4883
|
return id;
|
|
4709
4884
|
}
|
|
4710
|
-
async openerFor(owner) {
|
|
4885
|
+
async openerFor(owner, signal) {
|
|
4711
4886
|
const existing = this.openers.get(owner);
|
|
4712
4887
|
if (existing !== void 0 && !existing.isClosed()) return existing;
|
|
4713
|
-
const opener = await this.openWindow();
|
|
4888
|
+
const opener = await this.openWindow(signal);
|
|
4714
4889
|
this.openers.set(owner, opener);
|
|
4715
|
-
await this.arm(opener, owner, void 0);
|
|
4890
|
+
await this.arm(opener, owner, void 0, signal);
|
|
4716
4891
|
return opener;
|
|
4717
4892
|
}
|
|
4718
|
-
async openWindow() {
|
|
4893
|
+
async openWindow(signal) {
|
|
4719
4894
|
const session = this.browserSession;
|
|
4720
4895
|
if (session === void 0) throw new Error("browser is not running");
|
|
4721
|
-
const created = await this.createWindowTarget(session);
|
|
4722
|
-
|
|
4896
|
+
const created = await this.createWindowTarget(session, signal);
|
|
4897
|
+
const pending = this.waitForTarget(created.targetId);
|
|
4898
|
+
return this.claimedPage(pending, signal);
|
|
4723
4899
|
}
|
|
4724
4900
|
/** Headed windows pass `focus: false`. A browser that rejects it is opened without the flag. */
|
|
4725
|
-
async createWindowTarget(session) {
|
|
4901
|
+
async createWindowTarget(session, signal) {
|
|
4726
4902
|
const base = {
|
|
4727
4903
|
url: "about:blank",
|
|
4728
4904
|
newWindow: true
|
|
4729
4905
|
};
|
|
4730
|
-
if (!this.headed) return
|
|
4906
|
+
if (!this.headed) return this.cdp(session, "Target.createTarget", base, signal);
|
|
4731
4907
|
try {
|
|
4732
|
-
return await
|
|
4908
|
+
return await this.cdp(session, "Target.createTarget", {
|
|
4733
4909
|
...base,
|
|
4734
4910
|
focus: false
|
|
4735
|
-
});
|
|
4736
|
-
} catch {
|
|
4737
|
-
|
|
4911
|
+
}, signal);
|
|
4912
|
+
} catch (error) {
|
|
4913
|
+
if (signal?.aborted === true || isInterrupted(error)) throw error;
|
|
4914
|
+
return this.cdp(session, "Target.createTarget", base, signal);
|
|
4738
4915
|
}
|
|
4739
4916
|
}
|
|
4740
|
-
async openTabIn(anchor) {
|
|
4741
|
-
|
|
4742
|
-
if (context === void 0) throw new Error("browser is not running");
|
|
4917
|
+
async openTabIn(anchor, signal) {
|
|
4918
|
+
if (this.context === void 0) throw new Error("browser is not running");
|
|
4743
4919
|
const claim = this.claimBlank();
|
|
4744
4920
|
try {
|
|
4745
|
-
const session = await
|
|
4921
|
+
const session = await this.pageSession(anchor, signal);
|
|
4746
4922
|
try {
|
|
4747
|
-
const tree = await
|
|
4748
|
-
const world = await
|
|
4923
|
+
const tree = await this.cdp(session, "Page.getFrameTree", void 0, signal);
|
|
4924
|
+
const world = await this.cdp(session, "Page.createIsolatedWorld", {
|
|
4749
4925
|
frameId: tree.frameTree.frame.id,
|
|
4750
4926
|
worldName: WORLD,
|
|
4751
4927
|
grantUniveralAccess: true
|
|
4752
|
-
});
|
|
4753
|
-
await
|
|
4928
|
+
}, signal);
|
|
4929
|
+
await this.cdp(session, "Runtime.evaluate", {
|
|
4754
4930
|
expression: `window.open(${JSON.stringify(claim.url)},'_blank','noopener,noreferrer')`,
|
|
4755
4931
|
contextId: world.executionContextId
|
|
4756
|
-
});
|
|
4932
|
+
}, signal);
|
|
4757
4933
|
} finally {
|
|
4758
|
-
await
|
|
4934
|
+
await this.shutdownSession(session);
|
|
4759
4935
|
}
|
|
4760
|
-
return await claim.page;
|
|
4936
|
+
return await this.claimedPage(claim.page, signal);
|
|
4761
4937
|
} finally {
|
|
4762
4938
|
claim.drop();
|
|
4763
4939
|
}
|
|
@@ -4800,22 +4976,23 @@ var Windows = class {
|
|
|
4800
4976
|
};
|
|
4801
4977
|
}
|
|
4802
4978
|
/** The extension's id for the only tab in a window just opened. */
|
|
4803
|
-
async extensionTabIn(extension, windowId) {
|
|
4979
|
+
async extensionTabIn(extension, windowId, signal) {
|
|
4804
4980
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
4805
|
-
|
|
4981
|
+
if (signal?.aborted === true) return void 0;
|
|
4982
|
+
const id = (await extension.tabIdsIn(windowId, signal))[0];
|
|
4806
4983
|
if (id !== void 0) return id;
|
|
4807
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4984
|
+
await untilAborted(new Promise((resolve) => setTimeout(resolve, 100)), signal).catch(() => void 0);
|
|
4808
4985
|
}
|
|
4809
4986
|
}
|
|
4810
4987
|
/** Ask the extension to open a blank tab in `windowId`, and claim it. */
|
|
4811
|
-
async openTabViaExtension(windowId) {
|
|
4988
|
+
async openTabViaExtension(windowId, signal) {
|
|
4812
4989
|
const extension = this.extension;
|
|
4813
4990
|
if (extension === void 0) throw new Error("browser is not running");
|
|
4814
4991
|
const claim = this.claimBlank();
|
|
4815
4992
|
try {
|
|
4816
|
-
const chromeTabId = await extension.createTab(windowId, claim.url);
|
|
4993
|
+
const chromeTabId = await extension.createTab(windowId, claim.url, signal);
|
|
4817
4994
|
return {
|
|
4818
|
-
page: await claim.page,
|
|
4995
|
+
page: await this.claimedPage(claim.page, signal),
|
|
4819
4996
|
chromeTabId
|
|
4820
4997
|
};
|
|
4821
4998
|
} finally {
|
|
@@ -4823,28 +5000,29 @@ var Windows = class {
|
|
|
4823
5000
|
}
|
|
4824
5001
|
}
|
|
4825
5002
|
/** Put a Session's tab in its group. The person's window is not a Session and stays ungrouped. */
|
|
4826
|
-
async groupTab(tab) {
|
|
5003
|
+
async groupTab(tab, signal) {
|
|
4827
5004
|
const extension = this.extension;
|
|
4828
5005
|
const chromeTabId = tab.chromeTabId;
|
|
4829
5006
|
if (extension === void 0 || chromeTabId === void 0 || tab.owner === "person") return;
|
|
4830
5007
|
try {
|
|
4831
5008
|
const known = this.groups.get(tab.owner);
|
|
4832
|
-
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }));
|
|
5009
|
+
if (known === void 0) this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4833
5010
|
else try {
|
|
4834
|
-
await extension.group(chromeTabId, { groupId: known });
|
|
4835
|
-
} catch {
|
|
4836
|
-
|
|
5011
|
+
await extension.group(chromeTabId, { groupId: known }, signal);
|
|
5012
|
+
} catch (error) {
|
|
5013
|
+
if (isInterrupted(error)) return;
|
|
5014
|
+
this.groups.set(tab.owner, await extension.group(chromeTabId, { windowId: tab.windowId }, signal));
|
|
4837
5015
|
}
|
|
4838
|
-
await this.applyGroupStyle(tab.owner);
|
|
5016
|
+
await this.applyGroupStyle(tab.owner, signal);
|
|
4839
5017
|
} catch {}
|
|
4840
5018
|
}
|
|
4841
|
-
async applyGroupStyle(owner) {
|
|
5019
|
+
async applyGroupStyle(owner, signal) {
|
|
4842
5020
|
const extension = this.extension;
|
|
4843
5021
|
const groupId = this.groups.get(owner);
|
|
4844
5022
|
if (extension === void 0 || groupId === void 0) return;
|
|
4845
5023
|
const title = this.titles.get(owner) ?? "";
|
|
4846
5024
|
const waiting = this.waiting.has(owner);
|
|
4847
|
-
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue");
|
|
5025
|
+
await extension.styleGroup(groupId, waiting ? `${WAITING_TITLE}${title}` : title, waiting ? "red" : "blue", signal);
|
|
4848
5026
|
}
|
|
4849
5027
|
async ingest(page) {
|
|
4850
5028
|
if (this.byPage.has(page)) return;
|
|
@@ -4866,6 +5044,10 @@ var Windows = class {
|
|
|
4866
5044
|
slot.accept(page);
|
|
4867
5045
|
return;
|
|
4868
5046
|
}
|
|
5047
|
+
if (page.url().startsWith("about:blank#mh-")) {
|
|
5048
|
+
await this.shutdown(page);
|
|
5049
|
+
return;
|
|
5050
|
+
}
|
|
4869
5051
|
const opener = await page.opener().catch(() => null);
|
|
4870
5052
|
const fromOpener = opener === null ? void 0 : this.byPage.get(opener);
|
|
4871
5053
|
if (fromOpener !== void 0) {
|
|
@@ -4907,10 +5089,9 @@ var Windows = class {
|
|
|
4907
5089
|
* or a close the service did not start counts as well. A page the content
|
|
4908
5090
|
* tab opened itself is not counted for its first navigation or its close.
|
|
4909
5091
|
*/
|
|
4910
|
-
async arm(page, owner, tab) {
|
|
5092
|
+
async arm(page, owner, tab, signal) {
|
|
4911
5093
|
if (this.armed.has(page)) return;
|
|
4912
|
-
|
|
4913
|
-
if (context === void 0) return;
|
|
5094
|
+
if (this.context === void 0) return;
|
|
4914
5095
|
this.armed.add(page);
|
|
4915
5096
|
page.on("close", () => {
|
|
4916
5097
|
const live = this.context;
|
|
@@ -4928,19 +5109,19 @@ var Windows = class {
|
|
|
4928
5109
|
});
|
|
4929
5110
|
let session;
|
|
4930
5111
|
try {
|
|
4931
|
-
session = await
|
|
5112
|
+
session = await this.pageSession(page, signal);
|
|
4932
5113
|
this.sessions.set(page, session);
|
|
4933
|
-
await
|
|
4934
|
-
await
|
|
4935
|
-
await
|
|
5114
|
+
await this.cdp(session, "Runtime.enable", void 0, signal);
|
|
5115
|
+
await this.cdp(session, "Page.enable", void 0, signal);
|
|
5116
|
+
await this.cdp(session, "Runtime.addBinding", {
|
|
4936
5117
|
name: BINDING,
|
|
4937
5118
|
executionContextName: WORLD
|
|
4938
|
-
});
|
|
4939
|
-
await
|
|
5119
|
+
}, signal);
|
|
5120
|
+
await this.cdp(session, "Page.addScriptToEvaluateOnNewDocument", {
|
|
4940
5121
|
source: LISTENER,
|
|
4941
5122
|
worldName: WORLD,
|
|
4942
5123
|
runImmediately: true
|
|
4943
|
-
});
|
|
5124
|
+
}, signal);
|
|
4944
5125
|
} catch (error) {
|
|
4945
5126
|
if (closedTarget(error)) {
|
|
4946
5127
|
const known = this.byPage.get(page);
|
|
@@ -4997,21 +5178,35 @@ var Windows = class {
|
|
|
4997
5178
|
this.sessions.delete(page);
|
|
4998
5179
|
session?.detach().catch(() => void 0);
|
|
4999
5180
|
}
|
|
5000
|
-
async targetIdOf(page) {
|
|
5001
|
-
const
|
|
5002
|
-
if (context === void 0) throw new Error("browser is not running");
|
|
5003
|
-
const session = await context.newCDPSession(page);
|
|
5181
|
+
async targetIdOf(page, signal) {
|
|
5182
|
+
const session = await this.pageSession(page, signal);
|
|
5004
5183
|
try {
|
|
5005
|
-
return (await
|
|
5184
|
+
return (await this.cdp(session, "Target.getTargetInfo", void 0, signal)).targetInfo.targetId;
|
|
5006
5185
|
} finally {
|
|
5007
|
-
await
|
|
5186
|
+
await this.shutdownSession(session);
|
|
5008
5187
|
}
|
|
5009
5188
|
}
|
|
5010
|
-
async windowIdOf(page) {
|
|
5189
|
+
async windowIdOf(page, signal) {
|
|
5011
5190
|
const session = this.browserSession;
|
|
5012
5191
|
if (session === void 0) throw new Error("browser is not running");
|
|
5013
|
-
const targetId = await this.targetIdOf(page);
|
|
5014
|
-
return (await
|
|
5192
|
+
const targetId = await this.targetIdOf(page, signal);
|
|
5193
|
+
return (await this.cdp(session, "Browser.getWindowForTarget", { targetId }, signal)).windowId;
|
|
5194
|
+
}
|
|
5195
|
+
pageSession(page, signal) {
|
|
5196
|
+
const context = this.context;
|
|
5197
|
+
if (context === void 0) return Promise.reject(/* @__PURE__ */ new Error("browser is not running"));
|
|
5198
|
+
return untilAborted(context.newCDPSession(page), capped(signal, CALL_MS));
|
|
5199
|
+
}
|
|
5200
|
+
cdp(session, method, params, signal) {
|
|
5201
|
+
const send = session.send.bind(session);
|
|
5202
|
+
return untilAborted(params === void 0 ? send(method) : send(method, params), capped(signal, CALL_MS));
|
|
5203
|
+
}
|
|
5204
|
+
/** Close a page that may no longer answer. A stall ends at `CLOSE_MS`. */
|
|
5205
|
+
shutdown(page) {
|
|
5206
|
+
return untilAborted(page.close(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
5207
|
+
}
|
|
5208
|
+
shutdownSession(session) {
|
|
5209
|
+
return untilAborted(session.detach(), AbortSignal.timeout(CLOSE_MS)).catch(() => void 0);
|
|
5015
5210
|
}
|
|
5016
5211
|
};
|
|
5017
5212
|
/** Playwright's error once the page, its context, or the browser has gone. */
|
|
@@ -5104,6 +5299,10 @@ const RELAUNCH_WINDOW_MS = 6e4;
|
|
|
5104
5299
|
const DOCUMENT_CAP = 64;
|
|
5105
5300
|
const OPEN_ADDRESS_REASON = "only http and https addresses are opened";
|
|
5106
5301
|
const STOP_WAIT_MS = 5e3;
|
|
5302
|
+
/** A page read the renderer no longer answers. The caller's signal can end it sooner. */
|
|
5303
|
+
const READ_MS = 15e3;
|
|
5304
|
+
/** Closing one Session's windows must not wait on a renderer that stopped answering. */
|
|
5305
|
+
const DISPOSE_MS = 3e4;
|
|
5107
5306
|
const WAITING_STOPPED = "stopped while waiting";
|
|
5108
5307
|
const STOPPED = Symbol("stopped");
|
|
5109
5308
|
const NAVIGATED_DURING_EVALUATE = "The page navigated before the function returned.";
|
|
@@ -5184,7 +5383,7 @@ let MhBrowserService = (() => {
|
|
|
5184
5383
|
ctx.on("session/disposed", (session) => {
|
|
5185
5384
|
const owner = session.id;
|
|
5186
5385
|
if (owner === void 0) return;
|
|
5187
|
-
this.exclusive(owner, () => this.windows.closeOwner(owner)).catch((error) => {
|
|
5386
|
+
this.exclusive(owner, () => this.windows.closeOwner(owner, AbortSignal.timeout(DISPOSE_MS)), AbortSignal.timeout(DISPOSE_MS)).catch((error) => {
|
|
5188
5387
|
ctx.logger.warn("closing the windows of %s failed: %o", owner, error);
|
|
5189
5388
|
});
|
|
5190
5389
|
this.book.forget(owner);
|
|
@@ -5206,22 +5405,30 @@ let MhBrowserService = (() => {
|
|
|
5206
5405
|
}
|
|
5207
5406
|
async open(owner, items, signal, options) {
|
|
5208
5407
|
const reveal = options?.reveal === true;
|
|
5209
|
-
|
|
5210
|
-
await this.
|
|
5211
|
-
|
|
5212
|
-
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5408
|
+
try {
|
|
5409
|
+
return await this.exclusive(owner, async () => {
|
|
5410
|
+
await this.up(owner === PERSON_OWNER);
|
|
5411
|
+
const views = [];
|
|
5412
|
+
for (const item of items) {
|
|
5413
|
+
if (signal?.aborted) {
|
|
5414
|
+
views.push({
|
|
5415
|
+
url: item.url,
|
|
5416
|
+
reason: WAITING_STOPPED
|
|
5417
|
+
});
|
|
5418
|
+
continue;
|
|
5419
|
+
}
|
|
5420
|
+
views.push(await this.openAddress(owner, item, signal, reveal));
|
|
5219
5421
|
}
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
|
|
5223
|
-
|
|
5224
|
-
|
|
5422
|
+
await this.enforceCap(owner);
|
|
5423
|
+
return views;
|
|
5424
|
+
}, signal);
|
|
5425
|
+
} catch (error) {
|
|
5426
|
+
if (!isInterrupted(error)) throw error;
|
|
5427
|
+
return items.map((item) => ({
|
|
5428
|
+
url: item.url,
|
|
5429
|
+
reason: WAITING_STOPPED
|
|
5430
|
+
}));
|
|
5431
|
+
}
|
|
5225
5432
|
}
|
|
5226
5433
|
async view(owner, tab, signal) {
|
|
5227
5434
|
return this.exclusive(owner, async () => {
|
|
@@ -5233,8 +5440,8 @@ let MhBrowserService = (() => {
|
|
|
5233
5440
|
url: live.url,
|
|
5234
5441
|
reason: WAITING_STOPPED
|
|
5235
5442
|
};
|
|
5236
|
-
return this.refreshView(live);
|
|
5237
|
-
});
|
|
5443
|
+
return this.refreshView(live, signal);
|
|
5444
|
+
}, signal);
|
|
5238
5445
|
}
|
|
5239
5446
|
/** Pages whose current representation is already in the model's presence. */
|
|
5240
5447
|
rememberPresence(owner, urls) {
|
|
@@ -5300,7 +5507,7 @@ let MhBrowserService = (() => {
|
|
|
5300
5507
|
text: planned.text,
|
|
5301
5508
|
record: planned.record
|
|
5302
5509
|
};
|
|
5303
|
-
});
|
|
5510
|
+
}, opts?.signal);
|
|
5304
5511
|
}
|
|
5305
5512
|
/**
|
|
5306
5513
|
* Where a download the page starts may be written: the session's writable
|
|
@@ -5342,7 +5549,7 @@ let MhBrowserService = (() => {
|
|
|
5342
5549
|
kind: "response",
|
|
5343
5550
|
finalUrl: response.url(),
|
|
5344
5551
|
status: response.status(),
|
|
5345
|
-
body: new Uint8Array(await response.body())
|
|
5552
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(opts?.signal, READ_MS)))
|
|
5346
5553
|
};
|
|
5347
5554
|
const mediaType = headerMap["content-type"];
|
|
5348
5555
|
const etag = headerMap.etag;
|
|
@@ -5396,7 +5603,7 @@ let MhBrowserService = (() => {
|
|
|
5396
5603
|
try {
|
|
5397
5604
|
if (live.dialogHeld && op.kind !== "handle_dialog") throw new MhBrowserError("MODAL_STATE", "a dialog is open; use browser_handle_dialog", await this.blockedView(live));
|
|
5398
5605
|
if (live.fileChooser !== void 0 && op.kind !== "file_upload") {
|
|
5399
|
-
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" });
|
|
5606
|
+
const blocked = await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal);
|
|
5400
5607
|
blocked.dialog = {
|
|
5401
5608
|
type: "filechooser",
|
|
5402
5609
|
message: live.fileChooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
@@ -5409,8 +5616,8 @@ let MhBrowserService = (() => {
|
|
|
5409
5616
|
else this.windows.holdInput(live);
|
|
5410
5617
|
} else if (outcome.navigated) this.windows.onNavigated(live);
|
|
5411
5618
|
this.windows.touch(live);
|
|
5412
|
-
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null) : await this.refreshSnapshot(live);
|
|
5413
|
-
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op) : void 0;
|
|
5619
|
+
const shown = outcome.navigated ? await this.discover(live, live.page.url(), null, signal) : await this.refreshSnapshot(live, signal);
|
|
5620
|
+
const scoped = op.kind === "snapshot" && (op.target !== void 0 || op.depth !== void 0 || op.boxes === true) ? await this.scopedSnapshot(live, op, signal) : void 0;
|
|
5414
5621
|
this.shown.set(live.id, shown);
|
|
5415
5622
|
this.windows.noteUrl(live, live.page.url());
|
|
5416
5623
|
const view = await this.pageFromShown(live, shown, {
|
|
@@ -5418,7 +5625,7 @@ let MhBrowserService = (() => {
|
|
|
5418
5625
|
...outcome.dialog !== void 0 ? { dialog: outcome.dialog } : {},
|
|
5419
5626
|
...outcome.incomplete !== void 0 ? { incomplete: outcome.incomplete } : {},
|
|
5420
5627
|
...outcome.waited !== void 0 ? { waited: outcome.waited } : {}
|
|
5421
|
-
});
|
|
5628
|
+
}, signal);
|
|
5422
5629
|
if (scoped !== void 0 && view.representation.kind === "snapshot") view.representation = {
|
|
5423
5630
|
...view.representation,
|
|
5424
5631
|
text: scoped
|
|
@@ -5435,12 +5642,12 @@ let MhBrowserService = (() => {
|
|
|
5435
5642
|
} catch (error) {
|
|
5436
5643
|
if (error instanceof MhBrowserError && error.code === "TAKEN_OVER") throw error;
|
|
5437
5644
|
if (this.control.ofTab(owner, tab) === "person") throw await this.takenOver(live);
|
|
5438
|
-
if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }));
|
|
5645
|
+
if (error instanceof MhBrowserError && error.code === "STALE_REF" && error.page === void 0) throw new MhBrowserError(error.code, error.message, await this.pageFromShown(live, this.shown.get(live.id), { surface: "snapshot" }, signal));
|
|
5439
5646
|
throw error;
|
|
5440
5647
|
} finally {
|
|
5441
5648
|
linked.release();
|
|
5442
5649
|
}
|
|
5443
|
-
});
|
|
5650
|
+
}, signal);
|
|
5444
5651
|
}
|
|
5445
5652
|
holdHandover(owner, tab) {
|
|
5446
5653
|
const live = this.windows.get(owner, tab);
|
|
@@ -5458,15 +5665,16 @@ let MhBrowserService = (() => {
|
|
|
5458
5665
|
const live = this.windows.get(owner, tab);
|
|
5459
5666
|
if (live === void 0 || live.dialogHeld) return void 0;
|
|
5460
5667
|
try {
|
|
5461
|
-
const
|
|
5668
|
+
const limit = capped(signal, READ_MS);
|
|
5669
|
+
const bytes = await untilAborted(live.page.screenshot({
|
|
5462
5670
|
type: "png",
|
|
5463
|
-
|
|
5464
|
-
});
|
|
5671
|
+
signal: limit
|
|
5672
|
+
}), limit);
|
|
5465
5673
|
return new Uint8Array(bytes);
|
|
5466
5674
|
} catch {
|
|
5467
5675
|
return;
|
|
5468
5676
|
}
|
|
5469
|
-
});
|
|
5677
|
+
}, signal);
|
|
5470
5678
|
}
|
|
5471
5679
|
invoke(owner, _page, call, opts) {
|
|
5472
5680
|
return this.exclusive(owner, async () => {
|
|
@@ -5483,7 +5691,7 @@ let MhBrowserService = (() => {
|
|
|
5483
5691
|
}, call);
|
|
5484
5692
|
await this.enforceCap(owner);
|
|
5485
5693
|
return sent;
|
|
5486
|
-
});
|
|
5694
|
+
}, opts?.signal);
|
|
5487
5695
|
}
|
|
5488
5696
|
/**
|
|
5489
5697
|
* Not queued: it opens no tab, and a long body must not hold up the
|
|
@@ -5515,11 +5723,11 @@ let MhBrowserService = (() => {
|
|
|
5515
5723
|
const live = this.windows.get(owner, req.tab);
|
|
5516
5724
|
if (live === void 0) throw new MhBrowserError(UNKNOWN_TAB, UNKNOWN_TAB_REASON);
|
|
5517
5725
|
this.windows.holdHandover(live);
|
|
5518
|
-
await this.windows.activate(live).catch(() => void 0);
|
|
5726
|
+
await this.windows.activate(live, signal).catch(() => void 0);
|
|
5519
5727
|
}
|
|
5520
5728
|
this.control.handover(owner, req.tab);
|
|
5521
5729
|
this.windows.markWaiting(owner, true);
|
|
5522
|
-
});
|
|
5730
|
+
}, signal);
|
|
5523
5731
|
}
|
|
5524
5732
|
/** Remember the Session title for its tab group. Empty when the host has none. */
|
|
5525
5733
|
noteSessionTitle(owner, title) {
|
|
@@ -5530,7 +5738,7 @@ let MhBrowserService = (() => {
|
|
|
5530
5738
|
await this.ready;
|
|
5531
5739
|
const live = this.windows.latest(owner);
|
|
5532
5740
|
if (live === void 0) return false;
|
|
5533
|
-
await live.page.bringToFront();
|
|
5741
|
+
await untilAborted(live.page.bringToFront(), AbortSignal.timeout(READ_MS));
|
|
5534
5742
|
return true;
|
|
5535
5743
|
}
|
|
5536
5744
|
/** The person asked to see the page this Session handed over. */
|
|
@@ -5541,11 +5749,12 @@ let MhBrowserService = (() => {
|
|
|
5541
5749
|
const named = this.control.namedTab(sessionId);
|
|
5542
5750
|
const live = (named === void 0 ? void 0 : this.windows.get(sessionId, named)) ?? this.windows.latest(sessionId);
|
|
5543
5751
|
if (live === void 0) return { status: "no-tab" };
|
|
5544
|
-
await live.page.bringToFront();
|
|
5752
|
+
await untilAborted(live.page.bringToFront(), capped(signal, READ_MS));
|
|
5545
5753
|
return { status: "revealed" };
|
|
5546
5754
|
}
|
|
5547
5755
|
/** The person answered. Returns the note, the operation count, and the named tab. */
|
|
5548
5756
|
async handBack(owner, note) {
|
|
5757
|
+
const signal = capped(void 0, READ_MS);
|
|
5549
5758
|
return this.exclusive(owner, async () => {
|
|
5550
5759
|
await this.ready;
|
|
5551
5760
|
const taken = this.control.finish(owner);
|
|
@@ -5554,14 +5763,14 @@ let MhBrowserService = (() => {
|
|
|
5554
5763
|
this.windows.markWaiting(owner, false);
|
|
5555
5764
|
const urls = this.presenceUrls(owner, taken.tabs);
|
|
5556
5765
|
for (const touch of this.personTouch) touch(owner, urls);
|
|
5557
|
-
const page = await this.snapshotNamed(owner, taken.tab);
|
|
5766
|
+
const page = await this.snapshotNamed(owner, taken.tab, signal);
|
|
5558
5767
|
return {
|
|
5559
5768
|
...taken,
|
|
5560
5769
|
urls,
|
|
5561
5770
|
...note !== void 0 && note !== "" ? { note } : {},
|
|
5562
5771
|
...page !== void 0 ? { page } : {}
|
|
5563
5772
|
};
|
|
5564
|
-
});
|
|
5773
|
+
}, signal);
|
|
5565
5774
|
}
|
|
5566
5775
|
/** A stopped handover gives control back. A finished hand-back is left alone. */
|
|
5567
5776
|
releaseIfHeld(owner) {
|
|
@@ -5603,7 +5812,7 @@ let MhBrowserService = (() => {
|
|
|
5603
5812
|
}
|
|
5604
5813
|
const opened = await this.openOne(owner, item.url, item.waitFor, signal, item.snapshot === true, reveal);
|
|
5605
5814
|
this.keepAgentView(opened);
|
|
5606
|
-
if (!wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5815
|
+
if (opened.live !== void 0 && !wantsTab && opened.fresh && this.closable(owner, opened.live) && "representation" in opened.view && opened.view.representation.kind === "agent-view") {
|
|
5607
5816
|
await this.windows.closeTab(opened.live);
|
|
5608
5817
|
const view = { ...opened.view };
|
|
5609
5818
|
delete view.tab;
|
|
@@ -5629,6 +5838,7 @@ let MhBrowserService = (() => {
|
|
|
5629
5838
|
}
|
|
5630
5839
|
/** A readable Agent View puts its origin on the list and keeps the document for the next read. */
|
|
5631
5840
|
keepAgentView(opened) {
|
|
5841
|
+
if (opened.live === void 0) return;
|
|
5632
5842
|
if (!("representation" in opened.view)) return;
|
|
5633
5843
|
const representation = opened.view.representation;
|
|
5634
5844
|
if (!(representation.kind === "agent-view" || representation.kind === "snapshot" && representation.agentView !== void 0)) return;
|
|
@@ -5760,7 +5970,7 @@ let MhBrowserService = (() => {
|
|
|
5760
5970
|
kind: "response",
|
|
5761
5971
|
finalUrl: current,
|
|
5762
5972
|
status,
|
|
5763
|
-
body: readsAsText(mediaType) ? new Uint8Array(await response.body()) : /* @__PURE__ */ new Uint8Array()
|
|
5973
|
+
body: readsAsText(mediaType) ? new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))) : /* @__PURE__ */ new Uint8Array()
|
|
5764
5974
|
};
|
|
5765
5975
|
const etag = headerMap.etag;
|
|
5766
5976
|
const link = headerMap.link;
|
|
@@ -5773,8 +5983,20 @@ let MhBrowserService = (() => {
|
|
|
5773
5983
|
async openOne(owner, url, waitFor, signal, snapshot = false, reveal = false) {
|
|
5774
5984
|
assertWaitFor(waitFor);
|
|
5775
5985
|
const reused = this.idleTabOn(owner, url);
|
|
5776
|
-
const live = reused ?? await this.windows.openBlank(owner);
|
|
5777
5986
|
const fresh = reused === void 0;
|
|
5987
|
+
let live;
|
|
5988
|
+
try {
|
|
5989
|
+
live = reused ?? await this.windows.openBlank(owner, signal);
|
|
5990
|
+
} catch (error) {
|
|
5991
|
+
if (signal?.aborted === true || isInterrupted(error)) return {
|
|
5992
|
+
view: {
|
|
5993
|
+
url,
|
|
5994
|
+
reason: WAITING_STOPPED
|
|
5995
|
+
},
|
|
5996
|
+
fresh
|
|
5997
|
+
};
|
|
5998
|
+
throw error;
|
|
5999
|
+
}
|
|
5778
6000
|
const linked = this.control.link(owner, signal, live.id);
|
|
5779
6001
|
if (reveal && !linked.signal.aborted) await live.page.bringToFront().catch(() => void 0);
|
|
5780
6002
|
const nextDialog = this.windows.nextDialog(live);
|
|
@@ -5797,7 +6019,7 @@ let MhBrowserService = (() => {
|
|
|
5797
6019
|
nextDialog.cancel();
|
|
5798
6020
|
if (winner === "failed" && seenDialog === void 0) {
|
|
5799
6021
|
if (isDownloadStart(navigationError) || live.pendingDownload !== void 0) {
|
|
5800
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6022
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
5801
6023
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
5802
6024
|
this.applyDocumentType(live, view);
|
|
5803
6025
|
return {
|
|
@@ -5848,8 +6070,8 @@ let MhBrowserService = (() => {
|
|
|
5848
6070
|
...optionalFragment(address),
|
|
5849
6071
|
snapshot: "",
|
|
5850
6072
|
relocate: false,
|
|
5851
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
5852
|
-
}) : await this.discover(live, address, response);
|
|
6073
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6074
|
+
}) : await this.discover(live, address, response, signal);
|
|
5853
6075
|
this.shown.set(live.id, shown);
|
|
5854
6076
|
const view = await this.pageFromShown(live, shown, {
|
|
5855
6077
|
...snapshot ? { surface: "snapshot" } : {},
|
|
@@ -5859,7 +6081,7 @@ let MhBrowserService = (() => {
|
|
|
5859
6081
|
} },
|
|
5860
6082
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {},
|
|
5861
6083
|
...waited !== void 0 ? { waited } : {}
|
|
5862
|
-
});
|
|
6084
|
+
}, signal);
|
|
5863
6085
|
return {
|
|
5864
6086
|
view: await this.attachDownload(live, view, false),
|
|
5865
6087
|
fresh,
|
|
@@ -5894,7 +6116,7 @@ let MhBrowserService = (() => {
|
|
|
5894
6116
|
if (op.kind === "snapshot") return { navigated: false };
|
|
5895
6117
|
if (op.kind === "find") return {
|
|
5896
6118
|
navigated: false,
|
|
5897
|
-
summary: findInSnapshot(await this.aria(live), op)
|
|
6119
|
+
summary: findInSnapshot(await this.aria(live, signal), op)
|
|
5898
6120
|
};
|
|
5899
6121
|
if (op.kind === "evaluate") {
|
|
5900
6122
|
const snapshot = this.shown.get(live.id)?.snapshot ?? "";
|
|
@@ -6086,15 +6308,16 @@ let MhBrowserService = (() => {
|
|
|
6086
6308
|
};
|
|
6087
6309
|
}
|
|
6088
6310
|
/** A snapshot limited to a target, a depth, or boxes. Refs still resolve against the full one. */
|
|
6089
|
-
async scopedSnapshot(tab, op) {
|
|
6311
|
+
async scopedSnapshot(tab, op, signal) {
|
|
6090
6312
|
const options = {
|
|
6091
6313
|
mode: "ai",
|
|
6092
6314
|
...op.depth !== void 0 ? { depth: op.depth } : {},
|
|
6093
6315
|
...op.boxes === true ? { boxes: true } : {}
|
|
6094
6316
|
};
|
|
6095
6317
|
const snapshot = this.shown.get(tab.id)?.snapshot ?? "";
|
|
6096
|
-
|
|
6097
|
-
return tab.page.ariaSnapshot(options);
|
|
6318
|
+
const limit = capped(signal, READ_MS);
|
|
6319
|
+
if (op.target !== void 0) return untilAborted((await locate(tab.page, op.target, snapshot)).ariaSnapshot(options), limit);
|
|
6320
|
+
return untilAborted(tab.page.ariaSnapshot(options), limit);
|
|
6098
6321
|
}
|
|
6099
6322
|
/** A file chooser the action opened. The tab stays in use until it navigates. */
|
|
6100
6323
|
chooserHeld(live, chooser) {
|
|
@@ -6105,9 +6328,9 @@ let MhBrowserService = (() => {
|
|
|
6105
6328
|
message: chooser.isMultiple() ? "choose files (several allowed) with browser_file_upload" : "choose one file with browser_file_upload"
|
|
6106
6329
|
};
|
|
6107
6330
|
}
|
|
6108
|
-
async refreshSnapshot(tab) {
|
|
6331
|
+
async refreshSnapshot(tab, signal) {
|
|
6109
6332
|
const previous = this.shown.get(tab.id);
|
|
6110
|
-
const snapshot = tab.dialogHeld ? "" : await this.aria(tab);
|
|
6333
|
+
const snapshot = tab.dialogHeld ? "" : await this.aria(tab, signal);
|
|
6111
6334
|
if (previous === void 0) return {
|
|
6112
6335
|
pageUrl: canonical(tab.page.url()),
|
|
6113
6336
|
digest: "",
|
|
@@ -6142,20 +6365,20 @@ let MhBrowserService = (() => {
|
|
|
6142
6365
|
this.windows.endDispatch(page);
|
|
6143
6366
|
}
|
|
6144
6367
|
}
|
|
6145
|
-
async discover(tab, address, response) {
|
|
6146
|
-
const main = response === null ? void 0 : await mainDocument(response);
|
|
6368
|
+
async discover(tab, address, response, signal) {
|
|
6369
|
+
const main = response === null ? void 0 : await mainDocument(response, signal);
|
|
6147
6370
|
const relocated = main !== void 0 && !samePage(main.finalUrl, address);
|
|
6148
|
-
const domLinks = relocated ? [] : await readDomLinks(tab.page);
|
|
6371
|
+
const domLinks = relocated ? [] : await readDomLinks(tab.page, signal);
|
|
6149
6372
|
const previous = this.shown.get(tab.id);
|
|
6150
6373
|
return loadShown({
|
|
6151
6374
|
pageUrl: canonical(address),
|
|
6152
6375
|
...optionalFragment(address),
|
|
6153
|
-
snapshot: await this.aria(tab),
|
|
6376
|
+
snapshot: await this.aria(tab, signal),
|
|
6154
6377
|
...main !== void 0 && !relocated ? { main } : {},
|
|
6155
6378
|
...domLinks.length > 0 ? { domLinks } : {},
|
|
6156
6379
|
...previous !== void 0 ? { previous } : {},
|
|
6157
6380
|
relocate: relocated,
|
|
6158
|
-
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch)
|
|
6381
|
+
fetch: (target, accept, ifNoneMatch) => this.fetchForRead(target, accept, ifNoneMatch, signal)
|
|
6159
6382
|
});
|
|
6160
6383
|
}
|
|
6161
6384
|
fetchForRead(url, accept, ifNoneMatch, signal) {
|
|
@@ -6165,13 +6388,13 @@ let MhBrowserService = (() => {
|
|
|
6165
6388
|
...signal !== void 0 ? { signal } : {}
|
|
6166
6389
|
});
|
|
6167
6390
|
}
|
|
6168
|
-
async aria(tab) {
|
|
6169
|
-
return tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6391
|
+
async aria(tab, signal) {
|
|
6392
|
+
return untilAborted(tab.page.ariaSnapshot({ mode: "ai" }).catch((error) => {
|
|
6170
6393
|
this.ctx.logger.debug("aria snapshot failed: %s", errorText(error));
|
|
6171
6394
|
return "";
|
|
6172
|
-
});
|
|
6395
|
+
}), capped(signal, READ_MS));
|
|
6173
6396
|
}
|
|
6174
|
-
async pageFromShown(tab, shown, extra) {
|
|
6397
|
+
async pageFromShown(tab, shown, extra, signal) {
|
|
6175
6398
|
const current = shown ?? {
|
|
6176
6399
|
pageUrl: canonical(tab.url),
|
|
6177
6400
|
digest: "",
|
|
@@ -6179,10 +6402,10 @@ let MhBrowserService = (() => {
|
|
|
6179
6402
|
text: "",
|
|
6180
6403
|
snapshot: ""
|
|
6181
6404
|
};
|
|
6182
|
-
const title = tab.dialogHeld ? "" : await tab.page.title().catch((error) => {
|
|
6405
|
+
const title = tab.dialogHeld ? "" : await untilAborted(tab.page.title().catch((error) => {
|
|
6183
6406
|
this.ctx.logger.debug("page title failed: %s", errorText(error));
|
|
6184
6407
|
return "";
|
|
6185
|
-
});
|
|
6408
|
+
}), capped(signal, READ_MS));
|
|
6186
6409
|
this.windows.noteTitle(tab, title);
|
|
6187
6410
|
const view = {
|
|
6188
6411
|
tab: tab.id,
|
|
@@ -6227,7 +6450,7 @@ let MhBrowserService = (() => {
|
|
|
6227
6450
|
if (via !== void 0) return observationOf(url, via);
|
|
6228
6451
|
}
|
|
6229
6452
|
const existing = this.tabOn(owner, url);
|
|
6230
|
-
if (existing !== void 0) return observationOf(url, await this.refreshView(existing));
|
|
6453
|
+
if (existing !== void 0) return observationOf(url, await this.refreshView(existing, signal));
|
|
6231
6454
|
const opened = await this.openOne(owner, url, void 0, signal, false);
|
|
6232
6455
|
this.keepAgentView(opened);
|
|
6233
6456
|
const view = opened.view;
|
|
@@ -6251,22 +6474,22 @@ let MhBrowserService = (() => {
|
|
|
6251
6474
|
tabs.sort((left, right) => right.touchedAt - left.touchedAt);
|
|
6252
6475
|
return tabs[0];
|
|
6253
6476
|
}
|
|
6254
|
-
async refreshView(live) {
|
|
6477
|
+
async refreshView(live, signal) {
|
|
6255
6478
|
this.windows.touch(live);
|
|
6256
|
-
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id));
|
|
6479
|
+
if (live.dialogHeld) return this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6257
6480
|
const address = live.page.url();
|
|
6258
6481
|
const previous = this.shown.get(live.id);
|
|
6259
6482
|
const shown = await loadShown({
|
|
6260
6483
|
pageUrl: canonical(address),
|
|
6261
6484
|
...optionalFragment(address),
|
|
6262
|
-
snapshot: await this.aria(live),
|
|
6485
|
+
snapshot: await this.aria(live, signal),
|
|
6263
6486
|
...previous !== void 0 ? { previous } : {},
|
|
6264
6487
|
relocate: previous !== void 0 && !samePage(previous.pageUrl, address),
|
|
6265
|
-
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch)
|
|
6488
|
+
fetch: (url, accept, ifNoneMatch) => this.fetchForRead(url, accept, ifNoneMatch, signal)
|
|
6266
6489
|
});
|
|
6267
6490
|
this.shown.set(live.id, shown);
|
|
6268
6491
|
this.windows.noteUrl(live, address);
|
|
6269
|
-
return this.pageFromShown(live, shown);
|
|
6492
|
+
return this.pageFromShown(live, shown, void 0, signal);
|
|
6270
6493
|
}
|
|
6271
6494
|
/** The most recently touched idle tab on this address. A tab the person holds is left alone. */
|
|
6272
6495
|
idleTabOn(owner, url) {
|
|
@@ -6299,7 +6522,7 @@ let MhBrowserService = (() => {
|
|
|
6299
6522
|
const load = await waitForLoad(live.page, signal);
|
|
6300
6523
|
if (load.dialog !== void 0) this.windows.holdDialog(live);
|
|
6301
6524
|
this.windows.touch(live);
|
|
6302
|
-
const shown = await this.discover(live, live.page.url(), response);
|
|
6525
|
+
const shown = await this.discover(live, live.page.url(), response, signal);
|
|
6303
6526
|
this.shown.set(live.id, shown);
|
|
6304
6527
|
const view = await this.pageFromShown(live, shown, {
|
|
6305
6528
|
...load.load ? {} : { incomplete: {
|
|
@@ -6307,11 +6530,11 @@ let MhBrowserService = (() => {
|
|
|
6307
6530
|
pendingRequests: load.pendingRequests
|
|
6308
6531
|
} },
|
|
6309
6532
|
...load.dialog !== void 0 ? { dialog: load.dialog } : {}
|
|
6310
|
-
});
|
|
6533
|
+
}, signal);
|
|
6311
6534
|
return this.attachDownload(live, view, false);
|
|
6312
6535
|
} catch (error) {
|
|
6313
6536
|
if (isDownloadStart(error) || live.pendingDownload !== void 0) {
|
|
6314
|
-
const view = await this.pageFromShown(live, this.shown.get(live.id));
|
|
6537
|
+
const view = await this.pageFromShown(live, this.shown.get(live.id), void 0, signal);
|
|
6315
6538
|
if (view.url === "" || view.url === "about:blank") view.url = canonical(url);
|
|
6316
6539
|
this.applyDocumentType(live, view);
|
|
6317
6540
|
return this.attachDownload(live, view, true);
|
|
@@ -6343,7 +6566,7 @@ let MhBrowserService = (() => {
|
|
|
6343
6566
|
return {
|
|
6344
6567
|
kind: "response",
|
|
6345
6568
|
status: response.status(),
|
|
6346
|
-
body: new Uint8Array(await response.body()),
|
|
6569
|
+
body: new Uint8Array(await untilAborted(response.body(), capped(signal, READ_MS))),
|
|
6347
6570
|
...location === void 0 || location === "" ? {} : { location },
|
|
6348
6571
|
...mediaType === void 0 ? {} : { mediaType }
|
|
6349
6572
|
};
|
|
@@ -6537,22 +6760,21 @@ let MhBrowserService = (() => {
|
|
|
6537
6760
|
}
|
|
6538
6761
|
return urls;
|
|
6539
6762
|
}
|
|
6540
|
-
async snapshotNamed(owner, tab) {
|
|
6763
|
+
async snapshotNamed(owner, tab, signal) {
|
|
6541
6764
|
if (tab === void 0) return void 0;
|
|
6542
6765
|
const live = this.windows.get(owner, tab);
|
|
6543
6766
|
if (live === void 0) return void 0;
|
|
6544
|
-
const shown = await this.refreshSnapshot(live);
|
|
6767
|
+
const shown = await this.refreshSnapshot(live, signal);
|
|
6545
6768
|
this.shown.set(live.id, shown);
|
|
6546
|
-
return this.pageFromShown(live, shown, { surface: "snapshot" });
|
|
6547
|
-
}
|
|
6548
|
-
exclusive(owner, op) {
|
|
6549
|
-
const
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
|
|
6553
|
-
if (this.queues.get(owner) === tail) this.queues.delete(owner);
|
|
6769
|
+
return this.pageFromShown(live, shown, { surface: "snapshot" }, signal);
|
|
6770
|
+
}
|
|
6771
|
+
exclusive(owner, op, signal) {
|
|
6772
|
+
const queued = enqueue(this.queues.get(owner) ?? Promise.resolve(), op, signal);
|
|
6773
|
+
this.queues.set(owner, queued.tail);
|
|
6774
|
+
queued.tail.then(() => {
|
|
6775
|
+
if (this.queues.get(owner) === queued.tail) this.queues.delete(owner);
|
|
6554
6776
|
});
|
|
6555
|
-
return run;
|
|
6777
|
+
return queued.run;
|
|
6556
6778
|
}
|
|
6557
6779
|
};
|
|
6558
6780
|
})();
|
|
@@ -6600,24 +6822,24 @@ function fragmentOf(url) {
|
|
|
6600
6822
|
return;
|
|
6601
6823
|
}
|
|
6602
6824
|
}
|
|
6603
|
-
async function mainDocument(response) {
|
|
6825
|
+
async function mainDocument(response, signal) {
|
|
6604
6826
|
const headers = response.headers();
|
|
6605
6827
|
const mediaType = headers["content-type"];
|
|
6606
6828
|
const link = headers.link;
|
|
6607
6829
|
return {
|
|
6608
6830
|
status: response.status(),
|
|
6609
6831
|
finalUrl: response.url(),
|
|
6610
|
-
body: await response.text(),
|
|
6832
|
+
body: await untilAborted(response.text(), capped(signal, READ_MS)),
|
|
6611
6833
|
...mediaType !== void 0 ? { mediaType } : {},
|
|
6612
6834
|
...link !== void 0 ? { link } : {}
|
|
6613
6835
|
};
|
|
6614
6836
|
}
|
|
6615
|
-
async function readDomLinks(page) {
|
|
6616
|
-
return (await page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
6837
|
+
async function readDomLinks(page, signal) {
|
|
6838
|
+
return (await untilAborted(page.locator("link").evaluateAll((elements) => elements.map((element) => ({
|
|
6617
6839
|
rel: element.getAttribute("rel") ?? "",
|
|
6618
6840
|
href: element.getAttribute("href") ?? "",
|
|
6619
6841
|
title: element.getAttribute("title") ?? ""
|
|
6620
|
-
}))).catch(() => [])).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
6842
|
+
}))).catch(() => []), capped(signal, READ_MS))).filter((row) => row.rel !== "" && row.href !== "").map((row) => ({
|
|
6621
6843
|
rel: row.rel,
|
|
6622
6844
|
href: row.href,
|
|
6623
6845
|
...row.title !== "" ? { title: row.title } : {}
|
|
@@ -6890,9 +7112,10 @@ function browserHandoverTool(ctx, reader, headed = true) {
|
|
|
6890
7112
|
if (exec.signal.aborted) return quiet();
|
|
6891
7113
|
const session = sessionFor(ctx, exec);
|
|
6892
7114
|
try {
|
|
7115
|
+
const tab = givenString(args.tab);
|
|
6893
7116
|
await ctx.mhBrowser.handover(session.id, {
|
|
6894
7117
|
reason: args.reason,
|
|
6895
|
-
...
|
|
7118
|
+
...tab === void 0 ? {} : { tab }
|
|
6896
7119
|
}, exec.signal);
|
|
6897
7120
|
const answer = await askUntilAnswered(ctx, args.reason, exec, headed);
|
|
6898
7121
|
if (answer === ABORT || exec.signal.aborted) return quiet();
|
|
@@ -7036,6 +7259,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7036
7259
|
if (resolved === void 0) throw new BrowserToolError("NOT_DELIVERED", `${NOT_DELIVERED_REASON}: ${args.page}`);
|
|
7037
7260
|
if ("ambiguous" in resolved) throw new BrowserToolError("INVALID_ARGS", `${args.page} is the id of more than one page shown to you; give the url of one: ${resolved.ambiguous.join(", ")}`);
|
|
7038
7261
|
const input = args.input === void 0 ? void 0 : asObject(args.input);
|
|
7262
|
+
const item = givenString(args.item);
|
|
7039
7263
|
try {
|
|
7040
7264
|
const sent = await ctx.mhBrowser.invoke(session.id, resolved.url, {
|
|
7041
7265
|
name: args.name,
|
|
@@ -7043,7 +7267,7 @@ function browserInvokeTool(ctx, reader) {
|
|
|
7043
7267
|
url: resolved.url,
|
|
7044
7268
|
text: resolved.text
|
|
7045
7269
|
},
|
|
7046
|
-
...
|
|
7270
|
+
...item === void 0 ? {} : { item },
|
|
7047
7271
|
...input === void 0 ? {} : { input }
|
|
7048
7272
|
}, {
|
|
7049
7273
|
signal: exec.signal,
|
|
@@ -7168,18 +7392,19 @@ function browserOpenTool(ctx, reader) {
|
|
|
7168
7392
|
}),
|
|
7169
7393
|
async execute(args, exec) {
|
|
7170
7394
|
if (args.pages.length < 1 || args.pages.length > 8) throw new BrowserToolError("INVALID_ARGS", "pages takes 1 to 8 entries");
|
|
7171
|
-
|
|
7172
|
-
const waitFor = page.waitFor;
|
|
7395
|
+
const items = args.pages.map((page) => {
|
|
7396
|
+
const waitFor = page.waitFor === void 0 ? void 0 : compactWait(page.waitFor);
|
|
7173
7397
|
if (waitFor !== void 0 && waitFor.text === void 0 && waitFor.textGone === void 0 && waitFor.time === void 0) throw new BrowserToolError("INVALID_ARGS", "waitFor needs text, textGone, or time");
|
|
7174
|
-
|
|
7398
|
+
return {
|
|
7399
|
+
url: page.url,
|
|
7400
|
+
...page.snapshot === true ? { snapshot: true } : {},
|
|
7401
|
+
...waitFor === void 0 ? {} : { waitFor }
|
|
7402
|
+
};
|
|
7403
|
+
});
|
|
7175
7404
|
const session = sessionFor(ctx, exec);
|
|
7176
7405
|
ctx.mhBrowser.noteWorkspace(session.id, writeTarget(ctx, session));
|
|
7177
7406
|
try {
|
|
7178
|
-
const pages = await ctx.mhBrowser.open(session.id,
|
|
7179
|
-
url: page.url,
|
|
7180
|
-
...page.snapshot === true ? { snapshot: true } : {},
|
|
7181
|
-
...page.waitFor === void 0 ? {} : { waitFor: compactWait(page.waitFor) }
|
|
7182
|
-
})), exec.signal);
|
|
7407
|
+
const pages = await ctx.mhBrowser.open(session.id, items, exec.signal);
|
|
7183
7408
|
const block = tabBlock(ctx, reader, session);
|
|
7184
7409
|
return {
|
|
7185
7410
|
text: `${renderOutcome(pages, void 0, true)}${block.suffix}`,
|
|
@@ -7196,10 +7421,13 @@ function browserOpenTool(ctx, reader) {
|
|
|
7196
7421
|
});
|
|
7197
7422
|
}
|
|
7198
7423
|
function compactWait(waitFor) {
|
|
7424
|
+
const text = givenString(waitFor.text);
|
|
7425
|
+
const textGone = givenString(waitFor.textGone);
|
|
7426
|
+
const time = givenTime(waitFor.time);
|
|
7199
7427
|
return {
|
|
7200
|
-
...
|
|
7201
|
-
...
|
|
7202
|
-
...
|
|
7428
|
+
...text === void 0 ? {} : { text },
|
|
7429
|
+
...textGone === void 0 ? {} : { textGone },
|
|
7430
|
+
...time === void 0 ? {} : { time }
|
|
7203
7431
|
};
|
|
7204
7432
|
}
|
|
7205
7433
|
function titleOf(name, url) {
|