rxjs/operators#mergeAll TypeScript Examples

The following examples show how to use rxjs/operators#mergeAll. 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: twitch-button.component.ts    From App with MIT License 6 votes vote down vote up
open(): void {
		scheduled([
			this.oauthService.openAuthorizeWindow<{ token: string }>().pipe(
				tap(data => this.clientService.setToken(data.token)),
				switchMap(() => this.restService.v2.GetUser('@me', { includeEditorIn: true }).pipe(
					map(res => this.clientService.pushData(res?.user ?? null))
				))
			),
			defer(() => this.oauthService.navigateTo(this.restService.v2.GetAuthURL()))
		], asyncScheduler).pipe(
			mergeAll(),
			switchMapTo(EMPTY)
		).subscribe({
			error: (err) => {
				this.dialogRef.open(ErrorDialogComponent, {
					data: {
						errorCode: err.status,
						errorMessage: err.error?.error ?? err.message,
						errorName: 'Could not sign in'
					} as ErrorDialogComponent.Data
				});
				this.logger.error('Could not sign in', err);
				this.clientService.logout();
				this.oauthService.openedWindow?.close();
			}
		});
	}
Example #2
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 #3
Source File: connector.ts    From majsoul-api with MIT License 6 votes vote down vote up
function createContestIds$(mongoStore: store.Store): Observable<ObjectId> {
	return merge(
		mongoStore.ContestChanges.pipe(
			filter(changeEvent => changeEvent.operationType === "insert"),
			map((changeEvent: ChangeEventCR<store.Contest<ObjectId>>) => changeEvent.documentKey._id)
		),
		defer(() => from(mongoStore.contestCollection.find().toArray()))
			.pipe(
				mergeAll(),
				map(contest => contest._id)
			)
	);
}
Example #4
Source File: MixedDataSource.ts    From grafana-chinese with Apache License 2.0 6 votes vote down vote up
batchQueries(mixed: BatchedQueries[], request: DataQueryRequest<DataQuery>): Observable<DataQueryResponse> {
    const runningQueries = mixed.filter(this.isQueryable).map((query, i) =>
      from(query.datasource).pipe(
        mergeMap((api: DataSourceApi) => {
          const dsRequest = cloneDeep(request);
          dsRequest.requestId = `mixed-${i}-${dsRequest.requestId || ''}`;
          dsRequest.targets = query.targets;

          return from(api.query(dsRequest)).pipe(
            map(response => {
              return {
                ...response,
                data: response.data || [],
                state: LoadingState.Loading,
                key: `mixed-${i}-${response.key || ''}`,
              } as DataQueryResponse;
            })
          );
        })
      )
    );

    return forkJoin(runningQueries).pipe(map(this.markAsDone), mergeAll());
  }
Example #5
Source File: static-interceptors-consumer.ts    From pebula-node with MIT License 6 votes vote down vote up
private buildRunner(interceptors: SbInterceptor[], context: SbContext, lastPipe: Observable<any>) {
    const len = interceptors.length;
    const nextFn = (i: number) => async () => {
      if (i >= len) {
        return lastPipe;
      }
      const handler: CallHandler = {
        handle: () => fromPromise(nextFn(i + 1)()).pipe(mergeAll()),
      };
      return interceptors[i].intercept(context, handler);
    };
    return nextFn(0);
  }
Example #6
Source File: PositionsByTraderTask.ts    From guardian with Apache License 2.0 6 votes vote down vote up
async start(guardian: LaminarGuardian) {
    const { apiRx } = await guardian.isReady();

    const accounts = castArray(this.arguments.account);

    const getSwap = accumulatedSwap(apiRx);
    const getUnrealizedPL = unrealizedPL(apiRx);

    return from(accounts).pipe(
      switchMap((account) => apiRx.query.marginProtocol.positionsByTrader.entries(account)),
      mergeAll(),
      filter(([, value]) => !value.isEmpty),
      mergeMap(([storageKey]) => {
        const positionId = storageKey.args[1][1];
        return apiRx.query.marginProtocol.positions(positionId).pipe(
          filter((x) => x.isSome),
          map((x) => x.unwrap()),
          mergeMap((position) => combineLatest([of(position), getSwap(position), getUnrealizedPL(position)])),
          map(([position, swap, profit]) => {
            const { owner, poolId, pair, leverage, marginHeld } = position;
            return {
              account: owner.toString(),
              liquidityPoolId: poolId.toString(),
              positionId: positionId.toString(),
              pair: {
                base: pair.base.toString(),
                quote: pair.quote.toString()
              },
              leverage: leverage.toString(),
              marginHeld: marginHeld.toString(),
              accumulatedSwap: swap ? swap.toFixed(0) : '',
              profit: profit ? profit.toFixed(0) : ''
            };
          })
        );
      })
    );
  }
Example #7
Source File: wardrobe.component.ts    From App with MIT License 6 votes vote down vote up
private parsePaints(): Observable<WardrobeComponent.PaintCosmetic[]> {
		return this.clientService.getCosmetics().pipe(
			mergeAll(),
			filter(c => c.kind === 'PAINT'),
			map(c => ({
				...JSON.parse(c.data),
				...c
			})),
			toArray()
		);
	}
Example #8
Source File: wardrobe.component.ts    From App with MIT License 6 votes vote down vote up
parseBadges(): Observable<WardrobeComponent.BadgeCosmetic[]> {
		return this.clientService.getCosmetics().pipe(
			mergeAll(),
			filter(c => c.kind === 'BADGE'),
			map(c => ({
				...JSON.parse(c.data),
				...c
			})),
			toArray()
		);
	}
Example #9
Source File: user.structure.ts    From App with MIT License 6 votes vote down vote up
getEditorIn(): Observable<UserStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.editor_in ?? []),
			mergeAll(),
			map(u => this.dataService.get('user', { id: u.id })),
			mergeAll(),
			toArray()
		);
	}
Example #10
Source File: emote.component.ts    From App with MIT License 6 votes vote down vote up
interactError = new Subject<string>().pipe(
		mergeMap(x => scheduled([
			of(!!x ? 'ERROR: ' + x : ''),
			timer(5000).pipe(
				takeUntil(this.interactError),
				mapTo('')
			)
		], asyncScheduler).pipe(mergeAll()))
	) as Subject<string>;
Example #11
Source File: emote.structure.ts    From App with MIT License 6 votes vote down vote up
/**
	 * Get the emote's alias, per the client user or the user they are editing
	 */
	getAlias(): Observable<string> {
		const client = this.getRestService()?.clientService;
		if (!client) {
			return of('');
		}

		return of(client.isImpersonating).pipe(
			switchMap(isEditor => iif(() => isEditor === true,
				client.impersonating.pipe(take(1)),
				of(client)
			)),
			map(usr => usr as UserStructure),
			switchMap(usr => from(usr.getEmoteAliases())),
			mergeAll(),
			filter(alias => alias.emoteID === this.id),
			map(alias => alias.name),
			defaultIfEmpty('')
		);
	}
Example #12
Source File: store-leaderboards.component.ts    From App with MIT License 6 votes vote down vote up
ngOnInit(): void {
		this.restService.egvault.Subscriptions.GetLeaderboards().pipe(
			RestService.onlyResponse(),
			map(res => this.gifts.next(res.body?.gift_subscriptions ?? [])),

			switchMap(() => scheduled([
				this.getPosition(1).pipe(tap(x => this.firstPlace = { user: x[0], count: x[1] })),
				this.getPosition(2).pipe(tap(x => this.secondPlace = { user: x[0], count: x[1] })),
				this.getPosition(3).pipe(tap(x => this.thirdPlace = { user: x[0], count: x[1] }))
			], asyncScheduler).pipe(mergeAll()))
		).subscribe({
			next: () => this.cdr.markForCheck()
		});
	}
Example #13
Source File: store-leaderboards.component.ts    From App with MIT License 6 votes vote down vote up
private getPosition(pos: number): Observable<[UserStructure, number]> {
		return this.gifts.pipe(
			mergeAll(),
			skip(pos - 1),
			take(1),
			switchMap(g => this.restService.v2.GetUser(g.user_id).pipe(
				map(res => ({
					user: this.dataService.add('user', res.user)[0],
					count: g.count
				}))
			)),
			map(x => [x.user, x.count])
		);
	}
Example #14
Source File: app.service.ts    From App with MIT License 6 votes vote down vote up
updateSubscriptionData(): void {
		// EgVault - Payment API State
		scheduled([
			this.restService.egvault.Root().pipe(
				RestService.onlyResponse(),
				tap(() => this.egvaultOK.next(true)),
				catchError(() => defer(() => this.egvaultOK.next(false)))
			),
			this.restService.awaitAuth().pipe(
				filter(ok => ok === true),
				switchMap(() => this.restService.egvault.Subscriptions.Get('@me').pipe(RestService.onlyResponse())),
				tap(res => {
					if (!res.body?.subscription) {
						this.subscription.next(null);
						return undefined;
					}

					res.body.subscription.renew = res.body.renew;
					res.body.subscription.ending_at = new Date(res.body.end_at);
					this.subscription.next(res.body.subscription);
					return undefined;
				}),
				mapTo(undefined)
			)
		], asyncScheduler).pipe(mergeAll()).subscribe({
			error: err => console.error(err)
		});
	}
Example #15
Source File: notify-button.component.ts    From App with MIT License 6 votes vote down vote up
ngOnInit(): void {
		scheduled([
			this.clientService.isAuthenticated().pipe(filter(ok => ok === true)),
			this.clientService.impersonating.pipe(mapTo(true))
		], asapScheduler).pipe(
			mergeAll(),
			switchMap(() => this.clientService.getActorUser()),
			switchMap(actor => actor.fetchNotificationCount()),
			tap(nCount => this.count.next(nCount))
		).subscribe();

		this.beginPolling();
	}
Example #16
Source File: emote-list.component.ts    From App with MIT License 6 votes vote down vote up
getEmotes(page = 0, options?: Partial<RestV2.GetEmotesOptions>): Observable<EmoteStructure[]> {
		this.emotes.next([]);
		this.newPage.next(page);
		const timeout = setTimeout(() => this.loading.next(true), 1000);
		const cancelSpinner = () => {
			this.loading.next(false);
			clearTimeout(timeout);
		};

		const size = this.calculateSizedRows();
		return this.restService.awaitAuth().pipe(
			switchMap(() => this.restService.v2.SearchEmotes(
				(this.pageOptions?.pageIndex ?? 0) + 1,
				Math.max(EmoteListComponent.MINIMUM_EMOTES, size ?? EmoteListComponent.MINIMUM_EMOTES),
				options ?? this.currentSearchOptions
			)),

			takeUntil(this.newPage.pipe(take(1))),
			tap(res => this.totalEmotes.next(res?.total_estimated_size ?? 0)),
			delay(200),
			map(res => res?.emotes ?? []),
			mergeAll(),
			map(data => this.dataService.add('emote', data)[0]),
			toArray(),
			defaultIfEmpty([] as EmoteStructure[]),

			tap(() => cancelSpinner()),
			catchError(() => defer(() => cancelSpinner()))
		) as Observable<EmoteStructure[]>;
	}
Example #17
Source File: emote.structure.ts    From App with MIT License 5 votes vote down vote up
getAuditActivityString(): Observable<string> {
		return this.dataOnce().pipe(
			take(1),
			map(emote => (emote?.audit_entries as unknown as string[] ?? []) as string[]),
			mergeAll(),
			filter(entry => typeof entry === 'string')
		);
	}
Example #18
Source File: user.structure.ts    From App with MIT License 5 votes vote down vote up
getEmotes(): Observable<EmoteStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.emotes ?? []),
			mergeAll(),
			map(e => this.dataService.get('emote', { id: e.id })[0]),
			toArray()
		);
	}
Example #19
Source File: user.structure.ts    From App with MIT License 5 votes vote down vote up
getEditors(): Observable<UserStructure[]> {
		return this.dataOnce().pipe(
			map(data => data?.editors ?? []),
			mergeAll(),
			map(u => this.dataService.get('user', { id: u.id })[0]),
			toArray()
		);
	}
Example #20
Source File: app.service.ts    From App with MIT License 5 votes vote down vote up
constructor(
		titleService: Title,
		private restService: RestService,
		private dataService: DataService
	) {
		const attrMap = new Map<string, AppService.PageTitleAttribute>();
		this.pageTitleAttr.pipe(
			// map(attr => lastAttr ? [...lastAttr, ...(lastAttr = attr)] : [...attr]),
			mergeAll(),
			tap(attr => attrMap.set(attr.name, attr)),
			map(attr => [...Array.from(attrMap.values()), attr]),

			map(attr => ({ title: this.pageTitleSnapshot, attributes: attr })),
			map(({ attributes, title }) => {
				attributes.map(attr => title = title.replace(`%${attr.name}`, attr.value));

				titleService.setTitle(title.replace(AppService.PAGE_ATTR_REGEX, ''));
			})
		).subscribe();

		// Query App Meta
		restService.v2.gql.query<{ meta: { featured_broadcast: string; announcement: string; roles: string[]; }; }>({
			query: `
				query GetMeta() {
					meta() {
						announcement,
						featured_broadcast,
						roles
					}
				}
			`
		}).pipe(
			map(res => res?.body?.data.meta)
		).subscribe({
			next: (res) => {
				this.featuredBroadcast.next(res?.featured_broadcast ?? '');
				this.announcement.next(res?.announcement ?? '');

				// Add roles to data service
				if (Array.isArray(res?.roles)) {
					for (const s of res?.roles as string[]) {
						let role: DataStructure.Role;
						try {
							role = JSON.parse(s);
						} catch (err) {
							console.error('could not parse application roles,', err);
							continue;
						}

						this.dataService.add('role', role);
					}
				}
			}
		});

		this.updateSubscriptionData();
	}
Example #21
Source File: home.component.ts    From App with MIT License 5 votes vote down vote up
ngOnInit(): void {
		// Get Discord Widget
		this.restService.Discord.Widget().pipe(
			RestService.onlyResponse(),
			tap(res => this.discordWidget.next(res.body))
		).subscribe({
			error(): void {}
		});

		// Get extension versions
		this.restService.createRequest<HomeComponent.Platform[]>('get', '/webext').pipe(
			RestService.onlyResponse(),
			map(res => res.body as HomeComponent.Platform[]),
			mergeAll(),
			map(p => ({
				icon: this.browserIcons.filter(v => v.id === p.id)[0],
				platform: p
			})),
			filter(({ icon }) => !!icon && typeof icon.tag !== 'undefined'),
			map(({ icon, platform }) => {
				if (!icon.tag) {
					icon.tag = { };
				}

				icon.tag.new = platform.new;
				icon.tag.label = platform.version_tag;
				icon.variants = platform.variants?.map(v => {
					v.platform = platform;

					return v;
				});
			})
		).subscribe({
			complete: () => this.cdr.markForCheck()
		});

		// Get featured channel
		AppComponent.isBrowser.pipe(
			take(1),
			filter(b => b === true),

			switchMap(() => this.restService.v2.gql.query<{ featured_broadcast: string; }>({
				query: `
					query GetFeaturedBroadcast() {
						featured_broadcast()
					}
				`
			})),
			map(res => res?.body?.data.featured_broadcast),
			switchMap(login => !!login ? this.restService.v2.GetUser(login) : throwError('No Featured Broadcast')),
			map(res => this.dataService.add('user', res.user)[0])
		).subscribe({
			next: usr => {
				this.featuredUser.next(usr);
			},
			error: err => {
				this.logger.warn('No featured broadcast active: ', err);
			}
		});
	}
Example #22
Source File: emote-search.component.ts    From App with MIT License 5 votes vote down vote up
ngOnInit(): void {
		scheduled([
			this.form.get('query')?.valueChanges.pipe( // Look for changes to the name input form field
				mergeMap((value: string) => this.selectedSearchMode.pipe(take(1), map(mode => ({ mode, value })))),
				map(({ value, mode }) => ({ [mode.id]: value })) // Map SearchMode to value
			) ?? EMPTY,

			this.form.get('globalState')?.valueChanges.pipe( // Look for changes to the "show global"
				map((value: string) => ({ globalState: value }))
			) ?? EMPTY,

			this.form.get('channel')?.valueChanges.pipe(
				map((value: boolean) => ({ channel: value ? this.clientService.getSnapshot()?.login : '' }))
			) ?? EMPTY,
			this.form.get('zerowidth')?.valueChanges.pipe(
				map((value: boolean) => ({ filter: {
					visibility: value ? DataStructure.Emote.Visibility.ZERO_WIDTH : 0
				}}))
			) ?? EMPTY,

			this.form.get('sortBy')?.valueChanges.pipe(
				map((value: string) => ({ sortBy: value }))
			) ?? EMPTY,

			this.form.get('sortOrder')?.valueChanges.pipe(
				map((value: RestV2.GetEmotesOptions['sortOrder']) => ({ sortOrder: value }))
			) ?? EMPTY
		], asyncScheduler).pipe(
			mergeAll(),
			map(v => this.current = { ...this.current, ...v } as any),
			throttleTime(250)
		).subscribe({
			next: (v) => this.searchChange.next(v) // Emit the change
		});

		setTimeout(() => {
			if (!!this.defaultSearchOptions) {
				for (const k of Object.keys(this.form.getRawValue())) {
					const v = (this.defaultSearchOptions as any)[k as any];
					if (!v) {
						continue;
					}

					this.form.get(k)?.patchValue(v);
				}
			}
		}, 0);
	}
Example #23
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), {})
				);
			});
		})
	});
}