Skip to content

[epic] Optimistic store rewrite #427

@rbalicki2

Description

@rbalicki2

Optimistic store rewrite

Big picture

startUpdate (later, startOptimisticUpdate?) and optimistic responses will make use of the same machinery: a stack of mergeable updates. startUpdate by itself will be substantially simpler than doing the whole thing. But let's make sure that, while working on startUpdate, we do stuff in a way that's compatible with the bigger picture.

There can be any number of optimistic responses, and they can be interspersed with network responses. Let's model that as a doubly-linked list of optimistic nodes.

Example: making optimistic responses

The state of the store might be as follows

base
// optimistic update made
base <- opt1
// another optimistic response
base <- opt1 <- opt2
// opt1 cleared
base <- opt2
// network response received
base <- opt2 <- NR
// network response received, and is merged into the previously received network response
base <- opt2 <- NR
// opt2 is cleared, network response is merged into parent
base

Modeling the stack

Something like

type NetworkResponseNode = {
  kind: 'NetworkResponseNode';
  childNode: OptimisticNode | null;
  parent: OptimisticNode | null;
  data: DataLayer;
};
type OptimisticNode = {
  kind: 'OptimisticNode';
  childNode: OptimisticNode | NetworkResponseNode | null;
  parentNode: OptimisticNode | NetworkResponseNode | BaseNode;
  data: DataLayer;
};
  • So, we are ensuring that: a NetworkResponseNode must be the parent
  • No two NetworkResponseNodes can be adjacent

We could refine this to ensure that the base node's data has Query: { __ROOT: {} }. I don't know if it's worth it.

The Isograph environment

The environment only needs a pointer the bottom-most node in the stack.

Reading

  • Reading is always done from the bottom of the stack. If the scalar or linked field is present in the current item and is not undefined, then use that value, otherwise, try again in the next item in the stack

Proxies

  • startUpdate (and optimistic updaters) are passed a function with signature (proxy: TUpdatableData) => void. Call this function an updater
  • This proxy (call it a StoreProxy) starts with an empty DataLayer.
  • Writes will mutate that DataLayer.
    • If we write proxy.user.name = "John" and in the stack, that user already has the name "John", then we still write user.name = "John to the DataLayer.
    • This is important, because you can have base <- opt1, where opt1 had user.name = "John". Then we later introduce opt2, where we call user.name = "John". Then the user disposes of opt1. We want the user.name to still be John
  • Reads go through that DataLayer, and later through the bottom item in the stack.
    • So, writes are immediately visible inside of the updater.

Creating an optimistic update

  • Calling the updater and passing a proxy, thus generating a DataLayer
  • adding an empty OptimisticNode to the bottom of the stack, which contains that DataLayer
  • calling subscriptions
  • returning a destructor that removes the optimistic node from the stack and calls subscriptions
  • (later?) returning a function that converts the OptimisticNode to a NetworkResponseNode

Removing an item and upgrading to a NetworkResponseNode

  • If we do remove an optimistic node or upgrade, then we have to merge the stack to enforce that there are no adjacent NetworkResponseNodes

Receiving a network response

  • If we receive a network response, we add a NetworkResponseNode to the stack and merge, and call subscriptions

startUpdate

  • Call the updater and pass a proxy, thus generate a DataLayer
  • Add a NetworkResponseNode and merge
  • call subscriptions

Subscriptions

  • As a first pass, when we add/remove from the stack, we can trigger all subscriptions with overlapping records in that DataLayer (which is effectively what we do when we receive a network response anyway).
  • Each subscription is "smart" in that it re-reads and short circuits if nothing changed. So, triggering a few extra subscriptions is probably fine (e.g. if you remove opt1 in base <- opt1 <- opt2, the records that opt1 modified might be shadowed by opt2).

What it takes to implement startUpdate (but not optimistic updaters)

  • There is no stack, since startUpdate creates a network response node and that is immediately merged, so there is only ever one item
  • But proxies need to read through a DataLayer and then the base layer
  • But TBH I don't think the stuff to support optimistic updates is that much extra work

Misc: startOptimisticUpdate and (non-optimistic) updaters

  • It should be extremely easy to also add a startOptimisticUpdate function that does the exact same stuff, but creates an OptimisticNode and also returns a destructor (& promoter)
  • Non-optimistic updaters (which are called when the network response is received) should also be fairly straightforward

Misc: can updaters be called multiple times?

  • Yeah. Ideally, we should call optimistic updaters (and even the startUpdate call) many times! E.g. if we have base <- opt1 <- startUpdate, we could re-call the startUpdate after opt1 is removed
  • But TBH I think we can add this after the fact

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions