browser-broker 0.1.0 → 0.2.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/.env.example +6 -5
- package/README.md +59 -19
- package/RELEASES.md +255 -97
- package/dist/package.json +3 -2
- package/dist/src/adapter/conformance/service-subject.js +5 -1
- package/dist/src/browser/fake.js +16 -2
- package/dist/src/browser/real.js +10 -2
- package/dist/src/cli/adapter.js +37 -3
- package/dist/src/cli/commands.js +37 -1
- package/dist/src/cli/index.js +12 -1
- package/dist/src/cli/operations-commands.js +82 -4
- package/dist/src/cli/reconcile-command.js +35 -3
- package/dist/src/doctor/checks.js +64 -0
- package/dist/src/doctor/report.js +10 -1
- package/dist/src/service/arbitration.js +61 -0
- package/dist/src/service/bridge.js +148 -2
- package/dist/src/service/broker.js +52 -1
- package/dist/src/service/browser-session.js +66 -0
- package/dist/src/service/operations/claim.js +63 -0
- package/dist/src/service/operations/pages.js +66 -4
- package/dist/src/service/operations/status.js +8 -0
- package/dist/src/service/pages.js +81 -0
- package/dist/src/service/reconcile.js +75 -3
- package/dist/src/service/runtime.js +26 -1
- package/dist/src/service/tabs.js +70 -0
- package/dist/src/tool/session.js +17 -4
- package/dist/src/tool/tools.js +154 -3
- package/package.json +3 -2
package/.env.example
CHANGED
|
@@ -156,7 +156,9 @@
|
|
|
156
156
|
# browser named in both lists above, so a browser added here gains its own
|
|
157
157
|
# discovery and keeper-tab lines in the health report.
|
|
158
158
|
|
|
159
|
-
# Which
|
|
159
|
+
# Which engine the signed-in browsers name. Does not decide which binary
|
|
160
|
+
# launches: per-engine executable resolution is not built, so every browser
|
|
161
|
+
# launches the automation library's own Chromium. See RELEASES.md.
|
|
160
162
|
# Options: chrome, brave, msedge — all Chromium over the same remote-debugging
|
|
161
163
|
# protocol, which is why the choice is a path rather than a driver.
|
|
162
164
|
# One engine per kind, never per browser: an engine per entry would put an
|
|
@@ -164,10 +166,9 @@
|
|
|
164
166
|
# Default: msedge
|
|
165
167
|
# BROKER_REGULAR_BROWSER_ENGINE=msedge
|
|
166
168
|
|
|
167
|
-
# Which
|
|
168
|
-
#
|
|
169
|
-
#
|
|
170
|
-
# a clean-room browser at all.
|
|
169
|
+
# Which engine the clean-room browsers name. Accepted and validated, and read
|
|
170
|
+
# by nothing: one driver serves every browser in the process and only the
|
|
171
|
+
# regular engine is handed to it, so a differing value reaches no launch.
|
|
171
172
|
# Options: chrome, brave, msedge
|
|
172
173
|
# Default: msedge
|
|
173
174
|
# BROKER_PRIVATE_BROWSER_ENGINE=msedge
|
package/README.md
CHANGED
|
@@ -61,23 +61,33 @@ hanging the report.
|
|
|
61
61
|
service to keep running: the process is started by whatever calls it and exits with it. So getting it
|
|
62
62
|
working is an install and one more fetch below — there is no step after that.
|
|
63
63
|
|
|
64
|
-
You need **Node 22.18 or newer**.
|
|
64
|
+
You need **Node 22.18 or newer**. Either path below also needs a browser binary, which is a separate
|
|
65
|
+
fetch from either install step — see [Browser binary](#browser-binary) once you've picked a path.
|
|
65
66
|
|
|
66
67
|
### From the registry
|
|
67
68
|
|
|
68
|
-
The package ships compiled JavaScript, so nothing is
|
|
69
|
+
The package ships compiled JavaScript, so **nothing is compiled** on your machine. That is not the
|
|
70
|
+
same as nothing to set up: a browser binary is still owed, and this path does not fetch it — see
|
|
71
|
+
[Browser binary](#browser-binary) below before your first `broker doctor`.
|
|
69
72
|
|
|
70
73
|
```bash
|
|
71
|
-
npx browser-broker doctor
|
|
74
|
+
npx -p browser-broker broker doctor
|
|
72
75
|
```
|
|
73
76
|
|
|
77
|
+
The package installs two executables — `broker`, the command line, and `broker-tool`, the surface a
|
|
78
|
+
client spawns — and neither is named for the package, so `npx browser-broker` cannot tell which you
|
|
79
|
+
meant and refuses. `-p` names the package and the word after it names the executable.
|
|
80
|
+
|
|
74
81
|
A client that spawns the tool surface names the same package, and npm revalidates the version on
|
|
75
82
|
every run — so a published release arrives without anything being pulled or rebuilt by hand:
|
|
76
83
|
|
|
77
84
|
```json
|
|
78
85
|
{
|
|
79
86
|
"mcpServers": {
|
|
80
|
-
"browser-broker": {
|
|
87
|
+
"browser-broker": {
|
|
88
|
+
"command": "npx",
|
|
89
|
+
"args": ["-y", "-p", "browser-broker", "broker-tool"]
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
```
|
|
@@ -97,20 +107,8 @@ cd browser-broker
|
|
|
97
107
|
npm install
|
|
98
108
|
```
|
|
99
109
|
|
|
100
|
-
That compiles the one runtime dependency's native binding
|
|
101
|
-
|
|
102
|
-
distribution, precisely because the browser binary is spawned by this service, detached and by path,
|
|
103
|
-
rather than downloaded and managed by the package. `playwright-core` does not fetch a browser on
|
|
104
|
-
install, so a checkout that has never had one fetched by some other tooling has none, and `broker
|
|
105
|
-
doctor`'s automation check will genuinely fail with exit code 11 until you run:
|
|
106
|
-
|
|
107
|
-
```bash
|
|
108
|
-
npx playwright-core install chromium
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
Run this once per machine, before the first `broker doctor`. It is the same install mechanism the
|
|
112
|
-
full `playwright` package would run automatically on `npm install`; `playwright-core` just does not
|
|
113
|
-
run it for you. Then run the broker itself:
|
|
110
|
+
That compiles the one runtime dependency's native binding. It does not fetch a browser binary — see
|
|
111
|
+
[Browser binary](#browser-binary) below before your first `broker doctor`. Then run the broker itself:
|
|
114
112
|
|
|
115
113
|
```bash
|
|
116
114
|
node src/bin/broker.ts
|
|
@@ -134,6 +132,23 @@ To get the command on your path as `broker`, link the package from the checkout:
|
|
|
134
132
|
npm link # then: broker --help
|
|
135
133
|
```
|
|
136
134
|
|
|
135
|
+
### Browser binary
|
|
136
|
+
|
|
137
|
+
**Neither install path above fetches a browser.** This repository depends on `playwright-core`, not
|
|
138
|
+
the full `playwright` distribution, precisely because the browser binary is spawned by this service,
|
|
139
|
+
detached and by path, rather than downloaded and managed by the package. `playwright-core` does not
|
|
140
|
+
fetch a browser on install, so a machine that has never had one fetched by some other tooling has
|
|
141
|
+
none, and `broker doctor`'s automation check will genuinely fail with exit code 11 until you run:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
npx playwright-core install chromium
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Run this once per machine, before the first `broker doctor` — it applies whether you installed from
|
|
148
|
+
the registry or from a checkout, because it is a fetch neither install step performs. It is the same
|
|
149
|
+
install mechanism the full `playwright` package would run automatically on `npm install`;
|
|
150
|
+
`playwright-core` just does not run it for you.
|
|
151
|
+
|
|
137
152
|
### Configuring it
|
|
138
153
|
|
|
139
154
|
**Nothing needs setting.** Every value is an environment variable with a working default, so the
|
|
@@ -156,9 +171,16 @@ called, and a caller that names no browser gets the first signed-in one:
|
|
|
156
171
|
BROKER_REGULAR_BROWSERS=regular,checkout # persistent, signed in, at most 3
|
|
157
172
|
BROKER_PRIVATE_BROWSERS=private # ephemeral, at most 3
|
|
158
173
|
BROKER_REGULAR_BROWSER_ENGINE=msedge # chrome | brave | msedge
|
|
159
|
-
BROKER_PRIVATE_BROWSER_ENGINE=msedge #
|
|
174
|
+
BROKER_PRIVATE_BROWSER_ENGINE=msedge # accepted and validated, and read by nothing
|
|
160
175
|
```
|
|
161
176
|
|
|
177
|
+
**Neither engine variable changes which binary launches.** Resolving an engine name to an executable
|
|
178
|
+
is deliberately not built, so every browser launches the automation library's own Chromium whichever
|
|
179
|
+
engine is named, and a machine still has to fetch that Chromium once. The private variable goes one
|
|
180
|
+
step less far than the regular one: a single driver serves every browser in the process and only the
|
|
181
|
+
regular engine is handed to it, so the private value is validated at startup and then read by nothing
|
|
182
|
+
at all. `RELEASES.md` has the fuller account.
|
|
183
|
+
|
|
162
184
|
Two signed-in browsers is how two identities are exercised at once: tabs within one browser share
|
|
163
185
|
its cookie jar, so they are isolated from other browsers and not from each other. **Note that each
|
|
164
186
|
browser is a process before it holds a single tab**, which the tab budget does not count —
|
|
@@ -198,6 +220,24 @@ it is rarely the checkout. Nothing else is required: there is no port to configu
|
|
|
198
220
|
issue, and no process to have started first — the client starts it, and it exits when the client
|
|
199
221
|
closes the pipe.
|
|
200
222
|
|
|
223
|
+
**If that configuration file is itself synchronised between machines, an absolute path is the one
|
|
224
|
+
thing in it that cannot travel.** A home directory differs per machine and often per user, so one
|
|
225
|
+
entry naming a checkout is correct on the machine it was written on and names nothing on the other
|
|
226
|
+
— where it fails as a connection that closes immediately, which reads as a broken service rather
|
|
227
|
+
than as a path that does not exist. Give each machine its own entry under its own server name, or
|
|
228
|
+
point the shared entry at the published package, which carries no machine's path:
|
|
229
|
+
|
|
230
|
+
```json
|
|
231
|
+
{
|
|
232
|
+
"mcpServers": {
|
|
233
|
+
"browser-broker": {
|
|
234
|
+
"command": "npx",
|
|
235
|
+
"args": ["-y", "-p", "browser-broker", "broker-tool"]
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
201
241
|
To point it at a store other than the default, add the environment to the same block:
|
|
202
242
|
|
|
203
243
|
```json
|
package/RELEASES.md
CHANGED
|
@@ -1,97 +1,255 @@
|
|
|
1
|
-
# Release notes
|
|
2
|
-
|
|
3
|
-
**What changes between versions, and specifically what changes *behaviour* on an installation that
|
|
4
|
-
sets nothing.**
|
|
5
|
-
|
|
6
|
-
That second half is the reason this file exists rather than the commit log being enough. A new
|
|
7
|
-
setting with a neutral default is not news: an installation that does not set it behaves exactly as
|
|
8
|
-
before. **A changed default is news**, because it moves an installation that has taken no action and
|
|
9
|
-
made no decision. `docs/plans/DECISIONS.md` §6.3 puts a changed default here rather than in a quiet
|
|
10
|
-
edit for that reason.
|
|
11
|
-
|
|
12
|
-
Entries are newest first. Each names what moved, what an installation has to do about it, and what
|
|
13
|
-
happens if it does nothing.
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
###
|
|
20
|
-
|
|
21
|
-
**What moved.**
|
|
22
|
-
|
|
23
|
-
rather than
|
|
24
|
-
|
|
25
|
-
**Why
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
the
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
and
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
1
|
+
# Release notes
|
|
2
|
+
|
|
3
|
+
**What changes between versions, and specifically what changes *behaviour* on an installation that
|
|
4
|
+
sets nothing.**
|
|
5
|
+
|
|
6
|
+
That second half is the reason this file exists rather than the commit log being enough. A new
|
|
7
|
+
setting with a neutral default is not news: an installation that does not set it behaves exactly as
|
|
8
|
+
before. **A changed default is news**, because it moves an installation that has taken no action and
|
|
9
|
+
made no decision. `docs/plans/DECISIONS.md` §6.3 puts a changed default here rather than in a quiet
|
|
10
|
+
edit for that reason.
|
|
11
|
+
|
|
12
|
+
Entries are newest first. Each names what moved, what an installation has to do about it, and what
|
|
13
|
+
happens if it does nothing.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 0.2.0
|
|
18
|
+
|
|
19
|
+
### ⚠ Behaviour change: `browser_status` asks whether the browser is still there
|
|
20
|
+
|
|
21
|
+
**What moved.** Liveness was derived from rows and a clock. `browser_status` now asks the operating
|
|
22
|
+
system whether the browser process is still answering, and a session that has died is let go, so the
|
|
23
|
+
next acquisition relaunches rather than handing back a dead one.
|
|
24
|
+
|
|
25
|
+
**Why this is a behaviour change and not a new check.** A lease whose browser had died reported
|
|
26
|
+
`active`, with its expiry advancing, and no sequence of calls from the tool surface could recover it:
|
|
27
|
+
releasing and reclaiming reached the same settled session, because the provider memoised it for the
|
|
28
|
+
life of the process. The lease looked healthy from every angle a caller could see, and every page
|
|
29
|
+
call on it failed.
|
|
30
|
+
|
|
31
|
+
**What an installation has to do.** Nothing.
|
|
32
|
+
|
|
33
|
+
**If it does nothing:** `browser_status` may report a browser gone where it reported `active`. **That
|
|
34
|
+
is the check working, not a new fault appearing** — the browser was already gone, and the report is
|
|
35
|
+
what changed.
|
|
36
|
+
|
|
37
|
+
**The discriminator is load-bearing, and worth knowing about.** A row that says `running` while
|
|
38
|
+
nothing answers is a browser that died; a row that says `stopped` never started. Acquisition is lazy,
|
|
39
|
+
so a freshly granted lease on a machine with no browser installed is the ordinary case, not an error
|
|
40
|
+
— probing without that distinction reports those leases as expired.
|
|
41
|
+
|
|
42
|
+
### A claim says when a browser's tabs are stranded
|
|
43
|
+
|
|
44
|
+
**What moved.** Three reports about tabs stranded mid-close:
|
|
45
|
+
|
|
46
|
+
- **A grant now carries the count**, from the same query and the same instant. A browser holding a
|
|
47
|
+
backlog of tabs left by sessions killed mid-lease can grant a lease that cannot be used: the claim
|
|
48
|
+
succeeds and every navigate on it fails. Five consecutive claims were granted against such a
|
|
49
|
+
backlog before anything said so.
|
|
50
|
+
- **`broker doctor` splits its stranded count per browser** rather than reporting one total, so
|
|
51
|
+
clearing one browser moves the number it is judged by.
|
|
52
|
+
- **`broker reconcile` says when the tab blocking it belongs to the caller's own lease**, given a
|
|
53
|
+
`--session-id` to know it by.
|
|
54
|
+
|
|
55
|
+
**What an installation has to do.** Nothing. The count rides on a grant that is genuinely
|
|
56
|
+
granted — capacity was taken and the lease is active — so this is a note on a successful response
|
|
57
|
+
rather than a refusal, and it is absent rather than zero when there is no backlog.
|
|
58
|
+
|
|
59
|
+
### Command-line help lists the flags a command is refused for omitting, and an unknown flag is refused
|
|
60
|
+
|
|
61
|
+
**What moved.** `broker claim --help` documents `--session-id` and `--purpose`, the two flags a claim
|
|
62
|
+
is refused by name for omitting, and `broker reconcile --help` documents its optional `--session-id`.
|
|
63
|
+
A help text listing three optional flags and neither required one teaches a reader how to call the
|
|
64
|
+
command unsuccessfully.
|
|
65
|
+
|
|
66
|
+
An unknown flag on `snapshot`, `events` or `reconcile` is refused, naming the flag and the accepted
|
|
67
|
+
set, rather than being accepted and discarded.
|
|
68
|
+
|
|
69
|
+
**What an installation has to do.** Nothing. A caller passing correct flags is unaffected.
|
|
70
|
+
|
|
71
|
+
**The refusal's reach is narrower than the help fix, and worth stating plainly:** it covers those
|
|
72
|
+
three commands. `claim` and the other operation commands parse their arguments by a different route,
|
|
73
|
+
so `broker claim --session x` still does not name the typo. The completeness gate covering the
|
|
74
|
+
documented commands is a hand-written list.
|
|
75
|
+
|
|
76
|
+
### `act emulate` says how long its effect lasts
|
|
77
|
+
|
|
78
|
+
**What moved.** An `emulate` result carries `emulationScope`, naming the bound the preference lives
|
|
79
|
+
within and the path that works: emulate and capture within one invocation, or use the tool surface,
|
|
80
|
+
where one connection spans the calls.
|
|
81
|
+
|
|
82
|
+
The preference is scoped to the connection that set it — the tab survives and the emulation binding
|
|
83
|
+
does not — so an emulate driven from a separate invocation than the capture that reads it returns
|
|
84
|
+
`accepted` and changes nothing on the page. A reviewer checking dark mode that way gets a clean
|
|
85
|
+
result and a light screenshot. The harm is the silence rather than the impersistence.
|
|
86
|
+
|
|
87
|
+
**What an installation has to do.** Nothing; the field is additive. `pageDriven` is unchanged and
|
|
88
|
+
still reports `true` here, because a browser genuinely was reached.
|
|
89
|
+
|
|
90
|
+
### `browser_capture` can ask for a higher-resolution tier
|
|
91
|
+
|
|
92
|
+
**What moved.** `browser_capture` takes `tier` — `"detail"` or `"max"` — and `reason` is bounded at 8
|
|
93
|
+
to 200 characters and required alongside `tier="max"`. The default has no name and is what passing
|
|
94
|
+
nothing gives you. A new refusal, `capture.tier_known`, names both accepted words when a tier is not
|
|
95
|
+
one of them.
|
|
96
|
+
|
|
97
|
+
The ladder was built end to end, and the pipeline refuses the top rung without a written reason,
|
|
98
|
+
while no surface could populate either field — so the escalation rollup could only ever read as
|
|
99
|
+
*nobody escalates*, which is a measurement of the surface rather than of any caller.
|
|
100
|
+
|
|
101
|
+
**What an installation has to do.** Nothing. A capture that names no tier gets the default.
|
|
102
|
+
|
|
103
|
+
### ⚠ Behaviour change: `wait_ms` on a navigate is now honoured
|
|
104
|
+
|
|
105
|
+
**What moved.** `browser_navigate` has advertised a `wait_ms` argument for some time, typed,
|
|
106
|
+
documented and accepted without complaint. **Nothing read it.** It now reaches the browser as the
|
|
107
|
+
navigation's timeout, and it is bounded by the lease: a wait longer than the lease can live is
|
|
108
|
+
refused, and the refusal names the accepted range rather than quietly clamping the value.
|
|
109
|
+
|
|
110
|
+
**Why this is a behaviour change and not a new feature.** An installation that has set nothing is
|
|
111
|
+
unaffected — but a *caller* that was already passing `wait_ms`, exactly as the schema invited, was
|
|
112
|
+
having it discarded and now is not. Nothing about that caller's code changes and its behaviour does.
|
|
113
|
+
|
|
114
|
+
**What an installation has to do.** Nothing. A caller passing no wait is unaffected: the argument is
|
|
115
|
+
omitted rather than defaulted, so the browser's own default still applies, and this service does not
|
|
116
|
+
invent a number the browser library owns.
|
|
117
|
+
|
|
118
|
+
**Worth knowing before relying on it:** the wait bounds the **load**, and does not cover work the
|
|
119
|
+
page starts afterwards. A canvas or a lazily-loaded region can still be unfinished when the call
|
|
120
|
+
returns, so two captures differing only in this argument tell you nothing about how long the page
|
|
121
|
+
was given to settle — see `browser_capture` on how to tell. An inert argument is worse than a
|
|
122
|
+
missing one precisely because a caller draws conclusions from it, and that is the conclusion most
|
|
123
|
+
easily drawn here.
|
|
124
|
+
|
|
125
|
+
### Dialog and form-filling become reachable from the command line, and a contradictory answer is refused
|
|
126
|
+
|
|
127
|
+
**What moved.** Two things a command-line caller had no way to do at all:
|
|
128
|
+
|
|
129
|
+
- `act dialog` and `act fill_form` take nested arguments that the flat command-line parser could not
|
|
130
|
+
produce, so **no argument a person could type would ever parse**. They now assemble from ordinary
|
|
131
|
+
flags — `--accept` / `--dismiss`, `--prompt-text`, and a repeatable `--field`.
|
|
132
|
+
- **A contradictory dialog answer is now refused instead of resolved silently.** Asking to accept
|
|
133
|
+
and dismiss the same dialog used to take whichever the code read first. A caller answering a
|
|
134
|
+
destructive `confirm()` deserves a refusal rather than a coin flip, and now gets one.
|
|
135
|
+
|
|
136
|
+
**What an installation has to do.** Nothing. Callers already sending a single, coherent answer — by
|
|
137
|
+
either surface — are unaffected.
|
|
138
|
+
|
|
139
|
+
### Capture responses point at `compare_to`
|
|
140
|
+
|
|
141
|
+
**What moved.** A capture response now carries a hint naming the comparison it could have made. The
|
|
142
|
+
comparison itself already existed and is roughly two orders of magnitude cheaper than reading a
|
|
143
|
+
screenshot back to answer *did this change* — and in all recorded history it had been used once,
|
|
144
|
+
because nothing pointed at it.
|
|
145
|
+
|
|
146
|
+
**What an installation has to do.** Nothing; the hint is additive. It is listed here because the
|
|
147
|
+
cheapest path through this service was, in practice, unreachable, and a caller that never learned
|
|
148
|
+
the verb existed was not making an informed choice.
|
|
149
|
+
|
|
150
|
+
### Closing a tab is recorded, and the doctor counts what was left behind
|
|
151
|
+
|
|
152
|
+
**What moved.** Tab closes now write their outcome, rows nothing could reach are settled, and
|
|
153
|
+
`broker doctor` reports tabs stranded in `closing`. Without that, a released lease can leave its tab
|
|
154
|
+
open with the record stuck mid-close and **nothing anywhere says so** — a state reachable only by
|
|
155
|
+
looking at the browser.
|
|
156
|
+
|
|
157
|
+
**What an installation has to do.** Nothing. **If it does nothing:** `broker doctor` may now report
|
|
158
|
+
stranded tabs that no check could see before. That is the check working, not a new fault appearing.
|
|
159
|
+
|
|
160
|
+
### The package is published, and it ships compiled JavaScript
|
|
161
|
+
|
|
162
|
+
**What moved.** The service is installable from the registry as `browser-broker`, so a caller can
|
|
163
|
+
spawn it without a checkout. The manifest's `bin` entries now name emitted JavaScript under `dist/`
|
|
164
|
+
rather than the TypeScript sources.
|
|
165
|
+
|
|
166
|
+
**Why the build exists**, given that the development path deliberately has none: **Node refuses to
|
|
167
|
+
strip types from any file under a `node_modules` path**, and an installed package is a directory
|
|
168
|
+
under `node_modules`. A manifest whose `bin` named a `.ts` file would install cleanly and then fail
|
|
169
|
+
on the machine of whoever installed it. There is no flag that changes this. The compiler therefore
|
|
170
|
+
runs once per release rather than on every machine that consumes the package, and `erasableSyntaxOnly`
|
|
171
|
+
stays on so the sources still run unbuilt — the two paths execute the same dialect.
|
|
172
|
+
|
|
173
|
+
**What an installation has to do.** Nothing. A checkout is unaffected: `node src/bin/broker.ts` still
|
|
174
|
+
runs the sources with no build. An installation that would rather not track a checkout can point at
|
|
175
|
+
the package instead, and npm revalidates the version on every spawn:
|
|
176
|
+
|
|
177
|
+
```json
|
|
178
|
+
{ "command": "npx", "args": ["-y", "-p", "browser-broker", "broker-tool"] }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
The `-p` is not decoration: the package ships two executables and neither is named for the package,
|
|
182
|
+
so `npx browser-broker` cannot choose between them and refuses to run at all.
|
|
183
|
+
|
|
184
|
+
**Worth weighing before switching:** `npx` performs a registry round-trip on every spawn, costing
|
|
185
|
+
seconds where a path on disk costs a fraction of one. It buys an upgrade path, not speed.
|
|
186
|
+
|
|
187
|
+
**One surface changes what it reports.** The tool handshake's `serverInfo.version` was the literal
|
|
188
|
+
`0.0.0` while the package was unversioned, and now reads the manifest — so a client logging it sees
|
|
189
|
+
the released version rather than a placeholder.
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
### The default browser engine is named Edge, but resolving it is not built yet
|
|
193
|
+
|
|
194
|
+
**What moved.** `BROKER_REGULAR_BROWSER_ENGINE` and `BROKER_PRIVATE_BROWSER_ENGINE` default to
|
|
195
|
+
**`msedge`** rather than being unset. `DECISIONS.md` §13i chose Edge as the default *name* because it
|
|
196
|
+
is present on every Windows machine, applying §6.1's *"a fresh install runs with nothing set"* to
|
|
197
|
+
which engine a fresh install prefers.
|
|
198
|
+
|
|
199
|
+
**What this does not do: it does not change which binary launches, and it does not remove the browser
|
|
200
|
+
download step.** Resolving an engine name to an executable path is deliberately not built (§13i) —
|
|
201
|
+
`executablePathForEngine` only looks up a path this process was *given* for that engine, and nothing
|
|
202
|
+
in this repository supplies one. So the launch still falls through to `chromium.executablePath()`
|
|
203
|
+
regardless of which engine is named, and a machine that has never fetched a Chromium build still
|
|
204
|
+
needs to, exactly as before this change. The earlier release note claiming this default removed the
|
|
205
|
+
download step was wrong and is corrected here.
|
|
206
|
+
|
|
207
|
+
**What an installation has to do.** Nothing, either way — the engine setting has no observable effect
|
|
208
|
+
yet. Setting it now is preparation for when per-engine resolution is built, not a way to skip the
|
|
209
|
+
browser fetch:
|
|
210
|
+
|
|
211
|
+
```bash
|
|
212
|
+
BROKER_REGULAR_BROWSER_ENGINE=chrome # chrome | brave | msedge — has no effect on which binary launches
|
|
213
|
+
BROKER_PRIVATE_BROWSER_ENGINE=chrome # accepted and validated, and read by nothing — see below
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Both are parsed and validated, and an unaccepted value is refused at startup and named — that much is
|
|
217
|
+
finished. **The private variable goes one step less far than the regular one:** one driver serves
|
|
218
|
+
every browser in a process, and only the regular engine is handed to it, so the private value is
|
|
219
|
+
validated and then never read by anything at all. It is not merely ineffective downstream; it has no
|
|
220
|
+
downstream.
|
|
221
|
+
|
|
222
|
+
**If it does nothing:** browsers keep launching under whichever Chromium build `playwright-core`
|
|
223
|
+
resolves, same as before this change. **No profile is signed out by setting an engine**, which an
|
|
224
|
+
earlier note claimed would happen: a profile directory is named for its *browser*, not for an engine,
|
|
225
|
+
so there is no engine-keyed profile to move between and nothing to sign in again.
|
|
226
|
+
`setup.profile_never_destroyed` holds, as it did before. The per-engine profile and sign-in
|
|
227
|
+
consequences described in `DECISIONS.md` §13i are what would follow once resolution is wired, and
|
|
228
|
+
describe nothing that happens now.
|
|
229
|
+
|
|
230
|
+
### Browsers are a configured list, and `browser` on a claim is optional
|
|
231
|
+
|
|
232
|
+
**What moved.** Two things, both reversals of recorded decisions — the argument for each is in
|
|
233
|
+
`DECISIONS.md` §13i:
|
|
234
|
+
|
|
235
|
+
- **The fixed pair of browsers becomes a bounded list per kind**, at most three each, named in
|
|
236
|
+
configuration. A name is what a caller claims by and what its profile directory is called.
|
|
237
|
+
- **`browser` on `browser_claim` becomes optional.** Unstated resolves to the first signed-in
|
|
238
|
+
browser; `regular` or `private` resolves to the first of that kind; a configured name resolves to
|
|
239
|
+
that browser exactly.
|
|
240
|
+
|
|
241
|
+
**What an installation has to do.** Nothing. The defaults name one browser of each kind, `regular`
|
|
242
|
+
and `private`, which is the pair that existed before — so an installation that sets nothing has the
|
|
243
|
+
same two browsers under the same two names, and a caller that states `browser` explicitly is
|
|
244
|
+
unaffected.
|
|
245
|
+
|
|
246
|
+
**Worth reading before configuring more than the default two:** the tab budget counts *tabs*, and
|
|
247
|
+
each browser costs a process **before it holds a single tab**. `BROKER_TAB_BUDGET=15` with six
|
|
248
|
+
browsers is six browser processes, plus up to fifteen tabs, plus six keeper tabs that are not
|
|
249
|
+
counted against the budget at all. `.env.example` states this beside the variables.
|
|
250
|
+
|
|
251
|
+
**One schema step.** The store gains a `kind` column on `browsers` and drops the check constraint
|
|
252
|
+
that limited a browser's name to two literals. It is applied on the next spawn, like every step, and
|
|
253
|
+
the two existing rows are backfilled to their own kinds. **A store stepped by this build is not
|
|
254
|
+
readable by an earlier one**, which is the ordinary direction — a build refuses a store newer than
|
|
255
|
+
itself rather than downgrading it.
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "browser-broker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Leases over tabs in a fixed set of browsers: bounded capacity, a queue, reclamation, and an enforced capture policy.",
|
|
6
6
|
"type": "module",
|
|
@@ -19,12 +19,13 @@
|
|
|
19
19
|
"check:operations": "node scripts/check-operations.mjs",
|
|
20
20
|
"check:argument-refusals": "node scripts/check-argument-refusals.mjs",
|
|
21
21
|
"check:injected-tests": "node scripts/check-injected-tests.mjs",
|
|
22
|
+
"check:argument-reachability": "node scripts/check-argument-reachability.mjs",
|
|
22
23
|
"typecheck": "tsc --noEmit",
|
|
23
24
|
"lint": "eslint .",
|
|
24
25
|
"format": "prettier --write .",
|
|
25
26
|
"format:check": "prettier --check .",
|
|
26
27
|
"test": "node --test \"tests/**/*.test.mjs\" \"tests/**/*.test.ts\"",
|
|
27
|
-
"check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:injected-tests",
|
|
28
|
+
"check": "npm run check:external-refs && npm run check:doc-links && npm run check:arbitration && npm run check:capture-isolation && npm run check:artifact-path && npm run typecheck && npm run lint && npm run format:check && npm test && npm run check:install && npm run check:operations && npm run check:argument-refusals && npm run check:argument-reachability && npm run check:injected-tests && npm run check:package",
|
|
28
29
|
"check:arbitration": "node scripts/check-arbitration.mjs",
|
|
29
30
|
"check:artifact-path": "node scripts/check-artifact-path.mjs",
|
|
30
31
|
"build": "tsc --project tsconfig.build.json",
|
|
@@ -150,7 +150,11 @@ export async function makeServiceSubject() {
|
|
|
150
150
|
return {
|
|
151
151
|
service: runtime.service,
|
|
152
152
|
// The fake's own log, which is what makes "a refusal touched no browser"
|
|
153
|
-
// checkable rather than asserted
|
|
153
|
+
// checkable rather than asserted — and the entries are passed whole, so a
|
|
154
|
+
// case can also ask what the browser was told, not merely that it was
|
|
155
|
+
// spoken to. Dropping the arguments here would make a route that forwards
|
|
156
|
+
// an argument and one that discards it indistinguishable to every case in
|
|
157
|
+
// the table.
|
|
154
158
|
driverCalls: () => driver.calls,
|
|
155
159
|
// **The same predicate the capacity check uses**, per `driver.ts`'s
|
|
156
160
|
// requirement for this reading — not a count of rows in `claims`, which
|
package/dist/src/browser/fake.js
CHANGED
|
@@ -402,8 +402,22 @@ export class FakeBrowserDriver {
|
|
|
402
402
|
this.#tabsFor(tab.browser).delete(tab.driverTabId);
|
|
403
403
|
return Promise.resolve();
|
|
404
404
|
},
|
|
405
|
-
navigate: (tab, url) => {
|
|
406
|
-
|
|
405
|
+
navigate: (tab, url, waitMs) => {
|
|
406
|
+
// The wait is recorded even though nothing here waits, because what a
|
|
407
|
+
// test needs to assert is that the service *asked* for it. An argument
|
|
408
|
+
// that is accepted and dropped between the caller and the driver looks
|
|
409
|
+
// identical from the outside to one that was honoured, and the only
|
|
410
|
+
// place that difference is observable is this log.
|
|
411
|
+
//
|
|
412
|
+
// Recorded as an absent key when the caller omitted it, rather than as
|
|
413
|
+
// an explicit undefined, so a test can tell "asked for no wait" from
|
|
414
|
+
// "asked for a wait of nothing".
|
|
415
|
+
const failure = this.#enter({
|
|
416
|
+
name: 'navigate',
|
|
417
|
+
browser,
|
|
418
|
+
tab,
|
|
419
|
+
detail: { url, ...(waitMs === undefined ? {} : { waitMs }) },
|
|
420
|
+
});
|
|
407
421
|
if (failure)
|
|
408
422
|
return Promise.reject(failure);
|
|
409
423
|
return Promise.resolve({ url, title: `fake page at ${url}`, status: 200 });
|
package/dist/src/browser/real.js
CHANGED
|
@@ -493,9 +493,17 @@ class RealBrowserSession {
|
|
|
493
493
|
* The address after redirects rather than the one asked for, because those
|
|
494
494
|
* differ constantly and the caller needs the one it got.
|
|
495
495
|
*/
|
|
496
|
-
async navigate(tab, url) {
|
|
496
|
+
async navigate(tab, url, waitMs) {
|
|
497
497
|
const page = await this.#page(tab);
|
|
498
|
-
|
|
498
|
+
// Passed as the navigation's own timeout rather than as a pause taken
|
|
499
|
+
// afterwards, so a page that arrives early returns early and the argument
|
|
500
|
+
// only ever costs what the page costs.
|
|
501
|
+
//
|
|
502
|
+
// Absent means the library's configured default applies. Spread rather
|
|
503
|
+
// than passed as `{ timeout: undefined }`, because an explicit undefined
|
|
504
|
+
// and an omitted key are the same to this library only by convention, and
|
|
505
|
+
// relying on that convention would make the default this service's to own.
|
|
506
|
+
const response = await page.goto(url, ...(waitMs === undefined ? [] : [{ timeout: waitMs }]));
|
|
499
507
|
return {
|
|
500
508
|
url: page.url(),
|
|
501
509
|
title: await page.title(),
|