<pre class='metadata'>
Title: Page Lifecycle
Group: WICG
Status: CG-DRAFT
Shortname: page-lifecycle
Level: 1
URL: https://wicg.github.io/page-lifecycle/
Editor: Shubhie Panicker, Google https://google.com, panicker@google.com
Editor: Domenic Denicola, Google https://google.com, d@domenic.me
Repository: wicg/page-lifecycle
Abstract: This document defines an API that supports browsers' ability to manage lifecycle of web pages.
Default Biblio Status: current
Markup Shorthands: markdown yes
</pre>

<pre class='link-defaults'>
spec:dom; type:interface; text:Document
spec: infra;
    type: dfn;
        text: list;
        for: set; text: append
        for: list; text: append
</pre>

<pre class='anchors'>
spec: HTML; urlPrefix: https://html.spec.whatwg.org/multipage/;
    type: attribute; text: persisted; for:PageTransitionEvent; url: browsing-the-web.html#dom-pagetransitionevent-persisted
    type: dfn; text: traverse the history; url: browsing-the-web.html#traverse-the-history
    type: dfn; text: list of the descendant browsing contexts; url: browsers.html#list-of-the-descendant-browsing-contexts
    type: dfn; text: browsing context; for: document; url: browsers.html#concept-document-bc
    type: dfn; text: readiness; for: document; url: dom.html#current-document-readiness
    type: dfn; text: iframe load in progress; url: iframe-embed-object.html#iframe-load-in-progress
    type: dfn; text: iframe load event steps; url: iframe-embed-object.html#iframe-load-event-steps
    type: dfn; text: Update the rendering; url: webappapis.html#update-the-rendering
    type: dfn; text: being rendered; url: rendering.html#being-rendered
    type: dfn; text: browsing context container; url: browsers.html#browsing-context-container
    type: dfn; text: media elements; url: media.html#media-element
    type: dfn; text: media pause; url: media.html#internal-pause-steps
    type: dfn; text: media play; url: media.html#internal-play-steps
    type: dfn; text: dedicated worker agent; url: webappapis.html#dedicated-worker-agent
    type: dfn; text: worklet agent; url: webappapis.html#worklet-agent
    type: dfn; text: posted message task source; url: web-messaging.html#posted-message-task-source
spec: CSS2; urlPrefix: https://www.w3.org/TR/CSS2/
    type: dfn; text: viewport; url: visuren.html#viewport
spec: IntersectionObserver; urlPrefix: https://w3c.github.io/IntersectionObserver/
    type: dfn; text: compute the intersection of a target element and the root; url: #calculate-intersection-rect-algo
spec: ECMA262; urlPrefix: https://tc39.github.io/ecma262/;
    type: dfn; text: blocked; url: #sec-forward-progress
    type: dfn; text: realm; url: #sec-code-realms
spec: CSS-Houdini; urlPrefix: https://drafts.css-houdini.org/worklets;
    type: dfn; text: owning document; for: worklet; url: #workletglobalscope-owner-document
spec: ServiceWorker; urlPrefix: https://w3c.github.io/ServiceWorker/
    type: dfn; text: create window client; url: #create-windowclient-algorithm
    type: dfn; text: create client; url: #create-client-algorithm
</pre>


Introduction {#intro}
=====================
With large numbers of web apps (and tabs) running, critical resources such as memory, CPU, battery, network, etc. easily get oversubscribed, leading to a bad end-user experience. Application lifecycle is a key way that modern OSs manage resources.

For a platform to support application lifecycle, it needs to:
    * provide developers with signals about transitions between the lifecycle states
    * provide lifecycle-compatible APIs that allow key capabilities to work even when the app is backgrounded or stopped.

This proposal attempts to define what the lifecycle of a web page is and add needed extensions to enable web applications to respond to two important lifecycle events commonly performed by user agents:
    * Tab discarding (for memory saving)
    * CPU suspension (for battery, data, CPU saving)

Page Lifecycle States {#sec-lifecycle-states}
==============================

This spec defines what the lifecycle of a web page is and adds extensions to enable web applications to respond to two important lifecycle events commonly performed by user agents:
    * CPU suspension (for conserving battery, data, CPU)
    * Tab discarding (for memory saving)

This spec formalizes two new lifecycle states to support the above:
    * Frozen: lifecycle state for CPU suspension. This means that the [=freeze steps=] algorithm was called on the {{Document}}'s [=document/browsing context=]. Normally HIDDEN pages will be [=/frozen=] to conserve resources.
    * Discarded: means that the [=discard=] algorithm was called on the {{Document}}'s [=document/browsing context=]. Normally [=/frozen=] frames will be moved to the discarded state to conserve resources.

TODO(panicker): Insert diagram

API {#sec-api}
=======================================

Page Lifecycle involves the following additions:

<pre class="idl">
    partial interface Document {
        attribute EventHandler onfreeze;
        attribute EventHandler onresume;
        readonly attribute boolean wasDiscarded;
    };
</pre>

The <dfn attribute for="Document">onfreeze</dfn> and <dfn attribute for="Document">onresume</dfn> attributes are [=event handler IDL attributes=] for the <code>freeze</code> and <code>resume</code> events, respectively.

The <dfn attribute for="Document">wasDiscarded</dfn> attribute's getter must return the value of this {{Document}}'s [=Document/discarded=] boolean.

    NOTE: these APIs are added on {{Document}}, instead of on {{Window}}, for consistency with the Page Visibility API; we expect these APIs to be used in tandem with that existing one. [[PAGE-VISIBILITY]]

    NOTE: In addition <a href="https://github.com/whatwg/html/issues/3378"><code>clientId</code> and <code>discardedClientId</code></a> will be added to {{Window}}, to support restoring view state when user revisits a discarded page, causing a reload. We expect those to be used by code that reacts to these events.

Usage example {#example}
------------------------

Example of handling freeze and resume:
<pre class="example highlight" highlight="js">
const prepareForFreeze = () => {
  // Close any open IndexedDB connections.
  // Release any web locks.
  // Stop timers or polling.
};

const reInitializeApp = () => {
  // Restore IndexedDB connections.
  // Re-acquire any needed web locks.
  // Restart timers or polling.
};

document.addEventListener('freeze', prepareForFreeze);
document.addEventListener('resume', reInitializeApp);
</pre>

Example of restoring view state after discard:
A user could have multiple tabs open for the same app & URL. If they are both in the background and are both discarded, then the app would need to distinguish between the two tabs to restore the correct state. clientId and lastClientId on the Window can be used for this purpose.
<pre class="example highlight" highlight="js">
// Persists state to IndexedDB, making sure to set the current value of
// `self.clientId` on the record, so it can be retrieved later using
// `getPersistedState()` (if the tab has to be reloaded after a discard).
const persistState = async (state) => {
  const record = {...state, cliendId: self.clientId};

  // Persist record to IndexedDB or SessionStorage....
}

// Retrieves the state record from IndexedDB based on the passed client ID.
const getPersistedState = async (clientId) => {
  // Lookup record in IndexedDB...
};

// If the tab was previously discarded, get the persisted state for the
// client ID of the discarded tab via `self.lastClientId`.
if (document.wasDiscarded) {
  getPersistedState(self.lastClientId);
}
</pre>

Feature Policies {#feature-policies}
=====================
Controlling the execution state of [=nested browsing contexts=] is desirable from a [=document/browsing context=] in order to control
the user experience. An application may desire that when a document is not [=being rendered=] or does not intersect the [=viewport=]
that all execution stops in the [=nested browsing context=] (including currently playing video, audio). Stopping all execution
can lead to an improvement in CPU utilization. To meet these needs, two feature policies are defined:
 * "<dfn export><code>execution-while-not-rendered</code></dfn>", which has a [=default allowlist=] of <code>*</code>
 * "<dfn export><code>execution-while-out-of-viewport</code></dfn>", which has a [=default allowlist=] of <code>*</code>

The "<a><code>execution-while-not-rendered</code></a>" policy controls whether
tasks should execute for [=nested browsing contexts=] whose [=browsing context container=] is not [=being rendered=].

The "<a><code>execution-while-out-of-viewport</code></a>" policy controls whether
tasks should execute for [=nested browsing contexts=] whose [=browsing context container=] does not intersect the [=viewport=] according
to [=compute the intersection of a target element and the root=].

[[#mod]] accomplishes this by placing the document (and its decendants) in a [=/frozen=] state when the following
conditions have been met:
 * the [=iframe load event steps=] have been run;
 * the policy is disabled for the document; and
 * the relevant policy condition applies (not rendered or scrolled out of view).

If these conditions aren't met, the document will be in the [=/unfrozen=] state.

<pre class="example highlight" highlight="html">
&lt;!-- The iframe will be frozen immediately after it is loaded. --&gt;

&lt;iframe allow="execution-while-not-rendered 'none'"
  src="subframe.html" style="display:none"&gt;&lt;/iframe&gt;
</pre>

<div algorithm>
    To run the <dfn export>update document frozenness steps</dfn> for a {{Document}} |document|:

    1. If |document|'s [=document/browsing context=] is not a [=nested browsing context=], then return.
    1. If |document|'s [=document/readiness=] is not "<code>complete</code>", then return.
    1. Let |element| be |document|'s [=document/browsing context=]'s [=browsing context container=].
    1. Let |frozenness| be false.
    1. Let |auto resume media| be false.
    1. If |document| is not [=allowed to use=] the "<a><code>execution-while-not-rendered</code></a>" feature, then:
        1. If |element| is not [=being rendered=],  set |frozenness| to true.
    1. Otherwise if |document| is not [=allowed to use=] the "<a><code>execution-while-out-of-viewport</code></a>" feature, then:
        1. If |element| does not intersect the [=viewport=] according to [=compute the intersection of a target element and the root=], set |frozenness| to true and set |auto resume media| to true.
    1. If |frozenness| does not equal |document|'s [=Document/frozenness=] state, [=change the frozenness of a document=] given |document|, |frozenness|, and |auto resume media|.
</div>

Processing model {#sec-processing-model}
========================================

Modifications to worklet and worker behavior {#worklets-workers}
----------------------------------------------------------------

Any [=worklet agent=] whose single [=/realm=]'s [=Realm/global object=]'s [=worklet/owning document=] is [=/frozen=] must become [=blocked=].
Any [=dedicated worker agent=] whose single [=/realm=]'s [=Realm/global object=]'s [=DedicatedWorkerGlobalScope/owning document=] is non-null and [=/frozen=] must become [=blocked=].

<div algorithm>
    To determine the <dfn for="DedicatedWorkerGlobalScope" export>owning document</dfn> for a {{DedicatedWorkerGlobalScope}} |workerGlobalScope|:

    1. If |workerGlobalScope|'s [=owner set=] consists of a single {{Document}} |document|, then return |document|.
    1. If |workerGlobalScope|'s [=owner set=] consists of a single {{DedicatedWorkerGlobalScope}} |parentWorkerGlobalScope|, then return |parentWorkerGlobalScope|'s [=owning document=].
    1. Return null.

    NOTE: A {{DedicatedWorkerGlobalScope}}'s [=owner set=] will always have exactly one item.
</div>


Modifications to the HTML Standard {#mod}
--------------------------------------------

### <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#unloading-documents">Unloading documents</a> and <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#history-traversal">history traversal</a> ### {#html-bfcache-dfn}

When documents move into and out of <a href="https://webkit.org/blog/427/webkit-page-cache-i-the-basics/">bfcache (back forward cache)</a> they will transition its [=Document/frozenness=] state to true and false respectively.

* In the [=unload a document=] algorithm, after Step #5, if the {{PageTransitionEvent/persisted}} attribute is true (i.e. we are moving to bfcache), [=change the frozenness of a document=], given |document| and true.
* In the [=traverse the history=] algorithm, before Step #4.6.4, if the {{PageTransitionEvent/persisted}} attribute is true (i.e. we are moving out of bfcache), [=change the frozenness of a document=], given |document| and false.

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#definitions-3">Event loop: definitions</a> ### {#html-html-event-loop-definitions}

Replace: A [=task=] is runnable if its [=document=] if either null or [=fully active=].

With: A [=task=] is runnable if its [=document=] is either null or [=fully active=], and is also [=/unfrozen=].

### <a href="https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model">Event loop: processing model</a> ### {#html-event-loop}

After Step #11 during the [=Update the rendering=] add the following step.

For each [=fully active=] {{Document}} |doc| in <var ignore>docs</var>, run the [=update document frozenness steps=] given |doc|.

### <a href="https://html.spec.whatwg.org/multipage/window-object.html#a-browsing-context-is-discarded">Discarding browsing contexts</a> ### {#html-discarding}

Rename the "<a href="https://html.spec.whatwg.org/multipage/window-object.html#a-browsing-context-is-discarded">discard</a>" concept, for both browsing contexts and documents, to "destroy". This allows us to use the "discarded" terminology for the user-facing {{Document/wasDiscarded}} attribute.

### <a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#initialise-the-document-object">Initialize the document</a> ### {#html-initialize-doc}

Before Step #3 add following:

If the browsing context was previously [=/discarded=], then set the {{Document}}'s [=Document/discarded=] boolean to true.

### [=iframe load event steps=] ### {#html-iframe-load}

After Step #5 add following:

Run the [=update document frozenness steps=] given <var ignore>child document</var>.

### {{HTMLMediaElement}} ### {#html-media-elements}

Each {{HTMLMediaElement}} has a <dfn for="HTMLMediaElement">resume frozen flag</dfn>, which is initially set to false.

### <a href="https://html.spec.whatwg.org/multipage/web-messaging.html#posting-messages">Posting messages</a> ### {#html-post-message}

Add a note:

NOTE: When posting a message to a {{Window}} that is [=/frozen=], events that are queued on the [=posted message task source=] will not run until the {{Window}} is [=unfrozen=]. If too many events are queued, the user agent might [=discard=] the {{Window}}'s [=Window/browsing context=] (as part of the general allowance for discarding under resource pressure).

Modifications to the Service Worker Standard {#serviceworker-mod}
--------------------------------------------

### <a href="https://w3c.github.io/ServiceWorker/#client-interface">`Client`</a> ### {#serviceworker-client-dfn}

<pre class="idl">
    partial interface Client {
        readonly attribute ClientLifecycleState lifecycleState;
    };

    enum ClientLifecycleState {
        "active",
        "frozen"
    };
</pre>

A {{Client}} object has an associated <dfn id="dfn-service-worker-client-lifecycle-state" for="Client">lifecycle state</dfn>, which is one of the {{ClientLifecycleState}} enumeration values.

#### {{ServiceWorkerClient/lifecycleState}} #### {#service-worker-client-lifecycle-state}

The <dfn attribute for="ServiceWorkerClient">lifecycleState</dfn> attribute <em>must</em> return the [=context object=]'s [=Client/lifecycle state=].

#### <a href="https://w3c.github.io/ServiceWorker/#clients-matchall">`matchAll(options)`</a> #### {#serviceworker-matchall-dfn}

Rename variable in Step #4.
1. Let |matchedClientData| be a new [=list=].

Before Step #2.5.1 insert

1. Let <var ignore>lifecycleState</var> be the result of running [=Get Client Lifecycle State=] with <var ignore>client</var>.

Append lifecycleState to list in Step #5.3.1
1. Let |windowData| be «[ "client" → |client|, "ancestorOriginsList" → a new [=list=], "lifecycleState" → |lifecycleState| ]».

Append lifecycleState to matchedClientData in Step #5.4
1. Add «[ "client" → |client|, "lifecycleState" → |lifecycleState| ]» to |matchedClientData|.

Pass windowData lifecycleState into Create Window Client algorithm in Step #6.2
1. Let <var ignore>windowClient</var> be the result of running [=Create Window Client=] algorithm with |windowData|["`client`"], |windowData|["`frameType`"], |windowData|["`visibilityState`"], |windowData|["`focusState`"], |windowData|["`ancestorOriginsList`"], and |windowData|["`lifecycleState`"] as the arguments.

Adjust Step #6.3
1. [=list/For each=] |clientData| in |matchedClientData|:
    1. Let |clientObject| be the result of running [=Create Client=] algorithm with |clientData|["`client`"], and |clientData|["`lifecycleState`"] as the arguments.
    1. [=Append=] |clientObject| to <var ignore>clientObjects</var>.


#### <a href="https://w3c.github.io/ServiceWorker/#dom-clients-openwindow">`openWindow(url)`</a> #### {#serviceworker-openwindow-dfn}

Before Step #7.5 insert
1. Let |lifecycleState| be the result of running [=Get Client Lifecycle State=] with [=context object=]'s associated [=service worker client=].

Adjust Step #7.8.2 to provide lifecycleState
1. Let |client| be the result of running [=Create Window Client=] with <var ignore>newContext</var>'s {{Window}} object's [=environment settings object=], <var ignore>frameType</var>, <var ignore>visibilityState</var>, <var ignore>focusState</var>, <var ignore>ancestorOriginsList</var>, and |lifecycleState| as the arguments.

### <a href="https://w3c.github.io/ServiceWorker/#algorithms">Algorithms</a>  ### {#serviceworker-algorithms-dfn}

<section algorithm>
    #### <dfn>Get Client Lifecycle State</dfn> #### {#get-client-lifecycle-state-algorithm}

    Append the following algorithm:

    : Input
    :: |client|, a [=/service worker client=]
    : Output
    :: |state|, a string

    1. Let |state| be {{ClientLifecycleState/"active"}}.
    1. If |client|'s [=environment settings object/global object=]'s [=owning document=] is [=frozen=], set |state| to be {{ClientLifecycleState/"frozen"}}
    1. Return |state|.
</section>

<section algorithm="create-client-monkeypatch">
    #### <a href="https://w3c.github.io/ServiceWorker/#create-client-algorithm">Create Client</a> #### {#serviceworker-createclient-dfn}

    To Input append
    |lifecycleState|, a string

    After Step #2 in Output append
    1. Set <var ignore>clientObject</var>'s [=Client/lifecycle state=] to |lifecycleState|.
</section>

<section algorithm="create-window-client-monkeypatch">
    #### <a href="https://w3c.github.io/ServiceWorker/#create-windowclient-algorithm">Create Window Client</a> #### {#serviceworker-createwindowclient-dfn}

    To Input append
    |lifecycleState|, a string

    After Step #5 in Output append
    1. Set <var ignore>windowClient</var>'s [=Client/lifecycle state=] to |lifecycleState|.
</section>

Page lifecycle processing model {#page-lifecycle}
--------------------------------------------

<h4 id="frozenness-state">FROZENNESS state</h4>
A document can be in one of the following <dfn for="Document">FROZENNESS</dfn> states:

* true: the document is <dfn export lt="frozen">frozen</dfn>, any tasks associated with the document will not run
* false: the document is <dfn export lt="unfrozen">unfrozen</dfn>, tasks associated with the document will run as usual

    NOTE: Per the [=change the frozenness of a top-level document=] algorithm, when the Document of the top level browsing context changes its [=Document/frozenness=] state then all documents of descendant browsing contexts will also change [=Document/frozenness=] to the same value (and be consistent with the Document of the top level browsing context).

The UA may choose to execute [=change the frozenness of a top-level document=] algorithm with true in certain situations.
For instance, if a top level browsing context is in the background or hidden, and a grace period has elapsed the UA could execute [=change the frozenness of a top-level document=] with true to conserve resources and maintain the quality of the (foreground) user experience.
Specific examples:

* In mobile Chrome, tabs that have been in background for (at least) 5 minutes, may be [=/frozen=], to conserve battery and data.
* In desktop Chrome, background tabs that are not important to the user (not used in some time) may be [=/discarded=], to conserve memory

    NOTE: background tabs that are actively doing work on behalf of the user (eg. playing audio) are generally not [=/frozen=] or [=/discarded=].

    NOTE: For a detailed list of heuristics & exclusions used by Chrome, see <a href=https://docs.google.com/document/d/1QJpuBTdllLVflMJSov0tlFX3e3yfSfd_-al2IBavbQM/edit>this doc</a>.

The UA will typically execute [=change the frozenness of a top-level document=] with false when the user revisits that browsing context. In addition, the UA may choose to periodically execute [=change the frozenness of a top-level document=] with false in the background, if plentiful resources are available.

<h4 id="changing-frozenness">Changing the frozenness of documents</h4>

<div algorithm>
    To <dfn>change the frozenness of a top-level document</dfn>, given a {{Document}} |topLevelDoc| and boolean [=Document/frozenness=] state |frozenness|:

    1. Assert: |doc|'s [=Document/browsing context=] is a [=top-level browsing context=].
    1. [=Change the frozenness of a document=] given |topLevelDoc|, |frozenness|, and false.
    1. Let |descendants| be the [=list of the descendant browsing contexts=] of |doc|.
    1. For each [=/browsing context=] |b| in |descendants|:
        1. Let |descendantDocument| be the [=active document=] of |b|.
        1. [=Change the frozenness of a document=] given |descendantDocument|, |frozenness|, and false.
</div>

<div algorithm>
    To <dfn>change the frozenness of a document</dfn>, given a {{Document}} |doc|, a boolean [=Document/frozenness=] state |frozenness|, and a boolean |auto resume frozen media|:

    1. If |frozenness| is true, run the [=freeze steps=] for |doc| given |auto resume frozen media|.
    1. Otherwise, run the [=resume steps=] given |doc|.
</div>

<div algorithm>
    To run the <dfn>freeze steps</dfn> for a {{Document}} <var>doc</var>, given a boolean |auto resume frozen media|:

    1. Set |doc|'s [=Document/frozenness=] state to true.
    1. [=Fire an event=] named <code>freeze</code> at |doc|.
    1. Let |elements| be all [=media elements=] that are [=shadow-including descendants=] of |doc|, in [=shadow-including tree order=].
    1. For each |element| in |elements|:
        1. If |element|'s {{HTMLMediaElement/paused}} is false, then:
            1. Set |element|'s [=HTMLMediaElement/resume frozen flag=] to |auto resume frozen media|.
            1. Execute [=media pause=] on |element|.

        NOTE: it is intentional that the ordering between the assignment of the of frozneness state
        occurs first before event firing.
</div>

<div algorithm>
    To run the <dfn export>resume steps</dfn> for a {{Document}} <var>doc</var>:

    1. Let |elements| be all [=media elements=] that are [=shadow-including descendants=] of |doc|, in [=shadow-including tree order=].
        1. For each |element| in |elements|:
            1. If |elements|'s [=HTMLMediaElement/resume frozen flag=] is true.
                1. Set |elements|'s [=HTMLMediaElement/resume frozen flag=] to false.
                1. Execute [=media play=] on |element|.
    1. [=Fire an event=] named <code>resume</code> at |doc|.
    1. Set |doc|'s [=Document/frozenness=] state to false.

        NOTE: it is intentional that the ordering between the assignment of the of frozneness state
        comes last after event firing.
</div>

<h4 id="discarding">Discarding</h4>
Each Document has a <dfn for="Document">discarded</dfn> boolean, which is initially false.

To <dfn export lt="discarded|discard">discard</dfn> a browsing context, <a href="https://html.spec.whatwg.org/multipage/window-object.html#a-browsing-context-is-discarded">destroy the browsing context</a>, and make note of the fact that the reason it and any descendant browsing contents were destroyed was because of discarding.

    NOTE: [=Discard=] is typically done to reclaim system memory, when memory and other resources are running low. On the other hand destroying a browser context is the normal teardown due to user leaving the page etc.

Browsing contexts -- that are in the background and have their documents in <a href="https://www.w3.org/TR/page-visibility-2/#visibility-states-and-the-visibilitystate-enum">VisibilityState hidden</a> -- can be [=/discarded=], under resource pressure (eg. low memory). Specific example:

* In desktop Chrome, background tabs that are not important to the user (not used in some time) may be [=/discarded=], to conserve memory

    NOTE: background tabs that are actively doing work on behalf of the user (eg. playing audio) are generally not [=/discarded=].

    NOTE: For a detailed list of heuristics & exclusions used by Chrome, see <a href=https://docs.google.com/document/d/1QJpuBTdllLVflMJSov0tlFX3e3yfSfd_-al2IBavbQM/edit>this doc</a>.

When a [=top-level browsing context=] (tab in the browser) is [=/discarded=] due to resource pressure (or unexpected events eg. process crash), and later the user revisits the tab in the browser, then the {{Document}}'s [=Document/discarded=] boolean will be true due to [[#html-initialize-doc]].


<!-- ============================================================ -->
<h2 id=acknowledgements>Acknowledgements</h2>
<!-- ============================================================ -->

Special thanks to
Dave Tapuska,
Fadi Meawad,
Ojan Vafai,
Olli Pettay,
Philip Walton, and
Todd Reifsteck
for their technical input and suggestions that led to improvements to this specification.
