From 16b11d94d98775b6e6b306f0cc83aa7e819dddb6 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Sat, 13 Jun 2026 18:56:08 +0300 Subject: [PATCH] feat: add MoexClient with rate-limited HTTP client, circuit breaker, and MOEX ISS data methods --- apps/backend/package.json | 28 +- .../modules/moex-client/moex-client.module.ts | 9 + .../moex-client/moex-client.service.spec.ts | 43 ++ .../moex-client/moex-client.service.ts | 330 +++++++++++++++ .../modules/moex-client/moex-client.types.ts | 131 ++++++ apps/backend/tsconfig.json | 3 +- apps/backend/vitest.config.ts | 22 + package-lock.json | 379 ++++++++++++++++++ 8 files changed, 931 insertions(+), 14 deletions(-) create mode 100644 apps/backend/src/modules/moex-client/moex-client.module.ts create mode 100644 apps/backend/src/modules/moex-client/moex-client.service.spec.ts create mode 100644 apps/backend/src/modules/moex-client/moex-client.service.ts create mode 100644 apps/backend/src/modules/moex-client/moex-client.types.ts create mode 100644 apps/backend/vitest.config.ts diff --git a/apps/backend/package.json b/apps/backend/package.json index 7e0eb4a..520478a 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -11,32 +11,34 @@ "test:watch": "vitest" }, "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", - "@nestjs/config": "^3.0.0", - "@nestjs/swagger": "^7.0.0", "@nestjs/axios": "^3.0.0", "@nestjs/cache-manager": "^2.0.0", - "cache-manager": "^5.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/config": "^3.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/swagger": "^7.0.0", "axios": "^1.6.0", + "cache-manager": "^5.0.0", + "class-transformer": "^0.5.0", + "class-validator": "^0.14.0", + "p-queue": "^7.3.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.0", - "class-validator": "^0.14.0", - "class-transformer": "^0.5.0", - "p-queue": "^7.3.0", "swagger-ui-express": "^5.0.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@swc/core": "^1.15.41", "@types/express": "^4.17.0", "@types/node": "^20.0.0", - "typescript": "^5.3.0", - "vitest": "^1.0.0", - "eslint": "^8.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0" + "@typescript-eslint/parser": "^7.0.0", + "eslint": "^8.0.0", + "typescript": "^5.3.0", + "unplugin-swc": "^1.5.9", + "vitest": "^1.0.0" } } diff --git a/apps/backend/src/modules/moex-client/moex-client.module.ts b/apps/backend/src/modules/moex-client/moex-client.module.ts new file mode 100644 index 0000000..9815aa7 --- /dev/null +++ b/apps/backend/src/modules/moex-client/moex-client.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { MoexClientService } from './moex-client.service'; + +@Global() +@Module({ + providers: [MoexClientService], + exports: [MoexClientService], +}) +export class MoexClientModule {} diff --git a/apps/backend/src/modules/moex-client/moex-client.service.spec.ts b/apps/backend/src/modules/moex-client/moex-client.service.spec.ts new file mode 100644 index 0000000..ff370da --- /dev/null +++ b/apps/backend/src/modules/moex-client/moex-client.service.spec.ts @@ -0,0 +1,43 @@ +import 'reflect-metadata'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigModule } from '@nestjs/config'; +import { MoexClientService } from './moex-client.service'; +import configuration from '../../config/configuration'; + +describe('MoexClientService', () => { + let service: MoexClientService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ load: [configuration] }), + ], + providers: [MoexClientService], + }).compile(); + + service = module.get(MoexClientService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('searchSecurities', () => { + it('should return results for SBER query', async () => { + const results = await service.searchSecurities('SBER'); + expect(Array.isArray(results)).toBe(true); + if (results.length > 0) { + expect(results[0].secid).toBeDefined(); + } + }, 15000); + }); + + describe('getShareMarketData', () => { + it('should return market data for SBER', async () => { + const data = await service.getShareMarketData('SBER'); + if (data) { + expect(data.secid).toBe('SBER'); + } + }, 15000); + }); +}); diff --git a/apps/backend/src/modules/moex-client/moex-client.service.ts b/apps/backend/src/modules/moex-client/moex-client.service.ts new file mode 100644 index 0000000..e940ff8 --- /dev/null +++ b/apps/backend/src/modules/moex-client/moex-client.service.ts @@ -0,0 +1,330 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import axios, { AxiosInstance } from 'axios'; +import PQueue from 'p-queue'; +import { + MoexSecurityDescription, + MoexShareMarketData, + MoexBondData, + MoexBondMarketData, + MoexDividend, + MoexCandle, + MoexHistoryEntry, + MoexBondHistoryEntry, +} from './moex-client.types'; + +@Injectable() +export class MoexClientService { + private readonly logger = new Logger(MoexClientService.name); + private readonly client: AxiosInstance; + private readonly queue: PQueue; + private circuitOpen = false; + private circuitErrorCount = 0; + private readonly threshold: number; + private readonly resetMs: number; + + constructor(private configService: ConfigService) { + const baseUrl = this.configService.get('app.moex.baseUrl')!; + this.threshold = this.configService.get( + 'app.moex.circuitBreakerThreshold', + 5, + ); + this.resetMs = + this.configService.get( + 'app.moex.circuitBreakerResetSeconds', + 30, + ) * 1000; + const rateLimit = this.configService.get( + 'app.moex.rateLimit', + 10, + ); + + this.client = axios.create({ + baseURL: baseUrl, + timeout: 10000, + paramsSerializer: { indexes: null }, + }); + + this.queue = new PQueue({ + interval: 1000, + intervalCap: rateLimit, + }); + } + + private async request(path: string, params?: Record): Promise { + if (this.circuitOpen) { + throw new Error('Circuit breaker is open — MOEX requests paused'); + } + + return this.queue.add(async () => { + try { + const response = await this.client.get(path, { + params: { ...params, 'iss.meta': 'off' }, + }); + this.circuitErrorCount = 0; + return response.data as T; + } catch (error) { + this.circuitErrorCount++; + if (this.circuitErrorCount >= this.threshold) { + this.circuitOpen = true; + this.logger.warn(`Circuit breaker opened after ${this.threshold} errors`); + setTimeout(() => { + this.circuitOpen = false; + this.circuitErrorCount = 0; + this.logger.log('Circuit breaker reset'); + }, this.resetMs); + } + throw error; + } + }) as Promise; + } + + private extractTable(data: Record, name: string): Record[] { + const table = data[name] as Record | undefined; + if (!table || !table.columns || !table.data) return []; + const columns = table.columns as string[]; + const rows = table.data as unknown[][]; + return rows.map((row) => { + const obj: Record = {}; + columns.forEach((col, i) => { + obj[col] = row[i]; + }); + return obj; + }); + } + + async searchSecurities(query: string): Promise { + const data = await this.request>('/securities', { + q: query, + }); + return this.extractTable(data, 'securities').map((s) => ({ + secid: s.secid as string, + isin: s.isin as string, + name: s.name as string, + shortName: s.shortName as string, + latName: (s.latName as string) || null, + listLevel: parseInt(s.listLevel as string, 10) || 0, + issueSize: parseInt(s.issuesize as string, 10) || 0, + faceValue: parseFloat(s.facevalue as string) || 0, + faceUnit: (s.faceunit as string) || '', + issueDate: (s.issuedate as string) || '', + typeName: (s.typename as string) || '', + group: (s.group as string) || '', + type: (s.type as string) || '', + isQualifiedInvestors: (s.isqualifiedinvestors as string) === '1', + morningSession: (s.morningsession as string) === '1', + eveningSession: (s.eveningsession as string) === '1', + })); + } + + async getSecurityDescription(secid: string): Promise { + const data = await this.request>(`/securities/${secid}`); + const rows = this.extractTable(data, 'description'); + if (rows.length === 0) return null; + const map = new Map(rows.map((r) => [r.name, r.value])); + return { + secid, + isin: (map.get('ISIN') as string) || '', + name: (map.get('NAME') as string) || '', + shortName: (map.get('SHORTNAME') as string) || '', + latName: (map.get('LATNAME') as string) || null, + listLevel: parseInt((map.get('LISTLEVEL') as string) || '0', 10), + issueSize: parseInt((map.get('ISSUESIZE') as string) || '0', 10), + faceValue: parseFloat((map.get('FACEVALUE') as string) || '0'), + faceUnit: (map.get('FACEUNIT') as string) || '', + issueDate: (map.get('ISSUEDATE') as string) || '', + typeName: (map.get('TYPENAME') as string) || '', + group: (map.get('GROUP') as string) || '', + type: (map.get('TYPE') as string) || '', + isQualifiedInvestors: (map.get('ISQUALIFIEDINVESTORS') as string) === '1', + morningSession: (map.get('MORNINGSESSION') as string) === '1', + eveningSession: (map.get('EVENINGSESSION') as string) === '1', + }; + } + + async getShareMarketData(secid: string, boardId = 'TQBR'): Promise { + const data = await this.request>( + `/engines/stock/markets/shares/securities/${secid}`, + { boards: boardId }, + ); + const rows = this.extractTable(data, 'securities'); + const share = rows.find((r) => r.BOARDID === boardId); + if (!share) return null; + + const mktRows = this.extractTable(data, 'marketdata'); + const mkt = mktRows.find((r) => r.BOARDID === boardId); + + return { + secid, + boardid: boardId, + bid: mkt ? parseFloat((mkt.BID as string) || '') : null, + offer: mkt ? parseFloat((mkt.OFFER as string) || '') : null, + open: mkt ? parseFloat((mkt.OPEN as string) || '') : null, + low: mkt ? parseFloat((mkt.LOW as string) || '') : null, + high: mkt ? parseFloat((mkt.HIGH as string) || '') : null, + last: mkt ? parseFloat((mkt.LAST as string) || '') : parseFloat((share.PREVPRICE as string) || ''), + lastChange: mkt ? parseFloat((mkt.LASTCHANGE as string) || '') : null, + lastChangePrcnt: mkt ? parseFloat((mkt.LASTCHANGEPRCNT as string) || '') : null, + volume: mkt ? parseInt((mkt.VOLTODAY as string) || '0', 10) : 0, + value: mkt ? parseFloat((mkt.VALTODAY as string) || '0') : 0, + waprice: mkt ? parseFloat((mkt.WAPRICE as string) || '') : null, + numtrades: mkt ? parseInt((mkt.NUMTRADES as string) || '0', 10) : 0, + issueCapitalization: mkt ? parseFloat((mkt.ISSUECAPITALIZATION as string) || '') : null, + tradingStatus: (mkt?.TRADINGSTATUS as string) || '', + updateTime: (mkt?.UPDATETIME as string) || '', + }; + } + + async getBondData(secid: string, boardId = 'TQCB'): Promise { + const data = await this.request>( + `/engines/stock/markets/bonds/securities/${secid}`, + { boards: boardId }, + ); + const rows = this.extractTable(data, 'securities'); + const bond = rows.find((r) => r.BOARDID === boardId); + if (!bond) return null; + + return { + secid, + boardid: boardId, + shortName: (bond.SHORTNAME as string) || '', + prevWaprice: parseFloat((bond.PREVWAPRICE as string) || '') || null, + yieldAtPrevWaprice: parseFloat((bond.YIELDATPREVWAPRICE as string) || '') || null, + couponValue: bond.COUPONVALUE != null ? parseFloat(bond.COUPONVALUE as string) : null, + nextCoupon: (bond.NEXTCOUPON as string) || null, + accruedInt: bond.ACCRUEDINT != null ? parseFloat(bond.ACCRUEDINT as string) : null, + prevPrice: parseFloat((bond.PREVPRICE as string) || '') || null, + lotSize: parseInt((bond.LOTSIZE as string) || '1', 10), + faceValue: parseFloat((bond.FACEVALUE as string) || '1000'), + matDate: (bond.MATDATE as string) || '', + couponPeriod: parseInt((bond.COUPONPERIOD as string) || '0', 10), + issueSize: parseInt((bond.ISSUESIZE as string) || '0', 10), + isin: (bond.ISIN as string) || '', + couponPercent: bond.COUPONPERCENT != null ? parseFloat(bond.COUPONPERCENT as string) : null, + offerDate: (bond.OFFERDATE as string) || null, + buybackDate: (bond.BUYBACKDATE as string) || null, + bondType: (bond.BONDTYPE as string) || '', + bondSubType: (bond.BONDSUBTYPE as string) || '', + listLevel: parseInt((bond.LISTLEVEL as string) || '0', 10), + }; + } + + async getBondMarketData(secid: string, boardId = 'TQCB'): Promise { + const data = await this.request>( + `/engines/stock/markets/bonds/securities/${secid}`, + { boards: boardId }, + ); + const mktRows = this.extractTable(data, 'marketdata'); + const mkt = mktRows.find((r) => r.SECID === secid); + if (!mkt) return null; + + return { + secid, + bid: mkt.BID != null ? parseFloat(mkt.BID as string) : null, + offer: mkt.OFFER != null ? parseFloat(mkt.OFFER as string) : null, + open: mkt.OPEN != null ? parseFloat(mkt.OPEN as string) : null, + low: mkt.LOW != null ? parseFloat(mkt.LOW as string) : null, + high: mkt.HIGH != null ? parseFloat(mkt.HIGH as string) : null, + last: mkt.LAST != null ? parseFloat(mkt.LAST as string) : null, + yield: mkt.YIELD != null ? parseFloat(mkt.YIELD as string) : null, + waprice: mkt.WAPRICE != null ? parseFloat(mkt.WAPRICE as string) : null, + yieldAtWaprice: mkt.YIELDATWAPRICE != null ? parseFloat(mkt.YIELDATWAPRICE as string) : null, + duration: mkt.DURATION != null ? parseFloat(mkt.DURATION as string) : null, + volume: parseInt((mkt.VOLTODAY as string) || '0', 10), + value: parseFloat((mkt.VALTODAY as string) || '0'), + numtrades: parseInt((mkt.NUMTRADES as string) || '0', 10), + tradingStatus: (mkt.TRADINGSTATUS as string) || '', + updateTime: (mkt.UPDATETIME as string) || '', + }; + } + + async getDividends(secid: string): Promise { + const data = await this.request>(`/securities/${secid}/dividends`); + return this.extractTable(data, 'dividends').map((d) => ({ + secid: d.secid as string, + isin: d.isin as string, + registryCloseDate: d.registryclosedate as string, + value: parseFloat(d.value as string), + currencyId: (d.currencyid as string) || 'RUB', + })); + } + + async getCandles( + engine: 'stock', + market: 'shares' | 'bonds', + secid: string, + interval: 1 | 10 | 60 | 24, + from: string, + till: string, + ): Promise { + const data = await this.request>( + `/engines/${engine}/markets/${market}/securities/${secid}/candles`, + { + interval: String(interval), + from, + till, + }, + ); + return this.extractTable(data, 'candles').map((c) => ({ + open: parseFloat(c.open as string), + close: parseFloat(c.close as string), + high: parseFloat(c.high as string), + low: parseFloat(c.low as string), + value: parseFloat(c.value as string), + volume: parseInt(c.volume as string, 10), + begin: c.begin as string, + end: c.end as string, + })); + } + + async getHistory( + secid: string, + from: string, + till: string, + ): Promise { + const data = await this.request>( + `/engines/stock/markets/shares/securities/${secid}`, + { from, till }, + ); + const tableName = Object.keys(data).find( + (k) => k.startsWith('history') && !k.includes('cursor'), + ); + if (!tableName) return []; + return this.extractTable(data, tableName).map((h) => ({ + tradeDate: h.TRADEDATE as string, + open: h.OPEN != null ? parseFloat(h.OPEN as string) : null, + low: h.LOW != null ? parseFloat(h.LOW as string) : null, + high: h.HIGH != null ? parseFloat(h.HIGH as string) : null, + close: h.CLOSE != null ? parseFloat(h.CLOSE as string) : null, + waprice: h.WAPRICE != null ? parseFloat(h.WAPRICE as string) : null, + volume: parseInt((h.VOLUME as string) || '0', 10), + value: parseFloat((h.VALUE as string) || '0'), + numtrades: parseInt((h.NUMTRADES as string) || '0', 10), + })); + } + + async getBondHistory( + secid: string, + from: string, + till: string, + ): Promise { + const data = await this.request>( + `/engines/stock/markets/bonds/securities/${secid}`, + { from, till }, + ); + const tableName = Object.keys(data).find( + (k) => k.startsWith('history') && !k.includes('cursor'), + ); + if (!tableName) return []; + return this.extractTable(data, tableName).map((h) => ({ + tradeDate: h.TRADEDATE as string, + close: h.CLOSE != null ? parseFloat(h.CLOSE as string) : null, + legalClosePrice: + h.LEGALCLOSEPRICE != null ? parseFloat(h.LEGALCLOSEPRICE as string) : null, + waprice: h.WAPRICE != null ? parseFloat(h.WAPRICE as string) : null, + yieldClose: h.YIELDCLOSE != null ? parseFloat(h.YIELDCLOSE as string) : null, + duration: h.DURATION != null ? parseFloat(h.DURATION as string) : null, + accruedInt: h.ACCINT != null ? parseFloat(h.ACCINT as string) : null, + })); + } +} diff --git a/apps/backend/src/modules/moex-client/moex-client.types.ts b/apps/backend/src/modules/moex-client/moex-client.types.ts new file mode 100644 index 0000000..245c158 --- /dev/null +++ b/apps/backend/src/modules/moex-client/moex-client.types.ts @@ -0,0 +1,131 @@ +export interface MoexSecurityDescription { + secid: string; + isin: string; + name: string; + shortName: string; + latName: string | null; + listLevel: number; + issueSize: number; + faceValue: number; + faceUnit: string; + issueDate: string; + typeName: string; + group: string; + type: string; + isQualifiedInvestors: boolean; + morningSession: boolean; + eveningSession: boolean; +} + +export interface MoexShareMarketData { + secid: string; + boardid: string; + bid: number | null; + offer: number | null; + open: number | null; + low: number | null; + high: number | null; + last: number | null; + lastChange: number | null; + lastChangePrcnt: number | null; + volume: number; + value: number; + waprice: number | null; + numtrades: number; + issueCapitalization: number | null; + tradingStatus: string; + updateTime: string; +} + +export interface MoexBondData { + secid: string; + boardid: string; + shortName: string; + prevWaprice: number | null; + yieldAtPrevWaprice: number | null; + couponValue: number | null; + nextCoupon: string | null; + accruedInt: number | null; + prevPrice: number | null; + lotSize: number; + faceValue: number; + matDate: string; + couponPeriod: number; + issueSize: number; + isin: string; + couponPercent: number | null; + offerDate: string | null; + buybackDate: string | null; + bondType: string; + bondSubType: string; + listLevel: number; +} + +export interface MoexBondMarketData { + secid: string; + bid: number | null; + offer: number | null; + open: number | null; + low: number | null; + high: number | null; + last: number | null; + yield: number | null; + waprice: number | null; + yieldAtWaprice: number | null; + duration: number | null; + volume: number; + value: number; + numtrades: number; + tradingStatus: string; + updateTime: string; +} + +export interface MoexDividend { + secid: string; + isin: string; + registryCloseDate: string; + value: number; + currencyId: string; +} + +export interface MoexCandle { + open: number; + close: number; + high: number; + low: number; + value: number; + volume: number; + begin: string; + end: string; +} + +export interface MoexHistoryEntry { + tradeDate: string; + open: number | null; + low: number | null; + high: number | null; + close: number | null; + waprice: number | null; + volume: number; + value: number; + numtrades: number; +} + +export interface MoexBondHistoryEntry { + tradeDate: string; + close: number | null; + legalClosePrice: number | null; + waprice: number | null; + yieldClose: number | null; + duration: number | null; + accruedInt: number | null; +} + +export interface MoexBoard { + secid: string; + boardid: string; + title: string; + isPrimary: boolean; + isTraded: boolean; + currencyid: string; +} diff --git a/apps/backend/tsconfig.json b/apps/backend/tsconfig.json index 6aff7e2..1030ac8 100644 --- a/apps/backend/tsconfig.json +++ b/apps/backend/tsconfig.json @@ -10,7 +10,8 @@ "baseUrl": "./", "paths": { "@/*": ["src/*"] - } + }, + "types": ["vitest/globals"] }, "include": ["src/**/*"] } diff --git a/apps/backend/vitest.config.ts b/apps/backend/vitest.config.ts new file mode 100644 index 0000000..0d9d8f1 --- /dev/null +++ b/apps/backend/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; +import swc from 'unplugin-swc'; + +export default defineConfig({ + test: { + globals: true, + }, + plugins: [ + swc.vite({ + jsc: { + target: 'es2022', + parser: { + syntax: 'typescript', + decorators: true, + }, + transform: { + decoratorMetadata: true, + }, + }, + }), + ], +}); diff --git a/package-lock.json b/package-lock.json index f79fd88..487b4ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,12 +37,14 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", + "@swc/core": "^1.15.41", "@types/express": "^4.17.0", "@types/node": "^20.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "eslint": "^8.0.0", "typescript": "^5.3.0", + "unplugin-swc": "^1.5.9", "vitest": "^1.0.0" } }, @@ -943,6 +945,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1576,6 +1589,49 @@ "node": ">=14" } }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.62.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", @@ -1933,6 +1989,268 @@ "dev": true, "license": "MIT" }, + "node_modules/@swc/core": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.41.tgz", + "integrity": "sha512-03nQq/082QRJJiOvp3FGbgxTGyyxMxohPTjhk/W9bD2J0tk4ukITI7goOhOO2WbaHn/lsPmo/zf8+DIXhwpgYQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.26" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/swc" + }, + "optionalDependencies": { + "@swc/core-darwin-arm64": "1.15.41", + "@swc/core-darwin-x64": "1.15.41", + "@swc/core-linux-arm-gnueabihf": "1.15.41", + "@swc/core-linux-arm64-gnu": "1.15.41", + "@swc/core-linux-arm64-musl": "1.15.41", + "@swc/core-linux-ppc64-gnu": "1.15.41", + "@swc/core-linux-s390x-gnu": "1.15.41", + "@swc/core-linux-x64-gnu": "1.15.41", + "@swc/core-linux-x64-musl": "1.15.41", + "@swc/core-win32-arm64-msvc": "1.15.41", + "@swc/core-win32-ia32-msvc": "1.15.41", + "@swc/core-win32-x64-msvc": "1.15.41" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.17" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@swc/core-darwin-arm64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.41.tgz", + "integrity": "sha512-kREh6J5paQFvP3i7f/4FbqRNOJREutVFVOkder4GVyCBQ39YmER55cW/y1NNjwrchzFqgYswFn0mMDCqbqKzrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-darwin-x64": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.41.tgz", + "integrity": "sha512-N8B56ESFazZAWZyIkecADSPCwlLEinW7QLMEeotCpv4J7VXwfH+OLkmRL8o96UZ+1355fwHxDTS6/wK7yucvkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm-gnueabihf": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.41.tgz", + "integrity": "sha512-6XrId2fyle0mS5xxON8rU84mPd2Cq1kDJRj+4BnQKTd7u+2kSA6Ww+JkOP0iTNqOqt9OXhPOEAjBHAuonWcdCg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.41.tgz", + "integrity": "sha512-ynLIarxlkVnqHn1D0fKOVht6mNU5ks6lrH+MY3kkS+XFaGGgDxFZVjWKJlkYTKm3RCvBTfA8Ng5fLufXheMRKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-arm64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.41.tgz", + "integrity": "sha512-dXu/5vd4gh8symyhRF+4G7gOPkjmb4pONhh7sl+6GSiW0LOKZlfu5kXmyFbTz9smOT7jgr002qY9b1nujjXt2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-ppc64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-ppc64-gnu/-/core-linux-ppc64-gnu-1.15.41.tgz", + "integrity": "sha512-XGO6zVPXoPE0gf/XnI4jBbafNT13AYgoh6ns0JCSdOetI/kqVf0vhpz7NuNgAzZrMVCsmieqjPoTwViDgh4mOQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-s390x-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-s390x-gnu/-/core-linux-s390x-gnu-1.15.41.tgz", + "integrity": "sha512-0WUglRwyZtW+iMi7J3iFdrCxreZZIKf4egTwEQfIYRsqFax69A0OrFj+NIoFSE03xBT/IFRrg+S8K6f9Ky+4hA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-gnu": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.41.tgz", + "integrity": "sha512-VxkuQK59c0tHm6uJZCUrS3cyA2JhGGfdU6e41SZz0x/JS+4Sm7C1mIc97In14vkZJopEt7yXA2TouCqZDSygEA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-linux-x64-musl": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.41.tgz", + "integrity": "sha512-/0qXIu1ZxggLuovLb22vFfKHq2AA4n6Whw5UwmVCHk4pkw7KWnPIQpMCEqUMPsNkFJig7PPp/TSYFu8ZEb2rtQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-arm64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.41.tgz", + "integrity": "sha512-Y481sMNZM6rECh9VO4+y26N1lWEDAyxnBZskUf37fl90uHE946VHfmiVQWT0uMFOhyJJFovGTRuF4W82dwewUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-ia32-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.41.tgz", + "integrity": "sha512-BAchBD5qeUzy3hiPSLJtaaoSm4blCLyYffOF1bGE4ETcV+OisqjUAwDQMJj++4bTpvMCDzwC+Bj3PmQyBCtscw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/core-win32-x64-msvc": { + "version": "1.15.41", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.41.tgz", + "integrity": "sha512-WOkA+fJ/ViVBQDsSV9JC52NACTe5PhlurA6viASDZGb7HR3KS01ZG7RZ+Bg6SVQFIoq3gSbTsskQVe6EbHFAYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=10" + } + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@swc/types": { + "version": "0.1.26", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.26.tgz", + "integrity": "sha512-lyMwd7WGgG79RS7EERZV3T8wMdmPq3xwyg+1nmAM64kIhx5yl+juO2PYIHb7vTiPgPCj8LYjsNV2T5wiQHUEaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz", @@ -5460,6 +5778,16 @@ "dev": true, "license": "MIT" }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, "node_modules/loader-runner": { "version": "4.3.2", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz", @@ -7740,6 +8068,50 @@ "node": ">= 0.8" } }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/unplugin-swc": { + "version": "1.5.9", + "resolved": "https://registry.npmjs.org/unplugin-swc/-/unplugin-swc-1.5.9.tgz", + "integrity": "sha512-RKwK3yf0M+MN17xZfF14bdKqfx0zMXYdtOdxLiE6jHAoidupKq3jGdJYANyIM1X/VmABhh1WpdO+/f4+Ol89+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.3.0", + "load-tsconfig": "^0.2.5", + "unplugin": "^2.3.11" + }, + "peerDependencies": { + "@swc/core": "^1.2.108" + } + }, + "node_modules/unplugin/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -8059,6 +8431,13 @@ "node": ">=10.13.0" } }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, "node_modules/webpack/node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",