Cancellable Network Operations
The VUEDA client cancels a request when its result is no longer wanted: a list's params change, a form's target object changes, or a component deactivates or is disposed. Cancelling keeps a superseded request from writing stale state into a list, an object, or a form. Two mechanisms do this work. Aborting a request stops the HTTP request in the browser. The isCancelled check discards a response that has already arrived.
Cancellation happens only in the client. An aborted request may already have reached the server, and the server finishes that work. A cancelled save can still commit.
This page describes how a promise carries its cancel method and which functions abort requests. It also explains how aborting differs from the isCancelled check, and which parts of the client cancel requests.
Cancellation Surface (Promise Identity)
A Cancellable Promise is a promise with a cancel method. VUEDA describes it with two types. CancellablePromise always has cancel. MaybeCancellablePromise may have it, for call sites that receive promises from more than one source.
The cancel method is a property of one promise object. Any step that returns a new promise leaves that property behind:
- An
asyncfunction always returns a new promise, even when its body returns a cancellable one. .then(),.catch(), and.finally()each return a new promise.
A function that passes cancel to its caller therefore returns the cancellable promise itself. The default CRUD Adapter functions are plain functions for this reason, and most carry the comment "This function cannot be async". The list and object instances in reactive-helpers follow the same rule. A function can still run async code inside. allPagePaginatedListCrudAdaptor runs its page fetches in an inner async function and attaches cancel to that function's promise before returning it. useModelAction chains .then() onto the instance's promise, then copies a cancel method onto the chained promise.
A caller that may receive a plain promise calls promise?.cancel?.(). Plain promises are common: a cached result comes back as an already resolved promise with no cancel.
When a function drops cancel, its caller cannot abort the request. A direct promise.cancel() call throws a MDN: TypeError, and promise?.cancel?.() does nothing. A list or object instance that receives a promise without cancel also cannot mark the run as cancelled, so the isCancelled check never discards its result. The superseded request runs to completion and writes its data.
Functions That Abort Requests
Four functions create an MDN: AbortController and abort its signal from cancel:
- fetchHelper passes its own signal to
fetchand replaces anysignalandcredentialsin the options you pass. You can abort its request only throughcancel. Itscanceltakes no reason and returns nothing. - reactive-helpers: cancellableFetch passes its own signal to
fetchand links it to anysignalyou pass, so aborting your controller also aborts the request. Itscancel(reason)aborts the request, then waits for the promise to settle. - defaultObjectCreate builds its promise from
fetch. Itscancelaborts the request, then waits for the promise to settle. allPagePaginatedListCrudAdaptorshares one controller across all its page requests. Itscancelaborts every page request, then waits for all of them to settle.
Waiting for the promise to settle keeps the rejection that the abort causes from surfacing as an unhandled rejection. The other default adapters use cancellableFetch. CRUD Adapter Layer describes what each adapter sends and how it reports errors.
Aborting a Request and the isCancelled Check
Aborting is synchronous. If the response has not arrived, fetch rejects. If the adapter is still reading the response body, the read rejects. Once the body has been read, aborting does not stop the adapter code that runs next.
The isCancelled check covers that gap. A list instance passes each run an isCancelled ref (reactive-helpers: ListArgsRaw lists the adapter arguments). When the instance cancels a run, it sets the ref to true first and then calls the adapter's cancel. singlePagePaginatedListCrudAdaptor reads the body, checks the ref, and returns without writing pagination or rows if the run was cancelled. The all-pages adapter checks the ref before each write. An adapter can also call setCancelled to mark its own run cancelled.
Aborting saves network and server work when the abort arrives in time. The check stops the stale write in every case where the instance knows the run was cancelled.
A cancelled run rejects, and the instance does not treat that rejection as an error. It stores no error for a run it cancelled, and a cancelled list run resolves to false.
Code that calls the fetch functions directly sees these rejections:
- A
cancellableFetchrequest aborted with a reason rejects with that reason. Without a reason, it rejects with a MDN: DOMException namedAbortError. fetchHelperwraps every fetch-level failure, an abort included, in itsErrorClass(FetchError by default). It has no separate branch for aborts, so a caller cannot tell a cancelled request from a network failure by the error class.
What Cancels Requests
reactive-helpers' reactive-helpers: useCancellableIntent runs the list and retrieve requests behind reactive-helpers: useList and reactive-helpers: useObject. It cancels the running request when the watched params change, when the component deactivates, and when the component's scope is disposed. It treats its own cancellation as no error.
The form composables cancel their submissions. useObjectForm cancels when the app, model, or pk changes, and when its scope is disposed. useActionForm cancels when its dry-run target changes, when the component deactivates, and when it unmounts. Both also number their submissions and ignore any result from an earlier one.
Store requests cannot be cancelled. Pinia wraps the promise of every store action in a new promise, so no store action returns a cancel method. fetchModelInfo also keeps only a chained promise for its request, so nothing can abort a Model Info request once it starts. A cancellation therefore never becomes one of a store's cached errors. Reactive Data Flow describes which store failures stay cached and when the Auth-Scoped Stores clear.
When setConfig or a user change supersedes an in-flight Model Config build, the model-info request keeps running. The superseded build answers its caller but caches nothing. Issue #178 tracks cancelling that request. Reactive Data Flow describes the build lifecycle.
Shared Lookup Requests
useLookupContext lets several components share one request for the same object. Its requestObject groups requests by app, model, fields, and expand. It waits 250 ms after the latest request (at most 1 s) and then fetches all requested pks for a group in one request. Each component receives its own cancellable promise.
Cancelling a component's promise removes that component from the shared request. The context aborts the shared request only when no component still waits on any pk in its group. One component's disposal therefore does not abort a request that another component needs.
A cancelled component promise never settles. It neither resolves nor rejects, so code awaiting it stops at the await.
The context keeps each resolved object for the context's lifetime. A later request for the same object gets a resolved promise with no cancel, and no request is sent.
The context logs a console warning in three cases: cancel runs after the context's records for that object are gone, cancel runs twice on one promise, or the last component cancels before the shared request has started. These warnings point to a component that cancels after its request has finished or been torn down.
useResolvedLookupObject resolves one object through the lookup context. It cancels its request when the app, model, or pk becomes empty, and when its scope is disposed. It logs a failed cancel as a console warning and does not throw. Issue #385 tracks two current behaviors:
- When the target changes from one object to another, the composable starts a new request and does not cancel the old one. If the old request settles last, its object replaces the current one.
- When the target is cleared during a request, the cancelled promise never settles.
loadingstaystrueuntil a later request finishes, and the cancellation does not seterrored.