Skip to content

Watch - high-performance replacement for subscribe() #1010

Open
@aboodman

Description

@aboodman

Replicache UIs are currently built using the subscribe() method.

The idea of subscribe is that it is an arbitrary function of a Replicache state that can return any JSONValue as a result. Since it is an arbitrary function there's a limit to how far it can be optimized. For example, the overwhelmingly most common subscription is just getting all entries with a particular key prefix:

rep.subscribe(async tx => {
  return await tx.scan({prefix: "todo/"}).entries().toArray();
})

We can optimize this subscription to only run when one of the accessed keys is modified (and we do currently do this). But since we can't know what the function does with that data, we are forced to run the function again each time it is invalidated, in its entirety.

This sucks since in this common case, Replicache knows how those keys changed and so it could in theory fix up the returned data surgically without re-running the scan.

We plan to support this common use case efficiently by introducing a new watch() method that looks something like:

class Replicache {
  // watch() allows developers to watch a query over the Repliache keyspace.
  // The watch is maintained incrementally, meaning that when Replicache
  // changes such that the watch is invalidated, only a small amount of work --
  // roughly proporitional to size of change -- is done to update the result.
  //
  // Specifically, in contrast to subscribe(), watch does *not* rescan the
  // keyspace on each change, nor does it do a deep compare of prev result to
  // new result to know whether a change has happened.
  watch(options?: WatchOptions): Unwatch;
}

type Entry<Key> = {
  key: Key,
  value: ReadOnlyJSONValue,
};

// Note: this is a long way of saying that the WatchOptions are the same as
// the ScanOptions but with the extension of WatchOptionsExt below. I think
// there is a refactor possible to clean this up, I can talk about that
// separately.
type WatchOptions = WatchNoIndexOptions | WatchIndexOptions;
type WatchNoIndexOptions = ScanNoIndexOptions & WatchOptionsExt<string>;
type WatchIndexOptions = ScanIndexOptions & WatchOptionsExt<[string, string]>;

type WatchOptionsExt<Key extends string | [string,string]> {
  filter?: (entry: Entry<Key>) => boolean,
  sort?: (a: Entry<Key>, b: Entry<Key>) => number,

  // Guarantees:
  // - Array items whose identities are unchanged are guaranteed to have
  //   unchanged contents. This is done specifically so that e.g., React devs
  //   can use React.memo() with these items.
  // - A single Replicache transaction results in at most 1 call to `onChange`.
  //
  // Non-guarantees:
  // - No guarantee as to identity of `result`. `result` may be reused across
  //   calls to `onChange` for performance reasons.
  // - Replicache may collapse multiple transactions into one call to
  //   `onResult`.
  onResult?: (result: Entry[]) => void,

  // Intended for use with systems like svelte, solid, mobx, etc.
  onChange?: (changes: WatchChange[]),
}

type Unwatch = () => void;

type WatchChange = {
  at: number,
  delete: number,
  insert: Entry[],
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    FutureSomething we want to fix but is not blocking next release

    Type

    No type

    Projects

    Status

    In Progress

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions