@remcp/runtime 0.2.185 → 0.2.187
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/extended/browser-read.mjs +34 -26
- package/src/extended/browser-session.mjs +17 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.186 — 2026-10-26
|
|
4
|
+
|
|
5
|
+
- Return from same-document CDP navigation promptly when Page.navigate provides no new loaderId instead of awaiting a full load event that never fires.
|
|
6
|
+
- Cancel and remove per-navigation CDP event waiters when aborted, completed or disconnected; retain the genuine-document navigation wait and existing browser safety policy.
|
|
7
|
+
- Add deterministic cancellation coverage and real isolated Chromium navigation regression tests.
|
|
8
|
+
|
|
3
9
|
## 0.2.185 — 2026-10-26
|
|
4
10
|
|
|
5
11
|
**Make read-only browser discovery more focused and patch application fail safely.**
|
package/package.json
CHANGED
|
@@ -76,35 +76,43 @@ export async function browserNavigate(args = {}) {
|
|
|
76
76
|
return withTarget(args, async (session, target) => {
|
|
77
77
|
await session.send('Page.enable');
|
|
78
78
|
const timeoutMs = clamp(args.timeout_ms, 15_000, 500, 120_000);
|
|
79
|
+
const loadWait = new AbortController();
|
|
79
80
|
const loaded = args.wait === false || ['back','forward'].includes(action)
|
|
80
81
|
? null
|
|
81
|
-
: session.waitFor('Page.loadEventFired', timeoutMs).catch(() => null);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
82
|
+
: session.waitFor('Page.loadEventFired', timeoutMs, loadWait.signal).catch(() => null);
|
|
83
|
+
try {
|
|
84
|
+
let frameId = null;
|
|
85
|
+
let errorText = null;
|
|
86
|
+
let historyUrl = null;
|
|
87
|
+
if (action === 'url') {
|
|
88
|
+
const result = await session.send('Page.navigate', { url });
|
|
89
|
+
requireSuccessfulBrowserNavigation(result);
|
|
90
|
+
// Same-document navigation (fragment or pushState) has no loaderId
|
|
91
|
+
// and does not emit Page.loadEventFired.
|
|
92
|
+
if (!result.loaderId) loadWait.abort();
|
|
93
|
+
frameId = result.frameId || null;
|
|
94
|
+
errorText = result.errorText || null;
|
|
95
|
+
} else if (action === 'reload') {
|
|
96
|
+
await session.send('Page.reload', { ignoreCache: Boolean(args.ignore_cache) });
|
|
97
|
+
} else {
|
|
98
|
+
const history = await session.send('Page.getNavigationHistory');
|
|
99
|
+
const current = Number(history.currentIndex || 0);
|
|
100
|
+
const nextIndex = action === 'back' ? current - 1 : current + 1;
|
|
101
|
+
const entry = Array.isArray(history.entries) ? history.entries[nextIndex] : null;
|
|
102
|
+
if (!entry) throw new Error(`Cannot navigate ${action}: no history entry is available`);
|
|
103
|
+
historyUrl = entry.url ? await assertResolvedPageUrl(entry.url, 'history URL') : null;
|
|
104
|
+
await session.send('Page.navigateToHistoryEntry', { entryId: entry.id });
|
|
105
|
+
}
|
|
106
|
+
if (loaded) await loaded;
|
|
107
|
+
if (args.wait !== false && ['back','forward'].includes(action)) {
|
|
108
|
+
await waitForHistoryReady(session, historyUrl, timeoutMs);
|
|
109
|
+
}
|
|
110
|
+
const currentUrl = await evaluate(session, 'location.href').catch(() => url || target.url || null);
|
|
111
|
+
if (currentUrl) await assertResolvedPageUrl(currentUrl);
|
|
112
|
+
return jsonResult({ target_id: target.id, action, url: currentUrl, frame_id: frameId, error_text: errorText });
|
|
113
|
+
} finally {
|
|
114
|
+
loadWait.abort();
|
|
104
115
|
}
|
|
105
|
-
const currentUrl = await evaluate(session, 'location.href').catch(() => url || target.url || null);
|
|
106
|
-
if (currentUrl) await assertResolvedPageUrl(currentUrl);
|
|
107
|
-
return jsonResult({ target_id: target.id, action, url: currentUrl, frame_id: frameId, error_text: errorText });
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
|
|
@@ -119,18 +119,31 @@ export class CdpSession {
|
|
|
119
119
|
});
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
waitFor(method, timeoutMs = this.timeoutMs) {
|
|
122
|
+
waitFor(method, timeoutMs = this.timeoutMs, signal = null) {
|
|
123
123
|
return new Promise((resolve, reject) => {
|
|
124
|
+
if (signal?.aborted) { reject(new Error(`${method} aborted`)); return; }
|
|
124
125
|
const listeners = this.waiters.get(method) || [];
|
|
125
|
-
const
|
|
126
|
-
listener.timer = setTimeout(() => {
|
|
126
|
+
const remove = () => {
|
|
127
127
|
const current = this.waiters.get(method) || [];
|
|
128
128
|
const next = current.filter(item => item !== listener);
|
|
129
129
|
if (next.length) this.waiters.set(method, next); else this.waiters.delete(method);
|
|
130
|
-
|
|
130
|
+
clearTimeout(listener.timer);
|
|
131
|
+
signal?.removeEventListener('abort', abort);
|
|
132
|
+
};
|
|
133
|
+
const listener = {
|
|
134
|
+
resolve:value => { signal?.removeEventListener('abort', abort); resolve(value); },
|
|
135
|
+
reject:error => { signal?.removeEventListener('abort', abort); reject(error); },
|
|
136
|
+
timer:null,
|
|
137
|
+
};
|
|
138
|
+
const abort = () => { remove(); listener.reject(new Error(`${method} aborted`)); };
|
|
139
|
+
listener.timer = setTimeout(() => {
|
|
140
|
+
remove();
|
|
141
|
+
listener.reject(new Error(`${method} timed out`));
|
|
131
142
|
}, timeoutMs);
|
|
132
143
|
listeners.push(listener);
|
|
133
144
|
this.waiters.set(method, listeners);
|
|
145
|
+
signal?.addEventListener('abort', abort, { once:true });
|
|
146
|
+
if (signal?.aborted) abort();
|
|
134
147
|
});
|
|
135
148
|
}
|
|
136
149
|
|