@anybrowser/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +78 -2
- package/dist/allowedSites.js +164 -0
- package/dist/allowedSites.js.map +1 -0
- package/dist/blockLedger.js +111 -0
- package/dist/blockLedger.js.map +1 -0
- package/dist/browser.js +1096 -0
- package/dist/browser.js.map +1 -0
- package/dist/browserDetect.js +53 -0
- package/dist/browserDetect.js.map +1 -0
- package/dist/cli.js +366 -0
- package/dist/cli.js.map +1 -0
- package/dist/connect.js +195 -0
- package/dist/connect.js.map +1 -0
- package/dist/daemon.js +475 -0
- package/dist/daemon.js.map +1 -0
- package/dist/hello.js +62 -0
- package/dist/hello.js.map +1 -0
- package/dist/idle.js +84 -0
- package/dist/idle.js.map +1 -0
- package/dist/install.js +63 -0
- package/dist/install.js.map +1 -0
- package/dist/paths.js +36 -0
- package/dist/paths.js.map +1 -0
- package/dist/profile.js +273 -0
- package/dist/profile.js.map +1 -0
- package/dist/proxy.js +40 -0
- package/dist/proxy.js.map +1 -0
- package/dist/siteProxy.js +354 -0
- package/dist/siteProxy.js.map +1 -0
- package/dist/transport.js +85 -0
- package/dist/transport.js.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +50 -4
- package/skills/anybrowser/SKILL.md +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dmitrii
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# anybrowser
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Let any AI agent drive a real browser that is already logged in. No browser extension, any browser, any agent that speaks MCP.
|
|
4
|
+
|
|
5
|
+
## Why
|
|
6
|
+
|
|
7
|
+
Agents are good at using websites. The browsers they are given are the problem. An automation browser starts empty: no logins, no history, and a fingerprint that many sites block on sight. A browser extension fixes that, but it ties you to one browser and one assistant, it has to be installed and allowed, and the extension may limit which sites the agent can touch.
|
|
8
|
+
|
|
9
|
+
anybrowser gives the agent a normal browser window with its own profile. You log in once, by hand, and the session stays. Each profile is for one site or purpose, such as `gmail-in-chrome`, so a mistake can only ever reach that one account.
|
|
10
|
+
|
|
11
|
+
It works with the Chromium browsers you already have (Chrome, Brave, Edge, Arc, Vivaldi, Opera) or Playwright's own Chromium, and with any agent host that supports MCP: Claude Code, Codex, Cursor, Gemini CLI and more.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
agent session ─┐
|
|
17
|
+
agent session ─┼─ anyb mcp ──► anyb daemon ──► one browser per profile
|
|
18
|
+
agent session ─┘ (stdio) (one per user) ├─ work-in-chrome
|
|
19
|
+
└─ shop-in-brave
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- Your agent host runs `anyb mcp`. It starts a background daemon on first use and forwards to it.
|
|
23
|
+
- The daemon owns every browser. Agent sessions share them, and each session works in its own tab.
|
|
24
|
+
- The browser tools are Playwright MCP's, unchanged except that each takes a `profile` argument.
|
|
25
|
+
- A profile's browser closes after 10 idle minutes, and the daemon exits once nothing is connected. Logins are written to disk before either closes.
|
|
26
|
+
- Each profile's traffic passes through a small proxy inside the daemon, which enforces the allowed-sites list below.
|
|
27
|
+
|
|
28
|
+
## Status
|
|
29
|
+
|
|
30
|
+
Pre-release, version 0.1. What works: profiles on installed Chromium browsers, login by hand, every Playwright MCP browser tool, shared sessions, idle shutdown, the allowed-sites list, and `anyb install` for Claude Code.
|
|
31
|
+
|
|
32
|
+
Not yet: Firefox and WebKit, installed-browser detection outside macOS (Playwright's Chromium works everywhere), and `anyb install` for hosts other than Claude Code.
|
|
33
|
+
|
|
34
|
+
## Quick start
|
|
35
|
+
|
|
36
|
+
You need Node 22.12 or newer.
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npm install -g @anybrowser/cli
|
|
40
|
+
anyb install
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Install it globally: your agent host runs the `anyb` command, so it has to be on your `PATH`. `anyb install` registers the MCP server with Claude Code and copies the agent skill. For any other MCP host, `anyb install --print` prints a config snippet to paste.
|
|
44
|
+
|
|
45
|
+
Make a profile and sign in to it once:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
anyb profile add work chrome
|
|
49
|
+
anyb profile login work-in-chrome https://github.com/login
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Sign in in the window that opens. Then open a new agent session and ask it to use the `work-in-chrome` profile. `anyb --help` lists every command.
|
|
53
|
+
|
|
54
|
+
## Safety
|
|
55
|
+
|
|
56
|
+
An agent in a logged-in browser can do real things, so the design limits how far a mistake can reach:
|
|
57
|
+
|
|
58
|
+
- **One login per profile.** A profile holds one site or purpose. Whatever goes wrong in `gmail-in-chrome` cannot touch any other account, and none of it touches your everyday browser. A profile can hold more than one login when a task needs it, for example a site that signs in through Google. Every login you add widens what a mistake in that profile can reach, so keep profiles as narrow as the work allows.
|
|
59
|
+
- **Optional allowed-sites list.** A profile can be pinned to the sites it is for. Every page load, every subresource (scripts, images, requests the page makes), every hop of a redirect chain and every WebSocket is checked against the list, and the first one that leaves it is refused. The check is by host and port, so the path of an HTTPS address is invisible to it: allowing `example.com` allows every page on it. A refusal is written to the daemon log and reported to the agent as an error that names the blocked address. A page that loads while some of its own requests are refused is not an error: the result carries a note naming the refused hosts and saying the page may be incomplete. You set the list with `anyb profile add <name> <browser> --allow <list>` or `anyb profile set <id> allowedOrigins=<list>`, and no MCP tool can change it. It is re-read on every request, so an edit applies to a running session at once, and a connection that is already open to a host you take off the list is closed within a second. Entries are literal: `example.com` does not cover `www.example.com`, `*.example.com` does not cover `example.com` itself, and a port has to be written out (`localhost:3000`, or `localhost:*` for any), so most sites need both the apex and the wildcard. A local dev server is reachable only if it is on the list; a pinned profile reaches nothing else on your machine. Two things are not covered. WebRTC connections are not checked. And an agent that can run shell commands can edit `profile.json` itself, so the list means nothing against one. It is a guard rail against a wrong turn, not a sandbox.
|
|
60
|
+
- **Ask before anything irreversible.** The agent is instructed to confirm with you before it posts, sends, buys, deletes or changes settings. This is guidance to the agent, not a technical lock, so watch the window when it matters. It stays visible by default for that reason.
|
|
61
|
+
|
|
62
|
+
## Development
|
|
63
|
+
|
|
64
|
+
Work is test-first: a failing test, then the code that makes it pass. `CLAUDE.md` has the rules, and `docs/DESIGN.md` records each design decision and why.
|
|
65
|
+
|
|
66
|
+
To run from source, clone the repository, then `npm install`, `npm run build` and `npm link`, which puts your build on your `PATH` as `anyb`.
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
npx playwright install chromium # once, for the browser tests
|
|
70
|
+
npm test # everything
|
|
71
|
+
npm run test:unit # no browser
|
|
72
|
+
npm run test:browser # headless Chromium against local pages only
|
|
73
|
+
npm run typecheck
|
|
74
|
+
npm run format:check
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Settings: `ANYBROWSER_HOME` moves the state directory from `~/.anybrowser`. `ANYBROWSER_IDLE_MS`, or `idleMinutes` in `config.json`, changes the idle timeout, and `off` disables it. `ANYBROWSER_HEADLESS=1` forces every browser headless.
|
|
78
|
+
|
|
79
|
+
MIT licensed.
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The allowed-sites list: validation and matching. Each entry is
|
|
3
|
+
* `[scheme://]host[:port]`:
|
|
4
|
+
*
|
|
5
|
+
* - `host` is a domain name, an IPv4 address or a bracketed IPv6 literal. A
|
|
6
|
+
* leading `*.` (names only) means any subdomain, NOT the apex: `example.com`
|
|
7
|
+
* and `*.example.com` are separate entries, on purpose. A list is a safety
|
|
8
|
+
* boundary, so nothing is allowed that was not written down.
|
|
9
|
+
* - `scheme` is `http` or `https`; without it an entry covers both.
|
|
10
|
+
* - `port` is a number from 1 to 65535 or `*` for any; without it an entry
|
|
11
|
+
* covers the scheme's default port only (80, 443).
|
|
12
|
+
*/
|
|
13
|
+
const HINT = "pass host patterns like example.com or *.example.com";
|
|
14
|
+
/** A domain label: letters or digits (any script) with inner hyphens. */
|
|
15
|
+
const LABEL = /^[\p{L}\p{N}](?:[\p{L}\p{N}-]*[\p{L}\p{N}])?$/u;
|
|
16
|
+
function fail(error) {
|
|
17
|
+
return { ok: false, error: `${error}; ${HINT}` };
|
|
18
|
+
}
|
|
19
|
+
/** Validates and normalizes one entry (see the module comment for the forms). */
|
|
20
|
+
export function parseAllowedOrigin(raw) {
|
|
21
|
+
const text = raw.trim();
|
|
22
|
+
if (text.length === 0) {
|
|
23
|
+
return fail("allowed-sites entry is empty");
|
|
24
|
+
}
|
|
25
|
+
if (/\s/.test(text)) {
|
|
26
|
+
return fail(`allowed-sites entry '${text}' contains whitespace`);
|
|
27
|
+
}
|
|
28
|
+
let rest = text;
|
|
29
|
+
let scheme = null;
|
|
30
|
+
const schemeMatch = /^([A-Za-z][A-Za-z0-9+.-]*):\/\//.exec(rest);
|
|
31
|
+
if (schemeMatch) {
|
|
32
|
+
const name = schemeMatch[1].toLowerCase();
|
|
33
|
+
if (name !== "http" && name !== "https") {
|
|
34
|
+
return fail(`allowed-sites entry '${text}' has an unsupported scheme '${name}' (only http and https)`);
|
|
35
|
+
}
|
|
36
|
+
scheme = name;
|
|
37
|
+
rest = rest.slice(schemeMatch[0].length);
|
|
38
|
+
}
|
|
39
|
+
if (/[/?#]/.test(rest)) {
|
|
40
|
+
return fail(`allowed-sites entry '${text}' has a path`);
|
|
41
|
+
}
|
|
42
|
+
const wildcard = rest.startsWith("*.");
|
|
43
|
+
if (wildcard) {
|
|
44
|
+
rest = rest.slice(2);
|
|
45
|
+
}
|
|
46
|
+
let host;
|
|
47
|
+
let portText;
|
|
48
|
+
if (rest.startsWith("[")) {
|
|
49
|
+
const close = rest.indexOf("]");
|
|
50
|
+
if (close === -1 ||
|
|
51
|
+
(close !== rest.length - 1 && rest[close + 1] !== ":")) {
|
|
52
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
53
|
+
}
|
|
54
|
+
host = rest.slice(0, close + 1);
|
|
55
|
+
portText = close === rest.length - 1 ? undefined : rest.slice(close + 2);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const parts = rest.split(":");
|
|
59
|
+
if (parts.length > 2) {
|
|
60
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
61
|
+
}
|
|
62
|
+
host = parts[0];
|
|
63
|
+
portText = parts[1];
|
|
64
|
+
}
|
|
65
|
+
let port = null;
|
|
66
|
+
if (portText !== undefined) {
|
|
67
|
+
if (portText === "*") {
|
|
68
|
+
port = "*";
|
|
69
|
+
}
|
|
70
|
+
else if (/^\d{1,5}$/.test(portText)) {
|
|
71
|
+
port = Number(portText);
|
|
72
|
+
if (port < 1 || port > 65535) {
|
|
73
|
+
return fail(`allowed-sites entry '${text}' has a port outside 1-65535`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const isIpv6 = host.startsWith("[");
|
|
81
|
+
if (wildcard && host === "") {
|
|
82
|
+
return fail(`allowed-sites entry '${text}' needs at least two labels after '*.'`);
|
|
83
|
+
}
|
|
84
|
+
if (!isIpv6 && !host.split(".").every((label) => LABEL.test(label))) {
|
|
85
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
86
|
+
}
|
|
87
|
+
let hostname;
|
|
88
|
+
try {
|
|
89
|
+
hostname = new URL(`http://${host}`).hostname;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
93
|
+
}
|
|
94
|
+
const isAddress = isIpv6 || /^\d+\.\d+\.\d+\.\d+$/.test(hostname);
|
|
95
|
+
if (wildcard && isAddress) {
|
|
96
|
+
return fail(`allowed-sites entry '${text}' is not a host pattern`);
|
|
97
|
+
}
|
|
98
|
+
if (!isAddress) {
|
|
99
|
+
const labels = hostname.split(".").length;
|
|
100
|
+
if (wildcard && labels < 2) {
|
|
101
|
+
return fail(`allowed-sites entry '${text}' needs at least two labels after '*.' (a wildcard over a whole top-level domain allows far too much)`);
|
|
102
|
+
}
|
|
103
|
+
if (!wildcard && labels < 2 && hostname !== "localhost") {
|
|
104
|
+
return fail(`allowed-sites entry '${text}' needs at least two labels (localhost is the one exception)`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const normalized = (scheme ? `${scheme}://` : "") +
|
|
108
|
+
(wildcard ? "*." : "") +
|
|
109
|
+
hostname +
|
|
110
|
+
(port === null ? "" : `:${port}`);
|
|
111
|
+
return {
|
|
112
|
+
ok: true,
|
|
113
|
+
entry: { scheme, wildcard, host: hostname, port },
|
|
114
|
+
normalized,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
const DEFAULT_PORTS = { http: 80, https: 443 };
|
|
118
|
+
const parsedCache = new Map();
|
|
119
|
+
function parsedEntry(raw) {
|
|
120
|
+
let cached = parsedCache.get(raw);
|
|
121
|
+
if (cached === undefined) {
|
|
122
|
+
const result = parseAllowedOrigin(raw);
|
|
123
|
+
cached = result.ok ? result.entry : null;
|
|
124
|
+
parsedCache.set(raw, cached);
|
|
125
|
+
}
|
|
126
|
+
return cached;
|
|
127
|
+
}
|
|
128
|
+
/** Whether `url` may be requested by a profile with this list. Non-network schemes (data, blob, about) never leave the browser and are allowed; any other scheme (file, ftp, ...) is not. */
|
|
129
|
+
export function isUrlAllowed(url, entries) {
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = new URL(url);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
const scheme = parsed.protocol.slice(0, -1);
|
|
138
|
+
if (scheme === "data" || scheme === "blob" || scheme === "about") {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const effectiveScheme = scheme === "ws" ? "http" : scheme === "wss" ? "https" : scheme;
|
|
142
|
+
if (effectiveScheme !== "http" && effectiveScheme !== "https") {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
const port = parsed.port === "" ? DEFAULT_PORTS[effectiveScheme] : Number(parsed.port);
|
|
146
|
+
return entries.some((raw) => {
|
|
147
|
+
const entry = parsedEntry(raw);
|
|
148
|
+
if (!entry) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
if (entry.scheme !== null && entry.scheme !== effectiveScheme) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const hostMatches = entry.wildcard
|
|
155
|
+
? parsed.hostname.endsWith(`.${entry.host}`)
|
|
156
|
+
: parsed.hostname === entry.host;
|
|
157
|
+
if (!hostMatches) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return (entry.port === "*" ||
|
|
161
|
+
port === (entry.port ?? DEFAULT_PORTS[effectiveScheme]));
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=allowedSites.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowedSites.js","sourceRoot":"","sources":["../src/allowedSites.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const MAX_ENTRIES = 300;
|
|
2
|
+
/** `host:port` of the destination a CONNECT refusal or a ws/https request URL names. */
|
|
3
|
+
function authorityOf(url) {
|
|
4
|
+
try {
|
|
5
|
+
const parsed = new URL(url);
|
|
6
|
+
const defaults = {
|
|
7
|
+
"https:": "443",
|
|
8
|
+
"wss:": "443",
|
|
9
|
+
"http:": "80",
|
|
10
|
+
"ws:": "80",
|
|
11
|
+
};
|
|
12
|
+
const port = parsed.port || defaults[parsed.protocol];
|
|
13
|
+
return port === undefined ? null : `${parsed.hostname}:${port}`;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function hrefOf(url) {
|
|
20
|
+
try {
|
|
21
|
+
return new URL(url).href;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class BlockLedger {
|
|
28
|
+
entries = [];
|
|
29
|
+
seq = 0;
|
|
30
|
+
changed = new Set();
|
|
31
|
+
/** A cursor: everything refused after this call is "new". */
|
|
32
|
+
mark() {
|
|
33
|
+
return this.seq;
|
|
34
|
+
}
|
|
35
|
+
/** Records one refusal made by the proxy. */
|
|
36
|
+
record(refusal) {
|
|
37
|
+
this.entries.push({ seq: ++this.seq, refusal });
|
|
38
|
+
this.entries = this.entries.slice(-MAX_ENTRIES);
|
|
39
|
+
this.notify();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A failed request whose URL names a destination the proxy refused a
|
|
43
|
+
* CONNECT to (https, wss, or a ws tunnel). Claims the newest unclaimed
|
|
44
|
+
* refusal for it; returns false when there is none, i.e. the failure was
|
|
45
|
+
* not a refusal.
|
|
46
|
+
*/
|
|
47
|
+
claimConnect(url, attribution) {
|
|
48
|
+
const wanted = authorityOf(url);
|
|
49
|
+
if (wanted === null) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const entry = this.newestUnclaimed((e) => e.refusal.kind === "connect" && authorityOf(e.refusal.url) === wanted);
|
|
53
|
+
if (!entry) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
entry.claim = attribution(entry.refusal.at);
|
|
57
|
+
this.notify();
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
/** A response carrying the proxy's block header for `url`; claims the newest matching plain-HTTP refusal. */
|
|
61
|
+
claimHttp(url, attribution) {
|
|
62
|
+
const wanted = hrefOf(url);
|
|
63
|
+
const entry = this.newestUnclaimed((e) => e.refusal.kind === "http" && hrefOf(e.refusal.url) === wanted);
|
|
64
|
+
if (!entry) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
entry.claim = attribution(entry.refusal.at);
|
|
68
|
+
this.notify();
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The NEWEST unclaimed refusal matching `matches`. An event trails its own
|
|
73
|
+
* refusal by moments, so the newest match is the one it describes; older
|
|
74
|
+
* unclaimed ones (a blocked WebSocket or the browser's own traffic emits no
|
|
75
|
+
* event, ever) are stale and must not shadow it.
|
|
76
|
+
*/
|
|
77
|
+
newestUnclaimed(matches) {
|
|
78
|
+
return this.entries.findLast((e) => e.claim === undefined && matches(e));
|
|
79
|
+
}
|
|
80
|
+
/** Entries refused after `mark`, oldest first. */
|
|
81
|
+
since(mark) {
|
|
82
|
+
return this.entries.filter((e) => e.seq > mark);
|
|
83
|
+
}
|
|
84
|
+
/** Resolves once every refusal after `mark` has been claimed by an event, or after `timeoutMs`. */
|
|
85
|
+
async whenClaimed(mark, timeoutMs) {
|
|
86
|
+
const pending = () => this.since(mark).some((e) => e.claim === undefined);
|
|
87
|
+
if (!pending()) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await new Promise((resolve) => {
|
|
91
|
+
const done = () => {
|
|
92
|
+
clearTimeout(timer);
|
|
93
|
+
this.changed.delete(check);
|
|
94
|
+
resolve();
|
|
95
|
+
};
|
|
96
|
+
const check = () => {
|
|
97
|
+
if (!pending()) {
|
|
98
|
+
done();
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
const timer = setTimeout(done, timeoutMs);
|
|
102
|
+
this.changed.add(check);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
notify() {
|
|
106
|
+
for (const listener of [...this.changed]) {
|
|
107
|
+
listener();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=blockLedger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blockLedger.js","sourceRoot":"","sources":["../src/blockLedger.ts"],"names":[],"mappings":"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"}
|