rxjs#NEVER TypeScript Examples

The following examples show how to use rxjs#NEVER. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: context-service.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 6 votes vote down vote up
public observe$<T>(name: string, options?: ContextLookupOptions): Observable<T | T[] | null> {
    if (Beans.get(IS_PLATFORM_HOST)) {
      return concat(of(options?.collect ? [] : null), NEVER);
    }

    return this._contextTreeChange$
      .pipe(
        filter(event => event.name === name),
        startWith(undefined as void),
        switchMap(() => this.lookupContextValue$<T>(name, options)),
      );
  }
Example #2
Source File: SetInterval.tsx    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
componentDidMount() {
    // Creating a subscription to propsSubject. This subject pushes values every time
    // SetInterval's props change
    this.subscription = this.propsSubject
      .pipe(
        // switchMap creates a new observables based on the input stream,
        // which becomes part of the propsSubject stream
        switchMap(props => {
          // If the query is live, empty value is emited. `of` creates single value,
          // which is merged to propsSubject stream
          if (RefreshPicker.isLive(props.interval)) {
            return of({});
          }

          // When query is loading, a new stream is merged. But it's a stream that emits no values(NEVER),
          // hence next call of this function will happen when query changes, and new props are passed into this component
          // When query is NOT loading, a new value is emited, this time it's an interval value,
          // which makes tap function below execute on that interval basis.
          return props.loading ? NEVER : interval(stringToMs(props.interval));
        }),
        // tap will execute function passed via func prop
        // * on value from `of` stream merged if query is live
        // * on specified interval (triggered by values emited by interval)
        tap(() => this.props.func())
      )
      .subscribe();

    // When component has mounted, propsSubject emits it's first value
    this.propsSubject.next(this.props);
  }
Example #3
Source File: get-connected-status.ts    From sdk with MIT License 6 votes vote down vote up
export function getConnectedStatus(provider: WalletConnectProvider): Observable<ConnectStatus> {
	if ("on" in provider) {
		return new Observable<ConnectStatus>(subscriber => {
			subscriber.next("connected")
			function handler() {
				subscriber.next("disconnected")
			}
			provider.on("disconnected", handler)
			if ("removeListener" in provider) {
				subscriber.add(() => {
					provider.removeListener("disconnected", handler)
				})
			}
		})
	} else {
		return concat(of("connected" as const), NEVER)
	}
}
Example #4
Source File: index.tsx    From houston with MIT License 6 votes vote down vote up
/**
 * Create a memoized observable and unsubscribe automatically if component unmount and a
 * refresh function
 * @returns [observableValue, error, completed, refreshFunction, loading]
 */
export default function useObservableRefresh<T>(
  observableGenerator: () => Observable<T>,
  deps: React.DependencyList
): [T | undefined, any, boolean, () => void, boolean] {
  const [data, setData] = React.useState<T | undefined>();
  const [error, setError] = React.useState();
  const doRetry$ = React.useRef(new BehaviorSubject<boolean>(true)).current;

  const [, , completed, loading] = useObservable(() => {
    return doRetry$.pipe(
      tap(() => {
        setData(undefined);
        setError(undefined);
      }),
      switchMap(() =>
        observableGenerator().pipe(
          tap(result => setData(result)),
          catchError(err => {
            getConfig().onUnhandledError(err, 'hooks');
            setError(err);
            return NEVER;
          })
        )
      )
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);

  const refresh = React.useCallback(() => doRetry$.next(true), [doRetry$]);

  return [data, error, completed, refresh, loading];
}
Example #5
Source File: microfrontend-fixture.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 6 votes vote down vote up
/**
   * Removes the iframe from the DOM, if any.
   */
  public removeIframe(): this {
    this._disposables.forEach(disposable => disposable());
    this._disposables.clear();

    if (this.iframe) {
      this.iframe.remove();
      this.iframe = null;
      this.message$ instanceof Subject && this.message$.complete();
      this.message$ = NEVER;
      this._unmount$.next();
    }
    return this;
  }
Example #6
Source File: microfrontend-fixture.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 6 votes vote down vote up
/**
   * Instructs the iframe to load content from given URL.
   */
  public setUrl(url: string): void {
    if (!this.iframe) {
      throw Error('[MicrofrontendFixtureError] Iframe not found.');
    }
    this.iframe.setAttribute('src', url);
    this.message$ = NEVER;
  }
Example #7
Source File: context-service.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 6 votes vote down vote up
/**
   * Observes the names of context values registered at any level in the context tree.
   *
   * @return An Observable that emits the names of context values registered at any level in the context tree.
   *         Upon subscription, it emits the names of context values currently registered, and then it emits whenever
   *         some value is registered or unregistered from a context. The Observable never completes.
   */
  public names$(): Observable<Set<string>> {
    if (Beans.get(IS_PLATFORM_HOST)) {
      return concat(of(new Set<string>()), NEVER);
    }

    return this._contextTreeChange$
      .pipe(
        startWith(undefined as void),
        switchMap(() => this.lookupContextNames$()),
      );
  }
Example #8
Source File: message-handler.script.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 5 votes vote down vote up
export async function connectToHostThenIntentClientOnIntent({symbolicName}): Promise<void> { // eslint-disable-line @typescript-eslint/typedef
  await MicrofrontendPlatform.connectToHost(symbolicName);
  Beans.get(IntentClient).onIntent<void>({type: 'capability'}, () => concat(of('initial'), NEVER));
}
Example #9
Source File: message-handler.script.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 5 votes vote down vote up
export async function connectToHostThenMessageClientOnMessage({symbolicName}): Promise<void> { // eslint-disable-line @typescript-eslint/typedef
  await MicrofrontendPlatform.connectToHost(symbolicName);
  Beans.get(MessageClient).onMessage<void, never>('topic', () => NEVER);
}
Example #10
Source File: broker-gateway.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 5 votes vote down vote up
public subscribeToTopic$<T>(topic: string): Observable<TopicMessage<T>> {
    return NEVER;
  }
Example #11
Source File: microfrontend-fixture.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 5 votes vote down vote up
/**
   * Messages sent by the script.
   *
   * Each time a script is loaded into the iframe, a new Observable is created. Upon subscription,
   * any old messages that the script has already emitted will be "replayed".
   */
  public message$: Observable<any> = NEVER;
Example #12
Source File: broker-gateway.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 5 votes vote down vote up
public requestReply$<T = any>(channel: MessagingChannel, message: IntentMessage | TopicMessage): Observable<TopicMessage<T>> {
    return NEVER;
  }
Example #13
Source File: universal-animation-frame.ts    From universal with MIT License 5 votes vote down vote up
UNIVERSAL_ANIMATION_FRAME: ValueProvider = {
    provide: ANIMATION_FRAME,
    useValue: NEVER,
}
Example #14
Source File: mockPools.ts    From guardian with Apache License 2.0 5 votes vote down vote up
MockApiRx = of({
  ...laminarRpc,
  query: {
    baseLiquidityPoolsForMargin: {
      pools: {
        entries: () => of([[poolsKey(0), POOL]])
      }
    },
    marginLiquidityPools: {
      poolTradingPairOptions: {
        entries: () => of([[poolTradingPairOptionsKey(0, PAIR), PAIR_OPTION]])
      },
      tradingPairOptions: () => of(registry.createType('MarginTradingPairOption', { maxSpread: 10 })),
      poolOptions: () => of(registry.createType('MarginPoolOption')),
      defaultMinLeveragedAmount: () => of(registry.createType('Balance'))
    },
    baseLiquidityPoolsForSynthetic: {
      pools: () => of(POOL),
      nextPoolId: () => of(1)
    },
    syntheticLiquidityPools: {
      poolCurrencyOptions: () => of(CURRENCY_OPTION)
    },
    syntheticTokens: {
      positions: () =>
        of(
          registry.createType('SyntheticPosition', {
            collateral: '2054470869988219348',
            synthetic: '1653740113770234636'
          })
        ),
      ratios: () => of(registry.createType('SyntheticTokensRatio'))
    },
    laminarOracle: {
      values: (token) => {
        if (token.toString() === 'FEUR') {
          return of(registry.createType('Option<TimestampedValueOf>', { value: '1200000000000000000' }));
        }
        if (token.toString() === 'LAMI') {
          return of(registry.createType('Option<TimestampedValueOf>', { value: '2000000000000000000' }));
        }
        return NEVER;
      }
    }
  },
  createType: (type: string, value: any) => registry.createType(type, value)
})
Example #15
Source File: connector.ts    From sdk with MIT License 5 votes vote down vote up
constructor(
		private readonly providers: ConnectionProvider<Option, Connection>[],
		private readonly stateProvider?: IConnectorStateProvider,
	) {
		Connector.initPageUnloadProtection()

		this.add = this.add.bind(this)
		this.connect = this.connect.bind(this)

		this.connection = concat(
			of(STATE_INITIALIZING),
			defer(() => this.checkAutoConnect()),
			this.provider.pipe(
				distinctUntilChanged(),
				switchMap(provider => {
					if (provider) {
						return concat(provider.getConnection(), NEVER).pipe(
							catchError(error => concat(of(getStateDisconnected({ error })), NEVER)),
						)
					} else {
						return concat(of(getStateDisconnected()), NEVER)
					}
				}),
			),
		).pipe(
			distinctUntilChanged((c1, c2) => {
				if (Connector.pageUnloading) return true
				if (c1 === c2) return true
				if (c1.status === "connected" && c2.status === "connected") {
					return c1.connection === c2.connection
				} else if (c1.status === "connecting" && c2.status === "connecting") {
					return c1.providerId === c2.providerId
				}
				return c1.status === c2.status
			}),
			shareReplay(1),
			map(conn => {
				if (conn.status === "connected") {
					return {
						...conn,
						disconnect: async () => {
							if (conn.disconnect !== undefined) {
								try {
									await conn.disconnect()
								} catch (e) {
									console.warn("caught on disconnect", e)
								}
							}
							this.provider.next(undefined)
						},
					}
				} else {
					return conn
				}
			}),
			tap(async conn => {
				if (conn.status === "disconnected" && !Connector.pageUnloading) {
					this.provider.next(undefined)
					const current = await this.stateProvider?.getValue()
					if (current !== undefined) {
						this.stateProvider?.setValue(undefined)
					}
				}
			}),
		)
	}
Example #16
Source File: map-to-latest-from.spec.ts    From s-libs with MIT License 5 votes vote down vote up
describe('mapToLatestFrom()', () => {
  it(
    'emits the latest value from the given observable',
    marbleTest(({ cold, expectObservable, expectSubscriptions }) => {
      const source = cold('-1---2--3------4-|');
      const inner = cold(' ---a------b--c----');
      const subs = '       ^----------------!';
      const expected = '   -----a--a------c-|';

      expectObservable(source.pipe(mapToLatestFrom(inner))).toBe(expected);
      expectSubscriptions(source.subscriptions).toBe(subs);
      expectSubscriptions(inner.subscriptions).toBe(subs);
    }),
  );

  it(
    'handles errors from the inner observable',
    marbleTest(({ cold, expectObservable, expectSubscriptions }) => {
      const source = cold('--1-|');
      const inner = cold(' -#---');
      const subs = '       ^!---';
      const expected = '   -#---';

      expectObservable(source.pipe(mapToLatestFrom(inner))).toBe(expected);
      expectSubscriptions(source.subscriptions).toBe(subs);
      expectSubscriptions(inner.subscriptions).toBe(subs);
    }),
  );

  it(
    'handles completion of the inner observable',
    marbleTest(({ cold, expectObservable }) => {
      const source = cold('--1-2-|');
      const inner = cold(' -a-|   ');
      const expected = '   --a-a-|';

      expectObservable(source.pipe(mapToLatestFrom(inner))).toBe(expected);
    }),
  );

  it(
    'passes along unsubscribes',
    testUnsubscribePropagation(() => mapToLatestFrom(NEVER)),
  );

  it(
    'passes along errors',
    testErrorPropagation(() => mapToLatestFrom(NEVER)),
  );

  it(
    'passes along completion',
    testCompletionPropagation(() => mapToLatestFrom(NEVER)),
  );
});
Example #17
Source File: index.ts    From dbm with Apache License 2.0 5 votes vote down vote up
/* ----------------------------------------------------------------------------
 * Functions
 * ------------------------------------------------------------------------- */

/**
 * Transform a stylesheet
 *
 * @param options - Options
 *
 * @returns File observable
 */
export function transformStyle(
  options: TransformOptions
): Observable<string> {
  return defer(() => promisify(sass)({
    file: options.from,
    outFile: options.to,
    includePaths: [
      "src/assets/stylesheets",
      "node_modules/modularscale-sass/stylesheets",
      "node_modules/material-design-color",
      "node_modules/material-shadows"
    ],
    sourceMap: true,
    sourceMapContents: true
  }))
    .pipe(
      switchMap(({ css, map }) => postcss([
        require("autoprefixer"),
        require("postcss-inline-svg")({
          paths: [
            `${base}/.icons`
          ],
          encode: false
        }),
        ...process.argv.includes("--optimize")
          ? [require("cssnano")]
          : []
      ])
        .process(css, {
          from: options.from,
          map: {
            prev: `${map}`,
            inline: false
          }
        })
      ),
      catchError(err => {
        console.log(err.formatted || err.message)
        return NEVER
      }),
      switchMap(({ css, map }) => {
        const file = digest(options.to, css)
        return concat(
          mkdir(path.dirname(file)),
          merge(
            write(`${file}.map`, `${map}`.replace(root, "")),
            write(`${file}`, css.replace(
              options.from,
              path.basename(file)
            )),
          )
        )
          .pipe(
            ignoreElements(),
            endWith(file)
          )
      })
    )
}
Example #18
Source File: microfrontend-fixture.spec.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 4 votes vote down vote up
describe('MicrofrontendFixture', () => {

  const disposables = new Set<Disposable>();

  afterEach(() => disposables.forEach(disposable => disposable()));

  it('should error if the script does not end with ".script.ts"', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    expect(() => fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/some-script.ts', 'noop')).toThrowError(/MicrofrontendFixtureError/);
  });

  it('should insert the iframe to the DOM', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().setUrl('about:blank');
    expect(Array.from(document.body.children).includes(fixture.iframe)).toBeTrue();
  });

  it('should load the passed script into the iframe', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_1');
    expect(fixture.iframe.contentDocument.querySelector('div.testee')).toBeDefined();
  });

  it('should allow to return data from the script', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_2');

    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.getValues()).toEqual(['a', 'b', 'c']);
  });

  it('should support reporting an error from the script', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    const whenScriptLoaded = fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_3');

    // Expect the script promise to reject.
    await expectAsync(whenScriptLoaded).toBeRejectedWithError('ERROR FROM SCRIPT');

    // Expect the data Observable to error.
    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.hasErrored()).toBeTrue();
    expect(captor.getError()).toEqual(Error('ERROR FROM SCRIPT'));
  });

  it('should report an error if script execution errors', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    const whenScriptLoaded = fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_4');

    // Expect the script promise to reject.
    await expectAsync(whenScriptLoaded).toBeRejectedWithError('SCRIPT EXECUTION ERROR');

    // Expect the data Observable to error.
    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.hasErrored()).toBeTrue();
    expect(captor.getError()).toEqual(Error('SCRIPT EXECUTION ERROR'));
  });

  it('should support completing the data observable from the script', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_5');

    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.hasCompleted()).toBeTrue();
  });

  it('should wait until finished loading the script', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_6');
    expect(fixture.iframe.contentDocument.querySelector('div.testee.delayed')).toBeDefined();
  });

  it('should allow the script to import project-specific types', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_7');
    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.getLastValue()).not.toBeUndefined();
  });

  it('should allow the script to import vendor-specific types', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_8');
    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.getLastValue()).not.toBeUndefined();
  });

  it('should support loading a web page', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    fixture.insertIframe().setUrl('about:blank');
    expect(fixture.iframe.contentWindow.location.href).toEqual('about:blank');
  });

  it('should support loading another script into the iframe', async () => {
    const fixture = registerFixture(new MicrofrontendFixture()).insertIframe();
    await fixture.loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_9a');
    expect(fixture.iframe.contentDocument.querySelector('div.testee-1')).toBeDefined();
    expect(fixture.iframe.contentDocument.querySelector('div.testee-2')).toBeNull();

    await fixture.loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_9b');
    expect(fixture.iframe.contentDocument.querySelector('div.testee-1')).toBeNull();
    expect(fixture.iframe.contentDocument.querySelector('div.testee-2')).toBeDefined();
  });

  it('should destroy the iframe on unmount', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    fixture.insertIframe();
    fixture.iframe.classList.add('testee');
    await fixture.loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'noop');
    expect(document.querySelector('iframe.testee')).toBeDefined();

    fixture.removeIframe();
    expect(fixture.iframe).toBeNull();
    expect(document.querySelector('iframe.testee')).toBeNull();
    expect(fixture.message$).toBe(NEVER);
  });

  it('should complete the data observable on unmount', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'noop');
    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);

    expect(captor.hasCompleted()).toBeFalse();

    fixture.removeIframe();
    expect(captor.hasCompleted()).toBeTrue();
    expect(fixture.message$).toBe(NEVER);
  });

  it('should support loading a script after unmounting the iframe', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    fixture.insertIframe();
    await fixture.loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_10a');
    const captor1 = new ObserveCaptor();

    fixture.message$.subscribe(captor1);
    expect(captor1.getValues()).toEqual(['ready (10a)']);

    fixture.removeIframe();

    // Mount a new iframe
    fixture.insertIframe();
    const captor2 = new ObserveCaptor();
    await fixture.loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_10b');
    fixture.message$.subscribe(captor2);
    expect(captor1.getValues()).toEqual(['ready (10a)']);
    expect(captor2.getValues()).toEqual(['ready (10b)']);
  });

  it('should allow passing arguments to the script', async () => {
    const fixture = registerFixture(new MicrofrontendFixture());
    const args = {stringArg: 'value1', numberArg: 123, booleanArg: true, objectArg: {key: 'value'}};
    await fixture.insertIframe().loadScript('./lib/testing/microfrontend-fixture/microfrontend-fixture.script.ts', 'testcase_11', args);

    const captor = new ObserveCaptor();
    fixture.message$.subscribe(captor);
    expect(captor.getLastValue()).toEqual(args);
  });

  /**
   * Registers passed fixture for destruction after test execution.
   */
  function registerFixture(fixture: MicrofrontendFixture): MicrofrontendFixture {
    disposables.add(() => fixture.removeIframe());
    return fixture;
  }
});
Example #19
Source File: attach-to-one-search.ts    From js-client with MIT License 4 votes vote down vote up
makeAttachToOneSearch = (context: APIContext) => {
	const subscribeToOneRawSearch = makeSubscribeToOneRawSearch(context);
	let rawSubscriptionP: ReturnType<typeof subscribeToOneRawSearch> | null = null;
	let closedSub: Subscription | null = null;

	return async (
		searchID: ID,
		options: { filter?: Omit<SearchFilter, 'elementFilters'> } = {},
	): Promise<SearchSubscription> => {
		if (isNull(rawSubscriptionP)) {
			rawSubscriptionP = subscribeToOneRawSearch();
			if (closedSub?.closed === false) {
				closedSub.unsubscribe();
			}

			// Handles websocket hangups from close or error
			closedSub = from(rawSubscriptionP)
				.pipe(
					concatMap(rawSubscription => rawSubscription.received$),
					catchError(() => EMPTY),
				)
				.subscribe({
					complete: () => {
						rawSubscriptionP = null;
					},
				});
		}
		const rawSubscription = await rawSubscriptionP;

		const searchAttachMsg = await attachSearch(rawSubscription, searchID);
		const searchTypeID = searchAttachMsg.data.Subproto;

		// The default dates are the StartRange and EndRange used to create the search
		const defaultStart = new Date(searchAttachMsg.data.Info.StartRange);
		const defaultEnd = new Date(searchAttachMsg.data.Info.EndRange);

		let closed = false;
		const close$ = new Subject<void>();

		const initialFilter: RequiredSearchFilter = {
			entriesOffset: {
				index: options.filter?.entriesOffset?.index ?? 0,
				count: options.filter?.entriesOffset?.count ?? 100,
			},
			dateRange:
				options.filter?.dateRange === 'preview'
					? ('preview' as const)
					: {
							start: options.filter?.dateRange?.start ?? defaultStart,
							end: options.filter?.dateRange?.end ?? defaultEnd,
					  },
			// *NOTE: The default granularity is recalculated when we receive the renderer type
			desiredGranularity: options.filter?.desiredGranularity ?? 100,
			overviewGranularity: options.filter?.overviewGranularity ?? 90,
			zoomGranularity: options.filter?.zoomGranularity ?? 90,
			elementFilters: [],
		};
		const initialFilterID = uniqueId(SEARCH_FILTER_PREFIX);

		const filtersByID: Record<string, SearchFilter | undefined> = {};
		filtersByID[initialFilterID] = initialFilter;

		const isResponseError = filterMessageByCommand(SearchMessageCommands.ResponseError);
		const searchMessages$ = rawSubscription.received$.pipe(
			filter(msg => msg.type === searchTypeID),
			tap(msg => {
				// Throw if the search message command is Error
				if (isResponseError(msg)) {
					throw new Error(msg.data.Error);
				}

				// Listen for close messages and emit on close$
				const isCloseMsg = filterMessageByCommand(SearchMessageCommands.Close);
				if (isCloseMsg(msg)) {
					close$.next();
					close$.complete();
					closed = true;
				}
			}),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);
		const rendererType = searchAttachMsg.data.RendererMod;

		type DateRange = { start: Date; end: Date };
		const previewDateRange: DateRange = await (async (): Promise<DateRange> => {
			// Not in preview mode, so return the initial filter date range, whatever, it won't be used
			if (initialFilter.dateRange !== 'preview') return initialFilter.dateRange;

			// In preview mode, so we need to request search details and use the timerange that we get back
			const detailsP = firstValueFrom(
				searchMessages$.pipe(filter(filterMessageByCommand(SearchMessageCommands.RequestDetails))),
			);
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails },
			};
			rawSubscription.send(requestDetailsMsg);
			const details = await detailsP;

			return {
				start: new Date(details.data.SearchInfo.StartRange),
				end: new Date(details.data.SearchInfo.EndRange),
			};
		})();

		const close = async (): Promise<void> => {
			if (closed) return undefined;

			const closeMsg: RawRequestSearchCloseMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.Close },
			};
			await rawSubscription.send(closeMsg);

			// Wait for closed message to be received
			await lastValueFrom(close$);
		};

		const progress$: Observable<Percentage> = searchMessages$.pipe(
			map(msg => (msg as Partial<RawResponseForSearchDetailsMessageReceived>).data?.Finished ?? null),
			filter(isBoolean),
			map(done => (done ? 1 : 0)),
			distinctUntilChanged(),
			map(rawPercentage => new Percentage(rawPercentage)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const entries$: Observable<SearchEntries> = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestEntriesWithinRange)),
			map(
				(msg): SearchEntries => {
					const base = toSearchEntries(rendererType, msg);
					const filterID = (msg.data.Addendum?.filterID as string | undefined) ?? null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;
					return { ...base, filter } as SearchEntries;
				},
			),
			tap(entries => {
				const defDesiredGranularity = getDefaultGranularityByRendererType(entries.type);
				initialFilter.desiredGranularity = defDesiredGranularity;
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const _filter$ = new BehaviorSubject<SearchFilter>(initialFilter);
		const setFilter = (filter: SearchFilter | null): void => {
			if (closed) return undefined;
			_filter$.next(filter ?? initialFilter);
		};

		const filter$ = createRequiredSearchFilterObservable({
			filter$: _filter$.asObservable(),
			initialFilter,
			previewDateRange,
			defaultValues: {
				dateStart: defaultStart,
				dateEnd: defaultEnd,
			},
		}).pipe(
			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const nextDetailsMsg = () =>
			firstValueFrom(
				searchMessages$.pipe(
					filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),
					// cleanup: Complete when/if the user calls .close()
					takeUntil(close$),
				),
			);

		let pollingSubs: Subscription;

		const requestEntries = async (filter: RequiredSearchFilter): Promise<void> => {
			if (closed) return undefined;

			if (!isNil(pollingSubs)) {
				pollingSubs.unsubscribe();
			}
			pollingSubs = new Subscription();

			const filterID = uniqueId(SEARCH_FILTER_PREFIX);
			filtersByID[filterID] = filter;

			const first = filter.entriesOffset.index;
			const last = first + filter.entriesOffset.count;
			const startDate = filter.dateRange === 'preview' ? previewDateRange.start : filter.dateRange.start;
			const start = startDate.toISOString();
			const endDate = filter.dateRange === 'preview' ? previewDateRange.end : filter.dateRange.end;
			const end = endDate.toISOString();
			// TODO: Filter by .desiredGranularity and .fieldFilters

			// Set up a promise to wait for the next details message
			const detailsMsgP = nextDetailsMsg();
			// Send a request for details
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails, Addendum: { filterID } },
			};
			const detailsP = rawSubscription.send(requestDetailsMsg);

			// Grab the results from the details response (we need it later)
			const detailsResults = await Promise.all([detailsP, detailsMsgP]);
			const detailsMsg = detailsResults[1];

			// Dynamic duration for debounce a after each event, starting from 1s and increasing 500ms after each event,
			// never surpass 4s, reset to 1s if the request is finished
			const debounceOptions = {
				initialDueTime: 1000,
				step: 500,
				maxDueTime: 4000,
				predicate: (isFinished: boolean) => !isFinished, // increase backoff while isFinished is false
			};

			// Keep sending requests for search details until Finished is true
			pollingSubs.add(
				rawSearchDetails$
					.pipe(
						// We've already received one details message - use it to start
						startWith(detailsMsg),

						// Extract the property that indicates if the data is finished
						map(details => (details ? details.data.Finished : false)),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestDetailsMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);

			const requestEntriesMsg: RawRequestSearchEntriesWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestEntriesWithinRange,
					Addendum: { filterID },
					EntryRange: {
						First: first,
						Last: last,
						StartTS: start,
						EndTS: end,
					},
				},
			};
			// Keep sending requests for entries until finished is true
			pollingSubs.add(
				entries$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(entries => (entries ? entries.finished : false)),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestEntriesMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const entriesP = rawSubscription.send(requestEntriesMsg);

			const requestStatsMessage: RawRequestSearchStatsMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestAllStats,
					Addendum: { filterID },
					Stats: { SetCount: filter.overviewGranularity },
				},
			};
			// Keep sending requests for stats until finished is true
			pollingSubs.add(
				rawSearchStats$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsMessage)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsP = rawSubscription.send(requestStatsMessage);

			const requestStatsWithinRangeMsg: RawRequestSearchStatsWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestStatsInRange,
					Addendum: { filterID },
					Stats: {
						SetCount: filter.zoomGranularity,
						SetEnd: recalculateZoomEnd(
							detailsMsg ? detailsMsg.data.SearchInfo.MinZoomWindow : 1,
							filter.zoomGranularity,
							startDate,
							endDate,
						).toISOString(),
						SetStart: start,
					},
				},
			};
			// Keep sending requests for stats-within-range until finished is true
			pollingSubs.add(
				rawStatsZoom$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						shareReplay({ bufferSize: 1, refCount: true }),
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsWithinRangeMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsRangeP = rawSubscription.send(requestStatsWithinRangeMsg);

			await Promise.all([entriesP, statsP, detailsP, statsRangeP]);
		};

		filter$.subscribe(filter => {
			requestEntries(filter);
			setTimeout(() => requestEntries(filter), 2000); // TODO: Change this
		});

		const rawSearchStats$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestAllStats)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawSearchDetails$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawStatsZoom$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestStatsInRange)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const stats$ = combineLatest([
			rawSearchStats$.pipe(distinctUntilChanged<RawResponseForSearchStatsMessageReceived>(isEqual)),
			rawSearchDetails$.pipe(distinctUntilChanged<RawResponseForSearchDetailsMessageReceived>(isEqual)),
		]).pipe(
			map(
				([rawStats, rawDetails]): SearchStats => {
					const filterID =
						(rawStats.data.Addendum?.filterID as string | undefined) ??
						(rawDetails.data.Addendum?.filterID as string | undefined) ??
						null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;

					const pipeline = rawStats.data.Stats.Set.map(s => s.Stats)
						.reduce<
							Array<Array<RawResponseForSearchStatsMessageReceived['data']['Stats']['Set'][number]['Stats'][number]>>
						>((acc, curr) => {
							curr.forEach((_curr, i) => {
								if (isUndefined(acc[i])) acc[i] = [];
								acc[i].push(_curr);
							});
							return acc;
						}, [])
						.map(s =>
							s
								.map(_s => ({
									module: _s.Name,
									arguments: _s.Args,
									duration: _s.Duration,
									input: {
										bytes: _s.InputBytes,
										entries: _s.InputCount,
									},
									output: {
										bytes: _s.OutputBytes,
										entries: _s.OutputCount,
									},
								}))
								.reduce((acc, curr) => ({
									...curr,
									duration: acc.duration + curr.duration,
									input: {
										bytes: acc.input.bytes + curr.input.bytes,
										entries: acc.input.entries + curr.input.entries,
									},
									output: {
										bytes: acc.output.bytes + curr.output.bytes,
										entries: acc.output.entries + curr.output.entries,
									},
								})),
						);

					return {
						id: rawDetails.data.SearchInfo.ID,
						userID: toNumericID(rawDetails.data.SearchInfo.UID),

						filter,
						finished: rawStats.data.Finished && rawDetails.data.Finished,

						query: searchAttachMsg.data.Info.UserQuery,
						effectiveQuery: searchAttachMsg.data.Info.EffectiveQuery,

						metadata: searchAttachMsg.data.Info.Metadata ?? {},
						entries: rawStats.data.EntryCount,
						duration: rawDetails.data.SearchInfo.Duration,
						start: new Date(rawDetails.data.SearchInfo.StartRange),
						end: new Date(rawDetails.data.SearchInfo.EndRange),
						minZoomWindow: rawDetails.data.SearchInfo.MinZoomWindow,
						downloadFormats: rawDetails.data.SearchInfo.RenderDownloadFormats,
						tags: searchAttachMsg.data.Info.Tags,

						storeSize: rawDetails.data.SearchInfo.StoreSize,
						processed: {
							entries: pipeline[0]?.input?.entries ?? 0,
							bytes: pipeline[0]?.input?.bytes ?? 0,
						},

						pipeline,
					};
				},
			),

			distinctUntilChanged<SearchStats>(isEqual),

			shareReplay({ bufferSize: 1, refCount: false }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsOverview$ = rawSearchStats$.pipe(
			map(set => {
				return { frequencyStats: countEntriesFromModules(set) };
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsZoom$ = rawStatsZoom$.pipe(
			map(set => {
				const filterID = (set.data.Addendum?.filterID as string | undefined) ?? null;
				const filter = filtersByID[filterID ?? ''] ?? undefined;

				const filterEnd = filter?.dateRange === 'preview' ? previewDateRange.end : filter?.dateRange?.end;
				const initialEnd = initialFilter.dateRange === 'preview' ? previewDateRange.end : initialFilter.dateRange.end;
				const endDate = filterEnd ?? initialEnd;

				return {
					frequencyStats: countEntriesFromModules(set).filter(f => !isAfter(f.timestamp, endDate)),
					filter,
				};
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const errors$: Observable<Error> = searchMessages$.pipe(
			// Skip every regular message. We only want to emit when there's an error
			skipUntil(NEVER),

			// When there's an error, catch it and emit it
			catchError(err => of(err)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		return {
			searchID,

			progress$,
			entries$,
			stats$,
			statsOverview$,
			statsZoom$,
			errors$,

			setFilter,
			close,
		};
	};
}
Example #20
Source File: subscribe-to-one-explorer-search.ts    From js-client with MIT License 4 votes vote down vote up
makeSubscribeToOneExplorerSearch = (context: APIContext) => {
	const modifyOneQuery = makeModifyOneQuery(context);
	const subscribeToOneRawSearch = makeSubscribeToOneRawSearch(context);
	let rawSubscriptionP: ReturnType<typeof subscribeToOneRawSearch> | null = null;
	let closedSub: Subscription | null = null;

	return async (
		query: Query,
		options: { filter?: SearchFilter; metadata?: RawJSON; noHistory?: boolean } = {},
	): Promise<ExplorerSearchSubscription> => {
		if (isNull(rawSubscriptionP)) {
			rawSubscriptionP = subscribeToOneRawSearch();
			if (closedSub?.closed === false) {
				closedSub.unsubscribe();
			}

			// Handles websocket hangups
			closedSub = from(rawSubscriptionP)
				.pipe(concatMap(rawSubscription => rawSubscription.received$))
				.subscribe({
					complete: () => {
						rawSubscriptionP = null;
					},
				});
		}
		const rawSubscription = await rawSubscriptionP;

		// The default end date is now
		const defaultEnd = new Date();

		// The default start date is one hour ago
		const defaultStart = subHours(defaultEnd, 1);

		let closed = false;
		const close$ = new Subject<void>();

		const initialFilter = {
			entriesOffset: {
				index: options.filter?.entriesOffset?.index ?? 0,
				count: options.filter?.entriesOffset?.count ?? 100,
			},
			dateRange:
				options.filter?.dateRange === 'preview'
					? ('preview' as const)
					: {
							start: options.filter?.dateRange?.start ?? defaultStart,
							end: options.filter?.dateRange?.end ?? defaultEnd,
					  },
			// *NOTE: The default granularity is recalculated when we receive the renderer type
			desiredGranularity: options.filter?.desiredGranularity ?? 100,

			overviewGranularity: options.filter?.overviewGranularity ?? 90,

			zoomGranularity: options.filter?.zoomGranularity ?? 90,

			elementFilters: options.filter?.elementFilters ?? [],
		};
		const initialFilterID = uniqueId(SEARCH_FILTER_PREFIX);

		const filtersByID: Record<string, SearchFilter | undefined> = {};
		filtersByID[initialFilterID] = initialFilter;

		const modifiedQuery =
			initialFilter.elementFilters.length === 0 ? query : await modifyOneQuery(query, initialFilter.elementFilters);

		const searchInitMsg = await initiateSearch(rawSubscription, modifiedQuery, {
			initialFilterID,
			metadata: options.metadata,
			range:
				initialFilter.dateRange === 'preview'
					? 'preview'
					: [initialFilter.dateRange.start, initialFilter.dateRange.end],
			noHistory: options.noHistory,
		});
		const searchTypeID = searchInitMsg.data.OutputSearchSubproto;
		const isResponseError = filterMessageByCommand(SearchMessageCommands.ResponseError);
		const searchMessages$ = rawSubscription.received$.pipe(
			filter(msg => msg.type === searchTypeID),
			tap(msg => {
				// Throw if the search message command is Error
				if (isResponseError(msg)) {
					throw new Error(msg.data.Error);
				}

				// Listen for close messages and emit on close$
				const isCloseMsg = filterMessageByCommand(SearchMessageCommands.Close);
				if (isCloseMsg(msg)) {
					close$.next();
					close$.complete();
					closed = true;
				}
			}),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);
		const rendererType = searchInitMsg.data.RenderModule;

		type DateRange = { start: Date; end: Date };
		const previewDateRange: DateRange = await (async (): Promise<DateRange> => {
			// Not in preview mode, so return the initial filter date range, whatever, it won't be used
			if (initialFilter.dateRange !== 'preview') return initialFilter.dateRange;

			// In preview mode, so we need to request search details and use the timerange that we get back
			const detailsP = firstValueFrom(
				searchMessages$.pipe(filter(filterMessageByCommand(SearchMessageCommands.RequestDetails))),
			);
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails },
			};
			rawSubscription.send(requestDetailsMsg);
			const details = await detailsP;

			return {
				start: new Date(details.data.SearchInfo.StartRange),
				end: new Date(details.data.SearchInfo.EndRange),
			};
		})();

		const close = async (): Promise<void> => {
			if (closed) return undefined;

			const closeMsg: RawRequestSearchCloseMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.Close },
			};
			await rawSubscription.send(closeMsg);

			// Wait for closed message to be received
			await lastValueFrom(close$);
		};

		const progress$: Observable<Percentage> = searchMessages$.pipe(
			map(msg => (msg as Partial<RawResponseForSearchDetailsMessageReceived>).data?.Finished ?? null),
			filter(isBoolean),
			map(done => (done ? 1 : 0)),
			distinctUntilChanged(),
			map(rawPercentage => new Percentage(rawPercentage)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const entries$: Observable<ExplorerSearchEntries> = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestExplorerEntriesWithinRange)),
			map(
				(msg): ExplorerSearchEntries => {
					const base = toSearchEntries(rendererType, msg);
					const filterID = (msg.data.Addendum?.filterID as string | undefined) ?? null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;
					const searchEntries = { ...base, filter } as SearchEntries;
					const explorerEntries = (msg.data.Explore ?? []).map(toDataExplorerEntry);
					return { ...searchEntries, explorerEntries };
				},
			),
			tap(entries => {
				const defDesiredGranularity = getDefaultGranularityByRendererType(entries.type);
				initialFilter.desiredGranularity = defDesiredGranularity;
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const _filter$ = new BehaviorSubject<SearchFilter>(initialFilter);
		const setFilter = (filter: SearchFilter | null): void => {
			if (closed) return undefined;
			_filter$.next(filter ?? initialFilter);
		};

		const filter$ = createRequiredSearchFilterObservable({
			filter$: _filter$.asObservable(),
			initialFilter,
			previewDateRange,
			defaultValues: {
				dateStart: defaultStart,
				dateEnd: defaultEnd,
			},
		}).pipe(
			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const nextDetailsMsg = () =>
			firstValueFrom(
				searchMessages$.pipe(
					filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),
					// cleanup: Complete when/if the user calls .close()
					takeUntil(close$),
				),
			);

		let pollingSubs: Subscription;

		const requestEntries = async (filter: RequiredSearchFilter): Promise<void> => {
			if (closed) return undefined;

			if (!isNil(pollingSubs)) {
				pollingSubs.unsubscribe();
			}
			pollingSubs = new Subscription();

			const filterID = uniqueId(SEARCH_FILTER_PREFIX);
			filtersByID[filterID] = filter;

			const first = filter.entriesOffset.index;
			const last = first + filter.entriesOffset.count;
			const startDate = filter.dateRange === 'preview' ? previewDateRange.start : filter.dateRange.start;
			const start = startDate.toISOString();
			const endDate = filter.dateRange === 'preview' ? previewDateRange.end : filter.dateRange.end;
			const end = endDate.toISOString();
			// TODO: Filter by .desiredGranularity and .fieldFilters

			// Set up a promise to wait for the next details message
			const detailsMsgP = nextDetailsMsg();
			// Send a request for details
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails, Addendum: { filterID } },
			};
			const detailsP = rawSubscription.send(requestDetailsMsg);

			// Grab the results from the details response (we need it later)
			const detailsResults = await Promise.all([detailsP, detailsMsgP]);
			const detailsMsg = detailsResults[1];

			// Dynamic duration for debounce a after each event, starting from 1s and increasing 500ms after each event,
			// never surpass 4s, reset to 1s if the request is finished
			const debounceOptions = {
				initialDueTime: 1000,
				step: 500,
				maxDueTime: 4000,
				predicate: (isFinished: boolean) => isFinished === false, // increase backoff while isFinished is false
			};

			// Keep sending requests for search details until Finished is true
			pollingSubs.add(
				rawSearchDetails$
					.pipe(
						// We've already received one details message - use it to start
						startWith(detailsMsg),

						// Extract the property that indicates if the data is finished
						map(details => (details ? details.data.Finished : false)),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestDetailsMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);

			const requestEntriesMsg: RawRequestExplorerSearchEntriesWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestExplorerEntriesWithinRange,
					Addendum: { filterID },
					EntryRange: {
						First: first,
						Last: last,
						StartTS: start,
						EndTS: end,
					},
				},
			};
			// Keep sending requests for entries until finished is true
			pollingSubs.add(
				entries$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(entries => (entries ? entries.finished : false)),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestEntriesMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const entriesP = rawSubscription.send(requestEntriesMsg);

			const requestStatsMessage: RawRequestSearchStatsMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestAllStats,
					Addendum: { filterID },
					Stats: { SetCount: filter.overviewGranularity },
				},
			};
			// Keep sending requests for stats until finished is true
			pollingSubs.add(
				rawSearchStats$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsMessage)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsP = rawSubscription.send(requestStatsMessage);

			const requestStatsWithinRangeMsg: RawRequestSearchStatsWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestStatsInRange,
					Addendum: { filterID },
					Stats: {
						SetCount: filter.zoomGranularity,
						SetEnd: recalculateZoomEnd(
							detailsMsg ? detailsMsg.data.SearchInfo.MinZoomWindow : 1,
							filter.zoomGranularity,
							startDate,
							endDate,
						).toISOString(),
						SetStart: start,
					},
				},
			};
			// Keep sending requests for stats-within-range until finished is true
			pollingSubs.add(
				rawStatsZoom$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsWithinRangeMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsRangeP = rawSubscription.send(requestStatsWithinRangeMsg);

			await Promise.all([entriesP, statsP, detailsP, statsRangeP]);
		};

		filter$.subscribe(filter => {
			requestEntries(filter);
			setTimeout(() => requestEntries(filter), 2000); // TODO: Change this
		});

		const rawSearchStats$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestAllStats)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawSearchDetails$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawStatsZoom$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestStatsInRange)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const stats$ = combineLatest([
			rawSearchStats$.pipe(distinctUntilChanged<RawResponseForSearchStatsMessageReceived>(isEqual)),
			rawSearchDetails$.pipe(distinctUntilChanged<RawResponseForSearchDetailsMessageReceived>(isEqual)),
		]).pipe(
			map(
				([rawStats, rawDetails]): SearchStats => {
					const filterID =
						(rawStats.data.Addendum?.filterID as string | undefined) ??
						(rawDetails.data.Addendum?.filterID as string | undefined) ??
						null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;

					const pipeline = rawStats.data.Stats.Set.map(s => s.Stats)
						.reduce<
							Array<Array<RawResponseForSearchStatsMessageReceived['data']['Stats']['Set'][number]['Stats'][number]>>
						>((acc, curr) => {
							curr.forEach((_curr, i) => {
								if (isUndefined(acc[i])) acc[i] = [];
								acc[i].push(_curr);
							});
							return acc;
						}, [])
						.map(s =>
							s
								.map(_s => ({
									module: _s.Name,
									arguments: _s.Args,
									duration: _s.Duration,
									input: {
										bytes: _s.InputBytes,
										entries: _s.InputCount,
									},
									output: {
										bytes: _s.OutputBytes,
										entries: _s.OutputCount,
									},
								}))
								.reduce((acc, curr) => ({
									...curr,
									duration: acc.duration + curr.duration,
									input: {
										bytes: acc.input.bytes + curr.input.bytes,
										entries: acc.input.entries + curr.input.entries,
									},
									output: {
										bytes: acc.output.bytes + curr.output.bytes,
										entries: acc.output.entries + curr.output.entries,
									},
								})),
						);

					return {
						id: rawDetails.data.SearchInfo.ID,
						userID: toNumericID(rawDetails.data.SearchInfo.UID),

						filter,
						finished: rawStats.data.Finished && rawDetails.data.Finished,

						query: searchInitMsg.data.RawQuery,
						effectiveQuery: searchInitMsg.data.SearchString,

						metadata: searchInitMsg.data.Metadata,
						entries: rawStats.data.EntryCount,
						duration: rawDetails.data.SearchInfo.Duration,
						start: new Date(rawDetails.data.SearchInfo.StartRange),
						end: new Date(rawDetails.data.SearchInfo.EndRange),
						minZoomWindow: rawDetails.data.SearchInfo.MinZoomWindow,
						downloadFormats: rawDetails.data.SearchInfo.RenderDownloadFormats,
						tags: searchInitMsg.data.Tags,

						storeSize: rawDetails.data.SearchInfo.StoreSize,
						processed: {
							entries: pipeline[0]?.input?.entries ?? 0,
							bytes: pipeline[0]?.input?.bytes ?? 0,
						},

						pipeline,
					};
				},
			),

			distinctUntilChanged<SearchStats>(isEqual),

			shareReplay({ bufferSize: 1, refCount: false }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsOverview$ = rawSearchStats$.pipe(
			map(set => {
				return { frequencyStats: countEntriesFromModules(set) };
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsZoom$ = rawStatsZoom$.pipe(
			map(set => {
				const filterID = (set.data.Addendum?.filterID as string | undefined) ?? null;
				const filter = filtersByID[filterID ?? ''] ?? undefined;

				const filterEnd = filter?.dateRange === 'preview' ? previewDateRange.end : filter?.dateRange?.end;
				const initialEnd = initialFilter.dateRange === 'preview' ? previewDateRange.end : initialFilter.dateRange.end;
				const endDate = filterEnd ?? initialEnd;

				return {
					frequencyStats: countEntriesFromModules(set).filter(f => !isAfter(f.timestamp, endDate)),
					filter,
				};
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const errors$: Observable<Error> = searchMessages$.pipe(
			// Skip every regular message. We only want to emit when there's an error
			skipUntil(NEVER),

			// When there's an error, catch it and emit it
			catchError(err => of(err)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		return {
			searchID: searchInitMsg.data.SearchID.toString(),

			progress$,
			entries$,
			stats$,
			statsOverview$,
			statsZoom$,
			errors$,

			setFilter,
			close,
		};
	};
}
Example #21
Source File: subscribe-to-one-search.ts    From js-client with MIT License 4 votes vote down vote up
makeSubscribeToOneSearch = (context: APIContext) => {
	const modifyOneQuery = makeModifyOneQuery(context);
	const subscribeToOneRawSearch = makeSubscribeToOneRawSearch(context);
	let rawSubscriptionP: ReturnType<typeof subscribeToOneRawSearch> | null = null;
	let closedSub: Subscription | null = null;

	return async (
		query: Query,
		options: { filter?: SearchFilter; metadata?: RawJSON; noHistory?: boolean } = {},
	): Promise<SearchSubscription> => {
		if (isNull(rawSubscriptionP)) {
			rawSubscriptionP = subscribeToOneRawSearch();
			if (closedSub?.closed === false) {
				closedSub.unsubscribe();
			}

			// Handles websocket hangups from close or error
			closedSub = from(rawSubscriptionP)
				.pipe(
					concatMap(rawSubscription => rawSubscription.received$),
					catchError(() => EMPTY),
				)
				.subscribe({
					complete: () => {
						rawSubscriptionP = null;
					},
				});
		}
		const rawSubscription = await rawSubscriptionP;

		// The default end date is now
		const defaultEnd = new Date();

		// The default start date is one hour ago
		const defaultStart = subHours(defaultEnd, 1);

		let closed = false;
		const close$ = new Subject<void>();

		const initialFilter = {
			entriesOffset: {
				index: options.filter?.entriesOffset?.index ?? 0,
				count: options.filter?.entriesOffset?.count ?? 100,
			},
			dateRange:
				options.filter?.dateRange === 'preview'
					? ('preview' as const)
					: {
							start: options.filter?.dateRange?.start ?? defaultStart,
							end: options.filter?.dateRange?.end ?? defaultEnd,
					  },
			// *NOTE: The default granularity is recalculated when we receive the renderer type
			desiredGranularity: options.filter?.desiredGranularity ?? 100,

			overviewGranularity: options.filter?.overviewGranularity ?? 90,

			zoomGranularity: options.filter?.zoomGranularity ?? 90,

			elementFilters: options.filter?.elementFilters ?? [],
		};
		const initialFilterID = uniqueId(SEARCH_FILTER_PREFIX);

		const filtersByID: Record<string, SearchFilter | undefined> = {};
		filtersByID[initialFilterID] = initialFilter;

		const modifiedQuery =
			initialFilter.elementFilters.length === 0 ? query : await modifyOneQuery(query, initialFilter.elementFilters);

		const searchInitMsg = await initiateSearch(rawSubscription, modifiedQuery, {
			initialFilterID,
			metadata: options.metadata,
			range:
				initialFilter.dateRange === 'preview'
					? 'preview'
					: [initialFilter.dateRange.start, initialFilter.dateRange.end],
			noHistory: options.noHistory,
		});
		const searchTypeID = searchInitMsg.data.OutputSearchSubproto;
		const isResponseError = filterMessageByCommand(SearchMessageCommands.ResponseError);
		const searchMessages$ = rawSubscription.received$.pipe(
			filter(msg => msg.type === searchTypeID),
			tap(msg => {
				// Throw if the search message command is Error
				if (isResponseError(msg)) {
					throw new Error(msg.data.Error);
				}

				// Listen for close messages and emit on close$
				const isCloseMsg = filterMessageByCommand(SearchMessageCommands.Close);
				if (isCloseMsg(msg)) {
					close$.next();
					close$.complete();
					closed = true;
				}
			}),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);
		const rendererType = searchInitMsg.data.RenderModule;

		type DateRange = { start: Date; end: Date };
		const previewDateRange: DateRange = await (async (): Promise<DateRange> => {
			// Not in preview mode, so return the initial filter date range, whatever, it won't be used
			if (initialFilter.dateRange !== 'preview') return initialFilter.dateRange;

			// In preview mode, so we need to request search details and use the timerange that we get back
			const detailsP = firstValueFrom(
				searchMessages$.pipe(filter(filterMessageByCommand(SearchMessageCommands.RequestDetails))),
			);
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails },
			};
			rawSubscription.send(requestDetailsMsg);
			const details = await detailsP;

			return {
				start: new Date(details.data.SearchInfo.StartRange),
				end: new Date(details.data.SearchInfo.EndRange),
			};
		})();

		const close = async (): Promise<void> => {
			if (closed) return undefined;

			const closeMsg: RawRequestSearchCloseMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.Close },
			};
			await rawSubscription.send(closeMsg);

			// Wait for closed message to be received
			await lastValueFrom(close$);
		};

		const progress$: Observable<Percentage> = searchMessages$.pipe(
			map(msg => (msg as Partial<RawResponseForSearchDetailsMessageReceived>).data?.Finished ?? null),
			filter(isBoolean),
			map(done => (done ? 1 : 0)),
			distinctUntilChanged(),
			map(rawPercentage => new Percentage(rawPercentage)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const entries$: Observable<SearchEntries> = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestEntriesWithinRange)),
			map(
				(msg): SearchEntries => {
					const base = toSearchEntries(rendererType, msg);
					const filterID = (msg.data.Addendum?.filterID as string | undefined) ?? null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;
					return { ...base, filter } as SearchEntries;
				},
			),
			tap(entries => {
				const defDesiredGranularity = getDefaultGranularityByRendererType(entries.type);
				initialFilter.desiredGranularity = defDesiredGranularity;
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const _filter$ = new BehaviorSubject<SearchFilter>(initialFilter);
		const setFilter = (filter: SearchFilter | null): void => {
			if (closed) return undefined;
			_filter$.next(filter ?? initialFilter);
		};

		const filter$ = createRequiredSearchFilterObservable({
			filter$: _filter$.asObservable(),
			initialFilter,
			previewDateRange,
			defaultValues: {
				dateStart: defaultStart,
				dateEnd: defaultEnd,
			},
		}).pipe(
			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const nextDetailsMsg = () =>
			firstValueFrom(
				searchMessages$.pipe(
					filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),
					// cleanup: Complete when/if the user calls .close()
					takeUntil(close$),
				),
			);

		let pollingSubs: Subscription;

		const requestEntries = async (filter: RequiredSearchFilter): Promise<void> => {
			if (closed) return undefined;

			if (!isNil(pollingSubs)) {
				pollingSubs.unsubscribe();
			}
			pollingSubs = new Subscription();

			const filterID = uniqueId(SEARCH_FILTER_PREFIX);
			filtersByID[filterID] = filter;

			const first = filter.entriesOffset.index;
			const last = first + filter.entriesOffset.count;
			const startDate = filter.dateRange === 'preview' ? previewDateRange.start : filter.dateRange.start;
			const start = startDate.toISOString();
			const endDate = filter.dateRange === 'preview' ? previewDateRange.end : filter.dateRange.end;
			const end = endDate.toISOString();
			// TODO: Filter by .desiredGranularity and .fieldFilters

			// Set up a promise to wait for the next details message
			const detailsMsgP = nextDetailsMsg();
			// Send a request for details
			const requestDetailsMsg: RawRequestSearchDetailsMessageSent = {
				type: searchTypeID,
				data: { ID: SearchMessageCommands.RequestDetails, Addendum: { filterID } },
			};
			const detailsP = rawSubscription.send(requestDetailsMsg);

			// Grab the results from the details response (we need it later)
			const detailsResults = await Promise.all([detailsP, detailsMsgP]);
			const detailsMsg = detailsResults[1];

			// Dynamic duration for debounce after each event, starting from 1s and increasing 500ms after each event,
			// never surpass 4s, reset to 1s if the request is finished
			const debounceOptions = {
				initialDueTime: 1000,
				step: 500,
				maxDueTime: 4000,
				predicate: (isFinished: boolean) => isFinished === false, // increase backoff while isFinished is false
			};

			// Keep sending requests for search details until Finished is true
			pollingSubs.add(
				rawSearchDetails$
					.pipe(
						// We've already received one details message - use it to start
						startWith(detailsMsg),

						// Extract the property that indicates if the data is finished
						map(details => details.data.Finished),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestDetailsMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);

			const requestEntriesMsg: RawRequestSearchEntriesWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestEntriesWithinRange,
					Addendum: { filterID },
					EntryRange: {
						First: first,
						Last: last,
						StartTS: start,
						EndTS: end,
					},
				},
			};
			// Keep sending requests for entries until finished is true
			pollingSubs.add(
				entries$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(entries => entries.finished),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestEntriesMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const entriesP = rawSubscription.send(requestEntriesMsg);

			const requestStatsMessage: RawRequestSearchStatsMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestAllStats,
					Addendum: { filterID },
					Stats: { SetCount: filter.overviewGranularity },
				},
			};
			// Keep sending requests for stats until finished is true
			pollingSubs.add(
				rawSearchStats$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsMessage)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsP = rawSubscription.send(requestStatsMessage);

			const requestStatsWithinRangeMsg: RawRequestSearchStatsWithinRangeMessageSent = {
				type: searchTypeID,
				data: {
					ID: SearchMessageCommands.RequestStatsInRange,
					Addendum: { filterID },
					Stats: {
						SetCount: filter.zoomGranularity,
						SetEnd: recalculateZoomEnd(
							detailsMsg.data.SearchInfo.MinZoomWindow,
							filter.zoomGranularity,
							startDate,
							endDate,
						).toISOString(),
						SetStart: start,
					},
				},
			};
			// Keep sending requests for stats-within-range until finished is true
			pollingSubs.add(
				rawStatsZoom$
					.pipe(
						// Extract the property that indicates if the data is finished
						map(stats => stats.data.Finished ?? false),

						// Add dynamic debounce after each message
						debounceWithBackoffWhile(debounceOptions),

						// Filter out finished events
						rxjsFilter(isFinished => isFinished === false),

						concatMap(() => rawSubscription.send(requestStatsWithinRangeMsg)),
						catchError(() => EMPTY),
						takeUntil(close$),
					)
					.subscribe(),
			);
			const statsRangeP = rawSubscription.send(requestStatsWithinRangeMsg);

			await Promise.all([entriesP, statsP, detailsP, statsRangeP]);
		};

		filter$.subscribe(filter => {
			requestEntries(filter);
			setTimeout(() => requestEntries(filter), 2000); // TODO: Change this
		});

		const rawSearchStats$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestAllStats)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawSearchDetails$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestDetails)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const rawStatsZoom$ = searchMessages$.pipe(
			filter(filterMessageByCommand(SearchMessageCommands.RequestStatsInRange)),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const stats$ = combineLatest([
			rawSearchStats$.pipe(distinctUntilChanged<RawResponseForSearchStatsMessageReceived>(isEqual)),
			rawSearchDetails$.pipe(distinctUntilChanged<RawResponseForSearchDetailsMessageReceived>(isEqual)),
		]).pipe(
			map(
				([rawStats, rawDetails]): SearchStats => {
					const filterID =
						(rawStats.data.Addendum?.filterID as string | undefined) ??
						(rawDetails.data.Addendum?.filterID as string | undefined) ??
						null;
					const filter = filtersByID[filterID ?? ''] ?? undefined;

					const pipeline = rawStats.data.Stats.Set.map(s => s.Stats)
						.reduce<
							Array<Array<RawResponseForSearchStatsMessageReceived['data']['Stats']['Set'][number]['Stats'][number]>>
						>((acc, curr) => {
							curr.forEach((_curr, i) => {
								if (isUndefined(acc[i])) acc[i] = [];
								acc[i].push(_curr);
							});
							return acc;
						}, [])
						.map(s =>
							s
								.map(_s => ({
									module: _s.Name,
									arguments: _s.Args,
									duration: _s.Duration,
									input: {
										bytes: _s.InputBytes,
										entries: _s.InputCount,
									},
									output: {
										bytes: _s.OutputBytes,
										entries: _s.OutputCount,
									},
								}))
								.reduce((acc, curr) => ({
									...curr,
									duration: acc.duration + curr.duration,
									input: {
										bytes: acc.input.bytes + curr.input.bytes,
										entries: acc.input.entries + curr.input.entries,
									},
									output: {
										bytes: acc.output.bytes + curr.output.bytes,
										entries: acc.output.entries + curr.output.entries,
									},
								})),
						);

					return {
						id: rawDetails.data.SearchInfo.ID,
						userID: toNumericID(rawDetails.data.SearchInfo.UID),

						filter,
						finished: rawStats.data.Finished && rawDetails.data.Finished,

						query: searchInitMsg.data.RawQuery,
						effectiveQuery: searchInitMsg.data.SearchString,

						metadata: searchInitMsg.data.Metadata,
						entries: rawStats.data.EntryCount,
						duration: rawDetails.data.SearchInfo.Duration,
						start: new Date(rawDetails.data.SearchInfo.StartRange),
						end: new Date(rawDetails.data.SearchInfo.EndRange),
						minZoomWindow: rawDetails.data.SearchInfo.MinZoomWindow,
						downloadFormats: rawDetails.data.SearchInfo.RenderDownloadFormats,
						tags: searchInitMsg.data.Tags,

						storeSize: rawDetails.data.SearchInfo.StoreSize,
						processed: {
							entries: pipeline[0]?.input?.entries ?? 0,
							bytes: pipeline[0]?.input?.bytes ?? 0,
						},

						pipeline,
					};
				},
			),

			distinctUntilChanged<SearchStats>(isEqual),

			shareReplay({ bufferSize: 1, refCount: false }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsOverview$ = rawSearchStats$.pipe(
			map(set => {
				return { frequencyStats: countEntriesFromModules(set) };
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const statsZoom$ = rawStatsZoom$.pipe(
			map(set => {
				const filterID = (set.data.Addendum?.filterID as string | undefined) ?? null;
				const filter = filtersByID[filterID ?? ''] ?? undefined;

				const filterEnd = filter?.dateRange === 'preview' ? previewDateRange.end : filter?.dateRange?.end;
				const initialEnd = initialFilter.dateRange === 'preview' ? previewDateRange.end : initialFilter.dateRange.end;
				const endDate = filterEnd ?? initialEnd;

				return {
					frequencyStats: countEntriesFromModules(set).filter(f => !isAfter(f.timestamp, endDate)),
					filter,
				};
			}),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		const errors$: Observable<Error> = searchMessages$.pipe(
			// Skip every regular message. We only want to emit when there's an error
			skipUntil(NEVER),

			// When there's an error, catch it and emit it
			catchError(err => of(err)),

			shareReplay({ bufferSize: 1, refCount: true }),

			// Complete when/if the user calls .close()
			takeUntil(close$),
		);

		return {
			searchID: searchInitMsg.data.SearchID.toString(),

			progress$,
			entries$,
			stats$,
			statsOverview$,
			statsZoom$,
			errors$,

			setFilter,
			close,
		};
	};
}
Example #22
Source File: twirp-transport.service.ts    From protobuf-ts with Apache License 2.0 4 votes vote down vote up
unary<I extends object, O extends object>(method: MethodInfo, input: I, options: RpcOptions): UnaryCall<I, O> {
    let opt = options as TwirpOptions,
      url = this.makeUrl(method, opt),
      requestBody = opt.sendJson ? method.I.toJsonString(input, opt.jsonOptions) : method.I.toBinary(input, opt.binaryOptions).buffer,
      defHeader = new Deferred<RpcMetadata>(),
      defMessage = new Deferred<O>(),
      defStatus = new Deferred<RpcStatus>(),
      defTrailer = new Deferred<RpcMetadata>(),
      abortObservable = !options.abort ? NEVER : (
        options.abort.aborted
          ? of(undefined)
          : fromEvent(options.abort, "abort")
      );

    this.http.request('POST', url, {
      body: requestBody,
      headers: createTwirpRequestHeader(!!opt.sendJson, opt.meta),
      responseType: "arraybuffer",
      observe: "response",
    })
      .pipe(
        takeUntil(abortObservable)
      )
      .toPromise()
      .then(ngResponse => {

        if (ngResponse === undefined && options.abort?.aborted)
          return undefined;

        defHeader.resolve(parseMetadataFromResponseHeaders(ngResponse.headers));

        if (!ngResponse.body)
          throw new RpcError('premature end of response', TwirpErrorCode[TwirpErrorCode.dataloss]);

        if (!ngResponse.ok)
          throw parseTwirpErrorResponse(ngResponse.body);

        if (opt.sendJson) {
          try {
            return method.O.fromJsonString(utf8read(new Uint8Array(ngResponse.body)), opt.jsonOptions);
          } catch (e) {
            throw new RpcError('unable to read response body as json', TwirpErrorCode[TwirpErrorCode.dataloss]);
          }
        }

        try {
          return method.O.fromBinary(new Uint8Array(ngResponse.body), opt.binaryOptions);
        } catch (e) {
          throw new RpcError('unable to read response body', TwirpErrorCode[TwirpErrorCode.dataloss]);
        }

      }, reason => {
        if (reason instanceof HttpErrorResponse) {
          if (reason.error instanceof ArrayBuffer)
            throw parseTwirpErrorResponse(reason.error);
          throw new RpcError(reason.message, TwirpErrorCode[TwirpErrorCode.unknown]);
        }
        throw new RpcError("unknown error", TwirpErrorCode[TwirpErrorCode.unknown]);
      })

      .then(message => {
        if (message === undefined &&  options.abort?.aborted)
          throw new RpcError("request cancelled", TwirpErrorCode[TwirpErrorCode.cancelled]);
        defMessage.resolve(message);
        defStatus.resolve({code: 'OK', detail: ''});
        defTrailer.resolve({});

      })

      .catch((reason: any) => {
        // RpcErrors are thrown by us, everything else is an internal error
        let error = reason instanceof RpcError ? reason
          : new RpcError(reason instanceof Error ? reason.message : reason, TwirpErrorCode[TwirpErrorCode.internal]);
        error.methodName = method.name;
        error.serviceName  = method.service.typeName;
        defHeader.rejectPending(error);
        defMessage.rejectPending(error);
        defStatus.rejectPending(error);
        defTrailer.rejectPending(error);
      });

    return new UnaryCall<I, O>(
      method,
      opt.meta ?? {},
      input,
      defHeader.promise,
      defMessage.promise,
      defStatus.promise,
      defTrailer.promise,
    );
  }