rxjs/operators#pairwise TypeScript Examples

The following examples show how to use rxjs/operators#pairwise. 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: app.module.ts    From rubic-app with GNU General Public License v3.0 6 votes vote down vote up
/**
   * Defines scroll strategy, when page url is changed.
   * Doesn't scroll if only query parameters are changed.
   */
  private setScrollStrategy(): void {
    this.router.events
      .pipe(
        filter((e: Event): e is Scroll => e instanceof Scroll),
        pairwise()
      )
      .subscribe(([prevEvent, event]) => {
        if (event.position) {
          // backward navigation
          this.viewportScroller.scrollToPosition(event.position);
        } else if (event.anchor) {
          // anchor navigation
          this.viewportScroller.scrollToAnchor(event.anchor);
        } else if (
          prevEvent.routerEvent.urlAfterRedirects.split('?')[0] !==
          event.routerEvent.urlAfterRedirects.split('?')[0]
        ) {
          // forward navigation
          this.viewportScroller.scrollToPosition([0, 0]);
        }
      });
  }
Example #2
Source File: swaps.service.ts    From rubic-app with GNU General Public License v3.0 6 votes vote down vote up
private subscribeOnForm(): void {
    this.swapFormService.inputValueChanges
      .pipe(startWith(null, this.swapFormService.inputValue), pairwise())
      .subscribe(([prevForm, curForm]) => {
        this.setSwapProviderType(curForm);

        if (
          (!TokensService.areTokensEqual(prevForm?.fromToken, curForm.fromToken) &&
            curForm.fromToken) ||
          (!TokensService.areTokensEqual(prevForm?.toToken, curForm.toToken) && curForm.toToken)
        ) {
          this.updateTokensPrices(curForm);
        }

        if (
          !TokensService.areTokensEqual(prevForm?.fromToken, curForm.fromToken) &&
          curForm.fromToken
        ) {
          this.updateTokenBalance(curForm.fromToken);
        }
      });
  }
Example #3
Source File: router-outlet.element.ts    From scion-microfrontend-platform with Eclipse Public License 2.0 6 votes vote down vote up
private installOutletUrlListener(): void {
    this._outletName$
      .pipe(
        switchMap(outlet => outletNavigate$(outlet).pipe(startWith(null! as Navigation))), // start with a `null` navigation in case no navigation took place yet
        tap(navigation => this._empty$.next(!navigation || navigation.url === 'about:blank')),
        distinctUntilChanged((url1, url2) => url1 === url2, navigation => navigation?.url),
        pairwise(),
        takeUntil(this._disconnect$),
      )
      .subscribe(([prevNavigation, currNavigation]: [Navigation, Navigation]) => runSafe(() => {
        // Emit a page deactivate event, unless not having a previous navigation
        prevNavigation && this.dispatchEvent(new CustomEvent('deactivate', {detail: prevNavigation.url}));
        // Change the outlet URL
        Beans.get(RouterOutletUrlAssigner).assign(this._iframe, currNavigation || {url: 'about:blank', pushStateToSessionHistoryStack: false}, prevNavigation);
        // Emit a page activate event, unless not having a current navigation
        currNavigation && this.dispatchEvent(new CustomEvent('activate', {detail: currNavigation.url}));
      }));
  }
Example #4
Source File: network.service.ts    From fyle-mobile-app with MIT License 6 votes vote down vote up
getConnectionStatus() {
    return this.isConnected$.pipe(
      startWith(true),
      pairwise(),
      switchMap(([previousConnectionStatus, currentConnectionStatus]) => {
        if (previousConnectionStatus === false && currentConnectionStatus === true) {
          return concat(
            of(ConnectionMessageStatus.onlineMessageShown),
            of(ConnectionMessageStatus.onlineMessageHidden).pipe(delay(3000))
          );
        } else if (previousConnectionStatus === true && currentConnectionStatus === true) {
          return of(ConnectionMessageStatus.onlineMessageHidden);
        } else {
          return of(ConnectionMessageStatus.disconnected);
        }
      })
    );
  }
Example #5
Source File: camera.service.ts    From ionic-pwa-example-moment with MIT License 6 votes vote down vote up
function revokePreviousImageUrl() {
  return (source$: Observable<string>) =>
    source$.pipe(
      startWith(undefined),
      pairwise(),
      tap(([previous]) => {
        if (previous) URL.revokeObjectURL(previous);
      }),
      concatMapTo(source$)
    );
}
Example #6
Source File: RestApi.ts    From majsoul-api with MIT License 6 votes vote down vote up
private getSessions(contest: store.Contest<ObjectId>): Observable<Session> {
		return concat(
			defer(() => from(
				this.mongoStore.sessionsCollection.find(
					{ contestId: contest._id },
					{ sort: { scheduledTime: 1 } }
				).toArray()
			)).pipe(
				mergeAll(),
			),
			of<store.Session<ObjectId>>(null)
		).pipe(
			pairwise(),
			map(([session, nextSession]) =>
				defer(() => from(this.getSessionSummary(contest, session, nextSession)))
					.pipe(
						map(totals => {
							return { ...session, totals, aggregateTotals: totals };
						})
					)
			),
			mergeAll(),
		);
	}
Example #7
Source File: gantt-dom.service.ts    From ngx-gantt with MIT License 6 votes vote down vote up
/**
     * @returns An observable that will emit outside the Angular zone. Note, consumers should re-enter the Angular zone
     * to run the change detection if needed.
     */
    getViewerScroll(options?: AddEventListenerOptions): Observable<ScrollEvent> {
        return new Observable<ScrollEvent>((subscriber) =>
            this.ngZone.runOutsideAngular(() =>
                fromEvent(this.mainContainer, 'scroll', options)
                    .pipe(
                        map(() => this.mainContainer.scrollLeft),
                        pairwise(),
                        map(([previous, current]) => {
                            const event: ScrollEvent = {
                                target: this.mainContainer,
                                direction: ScrollDirection.NONE
                            };
                            if (current - previous < 0) {
                                if (this.mainContainer.scrollLeft < scrollThreshold && this.mainContainer.scrollLeft > 0) {
                                    event.direction = ScrollDirection.LEFT;
                                }
                            }
                            if (current - previous > 0) {
                                if (
                                    this.mainContainer.scrollWidth - this.mainContainer.clientWidth - this.mainContainer.scrollLeft <
                                    scrollThreshold
                                ) {
                                    event.direction = ScrollDirection.RIGHT;
                                }
                            }
                            return event;
                        })
                    )
                    .subscribe(subscriber)
            )
        );
    }
Example #8
Source File: speed-chart.component.ts    From RcloneNg with MIT License 5 votes vote down vote up
ngOnInit() {
		this.browserSettingService
			.partialBrowserSetting$('rng.speedChart.windowSize')
			.subscribe(([v, err]) => (this.treadhold = v));
		const statsOut = this.stats$.getOutput();
		statsOut.subscribe(node => {
			if (node[1].length !== 0) return;
			const time = moment();
			let speed = 0;
			let avg = 0;
			if (node[0]['core-stats'].transferring) {
				node[0]['core-stats'].transferring.forEach(x => {
					if (x.speed) speed += x.speed;
					if (x.speedAvg) avg += x.speedAvg;
				});
			}
			const speedData = this.lineChartData[0].data as ChartPoint[];
			const avgData = this.lineChartData[1].data as ChartPoint[];
			const threadhold = time.clone().subtract(this.treadhold, 'seconds');
			while (speedData.length !== 0 && speedData[0].x < threadhold) {
				speedData.shift();
				avgData.shift();
			}
			speedData.push({ x: time, y: speed });
			avgData.push({ x: time, y: avg });
			this.chart.update();
		});
		statsOut.pipe(pairwise()).subscribe(([pre, cur]) => {
			if (pre[1].length !== 0 || cur[1].length !== 0) return;
			let preSpeed = 0;
			let curSpeed = 0;
			if (pre[0]['core-stats'].transferring) {
				pre[0]['core-stats'].transferring.forEach(x => {
					if (x.speed) preSpeed += x.speed;
				});
			}
			if (cur[0]['core-stats'].transferring) {
				cur[0]['core-stats'].transferring.forEach(x => {
					if (x.speed) curSpeed += x.speed;
				});
			}
			this.speedDiff = Math.round(curSpeed - preSpeed);
		});
	}
Example #9
Source File: ngx-hide-on-scroll.directive.ts    From Elastos.Essentials.App with MIT License 5 votes vote down vote up
init() {
    if (isPlatformServer(this.platformId)) {
      return;
    }

    this.reset();

    let elementToListenScrollEvent;
    let scrollingElement: HTMLElement;
    if (!this.scrollingElementSelector) {
      elementToListenScrollEvent = window;
      scrollingElement = this.getDefaultScrollingElement();
    } else {
      scrollingElement = document.querySelector(this.scrollingElementSelector) as HTMLElement;
      if (!scrollingElement) {
        console.warn(`NgxHideOnScroll: @Input() scrollingElementSelector\nElement with selector: "${this.scrollingElementSelector}" not found.`);
        return;
      }
      elementToListenScrollEvent = scrollingElement;
    }

    /* elementToListenScrollEvent.addEventListener("scroll", e => {
      console.log("MANUAL ON SCROLL", e)
    }) */

    /*  elementToListenScrollEvent.addEventListener("ionScroll", e => {
       console.log("MANUAL ON ION-SCROLL", e)
     })
  */
    const scroll$ = fromEvent<ScrollCustomEvent>(elementToListenScrollEvent, 'ionScroll').pipe(
      takeUntil(this.unsubscribeNotifier),
      throttleTime(50), // only emit every 50 ms
      map(event => event.detail.scrollTop), // get vertical scroll position
      pairwise(),  // look at this and the last emitted element
      // compare this and the last element to figure out scrolling direction
      map(([y1, y2]): ScrollDirection => (y2 < y1 ? ScrollDirection.Up : ScrollDirection.Down)),
      distinctUntilChanged(), // only emit when scrolling direction changed
      share(), // share a single subscription to the underlying sequence in case of multiple subscribers
    );

    const scrollUp$ = scroll$.pipe(
      filter(direction => direction === ScrollDirection.Up)
    );

    const scrollDown$ = scroll$.pipe(
      filter(direction => direction === ScrollDirection.Down)
    );

    let scrollUpAction: () => void;
    let scrollDownAction: () => void;
    if (this.hideOnScroll === 'Up') {
      scrollUpAction = () => this.hideElement(scrollingElement);
      scrollDownAction = () => this.showElement(scrollingElement);
    } else {
      scrollUpAction = () => this.showElement(scrollingElement);
      scrollDownAction = () => this.hideElement(scrollingElement);
    }

    scrollUp$.subscribe(() => {
      scrollUpAction()
    });
    scrollDown$.subscribe(() => {
      scrollDownAction()
    });
  }
Example #10
Source File: select.ts    From ble with Apache License 2.0 5 votes vote down vote up
entityMove: Epic = (action$, { store }) => {
	return action$.pipe (
		ofType('entityPointerDown'),
		filter(() => store.editor.mode === EditorMode.select),
		// middle click is panning only
		filter(({ ev }) => !(ev.data.pointerType === 'mouse' && ev.data.button === 1)),
		// it's important to use global and not original event
		// because TouchEvents don't have clientX
		pluck('ev', 'data', 'global'),
		// we copy the relevant data because react pools events
		map(({ x, y }) => ({
			x: x + store.editor.renderZone.x,
			y: y + store.editor.renderZone.y,
		})),
		tap(() => {
			store.undoManager.startGroup();
		}),
		switchMap(({ x, y }) => fromEvent<PointerEvent>(document, 'pointermove').pipe(
			map(({ clientX, clientY }) => new Vector(clientX, clientY)),
			startWith(new Vector(x, y)),
			pairwise(),
			map(([prev, curr]) => curr.clone().sub(prev)),
			filter((vec) => vec.len2() !== 0),
			map((vec) => vec.scale(1/store.editor.scale)),
			scan((acc, delta) => {
				const totalDelta = acc.clone().add(delta);

				const displacement = snapBoxToGrid(
					new Box(
						store.editor.selectionAsAabb.pos.clone().add(totalDelta),
						store.editor.selectionAsAabb.w,
						store.editor.selectionAsAabb.h,
					),
					store.editor.gridCellSize,
				);

				store.editor.selection.forEach((entity: IEntity) => {
					entity.params.move(totalDelta.x + displacement.x, totalDelta.y + displacement.y);
				});

				return displacement.reverse();
			}, new Vector(0, 0)),
			takeUntil(fromEvent(document, 'pointerup').pipe(
				tap(() => {
					store.undoManager.stopGroup();
				}),
			)),
		)),
		ignoreElements(),
	);
}
Example #11
Source File: CollateralAuctionsTask.ts    From guardian with Apache License 2.0 5 votes vote down vote up
async start(guardian: AcalaGuardian) {
    const { apiRx } = await guardian.isReady();

    const { account, currencyId } = this.arguments;

    let currencies: CurrencyId[] = [];
    const whitelist = apiRx.consts.cdpEngine.collateralCurrencyIds;

    // make sure provided currency id is whitelisted
    if (currencyId !== 'all') {
      currencies = castArray(currencyId).map((x) => apiRx.createType('CurrencyId', x));
      currencies.forEach((id) => {
        if (!whitelist.find((x) => x.eq(id))) throw Error('Collateral currency id not allowed!');
      });
    } else {
      currencies = whitelist;
    }

    const includesAccount = includesArgument<string>(account);
    const includesCurrency = includesArgument<CurrencyId>(currencies);

    const upcomingAuctions$ = apiRx.query.auction.auctionsIndex().pipe(
      pairwise(),
      filter(([, next]) => !next.isZero()),
      switchMap(([prev, next]) => range(prev.toNumber(), next.toNumber())),
      distinctUntilChanged(),
      mergeMap((auctionId) => {
        return combineLatest([
          of(auctionId),
          apiRx.query.auctionManager.collateralAuctions(auctionId),
          apiRx.query.auction.auctions(auctionId)
        ]);
      })
    );

    return apiRx.query.auctionManager.collateralAuctions.entries().pipe(
      mergeMap((entry) => entry),
      mergeMap((entry) => {
        const [storageKey, maybecollateralAuction] = entry;
        const [auctionId] = storageKey.args;
        return combineLatest([of(auctionId), of(maybecollateralAuction), apiRx.query.auction.auctions(auctionId)]);
      }),
      concatWith(upcomingAuctions$),
      filter(([, maybecollateralAuction, maybeAuction]) => {
        if (maybecollateralAuction.isNone) return false;
        if (maybeAuction.isNone) return false;

        const { refundRecipient, currencyId } = maybecollateralAuction.unwrap();

        if (!includesAccount(refundRecipient.toString())) return false;
        if (!includesCurrency(currencyId)) return false;

        return true;
      }),
      map(([auctionId, maybecollateralAuction, maybeAuction]) => {
        const collateralAuction = maybecollateralAuction.unwrap();
        const auction = maybeAuction.unwrap();

        const [lastBidder, lastBid] = auction.bid.isSome ? auction.bid.unwrap() : [];

        return {
          account: collateralAuction.refundRecipient.toString(),
          currencyId: collateralAuction.currencyId.asToken.toString(),
          auctionId: Number(auctionId.toString()),
          initialAmount: collateralAuction.initialAmount.toString(),
          amount: collateralAuction.amount.toString(),
          target: collateralAuction.target.toString(),
          startTime: Number(collateralAuction.startTime.toString()),
          endTime: auction.end.isSome ? Number(auction.end.toString()) : undefined,
          lastBidder: lastBidder && lastBidder.toString(),
          lastBid: lastBid && lastBid.toString()
        };
      }),
      drr()
    );
  }
Example #12
Source File: LiquidityPoolTask.ts    From guardian with Apache License 2.0 5 votes vote down vote up
getSyntheticPools = (apiRx: ApiRx) => (poolId: number | number[] | 'all') => {
  const upcomingPools$ = apiRx.query.baseLiquidityPoolsForSynthetic.nextPoolId().pipe(
    pairwise(),
    filter(([, next]) => !next.isZero()),
    switchMap(([prev, next]) => range(prev.toNumber(), next.toNumber())),
    distinctUntilChanged(),
    mergeMap((poolId) =>
      combineLatest([
        of(poolId.toString()),
        apiRx.query.baseLiquidityPoolsForSynthetic.pools(poolId).pipe(
          filter((x) => x.isSome),
          map((x) => x.unwrap())
        )
      ])
    )
  );

  if (poolId === 'all') {
    return apiRx.query.baseLiquidityPoolsForSynthetic.pools.entries().pipe(
      mergeMap((x) => x),
      filter(([, value]) => value.isSome),
      mergeMap(
        ([
          {
            args: [poolId]
          },
          pool
        ]) => combineLatest([of(poolId.toString()), of(pool.unwrap())])
      ),
      concatWith(upcomingPools$)
    );
  } else {
    return of(castArray(poolId)).pipe(
      mergeMap((x) => x),
      switchMap((poolId) =>
        combineLatest([of(poolId.toString()), apiRx.query.baseLiquidityPoolsForSynthetic.pools(poolId)])
      ),
      filter(([, pool]) => pool.isSome),
      mergeMap(([poolId, value]) => combineLatest([of(poolId), of(value.unwrap())]))
    );
  }
}
Example #13
Source File: RestApi.ts    From majsoul-api with MIT License 5 votes vote down vote up
private async getLeaguePhaseData({
		contest,
		transitions,
		phases
	}: PhaseInfo): Promise<LeaguePhase<ObjectID>[]> {
		const sessions = (await this.getSessions(contest).pipe(toArray()).toPromise())
			.sort((a, b) => a.scheduledTime - b.scheduledTime);
		return from(phases.concat(null)).pipe(
			pairwise(),
			mergeScan((completePhase, [phase, nextPhase]) => {
				const transition = transitions[phase.index];
				const phaseSessions = sessions.filter(
					session =>
						session.scheduledTime >= phase.startTime
						&& (nextPhase == null || session.scheduledTime < nextPhase.startTime)
				);
				const startingTotals = {
					...completePhase.sessions[completePhase.sessions.length - 1]?.aggregateTotals ?? {}
				};

				const rankedTeams = Object.entries(startingTotals)
					.map(([team, score]) => ({ team, score }))
					.sort((a, b) => b.score - a.score);

				const allowedTeams = transition.teams?.top
					? rankedTeams.slice(0, transition.teams.top)
						.reduce((total, next) => (total[next.team] = true, total), {} as Record<string, true>)
					: null;

				for (const team of Object.keys(startingTotals)) {
					if (allowedTeams && !(team in allowedTeams)) {
						delete startingTotals[team];
						continue;
					}

					if (transition.score?.half) {
						startingTotals[team] = Math.floor(startingTotals[team] / 2);
					} else if (transition.score?.nil) {
						startingTotals[team] = 0;
					}
				}

				return of({
					...phase,
					sessions: phaseSessions.reduce((total, next) => {
						const aggregateTotals = { ...(total[total.length - 1]?.aggregateTotals ?? startingTotals) };
						const filteredTotals = Object.entries(next.totals)
							.filter(([key]) => !allowedTeams || key in allowedTeams)
							.reduce((total, [key, value]) => (total[key] = value, total), {} as Record<string, number>);

						for (const team in filteredTotals) {
							if (aggregateTotals[team] == null) {
								aggregateTotals[team] = 0;
							}
							aggregateTotals[team] += filteredTotals[team];
						}

						total.push({
							...next,
							totals: filteredTotals,
							aggregateTotals,
						})
						return total;
					}, [] as Session<ObjectID>[]),
					aggregateTotals: startingTotals,
				} as LeaguePhase<ObjectID>);
			}, {
				sessions: [{
					aggregateTotals: (contest.teams ?? []).reduce(
						(total, next) => (total[next._id.toHexString()] = 0, total),
						{} as Record<string, number>
					)
				}]
			} as LeaguePhase<ObjectID>, 1),
			toArray(),
		).toPromise();
	}
Example #14
Source File: dashboard.component.ts    From RcloneNg with MIT License 4 votes vote down vote up
ngOnInit(): void {
		this.resp.getResponsiveSize.subscribe(data => {
			this.isSmallerThanSmSize = data === 'xs';
		});
		const outer = this;
		this.visable = true;
		this.stats$ = new (class extends CoreStatsFlow {
			public prerequest$ = combineLatest([
				outer.cmdService.rst$.getOutput(),
				outer.cmdService.listCmd$.verify(this.cmd),
			]).pipe(
				takeWhile(() => outer.visable),
				map(([, node]): CombErr<CoreStatsFlowInNode> => node)
			);
		})();
		this.stats$.deploy();

		this.mem$ = new (class extends CoreMemstatsFlow {
			protected cacheSupport = false;
			public prerequest$ = combineLatest([
				outer.cmdService.rst$.getOutput(),
				outer.cmdService.listCmd$.verify(this.cmd),
			]).pipe(
				takeWhile(() => outer.visable),
				map(x => x[1])
			);
		})();
		this.mem$.deploy();
		this.mem$.getOutput().subscribe(x => {
			if (x[1].length !== 0) return;
			this.memVals = { ...x[0]['mem-stats'] };
			for (const key in this.memVals) {
				if (this.memVals.hasOwnProperty(key)) {
					this.memVals[key] = FormatBytes(this.memVals[key], 3);
				}
			}
		});

		this.mem$
			.getOutput()
			.pipe(pairwise())
			.subscribe(([pre, cur]) => {
				if (pre[1].length !== 0 || cur[1].length !== 0) return;
				for (const key in pre[0]['mem-stats']) {
					if (this.memVals.hasOwnProperty(key)) {
						this.memDiff[key] = cur[0]['mem-stats'][key] - pre[0]['mem-stats'][key];
					}
				}
			});
		this.memTrigger.next(1);

		this.ver$ = new (class extends CoreVersionFlow {
			public prerequest$: Observable<CombErr<IRcloneServer>> = outer.cmdService.listCmd$.verify(
				this.cmd
			);
		})();
		this.ver$.deploy();
		this.ver$.getOutput().subscribe(x => {
			if (x[1].length !== 0) return;
			this.verVals = x[0];
		});

		this.bwlimit$ = new (class extends CoreBwlimitFlow {
			public prerequest$ = combineLatest([
				outer.cmdService.listCmd$.verify(this.cmd),
				outer.bwlimitTrigger.pipe(map(input => (input === '' ? {} : { rate: input }))),
			]).pipe(
				map(
					([userNode, params]): CombErr<CoreBwlimitFlowInNode> => [
						{ ...userNode[0], ...params },
						userNode[1],
					]
				)
			);
		})();
		this.bwlimit$.deploy();
		this.bwlimit$.getOutput().subscribe(x => {
			if (x[1].length !== 0) return;
			this.limitation = this.limitationServer = x[0].bandwidth.rate;
		});
		this.bwlimitTrigger.next(''); // query bandwidth
	}
Example #15
Source File: connector.ts    From majsoul-api with MIT License 4 votes vote down vote up
async function main() {
	const secrets = getSecrets();

	const mongoStore = new store.Store();
	try {
		await mongoStore.init(secrets.mongo?.username ?? "root", secrets.mongo?.password ?? "example");
	} catch (error) {
		console.log("failed to connect to mongo db: ", error);
		process.exit(1);
	}

	const userAgent = await getOrGenerateUserAgent(mongoStore);
	const [config] = await mongoStore.configCollection.find().toArray();

	const expireDeadline = Date.now() + 60 * 1000;
	const existingCookies = (config.loginCookies ?? []).filter(cookie => !cookie.expires || cookie.expires > expireDeadline);
	const {passport: dynamicPassport, loginCookies} = (await getPassport(
		{
			userId: secrets.majsoul.uid,
			accessToken: secrets.majsoul.accessToken,
			userAgent,
			existingCookies,
		}
	)) ?? {};

	await mongoStore.configCollection.updateOne(
		{
			_id: config._id,
		},
		{
			$set: {
				loginCookies
			},
		}
	);

	if (dynamicPassport) {
		await mongoStore.configCollection.updateOne(
			{
				_id: config._id,
			},
			{
				$set: {
					passportToken: dynamicPassport.accessToken
				},
			}
		);
	}

	const passportToken = dynamicPassport?.accessToken ?? config.passportToken ?? secrets.majsoul.passportToken;

	if (!passportToken) {
		console.log("failed to aquire passport");
		process.exit(1);
	}

	const passport: Passport  = {
		accessToken: passportToken,
		uid: secrets.majsoul.uid,
	};

	const apiResources = await majsoul.Api.retrieveApiResources();
	console.log(`Using api version ${apiResources.pbVersion}`);
	const adminApi = new AdminApi();
	const api = new majsoul.Api(apiResources);

	api.notifications.subscribe(n => console.log(n));
	await api.init();

	// console.log(api.majsoulCodec.decodeMessage(Buffer.from("", "hex")));

	await api.logIn(passport);

	api.errors$.subscribe((error => {
		console.log("error detected with api connection: ", error);
		process.exit(1);
	}));

	//spreadsheet.addGameDetails(await api.getGame(decodePaipuId("jijpnt-q3r346x6-y108-64fk-hbbn-lkptsjjyoszx_a925250810_2").split('_')[0]));


	// api.getGame(
	// 	// Codec.decodePaipuId("")
	// 	// ""
	// ).then(game => {
	// 	parseGameRecordResponse(game);
	// 	// console.log(util.inspect(game.head, false, null));
	// 	// console.log(util.inspect(parseGameRecordResponse(game), false, null));
	// });

	const googleAuth = new google.auth.OAuth2(
		secrets.google.clientId,
		secrets.google.clientSecret,
	);

	const googleTokenValid$ = process.env.NODE_ENV === "production"
		? concat(
			defer(() => googleAuth.getAccessToken()).pipe(
				map(response => response.token),
				catchError(() => of(null))
			),
			fromEvent<Credentials>(googleAuth, "tokens").pipe(
				map((tokens) => tokens?.access_token),
			)
		).pipe(
			distinctUntilChanged(),
			map(token => token != null),
			shareReplay(1),
		)
		: of(false);

	googleTokenValid$.subscribe(tokenIsValid => {
		console.log(`google token is ${tokenIsValid ? "" : "in"}valid`);
	})

	// oauth token
	merge(
		mongoStore.ConfigChanges.pipe(
			filter(change => change.operationType === "update"
				&& change.updateDescription.updatedFields.googleRefreshToken !== undefined
			),
			map((updateEvent: ChangeEventUpdate<store.Config<ObjectId>>) => updateEvent.updateDescription.updatedFields.googleRefreshToken)
		),
		defer(
			() => from(
				mongoStore.configCollection.find().toArray()
			).pipe(
				mergeAll(),
				map(config => config.googleRefreshToken)
			)
		)
	).subscribe(refresh_token => {
		if (googleAuth.credentials.refresh_token === refresh_token || refresh_token == null) {
			console.log(`refresh token not valid in database`);
			return;
		}

		googleAuth.setCredentials({
			refresh_token
		});
		googleAuth.getRequestHeaders();
	});

	// player search
	merge(
		mongoStore.PlayerChanges.pipe(
			filter(change => change.operationType === "insert"
				&& change.fullDocument.majsoulFriendlyId != null
			),
			map((insertEvent: ChangeEventCR<store.Player<ObjectId>>) => insertEvent.fullDocument)
		),
		defer(() => from(
			mongoStore.playersCollection.find({
				majsoulFriendlyId: {
					$exists: true
				}
			}).toArray()
		).pipe(mergeAll()))
	).subscribe(player => {
		api.findPlayerByFriendlyId(player.majsoulFriendlyId).then(async (apiPlayer) => {
			if (apiPlayer == null) {
				mongoStore.playersCollection.deleteOne({
					_id: player._id
				});
				return;
			}

			const update = await mongoStore.playersCollection.findOneAndUpdate(
				{
					majsoulId: apiPlayer.majsoulId
				},
				{
					$set: {
						...apiPlayer
					},
				}
			);

			if (update.value) {
				mongoStore.playersCollection.deleteOne({
					_id: player._id
				});
				return;
			}

			mongoStore.playersCollection.findOneAndUpdate(
				{
					_id: player._id
				},
				{
					$set: {
						...apiPlayer
					},
					$unset: {
						majsoulFriendlyId: true
					}
				}
			);
		})
	})

	// custom game id search
	merge(
		mongoStore.GameChanges.pipe(
			filter(change => change.operationType === "insert"
				&& change.fullDocument.contestMajsoulId == null
				&& change.fullDocument.majsoulId != null
			),
			map((insertEvent: ChangeEventCR<store.GameResult<ObjectId>>) => insertEvent.fullDocument)
		),
		defer(() => from(
			mongoStore.gamesCollection.find({
				notFoundOnMajsoul: {
					$exists: false
				},
				contestMajsoulId: {
					$exists: false
				}
			}).toArray()
		).pipe(mergeAll()))
	).subscribe(game => {
		console.log(`Custom game id added ${game.majsoulId}`);
		recordGame(
			game.contestId,
			game.majsoulId,
			mongoStore,
			api
		);
	});

	createContestIds$(mongoStore).subscribe((contestId) => {
		const tracker = new ContestTracker(contestId, mongoStore, api);

		concat(
			of(null),
			tracker.MajsoulId$.pipe(distinctUntilChanged())
		).pipe(pairwise())
			.subscribe(([previous, next]) => {
				if (next == null && previous != null) {
					mongoStore.gamesCollection.updateMany(
						{
							contestMajsoulId: previous
						},
						{
							$unset: {
								contestId: true,
							}
						}
					);
					return;
				}
				mongoStore.gamesCollection.updateMany(
					{
						contestMajsoulId: next
					},
					{
						$set: {
							contestId: contestId,
						}
					}
				);
			});

		tracker.AdminPlayerFetchRequested$
			.pipe(filter(fetchRequested => fetchRequested == true))
			.subscribe(async () => {
				const contest = await mongoStore.contestCollection.findOneAndUpdate(
					{ _id: contestId },
					{ $unset: { adminPlayerFetchRequested: true } },
					{ projection: {
						adminPlayerFetchRequested: true,
						majsoulId: true
					}}
				);
				console.log(`fetchRequested for contest #${contestId} #${contest.value.majsoulId}` );
				await adminApi.reconnect();
				try {
					await adminApi.logIn(passport);
					await adminApi.manageContest(contest.value.majsoulId);
					const { players, error } = await adminApi.fetchContestPlayers();
					if (error) {
						console.log(error);
						return;
					}

					const existingPlayers = await mongoStore.playersCollection.find({
						majsoulId: {
							$in: players.map(player => player.account_id)
						}
					}).toArray();

					const newPlayers = players.filter(player => existingPlayers.find(existingPlayer => existingPlayer.majsoulId === player.account_id) == null);

					if (!newPlayers.length) {
						console.log("No new players to add found");
						return;
					}

					console.log(`Inserting ${newPlayers.length} players`);

					await mongoStore.playersCollection.insertMany(
							newPlayers.map(player => ({
								nickname: player.nickname,
								majsoulId: player.account_id,
							}))
					);

				} finally {
					adminApi.disconnect();
				}
			});

		tracker.UpdateRequest$.subscribe(async (majsoulFriendlyId) => {
			const majsoulContest = await api.findContestByContestId(majsoulFriendlyId);
			if (majsoulContest == null) {
				mongoStore.contestCollection.findOneAndUpdate(
					{ _id: contestId },
					{ $set: { notFoundOnMajsoul: true } },
				);

				console.log(`contest ${majsoulFriendlyId} not found on majsoul`);
				return;
			}

			console.log(`updating contest ${majsoulFriendlyId}`);

			mongoStore.contestCollection.findOneAndUpdate(
				{ _id: contestId },
				{ $set: { ...majsoulContest } },
			);

			console.log(`updating contest ${majsoulFriendlyId} games`);
			for (const gameId of await api.getContestGamesIds(majsoulContest.majsoulId)) {
				await recordGame(contestId, gameId.majsoulId, mongoStore, api);
			}
		});

		tracker.LiveGames$.subscribe(gameId => {
			recordGame(contestId, gameId, mongoStore, api);
		});

		const spreadsheet$ = combineLatest([
			tracker.SpreadsheetId$,
			googleTokenValid$,
		]).pipe(
			filter(([spreadsheetId, tokenIsValid]) => tokenIsValid && spreadsheetId != null),
			share(),
		);

		spreadsheet$.subscribe(async ([spreadsheetId]) => {
			const spreadsheet = new Spreadsheet(spreadsheetId, googleAuth);
			try {
				await spreadsheet.init();
			} catch (error) {
				console.log(`Spreadsheet #${spreadsheetId} failed to initialise.`, error);
				return;
			}

			console.log(`Tracking [${spreadsheetId}]`);
			tracker.RecordedGames$.pipe(
				takeUntil(spreadsheet$),
			).subscribe(game => {
				spreadsheet.addGame(game);
				spreadsheet.addGameDetails(game);
			})

			tracker.Teams$.pipe(
				takeUntil(spreadsheet$),
			).subscribe(async (teams) => {
				const players = await mongoStore.playersCollection.find({
					_id: {
						$in: teams.map(team => team.players).flat().map(team => team._id)
					}
				}).toArray();

				spreadsheet.updateTeams(
					teams,
					players.reduce((total, next) => (total[next._id.toHexString()] = next, total), {})
				);
			});
		})
	});
}