From 0ddb62a3a63ae3cd94956135cfe3bc1e1bed53a4 Mon Sep 17 00:00:00 2001 From: Mekjah Date: Sun, 30 Aug 2026 08:32:26 +0100 Subject: [PATCH] refactor(indexer): extract magic values into named constants --- indexer/src/constants.ts | 33 ++++++++++ indexer/src/pipeline/parse.ts | 25 ++++---- indexer/src/pipeline/transform.ts | 7 ++- indexer/src/queries/eventQueries.ts | 3 +- indexer/src/queries/optimizedQueries.ts | 9 ++- indexer/src/queries/participantQueries.ts | 3 +- indexer/src/queries/search.ts | 19 +++--- packages/shared/src/logger.d.ts | 26 ++++++++ packages/shared/src/logger.d.ts.map | 1 + packages/shared/src/logger.js | 73 +++++++++++++++++++++++ packages/shared/src/logger.js.map | 1 + 11 files changed, 169 insertions(+), 31 deletions(-) create mode 100644 packages/shared/src/logger.d.ts create mode 100644 packages/shared/src/logger.d.ts.map create mode 100644 packages/shared/src/logger.js create mode 100644 packages/shared/src/logger.js.map diff --git a/indexer/src/constants.ts b/indexer/src/constants.ts index b63766dc..a4636257 100644 --- a/indexer/src/constants.ts +++ b/indexer/src/constants.ts @@ -4,6 +4,23 @@ export const WASTE_TYPES = ['Paper', 'Plastic', 'Metal', 'Glass', 'Organic', 'Electronic'] as const; export const PARTICIPANT_ROLES = ['Recycler', 'Collector', 'Manufacturer'] as const; +export const DEFAULT_WASTE_TYPE = WASTE_TYPES[0]; +export const DEFAULT_PARTICIPANT_ROLE = PARTICIPANT_ROLES[0]; + +export const CONTRACT_EVENT_TYPES = { + WASTE_REGISTERED: 'recycled', + PARTICIPANT_REGISTERED: 'reg', + WASTE_TRANSFERRED: 'transfer', + WASTE_CONFIRMED: 'confirmed', + TOKENS_REWARDED: 'rewarded', + WASTE_DEACTIVATED: 'deactive', + WASTE_GRADED: 'graded', + PROCESSING_STATUS_CHANGED: 'proc_upd', + WASTE_CONTAMINATED: 'contam', + AUCTION_CREATED: 'auc_cre', + AUCTION_ENDED: 'auc_end', + CARBON_CREDITS_EARNED: 'carbon', +} as const; export const QUERY_LIMITS = { DEFAULT: 20, @@ -21,3 +38,19 @@ export const PAGINATION = { DEFAULT_PAGE_SIZE: 20, MAX_PAGE_SIZE: 100, } as const; + +export const ANALYTICS = { + DEFAULT_TOP_RESULTS_LIMIT: 10, + DEFAULT_TRANSFER_ACTIVITY_DAYS: 30, +} as const; + +export const EVENT_PAGINATION = { + DEFAULT_RECENT_EVENTS_LIMIT: 50, + MAX_RECENT_EVENTS_LIMIT: 500, +} as const; + +export const CACHE = { + METRICS_TTL_MS: 30_000, +} as const; + +export const FULL_TEXT_SEARCH_CONFIG = 'english'; diff --git a/indexer/src/pipeline/parse.ts b/indexer/src/pipeline/parse.ts index fc6fae3a..8b2503b2 100644 --- a/indexer/src/pipeline/parse.ts +++ b/indexer/src/pipeline/parse.ts @@ -10,6 +10,7 @@ */ import { RawContractEvent } from '../types'; +import { CONTRACT_EVENT_TYPES } from '../constants'; import { ParsedEvent, EventMeta } from './types'; // --------------------------------------------------------------------------- @@ -283,18 +284,18 @@ function parseCarbonCreditsEarned(event: RawContractEvent): ParsedEvent { type ParserFn = (event: RawContractEvent) => ParsedEvent; const PARSERS: Record = { - recycled: parseWasteRegistered, - reg: parseParticipantRegistered, - transfer: parseWasteTransferred, - confirmed: parseWasteConfirmed, - rewarded: parseTokensRewarded, - deactive: parseWasteDeactivated, - graded: parseWasteGraded, - proc_upd: parseProcessingStatusChanged, - contam: parseWasteContaminated, - auc_cre: parseAuctionCreated, - auc_end: parseAuctionEnded, - carbon: parseCarbonCreditsEarned, + [CONTRACT_EVENT_TYPES.WASTE_REGISTERED]: parseWasteRegistered, + [CONTRACT_EVENT_TYPES.PARTICIPANT_REGISTERED]: parseParticipantRegistered, + [CONTRACT_EVENT_TYPES.WASTE_TRANSFERRED]: parseWasteTransferred, + [CONTRACT_EVENT_TYPES.WASTE_CONFIRMED]: parseWasteConfirmed, + [CONTRACT_EVENT_TYPES.TOKENS_REWARDED]: parseTokensRewarded, + [CONTRACT_EVENT_TYPES.WASTE_DEACTIVATED]: parseWasteDeactivated, + [CONTRACT_EVENT_TYPES.WASTE_GRADED]: parseWasteGraded, + [CONTRACT_EVENT_TYPES.PROCESSING_STATUS_CHANGED]: parseProcessingStatusChanged, + [CONTRACT_EVENT_TYPES.WASTE_CONTAMINATED]: parseWasteContaminated, + [CONTRACT_EVENT_TYPES.AUCTION_CREATED]: parseAuctionCreated, + [CONTRACT_EVENT_TYPES.AUCTION_ENDED]: parseAuctionEnded, + [CONTRACT_EVENT_TYPES.CARBON_CREDITS_EARNED]: parseCarbonCreditsEarned, }; // --------------------------------------------------------------------------- diff --git a/indexer/src/pipeline/transform.ts b/indexer/src/pipeline/transform.ts index 92fa5299..bebf4b42 100644 --- a/indexer/src/pipeline/transform.ts +++ b/indexer/src/pipeline/transform.ts @@ -11,6 +11,7 @@ */ import { WASTE_TYPE_MAP, ROLE_MAP } from '../types'; +import { DEFAULT_PARTICIPANT_ROLE, DEFAULT_WASTE_TYPE } from '../constants'; import { ParsedEvent, TransformedEvent } from './types'; // --------------------------------------------------------------------------- @@ -35,14 +36,14 @@ export function transformEvent(parsed: ParsedEvent): TransformedEvent { case 'WasteRegistered': return { ...parsed, - wasteType: WASTE_TYPE_MAP[parsed.wasteTypeNum] ?? 'Paper', + wasteType: WASTE_TYPE_MAP[parsed.wasteTypeNum] ?? DEFAULT_WASTE_TYPE, recycler: normalizeAddress(parsed.recycler), }; case 'ParticipantRegistered': return { ...parsed, - role: ROLE_MAP[parsed.roleNum] ?? 'Recycler', + role: ROLE_MAP[parsed.roleNum] ?? DEFAULT_PARTICIPANT_ROLE, address: normalizeAddress(parsed.address), name: parsed.name.trim(), }; @@ -90,7 +91,7 @@ export function transformEvent(parsed: ParsedEvent): TransformedEvent { case 'CarbonCreditsEarned': return { ...parsed, - wasteType: WASTE_TYPE_MAP[parsed.wasteTypeNum] ?? 'Paper', + wasteType: WASTE_TYPE_MAP[parsed.wasteTypeNum] ?? DEFAULT_WASTE_TYPE, participant: normalizeAddress(parsed.participant), }; } diff --git a/indexer/src/queries/eventQueries.ts b/indexer/src/queries/eventQueries.ts index 7aa3d0ff..a516fc0b 100644 --- a/indexer/src/queries/eventQueries.ts +++ b/indexer/src/queries/eventQueries.ts @@ -1,5 +1,6 @@ import { getPool } from '../db/client'; import { recordQueryMetric } from '../db/queryOptimizer'; +import { QUERY_LIMITS } from '../constants'; export interface EventFilter { eventType?: string; @@ -20,7 +21,7 @@ export interface EventQueryResult { export async function queryEvents(filter: EventFilter): Promise { const pool = getPool(); - const limit = Math.min(filter.limit ?? 100, 1000); + const limit = Math.min(filter.limit ?? QUERY_LIMITS.MAX, QUERY_LIMITS.MAX); const offset = filter.offset ?? 0; let sql = 'SELECT * FROM raw_events WHERE 1=1'; diff --git a/indexer/src/queries/optimizedQueries.ts b/indexer/src/queries/optimizedQueries.ts index 235da2a0..c4a93bdc 100644 --- a/indexer/src/queries/optimizedQueries.ts +++ b/indexer/src/queries/optimizedQueries.ts @@ -10,6 +10,7 @@ import { getPool } from '../db/client'; import { recordQueryMetric } from '../db/queryOptimizer'; +import { CACHE, EVENT_PAGINATION } from '../constants'; // ── Types ───────────────────────────────────────────────────────────────────── @@ -157,11 +158,11 @@ export interface EventSummary { * index (created in migration 003) to avoid seq-scans on large tables. */ export async function getRecentEvents( - limit = 50, + limit = EVENT_PAGINATION.DEFAULT_RECENT_EVENTS_LIMIT, offset = 0, eventType?: string, ): Promise { - const params: unknown[] = [Math.min(limit, 500), offset]; + const params: unknown[] = [Math.min(limit, EVENT_PAGINATION.MAX_RECENT_EVENTS_LIMIT), offset]; const typeClause = eventType ? ` AND event_type = $3` : ''; if (eventType) {params.push(eventType);} @@ -182,15 +183,13 @@ const metricsCache: { value: SupplyChainStats | null; expiresAt: number } = { value: null, expiresAt: 0, }; -const METRICS_TTL_MS = 30_000; // 30 seconds - export async function getCachedSupplyChainStats(): Promise { if (metricsCache.value && Date.now() < metricsCache.expiresAt) { return metricsCache.value; } const fresh = await getSupplyChainStats(); metricsCache.value = fresh; - metricsCache.expiresAt = Date.now() + METRICS_TTL_MS; + metricsCache.expiresAt = Date.now() + CACHE.METRICS_TTL_MS; return fresh; } diff --git a/indexer/src/queries/participantQueries.ts b/indexer/src/queries/participantQueries.ts index cf6f3f99..60baccd0 100644 --- a/indexer/src/queries/participantQueries.ts +++ b/indexer/src/queries/participantQueries.ts @@ -1,5 +1,6 @@ import { getPool } from '../db/client'; import { recordQueryMetric } from '../db/queryOptimizer'; +import { QUERY_LIMITS } from '../constants'; export type ParticipantRole = 'Recycler' | 'Collector' | 'Manufacturer'; @@ -56,7 +57,7 @@ export async function queryParticipants( filter: ParticipantFilter ): Promise { const pool = getPool(); - const limit = Math.min(filter.limit ?? 100, 1000); + const limit = Math.min(filter.limit ?? QUERY_LIMITS.MAX, QUERY_LIMITS.MAX); const offset = filter.offset ?? 0; let sql = 'SELECT * FROM participants WHERE 1=1'; diff --git a/indexer/src/queries/search.ts b/indexer/src/queries/search.ts index 98aefcfa..90126f2a 100644 --- a/indexer/src/queries/search.ts +++ b/indexer/src/queries/search.ts @@ -1,13 +1,14 @@ import { getPool } from '../db/client'; +import { ANALYTICS, FULL_TEXT_SEARCH_CONFIG, QUERY_LIMITS } from '../constants'; // --- Full-text search --- -export async function searchParticipants(query: string, limit = 20) { +export async function searchParticipants(query: string, limit = QUERY_LIMITS.DEFAULT) { const { rows } = await getPool().query( `SELECT address, role, name, latitude, longitude, registered_at, - ts_rank(search_vector, plainto_tsquery('english', $1)) AS rank + ts_rank(search_vector, plainto_tsquery('${FULL_TEXT_SEARCH_CONFIG}', $1)) AS rank FROM participants - WHERE search_vector @@ plainto_tsquery('english', $1) + WHERE search_vector @@ plainto_tsquery('${FULL_TEXT_SEARCH_CONFIG}', $1) ORDER BY rank DESC LIMIT $2`, [query, limit] @@ -15,13 +16,13 @@ export async function searchParticipants(query: string, limit = 20) { return rows; } -export async function searchWastes(query: string, limit = 20) { +export async function searchWastes(query: string, limit = QUERY_LIMITS.DEFAULT) { const { rows } = await getPool().query( `SELECT w.id, w.recycler_address, w.waste_type, w.weight, w.is_confirmed, w.is_active, w.registered_at, - ts_rank(w.search_vector, plainto_tsquery('english', $1)) AS rank + ts_rank(w.search_vector, plainto_tsquery('${FULL_TEXT_SEARCH_CONFIG}', $1)) AS rank FROM wastes w - WHERE w.search_vector @@ plainto_tsquery('english', $1) + WHERE w.search_vector @@ plainto_tsquery('${FULL_TEXT_SEARCH_CONFIG}', $1) ORDER BY rank DESC LIMIT $2`, [query, limit] @@ -45,7 +46,7 @@ export async function getWasteStatsByType() { return rows; } -export async function getTopRecyclers(limit = 10) { +export async function getTopRecyclers(limit = ANALYTICS.DEFAULT_TOP_RESULTS_LIMIT) { const { rows } = await getPool().query( `SELECT p.address, p.name, p.role, COUNT(w.id) AS waste_count, @@ -74,7 +75,7 @@ export async function getRewardSummary() { return rows; } -export async function getTransferActivity(days = 30) { +export async function getTransferActivity(days = ANALYTICS.DEFAULT_TRANSFER_ACTIVITY_DAYS) { const { rows } = await getPool().query( `SELECT DATE_TRUNC('day', transferred_at) AS day, COUNT(*) AS transfer_count @@ -87,7 +88,7 @@ export async function getTransferActivity(days = 30) { return rows; } -export async function getCarbonCreditsByParticipant(limit = 10) { +export async function getCarbonCreditsByParticipant(limit = ANALYTICS.DEFAULT_TOP_RESULTS_LIMIT) { const { rows } = await getPool().query( `SELECT participant_address, SUM(credits) AS total_credits, diff --git a/packages/shared/src/logger.d.ts b/packages/shared/src/logger.d.ts new file mode 100644 index 00000000..2cca418e --- /dev/null +++ b/packages/shared/src/logger.d.ts @@ -0,0 +1,26 @@ +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; +interface LogEntry { + timestamp: string; + level: LogLevel; + message: string; + context?: Record; + error?: Error; +} +export declare class StructuredLogger { + private level; + private enableConsole; + private logs; + constructor(level?: LogLevel, enableConsole?: boolean); + private shouldLog; + private createEntry; + debug(message: string, context?: Record): void; + info(message: string, context?: Record): void; + warn(message: string, context?: Record): void; + error(message: string, error?: Error, context?: Record): void; + private formatEntry; + getLogs(level?: LogLevel): LogEntry[]; + clearLogs(): void; + setLevel(level: LogLevel): void; +} +export {}; +//# sourceMappingURL=logger.d.ts.map \ No newline at end of file diff --git a/packages/shared/src/logger.d.ts.map b/packages/shared/src/logger.d.ts.map new file mode 100644 index 00000000..15e11f92 --- /dev/null +++ b/packages/shared/src/logger.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["logger.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAE3D,UAAU,QAAQ;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,QAAQ,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,KAAK,CAAC;CACf;AASD,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,aAAa,CAAU;IAC/B,OAAO,CAAC,IAAI,CAAkB;gBAElB,KAAK,GAAE,QAAiB,EAAE,aAAa,GAAE,OAAc;IAKnE,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,WAAW;IAUnB,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAO9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAU9E,OAAO,CAAC,WAAW;IAMnB,OAAO,CAAC,KAAK,CAAC,EAAE,QAAQ,GAAG,QAAQ,EAAE;IAKrC,SAAS,IAAI,IAAI;IAIjB,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;CAGhC"} \ No newline at end of file diff --git a/packages/shared/src/logger.js b/packages/shared/src/logger.js new file mode 100644 index 00000000..e7447bf3 --- /dev/null +++ b/packages/shared/src/logger.js @@ -0,0 +1,73 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.StructuredLogger = void 0; +const LOG_LEVELS = { + debug: 0, + info: 1, + warn: 2, + error: 3, +}; +class StructuredLogger { + constructor(level = 'info', enableConsole = true) { + this.logs = []; + this.level = level; + this.enableConsole = enableConsole; + } + shouldLog(level) { + return LOG_LEVELS[level] >= LOG_LEVELS[this.level]; + } + createEntry(level, message, context, error) { + return { + timestamp: new Date().toISOString(), + level, + message, + context, + error, + }; + } + debug(message, context) { + if (this.shouldLog('debug')) { + this.logs.push(this.createEntry('debug', message, context)); + } + } + info(message, context) { + if (this.shouldLog('info')) { + this.logs.push(this.createEntry('info', message, context)); + } + } + warn(message, context) { + if (this.shouldLog('warn')) { + this.logs.push(this.createEntry('warn', message, context)); + if (this.enableConsole) + console.warn(this.formatEntry(this.logs[this.logs.length - 1])); + } + } + error(message, error, context) { + if (this.shouldLog('error')) { + this.logs.push(this.createEntry('error', message, context, error)); + if (this.enableConsole) { + console.error(this.formatEntry(this.logs[this.logs.length - 1])); + if (error) + console.error(error); + } + } + } + formatEntry(entry) { + const { timestamp, level, message, context } = entry; + const contextStr = context ? ` ${JSON.stringify(context)}` : ''; + return `[${timestamp}] ${level.toUpperCase()}: ${message}${contextStr}`; + } + getLogs(level) { + if (!level) + return this.logs; + return this.logs.filter((log) => LOG_LEVELS[log.level] >= LOG_LEVELS[level]); + } + clearLogs() { + this.logs = []; + } + setLevel(level) { + this.level = level; + } +} +exports.StructuredLogger = StructuredLogger; +//# sourceMappingURL=logger.js.map \ No newline at end of file diff --git a/packages/shared/src/logger.js.map b/packages/shared/src/logger.js.map new file mode 100644 index 00000000..a14af1b8 --- /dev/null +++ b/packages/shared/src/logger.js.map @@ -0,0 +1 @@ +{"version":3,"file":"logger.js","sourceRoot":"","sources":["logger.ts"],"names":[],"mappings":";;;AAUA,MAAM,UAAU,GAA6B;IAC3C,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,MAAa,gBAAgB;IAK3B,YAAY,QAAkB,MAAM,EAAE,gBAAyB,IAAI;QAF3D,SAAI,GAAe,EAAE,CAAC;QAG5B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;IACrC,CAAC;IAEO,SAAS,CAAC,KAAe;QAC/B,OAAO,UAAU,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACrD,CAAC;IAEO,WAAW,CAAC,KAAe,EAAE,OAAe,EAAE,OAAiC,EAAE,KAAa;QACpG,OAAO;YACL,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;YACnC,KAAK;YACL,OAAO;YACP,OAAO;YACP,KAAK;SACN,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,OAAiC;QACtD,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAiC;QACrD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;QAC7D,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAe,EAAE,OAAiC;QACrD,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;YAC3D,IAAI,IAAI,CAAC,aAAa;gBAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1F,CAAC;IACH,CAAC;IAED,KAAK,CAAC,OAAe,EAAE,KAAa,EAAE,OAAiC;QACrE,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC;YACnE,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBACvB,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;gBACjE,IAAI,KAAK;oBAAE,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;YAClC,CAAC;QACH,CAAC;IACH,CAAC;IAEO,WAAW,CAAC,KAAe;QACjC,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,KAAK,CAAC;QACrD,MAAM,UAAU,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,OAAO,IAAI,SAAS,KAAK,KAAK,CAAC,WAAW,EAAE,KAAK,OAAO,GAAG,UAAU,EAAE,CAAC;IAC1E,CAAC;IAED,OAAO,CAAC,KAAgB;QACtB,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,IAAI,CAAC;QAC7B,OAAO,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/E,CAAC;IAED,SAAS;QACP,IAAI,CAAC,IAAI,GAAG,EAAE,CAAC;IACjB,CAAC;IAED,QAAQ,CAAC,KAAe;QACtB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;CACF;AAvED,4CAuEC"} \ No newline at end of file