mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-09-19 10:59:28 -04:00
webapi: window.stop()
window.stop() is less destructive than other mechanisms we have. For one, it seems largely isolated to pending or inflight HTTP requests. For anther, it keeps the page intact. To achieve this, HttpClient gains an `cancelRequests` which is a gentler version of `abortOwner`. It cancels inflight/pending HTTP requests, which results in error callbacks (not shutdown callbacks) firing. Just like https://github.com/lightpanda-io/browser/pull/3189 I ran into the problem that I couldn't distinguish between an HTTP request that was canceled because of user-action (e.g. calling window.stop(), or xhr.abort()) and an HTTP request that was internally aborted. These now have distinct errors/flows so that we can present the correct state. Most places that aborted now all transfer.cancel() which results in a distinct `error.TransferCanceled` (some places still abort -> `error.Abort`). It should be possible to revisit 3189 now. The CDP "Page.stopLoading" now hooks into this new behavior. Fixes https://github.com/lightpanda-io/browser/issues/3351
This commit is contained in:
17 files changed
+311
-25
No files matched your search
@@ -524,6 +524,25 @@ pub fn abortRequests(_: *Client, owner: *Owner) void {
|
||||
// the owner itself is freed, no orphan transfer points at it.
|
||||
}
|
||||
|
||||
// Unlike abortOwner, the owner intends to stay alive (e.g. window.stop()). Our
|
||||
// shutdown has to be a little nicer, i.e. firing error callbacks.
|
||||
pub fn cancelRequests(self: *Client, owner: *Owner) void {
|
||||
const last_id = self.next_request_id;
|
||||
while (true) {
|
||||
// Don't walk the linked list "normally", instead keep poping the head...
|
||||
// cancel() can run JS which can mutate / invalidate nodes
|
||||
var n = owner.transfers.first;
|
||||
|
||||
const target = while (n) |node| : (n = node.next) {
|
||||
const t: *Transfer = @fieldParentPtr("owner_node", node);
|
||||
if (t.id <= last_id and !t._outcome_delivered and !t.isCompleteAwaitingDispatch()) {
|
||||
break t;
|
||||
}
|
||||
} else return;
|
||||
target.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
// Point-in-time snapshot of the client's outstanding work
|
||||
pub const Activity = struct {
|
||||
// in-flight + buffered-awaiting-dispatch + parked-for-CDP-interception
|
||||
@@ -2386,13 +2405,24 @@ pub const Transfer = struct {
|
||||
self.client.graveyard.append(&self._node);
|
||||
}
|
||||
|
||||
// Cancel this transfer with `err`. Fires error_callback once (latched
|
||||
// The consumer doesn't want this transfer. Reserved for things like
|
||||
// xhr.abort(), an AbortController, a worker.terminate(), i.e. mostly JS
|
||||
// driven (though, potentially indirectly).
|
||||
// HeaderResult.abort on the other hand is more internal, e.g. ScriptManager
|
||||
// getting a non-2xx response code.
|
||||
// cancel results in a `error.TransferCanceled` and abort results in an
|
||||
// error.Abort. Various places need to make the distinction between the two.
|
||||
pub fn cancel(self: *Transfer) void {
|
||||
self.abort(error.TransferCanceled);
|
||||
}
|
||||
|
||||
// Fail this transfer with `err`. Fires error_callback once (latched
|
||||
// via _notified_fail), then either deinits synchronously or, if
|
||||
// deliver() is running our callbacks, detaches and lets deliver()
|
||||
// deinit when its loop exits.
|
||||
//
|
||||
// This is the ONE entry point external callers should use to cancel
|
||||
// a transfer. Don't reach for kill() or requestFailed() directly —
|
||||
// abort() or cancel() are the only entry points external callers should use
|
||||
// to end a transfer. Don't reach for kill() or requestFailed() directly —
|
||||
// they're internal helpers.
|
||||
pub fn abort(self: *Transfer, err: anyerror) void {
|
||||
// error_callback can run JS that tears this transfer down again
|
||||
@@ -2406,6 +2436,37 @@ pub const Transfer = struct {
|
||||
self.detachOrDeinit();
|
||||
}
|
||||
|
||||
// take whatever we have, and deliver it.
|
||||
pub fn finishEarly(self: *Transfer) bool {
|
||||
if (self.state != .inflight or self.req.streaming or self._cache_intent == .revalidate) {
|
||||
return false;
|
||||
}
|
||||
const conn = self._conn orelse return false;
|
||||
const status = conn.getResponseCode() catch 0;
|
||||
if (status == 0 or (status >= 300 and status < 400)) {
|
||||
// we don't have a status code yet, or it's a 3xx. Nothing to deliver.
|
||||
return false;
|
||||
}
|
||||
|
||||
self.materializeResponse(conn, .{}) catch return false;
|
||||
self.client.removeConn(conn);
|
||||
self._conn = null;
|
||||
self._content_length = self.res.buffer.items.len;
|
||||
self.bufferEvents(self.res.buffer.items) catch |err| {
|
||||
self.failAsync(err);
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
// We have our events, and the last event is .done
|
||||
fn isCompleteAwaitingDispatch(self: *const Transfer) bool {
|
||||
if (self.state != .buffered) {
|
||||
return false;
|
||||
}
|
||||
const events = self._events.items;
|
||||
return events.len > 0 and events[events.len - 1] == .done;
|
||||
}
|
||||
|
||||
// A pipeline entry point failed. The pipeline still owns the transfer in
|
||||
// two states: .created and .inflight. In all other states, the owner
|
||||
// delivers the failure.
|
||||
@@ -2526,7 +2587,9 @@ pub const Transfer = struct {
|
||||
// A consumer callback failed mid-delivery: latch the failure, notify,
|
||||
// free. Only called from deliver().
|
||||
fn failDelivery(self: *Transfer, err: anyerror) void {
|
||||
log.err(.http, "delivery callback", .{ .err = err, .req = self });
|
||||
if (err != error.TransferCanceled) {
|
||||
log.err(.http, "delivery callback", .{ .err = err, .req = self });
|
||||
}
|
||||
self.requestFailed(err);
|
||||
self.finishDelivery();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user