From 4e4130b6c27b09903b0929a8fa544c42eafa5977 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Fri, 25 Jul 2025 13:44:01 +0100 Subject: [PATCH 1/4] Add a mechanism to fetch to track progress --- fetch.bs | 195 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 191 insertions(+), 4 deletions(-) diff --git a/fetch.bs b/fetch.bs index 4d01e315b..26026884c 100755 --- a/fetch.bs +++ b/fetch.bs @@ -114,6 +114,7 @@ urlPrefix:https://tc39.es/ecma262/#;type:dfn;spec:ecma-262 @@ -8512,7 +8513,18 @@ otherwise false.
 partial interface mixin WindowOrWorkerGlobalScope {
-  [NewObject] Promise<Response> fetch(RequestInfo input, optional RequestInit init = {});
+  [NewObject] Promise<Response> fetch(RequestInfo input, optional FetchInit init = {});
+};
+
+dictionary FetchInit : RequestInit {
+  FetchMonitorCallback monitor;
+};
+
+callback FetchMonitorCallback = undefined (FetchMonitor requestMonitor, FetchMonitor responseMonitor);
+
+[Exposed=(Window,Worker)]
+interface FetchMonitor : EventTarget {
+  attribute EventHandler onprogress;
 };
 
@@ -8584,10 +8596,65 @@ method steps are: https://github.com/whatwg/dom/issues/1031#issuecomment-1233206400 --> +
  • Let hasUploadListeners be false. + +

  • Let requestMonitor be null. + +

  • Let responseMonitor be null. + +

  • +

    If init["{{FetchInit/monitor}}"] exists, then: + +

      +
    1. Let monitorCallback be init["{{FetchInit/monitor}}"]. + +

    2. Set requestMonitor to a {{FetchMonitor}}. + +

    3. Set responseMonitor to a {{FetchMonitor}}. + +

    4. Let args be « requestMonitor, responseMonitor ». + +

    5. [=invoke|Invoke=] monitorCallback with args + and "rethrow". If this throws an exception, reject p with it + and return p. + +

    6. If one or more progress event listeners were added to + requestMonitor, then set hasUploadListeners to true. +

    + +
  • Let requestBodyTransmitted be 0. + +

  • Let requestBodyLength be request's body's + length, if request's body is non-null; + otherwise 0. + +

  • Assert: requestBodyLength is an integer. + +

  • +

    Let processRequestBodyChunkLength, given a bytesLength, be these steps: + +

      +
    1. Increase requestBodyTransmitted by bytesLength. + +

    2. If not roughly 50ms has passed since these steps were last invoked, then return. + +

    3. If hasUploadListeners is true, then fire a progress event named + progress at requestMonitor with requestBodyTransmitted + and requestBodyLength. +

    +
  • -

    Set controller to the result of calling fetch given - request and processResponse given response being - these steps: +

    Let processRequestEndOfBody be these steps: + +

      +
    1. If hasUploadListeners is false, then return. + +

    2. Fire a progress event named progress at requestMonitor + with requestBodyTransmitted and requestBodyLength. +

    + +
  • +

    Let processResponse given a response be these steps:

    1. If locallyAborted is true, then abort these steps. @@ -8615,10 +8682,17 @@ method steps are:

    2. Resolve p with responseObject.

    +
  • Set controller to the result of calling fetch given + request with processResponse set to processResponse, + processRequestBodyChunkLength set to processRequestBodyChunkLength, + and processRequestEndOfBody set to processRequestEndOfBody. +

  • Return p. +TEMPORARY progress +

    To abort a fetch() call with a promise, request, responseObject, and an error: @@ -9132,6 +9206,119 @@ done only by navigations). The fetch controller is also used to process the next manual redirect for requests with redirect mode set to "manual". +

    Interface {{ProgressEvent}}

    + +
    +[Exposed=(Window,Worker)]
    +interface ProgressEvent : Event {
    +  constructor(DOMString type, optional ProgressEventInit eventInitDict = {});
    +
    +  readonly attribute boolean lengthComputable;
    +  readonly attribute double loaded;
    +  readonly attribute double total;
    +};
    +
    +dictionary ProgressEventInit : EventInit {
    +  boolean lengthComputable = false;
    +  double loaded = 0;
    +  double total = 0;
    +};
    +
    + +

    Events using the {{ProgressEvent}} interface indicate some kind of progression. + +

    The +lengthComputable, +loaded, and +total +getter steps are to return the value they were initialized to. + + +

    Firing events using the {{ProgressEvent}} interface

    + +

    To fire a progress event named e at +target, given transmitted and length, means to fire an event +named e at target, using {{ProgressEvent}}, with the {{ProgressEvent/loaded}} +attribute initialized to transmitted, and if length is not 0, with the +{{ProgressEvent/lengthComputable}} attribute initialized to true and the {{ProgressEvent/total}} +attribute initialized to length. + + +

    Suggested names for events using the {{ProgressEvent}} interface

    + +

    This section is non-normative. + +

    The suggested {{Event/type}} +attribute values for use with +events using the +{{ProgressEvent}} interface are summarized in the table below. +Specification editors are free to tune the details to their specific +scenarios, though are strongly encouraged to discuss their usage with the +WHATWG community to ensure input from people familiar with the subject. + + + + + + + + + + + +
    {{Event/type}} attribute value + Description + Times + When +
    loadstart + Progress has begun. + Once. + First. +
    progress + In progress. + Once or more. + After loadstart has been + dispatched. +
    error + Progression failed. + Zero or once (mutually exclusive). + After the last progress has + been + dispatched. +
    abort + Progression is terminated. +
    timeout + Progression is terminated due to preset time expiring. +
    load + Progression is successful. +
    loadend + Progress has stopped. + Once. + After one of error, abort, + timeout or load has been + dispatched. +
    + +

    The error, abort, timeout, and +load event types are mutually exclusive. + +

    Throughout the web platform the error, abort, +timeout and load event types have +their {{Event/bubbles}} and {{Event/cancelable}} +attributes initialized to false, so it is suggested that for consistency all +events using the +{{ProgressEvent}} interface do the same. + + +

    Security considerations

    + +

    For cross-origin requests some kind of opt-in, e.g., the +CORS protocol, has to be used before events using the +{{ProgressEvent}} interface are +dispatched +as information (e.g., size) would be revealed that cannot be obtained +otherwise. +

    Acknowledgments

    From 047e82a10b95bdde06138b7555e108b192084017 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 9 Oct 2025 16:03:09 +0100 Subject: [PATCH 2/4] Single monitor with requestprogress and responseprogress names --- fetch.bs | 29 +++++++++++++---------------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/fetch.bs b/fetch.bs index 26026884c..663ba80b7 100755 --- a/fetch.bs +++ b/fetch.bs @@ -8520,11 +8520,12 @@ dictionary FetchInit : RequestInit { FetchMonitorCallback monitor; }; -callback FetchMonitorCallback = undefined (FetchMonitor requestMonitor, FetchMonitor responseMonitor); +callback FetchMonitorCallback = undefined (FetchMonitor monitor); [Exposed=(Window,Worker)] interface FetchMonitor : EventTarget { - attribute EventHandler onprogress; + attribute EventHandler onrequestprogress; + attribute EventHandler onresponseprogress; }; @@ -8598,9 +8599,7 @@ method steps are:
  • Let hasUploadListeners be false. -

  • Let requestMonitor be null. - -

  • Let responseMonitor be null. +

  • Let monitor be null.

  • If init["{{FetchInit/monitor}}"] exists, then: @@ -8608,18 +8607,16 @@ method steps are:

    1. Let monitorCallback be init["{{FetchInit/monitor}}"]. -

    2. Set requestMonitor to a {{FetchMonitor}}. - -

    3. Set responseMonitor to a {{FetchMonitor}}. +

    4. Set monitor to a {{FetchMonitor}}. -

    5. Let args be « requestMonitor, responseMonitor ». +

    6. Let args be « monitor ».

    7. [=invoke|Invoke=] monitorCallback with args and "rethrow". If this throws an exception, reject p with it and return p. -

    8. If one or more progress event listeners were added to - requestMonitor, then set hasUploadListeners to true. +

    9. If one or more requestprogress event listeners were added to + monitor, then set hasUploadListeners to true.

  • Let requestBodyTransmitted be 0. @@ -8639,7 +8636,7 @@ method steps are:

  • If not roughly 50ms has passed since these steps were last invoked, then return.

  • If hasUploadListeners is true, then fire a progress event named - progress at requestMonitor with requestBodyTransmitted + requestprogress at monitor with requestBodyTransmitted and requestBodyLength. @@ -8649,7 +8646,7 @@ method steps are:

    1. If hasUploadListeners is false, then return. -

    2. Fire a progress event named progress at requestMonitor +

    3. Fire a progress event named requestprogress at monitor with requestBodyTransmitted and requestBodyLength.

    @@ -8691,7 +8688,7 @@ method steps are: -TEMPORARY progress +TEMPORARY requestprogress

    To abort a fetch() call @@ -9269,8 +9266,8 @@ WHATWG community to ensure input from people familiar with the subject. Once. First. - progress - In progress. + requestprogress + In progress request. Once or more. After loadstart has been dispatched. From c03933dc05c05ed3ea1b7115335e23031e25a3b2 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 9 Oct 2025 16:44:46 +0100 Subject: [PATCH 3/4] Remove hasUploadListeners flag --- fetch.bs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/fetch.bs b/fetch.bs index 663ba80b7..9ea782ba3 100755 --- a/fetch.bs +++ b/fetch.bs @@ -8597,8 +8597,6 @@ method steps are: https://github.com/whatwg/dom/issues/1031#issuecomment-1233206400 --> -

  • Let hasUploadListeners be false. -

  • Let monitor be null.

  • @@ -8614,9 +8612,6 @@ method steps are:
  • [=invoke|Invoke=] monitorCallback with args and "rethrow". If this throws an exception, reject p with it and return p. - -

  • If one or more requestprogress event listeners were added to - monitor, then set hasUploadListeners to true.

  • Let requestBodyTransmitted be 0. @@ -8635,7 +8630,7 @@ method steps are:

  • If not roughly 50ms has passed since these steps were last invoked, then return. -

  • If hasUploadListeners is true, then fire a progress event named +

  • If monitor is not null, then fire a progress event named requestprogress at monitor with requestBodyTransmitted and requestBodyLength. @@ -8644,7 +8639,7 @@ method steps are:

    Let processRequestEndOfBody be these steps:

      -
    1. If hasUploadListeners is false, then return. +

    2. If monitor is null, then return.

    3. Fire a progress event named requestprogress at monitor with requestBodyTransmitted and requestBodyLength. From cd935acf3e9eece6317cf40758efb3ce8e828133 Mon Sep 17 00:00:00 2001 From: Luke Warlow Date: Thu, 9 Oct 2025 17:17:13 +0100 Subject: [PATCH 4/4] Add details to FetchMonitor interface Still using a ProgressEvent for now, but this will be switched to Event --- fetch.bs | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/fetch.bs b/fetch.bs index 9ea782ba3..490cfdf3d 100755 --- a/fetch.bs +++ b/fetch.bs @@ -115,6 +115,7 @@ urlPrefix:https://tc39.es/ecma262/#;type:dfn;spec:ecma-262

      @@ -8524,6 +8525,11 @@ callback FetchMonitorCallback = undefined (FetchMonitor monitor); [Exposed=(Window,Worker)] interface FetchMonitor : EventTarget { + readonly attribute double requestLoaded; + readonly attribute double responseLoaded; + readonly attribute double requestTotal; + readonly attribute double responseTotal; + attribute EventHandler onrequestprogress; attribute EventHandler onresponseprogress; }; @@ -8607,6 +8613,10 @@ method steps are:
    4. Set monitor to a {{FetchMonitor}}. +

    5. Set monitor's requestTotal to request's + body's length, if request's body + is non-null; otherwise 0. +

    6. Let args be « monitor ».

    7. [=invoke|Invoke=] monitorCallback with args @@ -8614,25 +8624,19 @@ method steps are: and return p.

    -
  • Let requestBodyTransmitted be 0. - -

  • Let requestBodyLength be request's body's - length, if request's body is non-null; - otherwise 0. - -

  • Assert: requestBodyLength is an integer. -

  • Let processRequestBodyChunkLength, given a bytesLength, be these steps:

      -
    1. Increase requestBodyTransmitted by bytesLength. +

    2. If monitor is null, then return. + +

    3. Increase monitor's requestLoaded by bytesLength.

    4. If not roughly 50ms has passed since these steps were last invoked, then return. -

    5. If monitor is not null, then fire a progress event named - requestprogress at monitor with requestBodyTransmitted - and requestBodyLength. +

    6. Fire a progress event named + requestprogress at monitor with + monitor's requestLoaded and monitor's requestTotal.

  • @@ -8641,8 +8645,9 @@ method steps are:
    1. If monitor is null, then return. -

    2. Fire a progress event named requestprogress at monitor - with requestBodyTransmitted and requestBodyLength. +

    3. Fire a progress event named requestprogress at + monitor with monitor's requestLoaded and + monitor's requestTotal.

  • @@ -9260,6 +9265,12 @@ WHATWG community to ensure input from people familiar with the subject. Progress has begun. Once. First. + + progress + In progress. + Once or more. + After loadstart has been + dispatched. requestprogress In progress request.