How to avoid setTimeout

I have written a plugin that is controlled by dispatching transactions with meta data. I can then respond to that meta data in the apply method, and return new plugin state.

This worked well, as it allowed me to generate new plugin state programatically:

if (tr.getMeta('action') === CANCEL) {
  // ...
}

Then I started to need to generate new plugin state by some inspecting the document state:

if (tr.getMeta('action') === CANCEL || autoCancel(state)) {
  // ...
}

(Imagine that autoCancel inspects some selection ranges).

And that’s when I ran into trouble.

Because the plugin also has a public interface like:

apply(tr) {
  if (tr.getMeta('action') === CANCEL || autoCancel(state)) {
    pluginConfig.onCancel(); // <-- this
    return null;
  }
}

Now if the consumer of the plugin wants to dispatch a transaction, they will run into timing issues, e.g.

myPlugin({
  onCancel() {
    insertText('foo')(state, dispatch)
  }
})

Because the onCancel method got fired whilst still inside the apply method of the plugin, they will be unable to update the document.

A hacky fix being setTimeout(..., 0);

I know I need to refactor, but I’m unsure which direction to take.

My first idea was to try the update method:

update(view) {
  const pluginState = this.getState(view.state)
 
  // `pluginState` now reflects the cancellation
  // but how to know whether to call `pluginConfig.onCancel()` ?
}

Firing the public actions could potentially work here, because the pluginState knows about the new state, but we do not know if it is an appropriate time to call them. I’d have to have another property like fireOnCancel: true on the pluginState, which I then set to false after firing onCancel. But that feels wrong, because I already have a nice way to control it with setMeta.

Can anyone guide me here?

1 Like

What exactly does onCancel do? If it just affects the plugin state, you could just reformulate it to do so locally in the apply function without dispatching a new transaction. If it has to have side effects you indeed cannot do that from a state apply method, because that should be a pure function that just updates the plugin state. In some cases appendTransaction is a good way to insert transactions in response to other transactions, but that too shouldn’t produce arbitrary side effects.

My example shows that onCancel can do anything, above, it calls insertText.

I also dabbled with appendTransaction, but didn’t get anywhere with it.

But yeh, like you say, the crux of it was that apply was not pure.

I was trying to avoid having to do this, but it did solve the issue:

view() {
  return {
    update(view) {
      const { scheduledActions } = pluginKey.getState(view.state);
      actions.forEach((action) => action());
    }
  };
}
apply(tr, pluginState) {
  const nextPluginState = { ...pluginState, scheduledActions: [] };

  if (tr.getMeta('action') === CANCEL) {
    // do internal cancellation (not real)
    nextPluginState.running = false;
    // and, expose cancellation to outside world:
    nextPlugnState.scheduledActions.push(pluginState.onCancel);
  }

  // ...

  return nextPluginState;
}