Update splatnet3-types

This commit is contained in:
Samuel Elliott
2022-10-21 23:31:30 +01:00
parent 1e6cec9303
commit 616c40888a
7 changed files with 373 additions and 46 deletions

14
package-lock.json generated
View File

@@ -21,7 +21,7 @@
"node-notifier": "^10.0.1",
"node-persist": "^3.1.0",
"read": "^1.0.7",
"splatnet3-types": "^0.1.20221020213035",
"splatnet3-types": "^0.1.20221021222422",
"supports-color": "^8.1.1",
"tslib": "^2.4.0",
"uuid": "^8.3.2",
@@ -4362,9 +4362,9 @@
"dev": true
},
"node_modules/splatnet3-types": {
"version": "0.1.20221020213035",
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221020213035.tgz",
"integrity": "sha512-IjetRtZCcr4kKVdKGVsmiqAHt8b6WqyOJB+8+2pj5imDiAXBWqTKeKp9++CkdIpNPRBO4BLpWUwM5wHaYI0YGA=="
"version": "0.1.20221021222422",
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221021222422.tgz",
"integrity": "sha512-eAzmc5x4/tVeFGXs0yASlw/fCte4YTV/BmeimkJvH83Y38mOV7J9fghcaKJyDMMvRTcp0hVr3v3feOCzh90o+g=="
},
"node_modules/sprintf-js": {
"version": "1.1.2",
@@ -8393,9 +8393,9 @@
"dev": true
},
"splatnet3-types": {
"version": "0.1.20221020213035",
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221020213035.tgz",
"integrity": "sha512-IjetRtZCcr4kKVdKGVsmiqAHt8b6WqyOJB+8+2pj5imDiAXBWqTKeKp9++CkdIpNPRBO4BLpWUwM5wHaYI0YGA=="
"version": "0.1.20221021222422",
"resolved": "https://registry.npmjs.org/splatnet3-types/-/splatnet3-types-0.1.20221021222422.tgz",
"integrity": "sha512-eAzmc5x4/tVeFGXs0yASlw/fCte4YTV/BmeimkJvH83Y38mOV7J9fghcaKJyDMMvRTcp0hVr3v3feOCzh90o+g=="
},
"sprintf-js": {
"version": "1.1.2",

View File

@@ -45,7 +45,7 @@
"node-notifier": "^10.0.1",
"node-persist": "^3.1.0",
"read": "^1.0.7",
"splatnet3-types": "^0.1.20221020213035",
"splatnet3-types": "^0.1.20221021222422",
"supports-color": "^8.1.1",
"tslib": "^2.4.0",
"uuid": "^8.3.2",

View File

@@ -1,9 +1,9 @@
import fetch, { Response } from 'node-fetch';
import createDebug from 'debug';
import { GraphQLRequest, GraphQLResponse, KnownRequestId, RequestId, RequestParametersFor, RequestResultFor } from 'splatnet3-types/splatnet3';
import { GraphQLRequest, GraphQLResponse, KnownRequestId, MyOutfitInput, RequestId, ResultTypes, VariablesTypes } from 'splatnet3-types/splatnet3';
import { WebServiceToken } from './coral-types.js';
import { NintendoAccountUser } from './na.js';
import { defineResponse, ErrorResponse, HasResponse } from './util.js';
import { defineResponse, ErrorResponse, HasResponse, ResponseSymbol } from './util.js';
import CoralApi from './coral.js';
import { timeoutSignal } from '../util/misc.js';
import { BulletToken } from './splatnet3-types.js';
@@ -35,6 +35,8 @@ const AUTH_ERROR_CODES = {
599: 'ERROR_SERVER',
} as const;
const REPLAY_CODE_REGEX = /^[A-Z0-9]{16}$/;
export default class SplatNet3Api {
onTokenShouldRenew: ((remaining: number, res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
onTokenExpired: ((res: Response) => Promise<SplatNet3AuthData | void>) | null = null;
@@ -85,7 +87,7 @@ export default class SplatNet3Api {
}).finally(() => {
this._renewToken = null;
});
return this.fetch(url, method, body, headers, _log, _attempt + 1);
return this.fetch(url, method, body, headers, _log, _attempt + 1);
}
if (response.status !== 200) {
@@ -109,11 +111,18 @@ export default class SplatNet3Api {
}
async persistedQuery<
I extends string,
T = I extends KnownRequestId ? RequestResultFor<I> : unknown,
V = I extends KnownRequestId ? RequestParametersFor<I> : unknown,
>(id: I, variables: V) {
const req: GraphQLRequest<V> = {
T = unknown, V = unknown,
/** @private */
_Id extends string = string,
/** @private */
_Result extends (T extends unknown ? _Id extends KnownRequestId ? ResultTypes[_Id] : unknown : T) =
(T extends unknown ? _Id extends KnownRequestId ? ResultTypes[_Id] : unknown : T),
/** @private */
_Variables extends (V extends unknown ? _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown : V) =
(V extends unknown ? _Id extends KnownRequestId ? VariablesTypes[_Id] : unknown : V),
>(id: _Id, variables: _Variables) {
const req: GraphQLRequest<_Variables> = {
variables,
extensions: {
persistedQuery: {
@@ -123,124 +132,442 @@ export default class SplatNet3Api {
},
};
const data = await this.fetch<GraphQLResponse<T>>('/graphql', 'POST', JSON.stringify(req), undefined,
const data = await this.fetch<GraphQLResponse<_Result>>('/graphql', 'POST', JSON.stringify(req), undefined,
'graphql query ' + id);
if ('errors' in data) {
throw new ErrorResponse('[splatnet3] GraphQL error: ' + data.errors.map(e => e.message).join(', '),
data[ResponseSymbol], data);
}
return data;
}
async getHome() {
return this.persistedQuery(RequestId.HomeQuery, {});
}
/** * */
async getCurrentFest() {
return this.persistedQuery(RequestId.CurrentFestQuery, {});
}
/** * */
async getConfigureAnalytics() {
return this.persistedQuery(RequestId.ConfigureAnalyticsQuery, {});
}
/** / */
async getHome() {
return this.persistedQuery(RequestId.HomeQuery, {});
}
/** / -> /setting */
async getSettings() {
return this.persistedQuery(RequestId.SettingQuery, {});
}
/** / -> /photo_album */
async getPhotoAlbum() {
return this.persistedQuery(RequestId.PhotoAlbumQuery, {});
}
/** / -> /photo_album -> pull-to-refresh */
async getPhotoAlbumRefetch() {
return this.persistedQuery(RequestId.PhotoAlbumRefetchQuery, {});
}
/** / -> /catalog_record */
async getCatalog() {
return this.persistedQuery(RequestId.CatalogQuery, {});
}
/** / -> /catalog_record -> pull-to-refresh */
async getCatalogRefetch() {
return this.persistedQuery(RequestId.CatalogRefetchQuery, {});
}
/** / -> /checkin */
async getCheckinHistory() {
return this.persistedQuery(RequestId.CheckinQuery, {});
}
/** / -> /checkin */
async checkin(id: string) {
return this.persistedQuery(RequestId.CheckinWithQRCodeMutation, {
checkinEventId: id,
});
}
/** / -> /friends */
async getFriends() {
return this.persistedQuery(RequestId.FriendListQuery, {});
}
/** / -> /friends -> pull-to-refresh */
async getFriendsRefetch() {
return this.persistedQuery(RequestId.FriendListRefetchQuery, {});
}
/** / -> /hero_record */
async getHeroRecords() {
return this.persistedQuery(RequestId.HeroHistoryQuery, {});
}
/** / -> /hero_record -> pull-to-refresh */
async getHeroRecordsRefetch() {
return this.persistedQuery(RequestId.HeroHistoryRefetchQuery, {});
}
/** / -> /history_record */
async getHistoryRecords() {
return this.persistedQuery(RequestId.HistoryRecordQuery, {});
}
/** / -> /history_record -> pull-to-refresh */
async getHistoryRecordsRefetch() {
return this.persistedQuery(RequestId.HistoryRecordRefetchQuery, {});
}
/** / -> /schedule */
async getSchedules() {
return this.persistedQuery(RequestId.StageScheduleQuery, {});
}
/** / -> /stage_record */
async getStageRecords() {
return this.persistedQuery(RequestId.StageRecordQuery, {});
}
/** / -> /stage_record -> pull-to-refresh */
async getStageRecordsRefetch() {
return this.persistedQuery(RequestId.StageRecordsRefetchQuery, {});
}
/** / -> /weapon_record */
async getWeaponRecords() {
return this.persistedQuery(RequestId.WeaponRecordQuery, {});
}
/** / -> /weapon_record -> pull-to-refresh */
async getWeaponRecordsRefetch() {
return this.persistedQuery(RequestId.WeaponRecordsRefetchQuery, {});
}
//
// Wandercrust
//
/** / -> /challenge */
async getChallengeHome() {
return this.persistedQuery(RequestId.ChallengeQuery, {});
}
/** / -> /challenge -> pull-to-refresh */
async getChallengeHomeRefetch() {
return this.persistedQuery(RequestId.ChallengeRefetchQuery, {});
}
/** / -> /challenge -> /challenge/{id} */
async getChallengeJourney(id: string) {
return this.persistedQuery(RequestId.JourneyQuery, {
id,
});
}
/** / -> /challenge -> /challenge/{id} -> pull-to-refresh */
async getChallengeJourneyRefetch(id: string) {
return this.persistedQuery(RequestId.JourneyRefetchQuery, {
id,
});
}
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/*s */
async getChallengeJourneyChallenges(id: string) {
return this.persistedQuery(RequestId.JourneyChallengeDetailQuery, {
journeyId: id,
});
}
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/* -> pull-to-refresh */
async getChallengeJourneyChallengesRefetch(id: string) {
return this.persistedQuery(RequestId.JourneyChallengeDetailRefetchQuery, {
journeyId: id,
});
}
/** / -> /challenge -> /challenge/{id} -> /challenge/{id}/* -> support */
async supportChallenge(id: string) {
return this.persistedQuery(RequestId.SupportButton_SupportChallengeMutation, {
id,
});
}
//
// Splatfests
//
/** / -> /fest_record */
async getFestRecords() {
return this.persistedQuery(RequestId.FestRecordQuery, {});
}
/** / -> /fest_record -> pull-to-refresh */
async getFestRecordsRefetch() {
return this.persistedQuery(RequestId.FestRecordRefetchQuery, {});
}
/** / -> /fest_record/{id} */
async getFestDetail(id: string) {
return this.persistedQuery(RequestId.DetailFestRecordDetailQuery, {
festId: id,
});
}
/** / -> /fest_record -> /fest_record/{id} -> pull-to-refresh */
async getFestDetailRefetch(id: string) {
return this.persistedQuery(RequestId.DetailFestRefethQuery, {
festId: id,
});
}
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} */
async getFestVotingStatus(id: string) {
return this.persistedQuery(RequestId.DetailVotingStatusQuery, {
festId: id,
});
}
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} -> pull-to-refresh */
async getFestVotingStatusRefetch(id: string) {
return this.persistedQuery(RequestId.DetailFestVotingStatusRefethQuery, {
festId: id,
});
}
/** / -> /fest_record -> /fest_record/{id} - not closed -> /fest_record/voting_status/{id} - not voted in game */
async updateFestPoll(id: string) {
return this.persistedQuery(RequestId.VotesUpdateFestVoteMutation, {
teamId: id,
});
}
async getFriends() {
return this.persistedQuery(RequestId.FriendListQuery, {});
/** / -> /fest_record -> /fest_record/{id} - closed -> /fest_record/ranking/{id} */
async getFestRanking(id: string) {
return this.persistedQuery(RequestId.DetailRankingQuery, {
festId: id,
});
}
async getFriendsRefetch() {
return this.persistedQuery(RequestId.FriendListRefetchQuery, {});
//
// SplatNet Shop
//
/** / -> /gesotown */
async getSaleGear() {
return this.persistedQuery(RequestId.GesotownQuery, {});
}
async getHistoryRecords() {
return this.persistedQuery(RequestId.HistoryRecordQuery, {});
/** / -> /gesotown -> pull-to-refresh */
async getSaleGearRefetch() {
return this.persistedQuery(RequestId.GesotownRefetchQuery, {});
}
async getSchedules() {
return this.persistedQuery(RequestId.StageScheduleQuery, {});
/** / -> /gesotown -> /gesotown/{id} */
async getSaleGearDetail(id: string) {
return this.persistedQuery(RequestId.SaleGearDetailQuery, {
saleGearId: id,
});
}
/** / -> /gesotown -> /gesotown/{id} -> order */
async orderSaleGear(id: string, force = false) {
return this.persistedQuery(RequestId.SaleGearDetailOrderGesotownGearMutation, {
input: {
id,
isForceOrder: force,
},
});
}
//
// Freshest Fits/my outfits
//
/** / -> /my_outfits */
async getMyOutfits() {
return this.persistedQuery(RequestId.MyOutfitsQuery, {});
}
/** / -> /my_outfits -> pull-to-refresh */
async getMyOutfitsRefetch() {
return this.persistedQuery(RequestId.MyOutfitsRefetchQuery, {});
}
/** / -> /my_outfits -> /my_outfits/{id} */
async getMyOutfitDetail(id: string) {
return this.persistedQuery(RequestId.MyOutfitDetailQuery, {
myOutfitId: id,
});
}
/** / -> /my_outfits -> /my_outfits/{id / create} */
async getEquipmentFilters(id: string) {
return this.persistedQuery(RequestId.MyOutfitCommonDataFilteringConditionQuery, {});
}
/** / -> /my_outfits -> /my_outfits/{id / create} */
async getEquipment(id: string) {
return this.persistedQuery(RequestId.MyOutfitCommonDataEquipmentsQuery, {});
}
/** / -> /my_outfits -> /my_outfits/{id / create} */
async createOutfit(data: MyOutfitInput) {
return this.persistedQuery(RequestId.CreateMyOutfitMutation, {
input: {
myOutfit: data,
},
connections: [
'client:root:__connection_myOutfits_connection',
],
});
}
/** / -> /my_outfits -> /my_outfits/{id / create} */
async updateOutfit(id: string, data: MyOutfitInput) {
return this.persistedQuery(RequestId.UpdateMyOutfitMutation, {
input: {
myOutfit: {
id,
...data,
},
},
});
}
//
// Replays
//
/** / -> /replay */
async getReplays() {
return this.persistedQuery(RequestId.ReplayQuery, {});
}
/** / -> /replay -> pull-to-refetch */
async getReplaysRefetch() {
return this.persistedQuery(RequestId.ReplayUploadedReplayListRefetchQuery, {});
}
/** / -> /replay -> enter code */
async getReplaySearchResult(code: string) {
if (!REPLAY_CODE_REGEX.test(code)) throw new Error('Invalid replay code');
return this.persistedQuery(RequestId.DownloadSearchReplayQuery, {
code,
});
}
/** / -> /replay -> enter code -> download */
async reserveReplayDownload(id: string) {
return this.persistedQuery(RequestId.ReplayModalReserveReplayDownloadMutation, {
input: {
id,
},
});
}
//
// Battle history
//
/** / -> /history */
async getBattleHistoryCurrentPlayer() {
return this.persistedQuery(RequestId.BattleHistoryCurrentPlayerQuery, {});
}
/** / -> /history */
async getLatestBattleHistories() {
return this.persistedQuery(RequestId.LatestBattleHistoriesQuery, {});
}
/** / -> /history */
async getRegularBattleHistories() {
return this.persistedQuery(RequestId.RegularBattleHistoriesQuery, {});
}
/** / -> /history */
async getBankaraBattleHistories() {
return this.persistedQuery(RequestId.BankaraBattleHistoriesQuery, {});
}
/** / -> /history */
async getPrivateBattleHistories() {
return this.persistedQuery(RequestId.PrivateBattleHistoriesQuery, {});
}
/** / -> /history -> /history/detail/{id} */
async getBattleHistoryDetail(id: string) {
return this.persistedQuery(RequestId.VsHistoryDetailQuery, {
vsResultId: id,
});
}
/** / -> /history -> /history/detail/{id} -> pull-to-refresh */
async getBattleHistoryDetailPagerRefetch(id: string) {
return this.persistedQuery(RequestId.VsHistoryDetailPagerRefetchQuery, {
vsResultId: id,
});
}
/** / -> /history -> /history/detail/* -> latest */
async getBattleHistoryLatest() {
return this.persistedQuery(RequestId.PagerLatestVsDetailQuery, {});
}
/** / -> /history -> /history/detail/* -> latest */
async getBattleHistoryPagerUpdateByVsMode() {
return this.persistedQuery(RequestId.PagerUpdateBattleHistoriesByVsModeQuery, {
isBankara: false,
isLeague: false,
isPrivate: false,
isRegular: false,
isXBattle: false,
});
}
//
// Salmon Run
//
/** / -> /coop */
async getCoopHistory() {
return this.persistedQuery(RequestId.CoopHistoryQuery, {});
}
/** / -> /coop */
async getCoopHistoryRefetch() {
return this.persistedQuery(RequestId.RefetchableCoopHistory_CoopResultQuery, {});
}
/** / -> /coop -> /coop/{id} */
async getCoopHistoryDetail(id: string) {
return this.persistedQuery(RequestId.CoopHistoryDetailQuery, {
coopHistoryDetailId: id,
});
}
/** / -> /coop -> /coop/{id} -> pull-to-refresh */
async getCoopHistoryDetailRefetch(id: string) {
return this.persistedQuery(RequestId.CoopHistoryDetailRefetchQuery, {
id,
});
}
/** / -> /coop -> /coop/* -> latest */
async getCoopHistoryLatest() {
return this.persistedQuery(RequestId.CoopPagerLatestCoopQuery, {});
}
//
//
async renewTokenWithCoral(nso: CoralApi, user: NintendoAccountUser) {
const data = await SplatNet3Api.loginWithCoral(nso, user);
this.setTokenWithSavedToken(data);

View File

@@ -2,7 +2,7 @@ import * as net from 'node:net';
import createDebug from 'debug';
import express from 'express';
import * as persist from 'node-persist';
import { BankaraMatchMode, BankaraMatchSetting, CoopSetting, DetailVotingStatusResult, FestMatchSetting, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, Friend as SplatNetFriend, FriendListResult, FriendOnlineState, GraphQLResponse, LeagueMatchSetting, RegularMatchSetting, StageScheduleResult, XMatchSetting } from 'splatnet3-types/splatnet3';
import { BankaraMatchMode, BankaraMatchSetting, CoopSetting, DetailVotingStatusResult, FestMatchSetting, FestState, FestTeam_schedule, FestTeam_votingStatus, FestVoteState, Fest_schedule, Friend as SplatNetFriend, FriendListResult, FriendOnlineState, GraphQLSuccessResponse, LeagueMatchSetting, RegularMatchSetting, StageScheduleResult, XMatchSetting } from 'splatnet3-types/splatnet3';
import type { Arguments as ParentArguments } from '../cli.js';
import { ArgumentsCamelCase, Argv, YargsArguments } from '../util/yargs.js';
import { initStorage } from '../util/storage.js';
@@ -92,8 +92,8 @@ export class SplatNet3User {
created_at = Date.now();
expires_at = Infinity;
schedules: GraphQLResponse<StageScheduleResult> | null = null;
fest_vote_status: GraphQLResponse<DetailVotingStatusResult> | null = null;
schedules: GraphQLSuccessResponse<StageScheduleResult> | null = null;
fest_vote_status: GraphQLSuccessResponse<DetailVotingStatusResult> | null = null;
promise = new Map<string, Promise<void>>();
@@ -109,7 +109,7 @@ export class SplatNet3User {
constructor(
public splatnet: SplatNet3Api,
public data: SavedBulletToken,
public friends: GraphQLResponse<FriendListResult>,
public friends: GraphQLSuccessResponse<FriendListResult>,
) {}
private async update(key: keyof SplatNet3User['updated'], callback: () => Promise<void>, ttl: number) {
@@ -261,19 +261,19 @@ function createApp(
for (const player of team.votes.nodes) {
if (player.userIcon.url !== friend.userIcon.url) continue;
match.splatoon3_fest_team = createFestVoteTeam(team, FestVoteState.VOTED);
break;
}
for (const player of team.preVotes.nodes) {
if (player.userIcon.url !== friend.userIcon.url) continue;
match.splatoon3_fest_team = createFestVoteTeam(team, FestVoteState.PRE_VOTED);
break;
}
}
if (!match.splatoon3_fest_team && fest_vote_status.undecidedVotes) {
match.splatoon3_fest_team = null;
}
@@ -310,7 +310,7 @@ function createApp(
splatoon3_vs_setting?:
RegularMatchSetting | BankaraMatchSetting | FestMatchSetting |
LeagueMatchSetting | XMatchSetting | null;
splatoon3_coop_setting?: CoopSetting | null;
splatoon3_coop_setting?: Pick<CoopSetting, '__typename' | 'coopStage' | 'weapons'> | null;
splatoon3_fest?: Fest_schedule | null;
} = {};

View File

@@ -23,7 +23,7 @@ export function builder(yargs: Argv<ParentArguments>) {
],
colWidths: [10, 42, 80],
});
for await (const data of iterateLocalStorage(storage)) {
const value = util.inspect(data.value, {
compact: true,

View File

@@ -1,7 +1,7 @@
import createDebug from 'debug';
import persist from 'node-persist';
import DiscordRPC from 'discord-rpc';
import { BankaraMatchMode, BankaraMatchSetting, CoopSchedule, CoopSetting, DetailVotingStatusResult, FestMatchSetting, FestState, FestTeamRole, FestTeam_schedule, FestTeam_votingStatus, Fest_schedule, FriendListResult, FriendOnlineState, GraphQLResponse, LeagueMatchSetting, RegularMatchSetting, StageScheduleResult, VsSchedule_bankara, VsSchedule_fest, VsSchedule_league, VsSchedule_regular, VsSchedule_xMatch, XMatchSetting } from 'splatnet3-types/splatnet3';
import { BankaraMatchMode, BankaraMatchSetting, CoopSchedule, CoopSetting, DetailVotingStatusResult, FestMatchSetting, FestState, FestTeamRole, FestTeam_schedule, FestTeam_votingStatus, Fest_schedule, FriendListResult, FriendOnlineState, GraphQLSuccessResponse, LeagueMatchSetting, RegularMatchSetting, StageScheduleResult, VsSchedule_bankara, VsSchedule_fest, VsSchedule_league, VsSchedule_regular, VsSchedule_xMatch, XMatchSetting } from 'splatnet3-types/splatnet3';
import { Game } from '../../../api/coral-types.js';
import SplatNet3Api from '../../../api/splatnet3.js';
import { DiscordPresenceExternalMonitorsConfiguration } from '../../../app/common/types.js';
@@ -21,9 +21,9 @@ export default class SplatNet3Monitor extends EmbeddedLoop {
splatnet: SplatNet3Api | null = null;
data: SavedBulletToken | null = null;
cached_friends: GraphQLResponse<FriendListResult> | null = null;
cached_schedules: GraphQLResponse<StageScheduleResult> | null = null;
cached_voting_status: GraphQLResponse<DetailVotingStatusResult> | null = null;
cached_friends: GraphQLSuccessResponse<FriendListResult> | null = null;
cached_schedules: GraphQLSuccessResponse<StageScheduleResult> | null = null;
cached_voting_status: GraphQLSuccessResponse<DetailVotingStatusResult> | null = null;
friend: FriendListResult['friends']['nodes'][0] | null = null;
@@ -32,7 +32,7 @@ export default class SplatNet3Monitor extends EmbeddedLoop {
fest_schedule: VsSchedule_fest | null = null;
league_schedule: VsSchedule_league | null = null;
x_schedule: VsSchedule_xMatch | null = null;
coop_schedule: CoopSchedule | null = null;
coop_schedule: Pick<CoopSchedule, 'startTime' | 'endTime' | 'setting'> | null = null;
fest: Fest_schedule | null = null;
fest_team_voting_status: FestTeam_votingStatus | null = null;
fest_team: FestTeam_schedule | null = null;

View File

@@ -95,7 +95,7 @@ export abstract class EmbeddedLoop extends Loop {
try {
const result = await this.loop(true);
if (result === LoopResult.STOP) return;
while (i === this._running) {
const result = await this.loop();
@@ -104,7 +104,7 @@ export abstract class EmbeddedLoop extends Loop {
return;
}
}
if (this._running === 0 && !this.onStop) {
// Run one more time after the loop ends
const result = await this.loopRun();