Model for asynchronous interaction with a server

In a web application it's important to accurately model the asynchronous interactions with a remote server. If you have some kind of resource that lives on the server and you need to synchronize it with the client, that resource has in the application at least three fundamental states:

  1. Loading: The resource is not available on the client, but the application has started loading it. The network request was sent out but has not finished yet. The application is likely to show some kind of loading indicator (e.g. throbber). This state is important so the client can keep track of which resources it has started to load, to avoid sending a GET request multiple times. Especially in a React application when the UI can be re-rendered at any time, and there is danger of triggering the network request multiple times.

  2. Loaded: The application has received the response and the resource is available.

  3. Failed: Synchronization failed. This can be due to various reasons, such as because the server is down, the resource was not found (404), the client doesn't have permission to read the resource, or the client could not parse the response. This state can include furher information about the type of failure. But for the UI it often doesn't make any difference, as it will likely show just an error indicator.

Depending on the implementation, there may be a fourth state: Empty, the resource is not available on the client but the client has not started loading yet. This state is usually very short-lived because the application will send out the network request immediately.

Also note that these states don't include the case when the resource is loaded (and thus available to the application) but also at the same time being saved to the server. In many cases this saving can happen in the background, but that doesn't change the availability of the value on the client.

Implementation details from Avers

Previously Avers used an enum with those four values. But recently I've rewritten the code to use three properties instead:

  • value: The value of the resource. Starts out as undefined, indicating that the resource is not available. Once the resource is loaded from the server, the value is initialized with it.

  • networkRequest: The active network request. This is to ensure that only one network request can be underway for a particular resource at any given point in time. It doesn't make sense to fetch the same resource twice, or send to parallel network requests to save it. If this is undefined then no network request is in flight and one can be created.

  • lastError: The last error resulting from a network request. Depending on the type of error, the client can continue using the resource, but resolving it may need some attention from the user.

Aborting an active network request

Theoretically network requests can be aborted (or more accurately, requested that when the client receives the response that it is ignored). But in practice it often doesn't make sense. For example after saving an Editable the client should respect the response and apply the changes regardless, as they have been persisted in the database anyway. Not doing that would bring the client and server out of sync.

In the case of the initial fetch, when the client receives the data it may just as well use it. The only reason for wanting to abort the request would be because the client decided it doesn't need the data. However that is better solved with a garbage collector.

Derived states

If we show all possible combinations of the three properties in a table we get these states:

State value netReq lastError
Empty
InitialLoad
Loaded
FailedInitialLoad
Saving / Resetting
Reloading
Failed
Retrying

As you can see in the table, we can represent many more states now (23 = 8 to be precise). Here is the description of each:

  • Empty: Same as in the introduction.
  • InitialLoad: If we just have a network request and nothing more, then it's presumably to fetch the data from the server.
  • Loaded: Same as in the introduction.
  • FailedInitialLoad: The initial load failed, we can retry again in the future, depending on the type of error. If it's 404 then maybe, if it's 401 then it probably doesn't make any sense unless the authentication status has changed (e.g. the user logs in).
  • Saving / Resetting: Depending on the type of network request, we can either be saving local changes or trying to reset the resource to the state on the server (i.e. discard all local changes).
  • Reloading: Retrying after a failed initial load.
  • Failed: The last request has failed, but we can still use the resource, the value is available. But depending on the type of error we may need to show some indication to the user. For example if saving failed, then warn the user that his changes were not persisted.
  • Retrying: Similar to Reloading, but can be any request, not just the initial load. Could be the request to save the data on the server.

These states could be simplified by introducing the invariant that networkRequest and lastError are mutually exclusive. Whenever we start a new network request it clears out lastError. That would effectively merge the Saving / Resetting with Retrying, and InitialLoad with Reloading.

Implications of the state for extracting values from Avers

The functions which extract values from Avers (e.g. lookupEditable) are optimistic. When the value is available, then the computation returns the value, regardless of any errors or pending network requests. In most cases the caller doesn't care whether there is some network request underway. If he does, then that information is available on the Editable.

When the value is not available but a network request is active, then the computation is pending. This corresponds to the InitialLoad or Reloading states. When the value is not available and neither is the last error, then the computation is pending as well (this corresponds to the Empty state).

Otherwise the computation fails with the last error.