Possible race condition in autocomplete extension

I’m using an autocomplete extension, and each Completion object I build has the additional info object, which is a function that looks like this:

info: async () => {
  const dom = document.createElement('div')
  onCreateTooltip(tooltipId, dom)
  return {
    dom,
    destroy: onDestroyTooltip,
  }
}

there seems to be some sort of race condition in when the destroy() callback is called? because i’m seeing that when i type the first character, the tooltip is rendered correctly, but when i type the second character, it disappears. and it looks like that’s happening because the onCreateTooltip() method for the new tooltip (now that 2 characters have been typed) is being executed before the onDestroyTooltip() method for the old tooltip (the one from when only 1 character had been typed). so the net effect is that no tooltip is rendered at all.

or, is this known and i’m supposed to be using unique IDs somehow?

What do onCreateTooltip and onDestroyTooltip do with that ID?

Ah right sorry – we keep a map from ID to some info that we use to create portals for the tooltips. So something like this:

    const onCreate = useCallbackRef((element: PortaledElement) => {
      setPortaledElements((prev) => ({ ...prev, [element.id]: element }))
    })

    const onDestroy = useCallbackRef((id: string) => {
      setPortaledElements((prev) => {
        const { [id]: _removed, ...rest } = prev
        return rest
      })
    })

where we create portal elements for each item in the map.

Is useCallbackRef asynchronous? If so, the race condition might be on your side.

No, it’s not async. The onCreate/onDestroy callbacks I’m passing are, but I don’t think that matters because it’s their invocation that seems to be out of order.

I don’t get it—the only thing that could introduce asynchronicity into the onCreate and onDestroy functions you show is useCallbackRef.

Looking at the code in @codemirror/autocomplete, I don’t see how that is possible. The current info panel is always destroyed before a new one is created.

i’m not super certain on what’s happening, but stepping through it in my browser, it seems like maybe there’s some weirdness bc the codemirror internals are adding elements to the dom here (https://github.com/codemirror/autocomplete/blob/53cc58393252659ac4a86162b40afef13eeb2241/src/tooltip.ts#L172C1-L186C4) so maybe in that regard things are happening in order, but our application is also adding elements to the dom (by way of keeping that map state and calling createPortal on the map items) and maybe in that regard things are out of order?

Because if here (https://github.com/codemirror/autocomplete/blob/53cc58393252659ac4a86162b40afef13eeb2241/src/tooltip.ts#L159) is where i’m adding an element to a map, but then we subsequently call this.addInfoPane(obj, completion) which calls this.destroyInfo(), that might be causing me to delete my newly added entry?