search ({ searchTerm, skipCache, skipChildren, tags, viewId }) {
    return new Observable((subscriber) => {
      searchTerm = dr.refang(searchTerm.trim());

      if (!searchTerm) { // nothing to do
        return subscriber.complete();
      }

      // NOTE: this assumes that the client has already fetched the integrations
      const doIntegrations = [];
      for (const integration of store.state.selectedIntegrations) {
        if (store.state.integrations[integration] && store.state.integrations[integration].doable) {
          doIntegrations.push(integration);
        }
      }

      fetch('api/integration/search', {
        method: 'POST',
        headers: setReqHeaders({ 'Content-Type': 'application/json' }),
        body: JSON.stringify({ query: searchTerm, skipCache, skipChildren, doIntegrations, tags, viewId })
      }).then((response) => {
        if (!response.ok) { // test for bad response code (only on first chunk)
          throw new Error(response.statusText);
        }
        return response.body;
      }).then((rStream) => {
        const reader = rStream.getReader();
        const sendChunk = this.sendChunk;
        const decoder = this.decoder;

        return new ReadableStream({
          start () {
            let remaining = '';

            function read () { // handle each data chunk
              reader.read().then(({ done, value }) => {
                if (done) { // stream is done
                  if (remaining.length) {
                    sendChunk(subscriber, remaining);
                  }
                  return subscriber.complete();
                }

                remaining += decoder(value);

                let pos = 0;
                while ((pos = remaining.indexOf('\n')) > -1) {
                  // - 1 = remove the trailing , or ]
                  sendChunk(subscriber, remaining.slice(0, pos - 1));
                  // keep the rest because it may not be complete
                  remaining = remaining.slice(pos + 1, remaining.length);
                }

                read(); // keep reading until done
              });
            }

            read();
          }
        });
      }).catch((err) => { // this catches an issue within the ^ .then
        subscriber.error(err);
        return subscriber.complete();
      });
    });
  }
Fetches integration data from the server given a search term.
Watches for a stream of data then reads and parses each chunk.
Sends chunks back to the subscriber of the search.
@param {String} searchTerm - The query to search the integrations for
@param {Boolean} skipCache - Whether to use the cached result or fetch it new
@param {Boolean} skipChildren - Whether to process the children queries
@param {string[]} tags - Tags applied at the time of search
@param {string | undefined} viewId - The ID of the view at the time of search (if any)
@returns {Observable} - The observable object to subscribe to updates