syncData.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  1. import path from "node:path";
  2. import { fileURLToPath } from "node:url";
  3. import { pinnacleGet, getPsteryRelations, updateBaseEvents, notifyException } from "./pinnacleClient.js";
  4. import { parseGame, parseIorDetail } from "./parseGameData.js";
  5. import Logs from "./logs.js";
  6. import { getData, setData } from "./cache.js";
  7. import { GLOBAL_DATA } from "./globalData.js";
  8. const __filename = fileURLToPath(import.meta.url);
  9. const __dirname = path.dirname(__filename);
  10. const gamesMapCacheFile = path.join(__dirname, '../data/gamesCache.json');
  11. const globalDataCacheFile = path.join(__dirname, '../data/globalDataCache.json');
  12. const TP = 'ps_9_9_1';
  13. const IS_DEV = process.env.NODE_ENV == 'development';
  14. /**
  15. * 重置版本计数
  16. */
  17. const resetVersionsCount = () => {
  18. GLOBAL_DATA.straightFixturesVersion = 0;
  19. GLOBAL_DATA.specialFixturesVersion = 0;
  20. GLOBAL_DATA.straightOddsVersion = 0;
  21. GLOBAL_DATA.specialsOddsVersion = 0;
  22. GLOBAL_DATA.straightFixturesCount = 0;
  23. GLOBAL_DATA.specialFixturesCount = 0;
  24. }
  25. /**
  26. * 增加版本计数
  27. */
  28. const incrementVersionsCount = () => {
  29. GLOBAL_DATA.straightFixturesCount += 1;
  30. GLOBAL_DATA.specialFixturesCount += 1;
  31. }
  32. /**
  33. * 获取指定时区当前日期或时间
  34. * @param {number} offsetHours - 时区相对 UTC 的偏移(例如:-4 表示 GMT-4)
  35. * @param {boolean} [withTime=false] - 是否返回完整时间(默认只返回日期)
  36. * @returns {string} 格式化的日期或时间字符串
  37. */
  38. const getDateInTimezone = (offsetHours, withTime = false) => {
  39. const nowUTC = new Date();
  40. const targetTime = new Date(nowUTC.getTime() + offsetHours * 60 * 60 * 1000);
  41. const year = targetTime.getUTCFullYear();
  42. const month = String(targetTime.getUTCMonth() + 1).padStart(2, '0');
  43. const day = String(targetTime.getUTCDate()).padStart(2, '0');
  44. if (!withTime) {
  45. return `${year}-${month}-${day}`;
  46. }
  47. const hours = String(targetTime.getUTCHours()).padStart(2, '0');
  48. const minutes = String(targetTime.getUTCMinutes()).padStart(2, '0');
  49. const seconds = String(targetTime.getUTCSeconds()).padStart(2, '0');
  50. return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
  51. }
  52. /**
  53. * 判断两个数组是否相等
  54. * @param {*} arr1
  55. * @param {*} arr2
  56. * @returns
  57. */
  58. const isArrayEqualUnordered = (arr1, arr2) => {
  59. if (arr1.length !== arr2.length) return false;
  60. const s1 = [...arr1].sort();
  61. const s2 = [...arr2].sort();
  62. return s1.every((v, i) => v === s2[i]);
  63. }
  64. /**
  65. * 获取已过滤比赛列表
  66. * @returns {Promise<void>}
  67. */
  68. const getFiltedGames = () => {
  69. return new Promise(resolve => {
  70. const updateFiltedGames = () => {
  71. getPsteryRelations()
  72. .then(res => {
  73. if (res.statusCode !== 200) {
  74. throw new Error(res.message);
  75. }
  76. // Logs.outDev('update filted games', res.data);
  77. const games = res.data.map(game => {
  78. const { eventId, leagueId } = game?.rel?.ps ?? {};
  79. return {
  80. eventId,
  81. leagueId,
  82. };
  83. });
  84. const newFiltedLeagues = [...new Set(games.map(game => game.leagueId).filter(leagueId => leagueId))];
  85. if (!isArrayEqualUnordered(newFiltedLeagues, GLOBAL_DATA.filtedLeagues)) {
  86. Logs.outDev('filted leagues changed', newFiltedLeagues);
  87. resetVersionsCount();
  88. GLOBAL_DATA.filtedLeagues = newFiltedLeagues;
  89. }
  90. GLOBAL_DATA.filtedGames = games.map(game => game.eventId).filter(eventId => eventId);
  91. resolve();
  92. setTimeout(updateFiltedGames, 1000 * 60);
  93. })
  94. .catch(err => {
  95. Logs.err('failed to update filted games:', err.message);
  96. setTimeout(updateFiltedGames, 1000 * 5);
  97. });
  98. }
  99. updateFiltedGames();
  100. });
  101. }
  102. /**
  103. * 获取胜平负盘口数据
  104. * @returns {Promise<Object>}
  105. */
  106. const getStraightFixtures = async () => {
  107. if (!GLOBAL_DATA.filtedLeagues.length) {
  108. return Promise.reject(new Error('no filted leagues'));
  109. }
  110. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  111. let since = GLOBAL_DATA.straightFixturesVersion;
  112. if (GLOBAL_DATA.straightFixturesCount >= 12) {
  113. since = 0;
  114. GLOBAL_DATA.straightFixturesCount = 0;
  115. }
  116. if (since == 0) {
  117. Logs.outDev('full update straight fixtures');
  118. }
  119. return pinnacleGet('/v3/fixtures', { sportId: 29, leagueIds, since })
  120. .then(data => {
  121. const { league, last } = data;
  122. if (!last) {
  123. return {};
  124. }
  125. GLOBAL_DATA.straightFixturesVersion = last;
  126. const games = league?.map(league => {
  127. const { id: leagueId, events } = league;
  128. return events?.map(event => {
  129. const { starts } = event;
  130. const timestamp = new Date(starts).getTime();
  131. return { leagueId, ...event, timestamp };
  132. });
  133. })
  134. .flat() ?? [];
  135. const update = since == 0 ? 'full' : 'increment';
  136. return { games, update };
  137. });
  138. }
  139. /**
  140. * 更新胜平负盘口数据
  141. * @returns {Promise<void>}
  142. */
  143. const updateStraightFixtures = async () => {
  144. return getStraightFixtures()
  145. .then(data => {
  146. const { games, update } = data;
  147. const { gamesMap } = GLOBAL_DATA;
  148. if (games?.length) {
  149. games.forEach(game => {
  150. const { id } = game;
  151. if (!gamesMap[id]) {
  152. gamesMap[id] = game;
  153. }
  154. else {
  155. Object.assign(gamesMap[id], game);
  156. }
  157. });
  158. }
  159. if (update && update == 'full') {
  160. const gamesSet = new Set(games.map(game => game.id));
  161. Object.keys(gamesMap)?.forEach(key => {
  162. if (!gamesSet.has(+key)) {
  163. delete gamesMap[key];
  164. }
  165. });
  166. }
  167. });
  168. }
  169. /**
  170. * 获取胜平负赔率数据
  171. * @returns {Promise<Array>}
  172. */
  173. const getStraightOdds = async () => {
  174. if (!GLOBAL_DATA.filtedLeagues.length) {
  175. return Promise.reject(new Error('no filted leagues'));
  176. }
  177. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  178. const since = GLOBAL_DATA.straightOddsVersion;
  179. // if (GLOBAL_DATA.straightOddsCount >= 27) {
  180. // since = 0;
  181. // GLOBAL_DATA.straightOddsCount = 3;
  182. // }
  183. if (since == 0) {
  184. Logs.outDev('full update straight odds');
  185. }
  186. return pinnacleGet('/v4/odds', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  187. .then(data => {
  188. const { leagues, last } = data;
  189. if (!last) {
  190. return [];
  191. }
  192. GLOBAL_DATA.straightOddsVersion = last;
  193. const games = leagues?.flatMap(league => league.events);
  194. // return games;
  195. return games?.map(item => {
  196. const { periods, ...rest } = item;
  197. const straight = periods?.find(period => period.number == 0);
  198. const straight1st = periods?.find(period => period.number == 1);
  199. const gameInfo = rest;
  200. if (straight || straight1st) {
  201. gameInfo.periods = {};
  202. }
  203. if (straight) {
  204. gameInfo.periods.straight = straight;
  205. }
  206. if (straight1st) {
  207. gameInfo.periods.straight1st = straight1st;
  208. }
  209. return gameInfo;
  210. }) ?? [];
  211. });
  212. }
  213. /**
  214. * 更新胜平负赔率数据
  215. * @returns {Promise<void>}
  216. */
  217. const updateStraightOdds = async () => {
  218. return getStraightOdds()
  219. .then(games => {
  220. if (games?.length) {
  221. const { gamesMap={} } = GLOBAL_DATA;
  222. games.forEach(game => {
  223. const { id, periods, ...rest } = game;
  224. const localGame = gamesMap[id];
  225. if (localGame) {
  226. Object.assign(localGame, rest);
  227. if (!localGame.periods && periods) {
  228. localGame.periods = periods;
  229. }
  230. else if (periods) {
  231. Object.assign(localGame.periods, periods);
  232. }
  233. }
  234. });
  235. }
  236. });
  237. }
  238. /**
  239. * 获取特殊盘口列表数据
  240. * @returns {Promise<Object>}
  241. */
  242. const getSpecialFixtures = async () => {
  243. if (!GLOBAL_DATA.filtedLeagues.length) {
  244. return Promise.reject(new Error('no filted leagues'));
  245. }
  246. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  247. let since = GLOBAL_DATA.specialFixturesVersion;
  248. if (GLOBAL_DATA.specialFixturesCount >= 18) {
  249. since = 0;
  250. GLOBAL_DATA.specialFixturesCount = 6;
  251. }
  252. if (since == 0) {
  253. Logs.outDev('full update special fixtures');
  254. }
  255. return pinnacleGet('/v2/fixtures/special', { sportId: 29, leagueIds, since })
  256. .then(data => {
  257. const { leagues, last } = data;
  258. if (!last) {
  259. return { specials: [], update: 'increment' };
  260. }
  261. GLOBAL_DATA.specialFixturesVersion = last;
  262. const specials = leagues?.map(league => {
  263. const { specials } = league;
  264. return specials?.filter(special => special.event)
  265. .map(special => {
  266. const { event: { id: eventId, periodNumber }, ...rest } = special ?? { event: {} };
  267. return { eventId, periodNumber, ...rest };
  268. }) ?? [];
  269. })
  270. .flat()
  271. .filter(special => {
  272. if (special.name.includes('Winning Margin') || special.name.includes('Exact Total Goals')) {
  273. return true;
  274. }
  275. return false;
  276. }) ?? [];
  277. const update = since == 0 ? 'full' : 'increment';
  278. return { specials, update };
  279. });
  280. }
  281. /**
  282. * 合并盘口数据
  283. * @param {*} localContestants
  284. * @param {*} remoteContestants
  285. * @returns
  286. */
  287. const mergeContestants = (localContestants=[], remoteContestants=[]) => {
  288. const localContestantsMap = new Map(localContestants.map(contestant => [contestant.id, contestant]));
  289. remoteContestants.forEach(contestant => {
  290. const localContestant = localContestantsMap.get(contestant.id);
  291. if (localContestant) {
  292. Object.assign(localContestant, contestant);
  293. }
  294. else {
  295. localContestants.push(contestant);
  296. }
  297. });
  298. const remoteContestantsMap = new Map(remoteContestants.map(contestant => [contestant.id, contestant]));
  299. for (let i = localContestants.length - 1; i >= 0; i--) {
  300. if (!remoteContestantsMap.has(localContestants[i].id)) {
  301. localContestants.splice(i, 1);
  302. }
  303. }
  304. return localContestants;
  305. }
  306. /**
  307. * 合并特殊盘口数据
  308. * @param {*} localSpecials
  309. * @param {*} remoteSpecials
  310. * @param {*} specialName
  311. * @returns
  312. */
  313. const mergeSpecials = (localSpecials, remoteSpecials, specialName) => {
  314. if (localSpecials[specialName] && remoteSpecials[specialName]) {
  315. const { contestants: specialContestants, ...specialRest } = remoteSpecials[specialName];
  316. Object.assign(localSpecials[specialName], specialRest);
  317. mergeContestants(localSpecials[specialName].contestants, specialContestants);
  318. }
  319. else if (remoteSpecials[specialName]) {
  320. localSpecials[specialName] = remoteSpecials[specialName];
  321. }
  322. }
  323. /**
  324. * 更新特殊盘口列表数据
  325. * @returns {Promise<void>}
  326. */
  327. const updateSpecialFixtures = async () => {
  328. return getSpecialFixtures()
  329. .then(data => {
  330. const { specials, update } = data;
  331. if (specials?.length) {
  332. const { gamesMap={} } = GLOBAL_DATA;
  333. const gamesSpecialsMap = {};
  334. specials.forEach(special => {
  335. const { eventId } = special;
  336. if (!gamesSpecialsMap[eventId]) {
  337. gamesSpecialsMap[eventId] = {};
  338. }
  339. if (special.name == 'Winning Margin') {
  340. gamesSpecialsMap[eventId].winningMargin = special;
  341. }
  342. else if (special.name == 'Exact Total Goals') {
  343. gamesSpecialsMap[eventId].exactTotalGoals = special;
  344. }
  345. else if (special.name == 'Winning Margin 1st Half') {
  346. gamesSpecialsMap[eventId].winningMargin1st = special;
  347. }
  348. else if (special.name == 'Exact Total Goals 1st Half') {
  349. gamesSpecialsMap[eventId].exactTotalGoals1st = special;
  350. }
  351. });
  352. Object.keys(gamesSpecialsMap).forEach(eventId => {
  353. if (!gamesMap[eventId]) {
  354. return;
  355. }
  356. if (!gamesMap[eventId].specials) {
  357. gamesMap[eventId].specials = {};
  358. }
  359. const localSpecials = gamesMap[eventId].specials;
  360. const remoteSpecials = gamesSpecialsMap[eventId];
  361. mergeSpecials(localSpecials, remoteSpecials, 'winningMargin');
  362. mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals');
  363. mergeSpecials(localSpecials, remoteSpecials, 'winningMargin1st');
  364. mergeSpecials(localSpecials, remoteSpecials, 'exactTotalGoals1st');
  365. });
  366. }
  367. });
  368. }
  369. /**
  370. * 获取特殊盘口赔率数据
  371. * @returns {Promise<Array>}
  372. */
  373. const getSpecialsOdds = async () => {
  374. if (!GLOBAL_DATA.filtedLeagues.length) {
  375. return Promise.reject(new Error('no filted leagues'));
  376. }
  377. const leagueIds = GLOBAL_DATA.filtedLeagues.join(',');
  378. const since = GLOBAL_DATA.specialsOddsVersion;
  379. // if (GLOBAL_DATA.specialsOddsCount >= 33) {
  380. // since = 0;
  381. // GLOBAL_DATA.specialsOddsCount = 9;
  382. // }
  383. if (since == 0) {
  384. Logs.outDev('full update specials odds');
  385. }
  386. return pinnacleGet('/v2/odds/special', { sportId: 29, oddsFormat: 'Decimal', leagueIds, since })
  387. .then(data => {
  388. const { leagues, last } = data;
  389. if (!last) {
  390. return [];
  391. }
  392. GLOBAL_DATA.specialsOddsVersion = last;
  393. return leagues?.flatMap(league => league.specials);
  394. });
  395. }
  396. /**
  397. * 更新特殊盘口赔率数据
  398. * @returns
  399. */
  400. const updateSpecialsOdds = async () => {
  401. return getSpecialsOdds()
  402. .then(specials => {
  403. if (specials.length) {
  404. const { gamesMap={} } = GLOBAL_DATA;
  405. const contestants = Object.values(gamesMap)
  406. .filter(game => game.specials)
  407. .map(game => {
  408. const { specials } = game;
  409. const contestants = Object.values(specials).map(special => {
  410. const { contestants } = special;
  411. return contestants.map(contestant => [contestant.id, contestant]);
  412. });
  413. return contestants;
  414. }).flat(2);
  415. const contestantsMap = new Map(contestants);
  416. const lines = specials?.flatMap(special => special.contestantLines) ?? [];
  417. lines.forEach(line => {
  418. const { id, handicap, lineId, max, price } = line;
  419. const contestant = contestantsMap.get(id);
  420. if (!contestant) {
  421. return;
  422. }
  423. contestant.handicap = handicap;
  424. contestant.lineId = lineId;
  425. contestant.max = max;
  426. contestant.price = price;
  427. });
  428. }
  429. });
  430. }
  431. /**
  432. * 获取滚球数据
  433. * @returns {Promise<Array>}
  434. */
  435. const getInRunning = async () => {
  436. return pinnacleGet('/v2/inrunning')
  437. .then(data => {
  438. const sportId = 29;
  439. const leagues = data.sports?.find(sport => sport.id == sportId)?.leagues ?? [];
  440. return leagues.filter(league => {
  441. const { id } = league;
  442. const filtedLeaguesSet = new Set(GLOBAL_DATA.filtedLeagues);
  443. return filtedLeaguesSet.has(id);
  444. }).flatMap(league => league.events);
  445. });
  446. }
  447. /**
  448. * 更新滚球数据
  449. * @returns {Promise<void>}
  450. */
  451. const updateInRunning = async () => {
  452. return getInRunning()
  453. .then(games => {
  454. if (!games.length) {
  455. return;
  456. }
  457. const { gamesMap={} } = GLOBAL_DATA;
  458. games.forEach(game => {
  459. const { id, state, elapsed } = game;
  460. const localGame = gamesMap[id];
  461. if (localGame) {
  462. Object.assign(localGame, { state, elapsed });
  463. }
  464. });
  465. });
  466. }
  467. /**
  468. * 获取币种数据
  469. * @returns {Promise<Array>}
  470. */
  471. const getCurrencies = async () => {
  472. return pinnacleGet('/v2/currencies');
  473. }
  474. /**
  475. * 更新币种数据
  476. * @returns {Promise<void>}
  477. */
  478. const updateCurrencies = async () => {
  479. return getCurrencies()
  480. .then(data => {
  481. GLOBAL_DATA.currencies = data;
  482. GLOBAL_DATA.currenciesUpdatedAt = Date.now();
  483. Logs.outDev('currencies updated');
  484. });
  485. }
  486. const getCurrenciesLoopDelay = () => {
  487. const minDelay = 10 * 60 * 1000;
  488. const maxDelay = 15 * 60 * 1000;
  489. return minDelay + Math.floor(Math.random() * (maxDelay - minDelay + 1));
  490. }
  491. /**
  492. * 同步币种数据循环
  493. */
  494. const currenciesLoop = () => {
  495. updateCurrencies()
  496. .catch(err => {
  497. Logs.err('failed to update currencies:', err.message, err.source);
  498. })
  499. .finally(() => {
  500. const delay = GLOBAL_DATA.currenciesUpdatedAt ? getCurrenciesLoopDelay() : 60 * 1000;
  501. setTimeout(currenciesLoop, delay);
  502. });
  503. }
  504. /**
  505. * 获取盘口数据
  506. * @returns {Object}
  507. */
  508. const getGames = () => {
  509. const { filtedGames, gamesMap={} } = GLOBAL_DATA;
  510. const filtedGamesSet = new Set(filtedGames);
  511. const nowTime = Date.now();
  512. const gamesData = {};
  513. Object.values(gamesMap).forEach(game => {
  514. const { id, liveStatus, parentId, resultingUnit, timestamp } = game;
  515. if (resultingUnit !== 'Regular') {
  516. return false; // 非常规赛事不处理
  517. }
  518. const gmtMinus4Date = getDateInTimezone(-4);
  519. const todayEndTime = new Date(`${gmtMinus4Date} 23:59:59 GMT-4`).getTime();
  520. const tomorrowEndTime = todayEndTime + 24 * 60 * 60 * 1000;
  521. if (liveStatus == 1 && timestamp < nowTime) {
  522. game.marketType = 2; // 滚球赛事
  523. }
  524. else if (liveStatus != 1 && timestamp > nowTime && timestamp <= todayEndTime) {
  525. game.marketType = 1; // 今日赛事
  526. }
  527. else if (liveStatus != 1 && timestamp > todayEndTime && timestamp <= tomorrowEndTime) {
  528. game.marketType = 0; // 明日早盘赛事
  529. }
  530. else {
  531. game.marketType = -1; // 非近期赛事
  532. }
  533. if (game.marketType < 0) {
  534. return false; // 非近期赛事不处理
  535. }
  536. let actived = false;
  537. if (liveStatus != 1 && filtedGamesSet.has(id)) {
  538. actived = true; // 在赛前列表中
  539. game.eventId = id;
  540. game.originId = 0;
  541. }
  542. else if (liveStatus == 1 && filtedGamesSet.has(parentId)) {
  543. actived = true; // 在滚球列表中
  544. game.eventId = parentId;
  545. game.originId = id;
  546. }
  547. if (actived) {
  548. const { filtedGames } = GLOBAL_DATA;
  549. parseGame(game, filtedGames)?.forEach(gameInfo => {
  550. const { marketType, ...rest } = gameInfo;
  551. if (!gamesData[marketType]) {
  552. gamesData[marketType] = [];
  553. }
  554. gamesData[marketType].push(rest);
  555. });
  556. }
  557. });
  558. return gamesData;
  559. }
  560. /**
  561. * 同步盘口数据循环
  562. */
  563. const pinnacleDataLoop = () => {
  564. updateStraightFixtures()
  565. .then(() => {
  566. return Promise.all([
  567. updateStraightOdds(),
  568. updateSpecialFixtures(),
  569. updateInRunning(),
  570. ]);
  571. })
  572. .then(() => {
  573. return updateSpecialsOdds();
  574. })
  575. .then(() => {
  576. if (!GLOBAL_DATA.loopActive) {
  577. GLOBAL_DATA.loopActive = true;
  578. Logs.out('loop active');
  579. notifyException('Pinnacle API startup.');
  580. }
  581. if (GLOBAL_DATA.requestErrorCount > 0) {
  582. GLOBAL_DATA.requestErrorCount = 0;
  583. Logs.out('request error count reset');
  584. }
  585. const nowTime = Date.now();
  586. const loopDuration = nowTime - GLOBAL_DATA.loopResultTime;
  587. GLOBAL_DATA.loopResultTime = nowTime;
  588. if (loopDuration > 15000) {
  589. Logs.out('loop duration is too long', loopDuration);
  590. }
  591. else {
  592. Logs.outDev('loop duration', loopDuration);
  593. }
  594. const timestamp = Date.now();
  595. const games = getGames();
  596. const data = { games, timestamp, tp: TP };
  597. updateBaseEvents(data);
  598. if (IS_DEV) {
  599. setData(gamesMapCacheFile, GLOBAL_DATA.gamesMap);
  600. }
  601. })
  602. .catch(err => {
  603. Logs.err(err.message, err.source);
  604. GLOBAL_DATA.requestErrorCount++;
  605. if (GLOBAL_DATA.loopActive && GLOBAL_DATA.requestErrorCount > 5) {
  606. const exceptionMessage = 'request errors have reached the limit';
  607. Logs.out(exceptionMessage);
  608. GLOBAL_DATA.loopActive = false;
  609. Logs.out('loop inactive');
  610. const exceptionList = ['Pinnacle API paused']
  611. if (exceptionMessage) {
  612. exceptionList.push(exceptionMessage);
  613. }
  614. if (err.source?.data?.message) {
  615. exceptionList.push(err.source.data.message);
  616. }
  617. else if (err.message) {
  618. exceptionList.push(err.message);
  619. }
  620. if (err.source?.username) {
  621. exceptionList.push(err.source.username);
  622. }
  623. notifyException(exceptionList.join('. '));
  624. }
  625. })
  626. .finally(() => {
  627. const { loopActive } = GLOBAL_DATA;
  628. let loopDelay = 1000 * 5;
  629. if (!loopActive) {
  630. loopDelay = 1000 * 60;
  631. resetVersionsCount();
  632. }
  633. else {
  634. incrementVersionsCount();
  635. }
  636. setTimeout(pinnacleDataLoop, loopDelay);
  637. });
  638. }
  639. /**
  640. * 缓存GLOBAL_DATA数据到文件
  641. * @returns {Promise<void>}
  642. */
  643. const saveGlobalDataToCache = async () => {
  644. return setData(globalDataCacheFile, GLOBAL_DATA);
  645. }
  646. /**
  647. * 从文件加载GLOBAL_DATA数据
  648. * @returns {Promise<void>}
  649. */
  650. const loadGlobalDataFromCache = async () => {
  651. return getData(globalDataCacheFile)
  652. .then(data => {
  653. if (!data) {
  654. return;
  655. }
  656. Object.keys(GLOBAL_DATA)?.forEach(key => {
  657. if (key in data) {
  658. GLOBAL_DATA[key] = data[key];
  659. }
  660. });
  661. });
  662. }
  663. /**
  664. * 监听进程退出事件,保存GLOBAL_DATA数据
  665. * @param {*} code
  666. */
  667. const saveExit = (code) => {
  668. saveGlobalDataToCache()
  669. .then(() => {
  670. Logs.out('global data saved');
  671. })
  672. .catch(err => {
  673. Logs.err('failed to save global data', err.message);
  674. })
  675. .finally(() => {
  676. process.exit(code);
  677. });
  678. }
  679. process.on('SIGINT', () => {
  680. saveExit(0);
  681. });
  682. process.on('SIGTERM', () => {
  683. saveExit(0);
  684. });
  685. process.on('SIGUSR2', () => {
  686. saveExit(0);
  687. });
  688. /**
  689. * 启动同步盘口数据
  690. */
  691. export const startSyncMarketsData = () => {
  692. loadGlobalDataFromCache()
  693. .then(() => {
  694. Logs.out('global data loaded');
  695. })
  696. .catch(err => {
  697. Logs.err('failed to load global data', err.message);
  698. })
  699. .finally(() => {
  700. GLOBAL_DATA.loopResultTime = Date.now();
  701. GLOBAL_DATA.loopActive = true;
  702. return getFiltedGames();
  703. })
  704. .then(() => {
  705. currenciesLoop();
  706. pinnacleDataLoop();
  707. });
  708. }