syncData.js 21 KB

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