npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

prosemirror-async-query

v0.0.4

Published

![](https://badgen.net/bundlephobia/min/prosemirror-async-query) ![](https://badgen.net/npm/v/prosemirror-async-query)

Downloads

11,431

Readme

Prosemirror Async Query

A declarative API for using promises in prosemirror plugins.

Live Demo

Installation

# npm
npm install prosemirror-async-query

# yarn
yarn add prosemirror-async-query

Documentation

This documentation assumes you have some familiarity with prosemirror. If you do not know about Prosemirror Plugins, in particular, methods such as view.update, state.apply, and state.init then you may want to start by reading the Prosemirror Guide.

Motivation

In the normal prosemirror data flow, the "editor displays editor state, and when something happens, it creates a transaction and broadcasts this. This transaction is then, typically, used to create new state, which is given to the view using its update state method" [^1].

[^1]: quoted from the prosemirror guide.

The naive way to add promises to this data flow is to set up the promise in state.apply() and then await the promise in view.update(). This pattern shows up in the real world but it has a couple of issues.

  • Because the view only updates when the promise returns, the user is unaware that anything is happening until the promise returns. This could be a usability issue if the promise takes longer than a couple of milliseconds.
  • If a transaction leads to a new promise, you may want to cancel the currently running promise, but creating communication between a state.apply() and a previous view.update() goes against the normal prosemirror data flow.

How Prosemirror Async Query Works

prosemirror-async-query enables easy integration of promises in prosemirror plugins by exposing a declarative API for keeping track of promise state using transactions and view updates.

To start you create a query in state.apply() and add it to your plugin state. At the very least you must provide a query function that returns a promise that resolves data or throws an error.

import { AsyncQuery } from "prosemirror-async-query";
import { Plugin } from "prosemirror-state";

const plugin = new Plugin({
  state: {
    init() {
      return { query: null };
    },
    apply(tr, prev) {
      // if the query does not exist create it.
      if (prev.query === null) {
        return {
          query: new AsyncQuery({ query: fetchTodos }),
        };
      }
      return prev;
    },
  },
});

Having a query in your plugin state does not do anything yet. The next step is to run the query using the query's viewUpdate and viewDestroy methods. These are meant to be called in the plugin's view.update() and view.destroy() methods respectively.

import { AsyncQuery } from "prosemirror-async-query";
import { Plugin, PluginKey } from "prosemirror-state";

const plugin = new Plugin({
  // use a plugin key so we can access plugin state
  key: new PluginKey("async-query-plugin"),
  view() {
    update(editor) {
      // run the query update method
      pluginKey.getState(editor.state)?.query?.viewUpdate(editor);
    },
    destroy() {
      // run the query destroy method
      pluginKey.getState(editor.state)?.query?.viewDestroy(editor);
    }
  },
  state: {
    init() {
      return { query: null };
    },
    apply(tr, prev) {
      if (prev.query === null) {
        return {
          query: new AsyncQuery({query: fetchTodos}),
        };
      } else {
        // check the query status (we'll improve this in a second)
        console.log(prev.query.status)
      }
      return prev;
    },
  },
});

Before you continue it is helpful to learn what the query.status means.

  • idle means the query exists but the query function has not been run or the query is not enabled (learn more about enabled in the source code comments).
  • loading means the query is fetching but has not returned yet.
  • error means the query was canceled or encountered an error.
  • success means the query returned successfully and has data.

When you create a new query with AsyncQuery() the query has a status of idle. When you call viewUpdate on a query, the method checks to see if the query has an idle status. If the query status is idle the viewUpdate method runs the query, updates the query status to loading, and dispatches a transaction indicating that the query status is loading. If the query function returns successfully viewUpdate sets the query status to success and dispatches a transaction indicating that the query status is success. If the query function throws an error or is canceled, viewUpdate sets the query status to error and dispatches a transaction indicating that the query status is error.

Because viewUpdate dispatches transactions whenever the queryStatus changes, you can handle any changes to the query in state.apply().

In the example we access the query status in state.apply() but the example has an issue. There are many transactions and we probably only want to react to query status changes once, not on every transaction. Luckily AsyncQuery makes this easy using the statusChanged method.

import { AsyncQuery } from "prosemirror-async-query";
import { Plugin, PluginKey } from "prosemirror-state";

const plugin = new Plugin({
  key: new PluginKey("async-query-plugin"),
  view() {
    update(editor) {
      pluginKey.getState(editor.state)?.query?.viewUpdate(editor);
    },
    destroy() {
      pluginKey.getState(editor.state)?.query?.viewDestroy(editor);
    }
  },
  state: {
    init() {
      return { query: null };
    },
    apply(tr, prev) {
      if (prev.query === null) {
        return {
          query: new AsyncQuery({query: fetchTodos}),
        };
      // check if the query status changed
      } else if (prev.query.statusChanged(tr)) {
        console.log("query status changed", prev.query.status);
      }
      return prev;
    },
  },
});

The statusChanged method only returns true for transactions dispatched by the query's viewUpdate method.

We now have a declarative API for defining an asynchronous function in state.apply(), running the asynchronous function in view.update(), and reacting to changes in the asynchronous function in state.apply().

Getting Fancy

As a final tip, both statusChanged and viewUpdate take flags which can help control the data flow. For example, you may only want to handle status changes when the query has returned successfully. You can filter transaction in statusChanged to only return true for specific statuses by passing a status value as the second argument to the function.

if (query.statusChanged(tr, "success")) {
  console.log(query.status === "success");
}

if (query.statusChanged(tr, ["success", "error"])) {
  console.log(query.status === "success" || query.status === "error");
}

However you can also avoid dispatching extra transactions altogether by passing ignore flags to viewUpdate.

// the query will only dispatch a transaction when the query returns successfully
pluginKey.getState(editor.state)?.query?.viewUpdate(editor, { ignoreLoading: true, ignoreError: true });

Here is an end to end example with more controlled data flow.

import { AsyncQuery } from "prosemirror-async-query";
import { Plugin, PluginKey } from "prosemirror-state";

const plugin = new Plugin({
  key: new PluginKey("async-query-plugin"),
  view() {
    update(editor) {
      pluginKey.getState(editor.state)?.query?.viewUpdate(
        editor,
        // don't send transactions for queries that are loading or errored
        {ignoreLoading: true, ignoreError: true}
      );
    },
    destroy() {
      pluginKey.getState(editor.state)?.query?.viewDestroy(editor);
    }
  },
  state: {
    init() {
      // added a query result to our state
      return { query: null, result: null };
    },
    apply(tr, prev) {
      if (prev.query === null) {
        return {
          query: new AsyncQuery({query: fetchTodos}),
        };
      // only handle the success case
      } else if (prev.query.statusChanged(tr, "success")) {
        console.log("query returned successfully");
        return {
          ...prev,
          result: query.data,
        }
      }
      return prev;
    },
  },
});

For more in depth documentation check out the comments in the source code as well as the example usage from the demo, and feel free to open an issue if you have any questions!