@coopenomics/parser 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +17 -0
- package/dist/cli/index.js +951 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/deserialize.worker.cjs +47 -0
- package/dist/deserialize.worker.cjs.map +1 -0
- package/dist/index.d.ts +1520 -0
- package/dist/index.js +1773 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/index.ts","../src/errors.ts","../src/adapters/ShipReaderAdapter.ts","../src/adapters/IoRedisStore.ts","../src/workers/WorkerPool.ts","../src/core/BlockProcessor.ts","../src/events/eventId.ts","../src/core/XtrimSupervisor.ts","../src/core/ForkDetector.ts","../src/redis/keys.ts","../src/abi/AbiStore.ts","../src/abi/AbiBootstrapper.ts","../src/core/Parser.ts","../src/client/ParserClient.ts","../src/client/SubscriptionLock.ts","../src/client/RedisConsumer.ts","../src/client/FailureTracker.ts","../src/client/filters.ts","../src/index.ts","../src/core/ReconnectSupervisor.ts","../src/logger.ts","../src/metrics/parserMetrics.ts","../src/metrics/clientMetrics.ts","../src/observability/HttpServer.ts"],"sourcesContent":["/**\n * Загрузка и валидация конфигурации парсера.\n *\n * Поддерживаемые форматы: YAML файл (fromConfigFile) или уже разобранный объект (parseConfig).\n *\n * Конвейер обработки:\n * 1. Чтение YAML → parseYaml → raw object.\n * 2. interpolateDeep: рекурсивно заменяет ${VAR} → process.env[VAR].\n * Если переменная не задана — оставляем плейсхолдер (не ломаем конфиг, но validate упадёт\n * если это обязательное поле).\n * 3. validate: проверяет обязательные поля (ship.url, redis.url) и enum-значения.\n * 4. checkPlainSecrets: запрещает хардкодированные пароли в Redis URL.\n * redis://:hardcoded-pass@host → ConfigSecurityError.\n * redis://${REDIS_PASSWORD}@host → OK (это плейсхолдер, не секрет).\n *\n * Почему env-интерполяция важна: операторы хранят конфиг в git без секретов,\n * инжектируя их через переменные среды в Kubernetes / Docker. Формат ${VAR} — стандарт.\n */\n\nimport { readFileSync } from 'node:fs'\nimport { parse as parseYaml } from 'yaml'\nimport { configSchema } from './schema.js'\nimport { ConfigValidationError, ConfigSecurityError } from '../errors.js'\n\n/**\n * Все настройки парсера в одном объекте.\n * Передаётся в конструктор Parser и ParserClient.\n * Все поля кроме ship и redis — опциональны (имеют дефолты в соответствующих модулях).\n */\nexport interface ParserOptions {\n /** SHiP WebSocket соединение. timeoutMs по умолчанию 10000. */\n ship: { url: string; timeoutMs?: number }\n /** Chain API для ABI fallback (abiFallback: 'rpc-current'). Опционален. */\n chain?: { url?: string; id?: string }\n /** Redis подключение. keyPrefix добавляет namespace к ключам (полезно при shared Redis). */\n redis: { url: string; password?: string; keyPrefix?: string }\n /** Piscina worker pool для десериализации. maxThreads по умолчанию = CPU count / 2. */\n workerPool?: { maxThreads?: number }\n /** Поведение при отсутствии ABI: 'rpc-current' = попробовать Chain API, 'fail' = ошибка. */\n abiFallback?: 'rpc-current' | 'fail'\n /** XtrimSupervisor: интервал проверки и включение/отключение автообрезки стрима. */\n xtrim?: { intervalMs?: number; enabled?: boolean }\n /** ReconnectSupervisor: максимум попыток и backoff-таблица в секундах. */\n reconnect?: { maxAttempts?: number; backoffSeconds?: number[] }\n /** Десериализатор ABI-данных. Единственный вариант — 'wharfkit'. */\n deserializer?: 'wharfkit'\n /** Pino logger настройки. pretty=true включает pino-pretty (для разработки). */\n logger?: { level?: string; pretty?: boolean }\n /** HTTP /health endpoint. Kubernetes liveness/readiness probe. */\n health?: { enabled?: boolean; port?: number; lagThresholdSeconds?: number }\n /** HTTP /metrics endpoint для Prometheus. */\n metrics?: { enabled?: boolean; port?: number }\n /** Обрабатывать только irreversible блоки (block_num <= lastIrreversible). */\n irreversibleOnly?: boolean\n /** Не устанавливать SIGTERM/SIGINT обработчики. Используется в тестах. */\n noSignalHandlers?: boolean\n}\n\n// configSchema экспортируется для внешних валидаторов (AJV, Ajv) и документации\nvoid configSchema\n\n/**\n * Паттерн для детекции хардкодированных паролей в Redis URL.\n * Срабатывает на: redis://:password@host или redis://user:pass@host\n * НЕ срабатывает на: redis://:${REDIS_PASSWORD}@host (env-переменная — ОК).\n * [^$\\s]* — не-$, не-пробел → означает отсутствие $ в начале пароля.\n */\nconst PLAIN_SECRET_RE = /redis:\\/\\/[^$\\s]*:[^@$\\s]+@/i\n\n/**\n * Заменяет одну ${VAR} подстановку в строке.\n * Если переменная не задана — возвращает исходный плейсхолдер (не падаем).\n */\nfunction interpolateEnv(value: string): string {\n return value.replace(/\\$\\{([^}]+)\\}/g, (_, varName: string) => {\n return process.env[varName] ?? `\\${${varName}}`\n })\n}\n\n/**\n * Рекурсивно обходит структуру данных и заменяет ${VAR} в строках.\n * Работает со строками, массивами и объектами.\n * Числа, булевы, null — возвращает без изменений.\n */\nfunction interpolateDeep(obj: unknown): unknown {\n if (typeof obj === 'string') return interpolateEnv(obj)\n if (Array.isArray(obj)) return obj.map(interpolateDeep)\n if (obj !== null && typeof obj === 'object') {\n const result: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {\n result[k] = interpolateDeep(v)\n }\n return result\n }\n return obj\n}\n\n/** Type guard: проверяет что значение является непустым объектом (не массивом). */\nfunction isObject(v: unknown): v is Record<string, unknown> {\n return v !== null && typeof v === 'object' && !Array.isArray(v)\n}\n\n/**\n * Ручная валидация конфига (без AJV).\n * Проверяет только обязательные инварианты: ship.url, redis.url, enum-значения.\n * Выбрасывает ConfigValidationError с описанием всех нарушений.\n */\nfunction validate(raw: unknown): raw is ParserOptions {\n const errors: string[] = []\n if (!isObject(raw)) {\n errors.push('(root) must be an object')\n throw new ConfigValidationError(`Config validation failed: ${errors.join('; ')}`)\n }\n if (!isObject(raw['ship']) || typeof (raw['ship'] as Record<string, unknown>)['url'] !== 'string') {\n errors.push('ship.url is required and must be a string')\n }\n if (!isObject(raw['redis']) || typeof (raw['redis'] as Record<string, unknown>)['url'] !== 'string') {\n errors.push('redis.url is required and must be a string')\n }\n const abiFallback = raw['abiFallback']\n if (abiFallback !== undefined && abiFallback !== 'rpc-current' && abiFallback !== 'fail') {\n errors.push('abiFallback must be \"rpc-current\" or \"fail\"')\n }\n const deserializer = raw['deserializer']\n if (deserializer !== undefined && deserializer !== 'wharfkit') {\n errors.push('deserializer must be \"wharfkit\"')\n }\n if (errors.length > 0) {\n throw new ConfigValidationError(`Config validation failed: ${errors.join('; ')}`)\n }\n return true\n}\n\n/**\n * Проверяет что секреты не хардкодированы в Redis URL.\n * Хардкодированные секреты: попадут в git, логи, env dumps → критичная утечка.\n * Правило: используй ${REDIS_PASSWORD} вместо прямого пароля.\n */\nfunction checkPlainSecrets(opts: ParserOptions): void {\n if (PLAIN_SECRET_RE.test(opts.redis.url)) {\n throw new ConfigSecurityError(\n 'Secrets must be injected via env variables, not hardcoded in config',\n )\n }\n}\n\n/**\n * Парсит и валидирует конфиг из уже разобранного объекта (результат parseYaml или тест).\n * Применяет env-интерполяцию, валидацию, проверку безопасности.\n */\nexport function parseConfig(raw: unknown): ParserOptions {\n const interpolated = interpolateDeep(raw)\n validate(interpolated)\n const opts = interpolated as ParserOptions\n checkPlainSecrets(opts)\n return opts\n}\n\n/**\n * Читает YAML файл по пути и возвращает валидированные ParserOptions.\n * Основная точка входа для CLI команд и пользовательского кода.\n * Выбрасывает ConfigValidationError/ConfigSecurityError/Error при любых проблемах.\n */\nexport function fromConfigFile(filePath: string): ParserOptions {\n const text = readFileSync(filePath, 'utf8')\n const raw = parseYaml(text) as unknown\n return parseConfig(raw)\n}\n","/**\n * Доменные ошибки пакета.\n *\n * Каждый класс расширяет Error и устанавливает `name`, чтобы стек-трейсы\n * содержали читаемое имя вместо просто \"Error\".\n */\n\n/** Конфигурационный YAML не прошёл структурную валидацию. */\nexport class ConfigValidationError extends Error {\n override readonly cause?: unknown\n constructor(message: string, cause?: unknown) {\n super(message)\n this.name = 'ConfigValidationError'\n this.cause = cause\n }\n}\n\n/**\n * Секреты обнаружены прямо в конфигурационном файле (например пароль Redis\n * захардкожен в URL). Правильный способ — переменные окружения ${VAR}.\n */\nexport class ConfigSecurityError extends Error {\n constructor(message: string) {\n super(message)\n this.name = 'ConfigSecurityError'\n }\n}\n\n/** Метод интерфейса объявлен, но не реализован в данном адаптере. */\nexport class NotImplementedError extends Error {\n constructor(method: string) {\n super(`${method} is not implemented`)\n this.name = 'NotImplementedError'\n }\n}\n\n/**\n * Chain ID в конфиге не совпал с реальным ID цепи, полученным из SHiP-рукопожатия.\n * Защищает от случайного подключения к неверной ноде.\n */\nexport class ChainIdMismatchError extends Error {\n constructor(expected: string, actual: string) {\n super(`Chain ID mismatch: expected ${expected}, got ${actual}`)\n this.name = 'ChainIdMismatchError'\n }\n}\n\n/**\n * ABI для указанного контракта не найден ни в Redis-кэше, ни по RPC,\n * а конфигурация abiFallback='fail' запрещает продолжение без него.\n */\nexport class AbiNotFoundError extends Error {\n constructor(contract: string, blockNum: number, abiFallback: string) {\n super(`ABI for ${contract} not found at block ${blockNum}, abiFallback=${abiFallback}`)\n this.name = 'AbiNotFoundError'\n }\n}\n","/**\n * Адаптер SHiP-клиента — реализует порт ChainClient через ShipClient из ship-reader.\n *\n * Роль: изолирует бизнес-логику (BlockProcessor, Parser) от деталей\n * WebSocket-протокола SHiP и специфики @coopenomics/coopos-ship-reader.\n *\n * Жизненный цикл:\n * 1. new ShipReaderAdapter(opts) — конструируем клиент\n * 2. connect() — WebSocket handshake, получаем chainId\n * 3. streamBlocks(opts) — начинаем принимать блоки\n * 4. ack(1) — после каждого блока подтверждаем получение\n * 5. close() — завершаем соединение\n */\n\nimport { ShipClient } from '@coopenomics/coopos-ship-reader'\nimport type {\n ShipBlock,\n ChainInfo,\n ShipDelta,\n GetBlocksOptions,\n NativeDeltaEvent as ShipNativeDeltaEvent,\n} from '@coopenomics/coopos-ship-reader'\nimport type { ChainClient } from '../ports/ChainClient.js'\n\nexport class ShipReaderAdapter implements ChainClient {\n private client: ShipClient\n\n /**\n * @param opts.url — WebSocket URL SHiP-ноды, например ws://localhost:29999.\n * @param opts.timeoutMs — таймаут WebSocket-подключения.\n * @param opts.chainUrl — HTTP URL chain API (для getRawAbi / getChainInfo).\n */\n constructor(opts: { url: string; timeoutMs?: number; chainUrl?: string }) {\n const shipCfg: { url: string; timeoutMs?: number } = { url: opts.url }\n if (opts.timeoutMs !== undefined) shipCfg.timeoutMs = opts.timeoutMs\n\n // chain-конфиг необязателен: без него getRawAbi и getChainInfo недоступны\n if (opts.chainUrl !== undefined) {\n this.client = new ShipClient({ ship: shipCfg, chain: { url: opts.chainUrl } })\n } else {\n this.client = new ShipClient({ ship: shipCfg })\n }\n }\n\n /**\n * Устанавливает WebSocket-соединение и выполняет SHiP-рукопожатие.\n * Рукопожатие возвращает chainId — hex-хэш genesis.json.\n */\n async connect(): Promise<{ chainId: string }> {\n await this.client.connect()\n const { chainId } = await this.client.handshake()\n return { chainId }\n }\n\n /** Начинает асинхронный поток блоков от указанной позиции. */\n streamBlocks(opts: GetBlocksOptions): AsyncIterable<ShipBlock> {\n return this.client.streamBlocks(opts)\n }\n\n /**\n * ACK n блоков: сигнализирует SHiP-ноде что мы готовы принять ещё.\n * SHiP использует оконное управление потоком: без ACK нода замолчит.\n */\n ack(n: number): void {\n this.client.ack(n)\n }\n\n /** Закрывает WebSocket. */\n async close(): Promise<void> {\n this.client.close()\n }\n\n /** GET /v1/chain/get_info — возвращает head block, last irreversible и т.д. */\n getChainInfo(): Promise<ChainInfo> {\n return this.client.getChainInfo()\n }\n\n /** GET /v1/chain/get_raw_abi — загружает сырые байты ABI для bootstrap. */\n getRawAbi(contract: string): Promise<Uint8Array> {\n return this.client.getRawAbi(contract)\n }\n\n /**\n * Десериализует нативную SHiP-дельту в типизированный объект.\n * Делегируется встроенному deserializer'у ship-reader, который\n * содержит hardcoded-схемы нативных таблиц (permission, account, …).\n */\n deserializeNativeDelta(delta: ShipDelta): ShipNativeDeltaEvent {\n return this.client.deserializer.deserializeNativeDelta(delta)\n }\n}\n","/**\n * Адаптер Redis — реализует интерфейс RedisStore через ioredis.\n *\n * Ioredis не имеет поля \"exports\" в package.json, поэтому для NodeNext\n * resolution используется динамический import() с явным приведением типа.\n *\n * Два Lua-скрипта реализуют атомарные операции для distributed lock:\n *\n * PEXPIRE_LUA — условное продление TTL:\n * «Продли TTL ключа key на ms миллисекунд, но только если его текущее\n * значение равно value (т.е. мы — владельцы lock'а)».\n * Атомарность важна: без неё возможен race condition между GET и PEXPIRE.\n *\n * DEL_LUA — условное удаление:\n * «Удали ключ key, но только если его текущее значение равно value».\n * Защищает от случайного удаления lock'а другого процесса, если наш TTL истёк.\n */\n\nimport type { RedisOptions } from 'ioredis'\nimport type { RedisStore, StreamMessage, XGroupInfo } from '../ports/RedisStore.js'\n\n// Ioredis не имеет \"exports\" поля — используем динамический import с приведением типа\ntype RedisConstructor = new (url: string, opts?: RedisOptions) => IRedisClient\n\n/**\n * Минимальный интерфейс ioredis-клиента с только нужными командами.\n * Сигнатуры точно соответствуют тому что возвращает ioredis — без обёрток.\n */\ninterface IRedisClient {\n connect(): Promise<void>\n xadd(stream: string, id: string, ...args: string[]): Promise<string | null>\n xtrim(stream: string, strategy: string, threshold: string): Promise<number>\n xgroup(action: string, stream: string, group: string, id: string, mkstream?: string): Promise<unknown>\n xinfo(subcommand: string, key: string): Promise<unknown>\n xreadgroup(\n group: string, groupName: string, consumerName: string,\n count: string, countVal: number,\n block: string, blockMs: number,\n streams: string, stream: string, id: string,\n ): Promise<Array<[string, Array<[string, string[]]>]> | null>\n xrange(key: string, start: string, end: string, count: string, countVal: number): Promise<Array<[string, string[]]>>\n xrevrange(key: string, end: string, start: string, count: string, countVal: number): Promise<Array<[string, string[]]>>\n xlen(key: string): Promise<number>\n xdel(key: string, ...ids: string[]): Promise<number>\n xack(stream: string, group: string, id: string): Promise<number>\n zadd(key: string, score: number, member: string): Promise<number>\n zrangebyscore(key: string, min: string, max: string, limit: string, offset: number, count: number): Promise<string[]>\n zrevrangebyscore(key: string, max: string, min: string, limit: string, offset: number, count: number): Promise<string[]>\n zcount(key: string, min: string, max: string): Promise<number>\n zremrangebyscore(key: string, min: string, max: string): Promise<number>\n zcard(key: string): Promise<number>\n hset(key: string, ...args: string[]): Promise<number>\n hget(key: string, field: string): Promise<string | null>\n hgetall(key: string): Promise<Record<string, string> | null>\n hincrby(key: string, field: string, increment: number): Promise<number>\n hdel(key: string, ...fields: string[]): Promise<number>\n set(key: string, value: string, nx: string, px: string, ms: number): Promise<string | null>\n eval(script: string, numkeys: number, ...args: string[]): Promise<unknown>\n expire(key: string, seconds: number): Promise<number>\n scan(cursor: string, match: string, pattern: string, count: string, countVal: number): Promise<[string, string[]]>\n quit(): Promise<string>\n}\n\nconst { default: RedisClass } = await import('ioredis') as unknown as { default: RedisConstructor }\n\n/**\n * Атомарно продлевает PEXPIRE только если мы — владелец lock'а.\n * ARGV[1] = ms (новый TTL), ARGV[2] = expectedValue.\n */\nconst PEXPIRE_LUA = `\nlocal current = redis.call('GET', KEYS[1])\nif current == ARGV[2] then\n redis.call('PEXPIRE', KEYS[1], ARGV[1])\n return 1\nend\nreturn 0\n`\n\n/**\n * Атомарно удаляет ключ только если мы — владелец lock'а.\n * ARGV[1] = expectedValue.\n */\nconst DEL_LUA = `\nlocal current = redis.call('GET', KEYS[1])\nif current == ARGV[1] then\n redis.call('DEL', KEYS[1])\n return 1\nend\nreturn 0\n`\n\n/**\n * Конвертирует сырой ответ XRANGE/XREADGROUP в массив StreamMessage.\n * Redis возвращает: [[id, [field1, val1, field2, val2, ...]], ...]\n * Мы конвертируем в: [{id, fields: {field1: val1, ...}}, ...]\n */\nfunction parseStreamEntries(raw: Array<[string, string[]]>): StreamMessage[] {\n const messages: StreamMessage[] = []\n for (const [msgId, rawFields] of raw) {\n const fields: Record<string, string> = {}\n // rawFields — плоский массив [key, val, key, val, ...], шагаем по 2\n for (let i = 0; i + 1 < rawFields.length; i += 2) {\n fields[rawFields[i] ?? ''] = rawFields[i + 1] ?? ''\n }\n messages.push({ id: msgId, fields })\n }\n return messages\n}\n\n/**\n * Конвертирует ответ XINFO GROUPS в XGroupInfo.\n * Redis < 7.0 возвращает плоский массив [key, val, key, val, ...].\n * Redis >= 7.0 может возвращать объект — обрабатываем оба случая.\n */\nfunction parseXGroupInfo(raw: unknown): XGroupInfo {\n let obj: Record<string, unknown>\n if (Array.isArray(raw)) {\n // Старый формат: плоский массив ключ-значение\n obj = {}\n for (let i = 0; i + 1 < raw.length; i += 2) {\n obj[raw[i] as string] = raw[i + 1]\n }\n } else {\n obj = raw as Record<string, unknown>\n }\n return {\n name: String(obj['name'] ?? ''),\n pending: Number(obj['pending'] ?? 0),\n // Поле 'last-delivered-id' в Redis (с дефисами), не 'lastDeliveredId'\n lastDeliveredId: String(obj['last-delivered-id'] ?? '0-0'),\n // lag появилось в Redis 7.0; null для старых версий\n lag: obj['lag'] != null ? Number(obj['lag']) : null,\n consumers: Number(obj['consumers'] ?? 0),\n }\n}\n\nexport class IoRedisStore implements RedisStore {\n /** Прямой доступ к ioredis-клиенту (для тестов и расширения). */\n readonly client: IRedisClient\n\n constructor(opts: { url: string; password?: string; keyPrefix?: string }) {\n const redisOpts: RedisOptions = {\n lazyConnect: true, // не подключаться в конструкторе — явный connect()\n enableReadyCheck: true, // проверять готовность перед командами\n }\n if (opts.password !== undefined) redisOpts.password = opts.password\n if (opts.keyPrefix !== undefined) redisOpts.keyPrefix = opts.keyPrefix\n\n this.client = new RedisClass(opts.url, redisOpts)\n }\n\n /** Явное подключение — вызывается один раз при старте Parser/ParserClient. */\n async connect(): Promise<void> {\n await this.client.connect()\n }\n\n /** XADD stream * field1 val1 … — возвращает присвоенный entry ID. */\n async xadd(stream: string, fields: Record<string, string>): Promise<string> {\n const args: string[] = []\n for (const [k, v] of Object.entries(fields)) args.push(k, v)\n const id = await this.client.xadd(stream, '*', ...args)\n return id ?? ''\n }\n\n /** XTRIM stream MINID minId — удаляет записи с ID < minId. */\n async xtrim(stream: string, minId: string): Promise<number> {\n return this.client.xtrim(stream, 'MINID', minId)\n }\n\n /**\n * XGROUP CREATE stream group startId MKSTREAM\n * MKSTREAM: создаёт стрим если не существует.\n * BUSYGROUP: группа уже существует — это нормально, поглощаем ошибку.\n */\n async xgroupCreate(stream: string, group: string, startId: string): Promise<void> {\n try {\n await this.client.xgroup('CREATE', stream, group, startId, 'MKSTREAM')\n } catch (err) {\n if (err instanceof Error && err.message.includes('BUSYGROUP')) return\n throw err\n }\n }\n\n /** XGROUP SETID stream group id — переставляет позицию group в стриме. */\n async xgroupSetId(stream: string, group: string, id: string): Promise<void> {\n await this.client.xgroup('SETID', stream, group, id)\n }\n\n /** XINFO GROUPS stream → список consumer groups с метриками. */\n async xinfoGroups(stream: string): Promise<XGroupInfo[]> {\n const raw = await this.client.xinfo('GROUPS', stream) as unknown[]\n return (raw ?? []).map(parseXGroupInfo)\n }\n\n /**\n * XREADGROUP GROUP group consumer COUNT count BLOCK blockMs STREAMS stream id\n * id='>' — только новые сообщения.\n * id='0' — PEL (pending): уже доставленные, но не подтверждённые (recovery).\n */\n async xreadGroup(\n stream: string,\n group: string,\n consumer: string,\n count: number,\n blockMs: number,\n id: string,\n ): Promise<StreamMessage[]> {\n const result = await this.client.xreadgroup(\n 'GROUP', group, consumer,\n 'COUNT', count,\n 'BLOCK', blockMs,\n 'STREAMS', stream, id,\n )\n if (!result) return []\n const messages: StreamMessage[] = []\n for (const [, entries] of result) {\n messages.push(...parseStreamEntries(entries))\n }\n return messages\n }\n\n /** XRANGE stream start end COUNT count. */\n async xrange(stream: string, start: string, end: string, count: number): Promise<StreamMessage[]> {\n const raw = await this.client.xrange(stream, start, end, 'COUNT', count)\n return parseStreamEntries(raw)\n }\n\n /** XREVRANGE stream end start COUNT count. */\n async xrevrange(stream: string, end: string, start: string, count: number): Promise<StreamMessage[]> {\n const raw = await this.client.xrevrange(stream, end, start, 'COUNT', count)\n return parseStreamEntries(raw)\n }\n\n /** XLEN stream. */\n async xlen(stream: string): Promise<number> {\n return this.client.xlen(stream)\n }\n\n /** XDEL stream id — удаляет запись по ID. */\n async xdel(stream: string, id: string): Promise<number> {\n return this.client.xdel(stream, id)\n }\n\n /** XACK stream group id — убирает из PEL. */\n async xack(stream: string, group: string, id: string): Promise<void> {\n await this.client.xack(stream, group, id)\n }\n\n /** ZADD key score member. */\n async zadd(key: string, score: number, member: string): Promise<void> {\n await this.client.zadd(key, score, member)\n }\n\n /**\n * ZREVRANGEBYSCORE key max min LIMIT 0 1\n * Возвращает максимум один элемент с score ≤ max.\n * Используется для поиска ABI: «последняя версия не позже блока N».\n */\n async zrangeByscoreRev(key: string, max: string, min: string): Promise<string[]> {\n return this.client.zrevrangebyscore(key, max, min, 'LIMIT', 0, 1)\n }\n\n /** ZRANGEBYSCORE key min max LIMIT 0 9999999 — все элементы в диапазоне. */\n async zrangeByScore(key: string, min: string, max: string): Promise<string[]> {\n return this.client.zrangebyscore(key, min, max, 'LIMIT', 0, 9_999_999)\n }\n\n /** ZCOUNT key min max. */\n async zcount(key: string, min: string, max: string): Promise<number> {\n return this.client.zcount(key, min, max)\n }\n\n /** ZREMRANGEBYSCORE key min max → число удалённых. */\n async zremRangeByScore(key: string, min: string, max: string): Promise<number> {\n return this.client.zremrangebyscore(key, min, max)\n }\n\n /** ZCARD key. */\n async zcard(key: string): Promise<number> {\n return this.client.zcard(key)\n }\n\n /** HSET key field1 val1 field2 val2 … */\n async hset(key: string, fields: Record<string, string>): Promise<void> {\n const args: string[] = []\n for (const [k, v] of Object.entries(fields)) args.push(k, v)\n if (args.length > 0) await this.client.hset(key, ...args)\n }\n\n /** HGET key field. */\n async hget(key: string, field: string): Promise<string | null> {\n return this.client.hget(key, field)\n }\n\n /** HGETALL key → пустой объект если ключ не существует (ioredis возвращает null). */\n async hgetAll(key: string): Promise<Record<string, string>> {\n const result = await this.client.hgetall(key)\n return result ?? {}\n }\n\n /** HINCRBY key field increment → новое значение счётчика. */\n async hincrby(key: string, field: string, increment: number): Promise<number> {\n return this.client.hincrby(key, field, increment)\n }\n\n /** HDEL key field. */\n async hdel(key: string, field: string): Promise<void> {\n await this.client.hdel(key, field)\n }\n\n /**\n * SET key value NX PX pxMs\n * NX: только если не существует. PX: TTL в миллисекундах.\n * Используется для захвата distributed lock'а.\n */\n async setNx(key: string, value: string, pxMs: number): Promise<boolean> {\n const result = await this.client.set(key, value, 'NX', 'PX', pxMs)\n return result === 'OK'\n }\n\n /**\n * Выполняет PEXPIRE_LUA: продлевает TTL lock'а только если мы — владелец.\n * Возвращает true если продление прошло успешно.\n */\n async pexpire(key: string, ms: number, value: string): Promise<boolean> {\n const result = await this.client.eval(PEXPIRE_LUA, 1, key, String(ms), value) as number\n return result === 1\n }\n\n /**\n * Выполняет DEL_LUA: удаляет lock только если мы — владелец.\n * Возвращает true если удаление прошло успешно.\n */\n async luaDel(key: string, value: string): Promise<boolean> {\n const result = await this.client.eval(DEL_LUA, 1, key, value) as number\n return result === 1\n }\n\n /** EXPIRE key seconds. */\n async expire(key: string, seconds: number): Promise<void> {\n await this.client.expire(key, seconds)\n }\n\n /**\n * Полный SCAN по паттерну: итерирует cursor пока не вернётся '0'.\n * @param count — подсказка Redis сколько ключей возвращать за итерацию.\n * @returns Полный список ключей (может быть большим для широких паттернов).\n */\n async scan(pattern: string, count = 100): Promise<string[]> {\n const keys: string[] = []\n let cursor = '0'\n do {\n const [nextCursor, batch] = await this.client.scan(cursor, 'MATCH', pattern, 'COUNT', count)\n keys.push(...batch)\n cursor = nextCursor\n } while (cursor !== '0')\n return keys\n }\n\n /** Закрывает соединение с Redis. */\n async quit(): Promise<void> {\n await this.client.quit()\n }\n}\n","/**\n * Пул worker-потоков для CPU-интенсивной ABI-десериализации.\n *\n * Использует Piscina — высокопроизводительный worker pool для Node.js.\n * Каждый worker держит собственный in-memory ABI-кэш, поэтому повторные\n * задания с одним и тем же abiJson не перепарсивают его.\n */\n\n// Piscina: default import через dynamic import() — TS не может статически\n// проверить тип, поэтому используем топ-левел await + приведение типа.\ntype PiscinaPool = {\n run(task: unknown): Promise<unknown>\n utilization: number\n destroy(): Promise<void>\n}\ntype PiscinaCtor = new (opts: { filename: string; maxThreads?: number }) => PiscinaPool\nconst { default: PiscinaClass } = await import('piscina') as unknown as { default: PiscinaCtor }\nimport { fileURLToPath } from 'node:url'\nimport { join, dirname, resolve } from 'node:path'\nimport { existsSync } from 'node:fs'\n\n// __dirname не доступен в ESM — восстанавливаем через import.meta.url\nconst __filename = fileURLToPath(import.meta.url)\nconst __dirname = dirname(__filename)\n\n/**\n * Находит путь к скомпилированному worker'у.\n *\n * Сценарии:\n * 1. Production (dist/index.cjs) — worker рядом: dist/deserialize.worker.cjs\n * 2. Source mode (src/workers/WorkerPool.ts) — worker должен быть предварительно\n * собран в dist/workers/deserialize.worker.cjs (для тестов: build перед test:integration)\n */\nfunction resolveWorkerPath(): string {\n const candidates = [\n // Production: рядом с WorkerPool\n join(__dirname, 'deserialize.worker.cjs'),\n // Source mode (tests): относительный путь к dist/\n resolve(__dirname, '../../dist/deserialize.worker.cjs'),\n resolve(__dirname, '../../dist/workers/deserialize.worker.cjs'),\n ]\n for (const path of candidates) {\n if (existsSync(path)) return path\n }\n throw new Error(\n `Could not find deserialize.worker.cjs. Tried: ${candidates.join(', ')}. ` +\n `Run \"pnpm build\" first if running from source.`,\n )\n}\n\nexport interface DeserializeTask {\n /** Сырые байты action data или table row для декодирования. */\n rawBinary: Uint8Array\n /** JSON-представление ABI нужного контракта. */\n abiJson: string\n contract: string\n /** Имя типа в ABI для декодирования. */\n typeName: string\n kind: 'action' | 'delta'\n}\n\nexport class WorkerPool {\n private pool: PiscinaPool\n\n /**\n * @param maxThreads — максимум параллельных worker-потоков.\n * Оптимум зависит от числа CPU и IO-нагрузки. Дефолт 2 подходит\n * для большинства серверов; 4+ потока ускоряют плотные блоки (много actions).\n */\n constructor(maxThreads = 2) {\n // Загружаем CJS-сборку worker'а, т.к. Piscina нативно работает с CJS\n this.pool = new PiscinaClass({\n filename: resolveWorkerPath(),\n maxThreads,\n })\n }\n\n /**\n * Запускает десериализацию в одном из свободных worker-потоков.\n * Блокирует промис пока worker не вернёт результат.\n */\n run(task: DeserializeTask): Promise<Record<string, unknown>> {\n return this.pool.run(task) as Promise<Record<string, unknown>>\n }\n\n /**\n * Доля занятых потоков от общего числа (0..1).\n * Полезно для метрики parser_worker_pool_queue_depth.\n */\n get utilization(): number {\n return this.pool.utilization\n }\n\n /** Завершает все worker-потоки (вызывается при остановке парсера). */\n destroy(): Promise<void> {\n return this.pool.destroy()\n }\n}\n","/**\n * Обработчик одного блока SHiP → список ParserEvent.\n *\n * Архитектура:\n * BlockProcessor получает ShipBlock и возвращает массив событий в порядке:\n * [ActionEvents..., DeltaEvents..., NativeDeltaEvents...]\n *\n * Три фазы обработки:\n * 1. Traces (транзакционные трассировки) → ActionEvent[]\n * - Для каждого trace: получить ABI → десериализовать action data в worker'е.\n * - Особый случай eosio::setabi: извлекаем новый ABI и сохраняем в AbiStore.\n *\n * 2. Deltas → DeltaEvent[] + NativeDeltaEvent[]\n * - account-дельта: содержит обновлённый ABI контракта → сохраняем в AbiStore.\n * - contract_row: строка пользовательской таблицы → DeltaEvent (ABI-декодирование).\n * - нативные таблицы (isNativeTableName): NativeDeltaEvent через chainClient.\n *\n * p-queue с concurrency=1 гарантирует последовательную обработку блоков:\n * блок N+1 не начнёт обрабатываться пока не завершится блок N.\n */\n\nimport PQueue from 'p-queue'\nimport { ABI, Blob as AntelopeBlob } from '@wharfkit/antelope'\nimport { isNativeTableName } from '@coopenomics/coopos-ship-reader'\nimport type { ShipBlock } from '@coopenomics/coopos-ship-reader'\nimport type { WorkerPool } from '../workers/WorkerPool.js'\nimport type { ParserEvent, ActionEvent, DeltaEvent, NativeDeltaEvent } from '../types.js'\nimport { computeEventId } from '../events/eventId.js'\nimport type { AbiBootstrapper } from '../abi/AbiBootstrapper.js'\nimport type { AbiStore } from '../abi/AbiStore.js'\nimport type { ChainClient } from '../ports/ChainClient.js'\n\ninterface BlockProcessorOptions {\n /** Идентификатор цепи — проставляется в каждое событие. */\n chainId: string\n /** Пул потоков для CPU-интенсивной ABI-десериализации. */\n workerPool: WorkerPool\n /** Загрузчик/кэш ABI: обеспечивает ABI перед каждым декодированием. */\n abiBootstrapper: AbiBootstrapper\n /** Прямой доступ к Redis-кэшу ABI для runtime-обновлений (setabi). */\n abiStore: AbiStore\n /** Блокчейн-клиент для десериализации нативных дельт. */\n chainClient: ChainClient\n}\n\n/**\n * Конвертирует сырые байты ABI в канонический JSON-формат для передачи в worker.\n * wharfkit ABI.from() умеет парсить base64-encoded raw bytes через AntelopeBlob.\n * .toJSON() возвращает стандартную ABI-схему с structs/actions/tables —\n * именно она нужна worker'у для повторного ABI.from() и Serializer.decode().\n * При ошибке возвращает '{}' — worker просто вернёт пустой объект.\n */\nfunction abiToJson(bytes: Uint8Array): string {\n try {\n const base64 = Buffer.from(bytes).toString('base64')\n const abi = ABI.from(AntelopeBlob.from(base64))\n return JSON.stringify(abi.toJSON())\n } catch {\n return '{}'\n }\n}\n\nexport class BlockProcessor {\n /** Очередь с concurrency=1: только один блок обрабатывается одновременно. */\n private queue: PQueue\n private chainId: string\n private workerPool: WorkerPool\n private abiBootstrapper: AbiBootstrapper\n private abiStore: AbiStore\n private chainClient: ChainClient\n\n constructor(opts: BlockProcessorOptions) {\n this.chainId = opts.chainId\n this.workerPool = opts.workerPool\n this.abiBootstrapper = opts.abiBootstrapper\n this.abiStore = opts.abiStore\n this.chainClient = opts.chainClient\n this.queue = new PQueue({ concurrency: 1 })\n }\n\n /**\n * Ставит блок в очередь на обработку.\n * Возвращает Promise который резолвится когда этот конкретный блок обработан.\n * Блоки обрабатываются строго последовательно (concurrency=1).\n */\n process(block: ShipBlock): Promise<ParserEvent[]> {\n return this.queue.add(() => this.processBlock(block)) as Promise<ParserEvent[]>\n }\n\n private async processBlock(block: ShipBlock): Promise<ParserEvent[]> {\n const actionEvents: ActionEvent[] = []\n const deltaEvents: DeltaEvent[] = []\n const nativeDeltaEvents: NativeDeltaEvent[] = []\n\n const blockNum = block.thisBlock.blockNum\n const blockId = block.thisBlock.blockId\n // blockTime берём из первой трассировки; если трассировок нет — текущее время\n const blockTime = block.traces[0]?.blockTime ?? new Date().toISOString()\n\n // ── Фаза 1: Action traces ─────────────────────────────────────────────────\n for (const trace of block.traces) {\n // Получаем ABI (из кэша или bootstrapping через RPC)\n const abiBytes = await this.abiBootstrapper.ensureAbi(trace.account, blockNum)\n const abiJson = abiBytes && abiBytes.length > 0 ? abiToJson(abiBytes) : '{}'\n\n let data: Record<string, unknown> = {}\n if (trace.actRaw.length > 0) {\n try {\n // Десериализация в worker-потоке: не блокируем event loop\n data = await this.workerPool.run({\n rawBinary: trace.actRaw,\n abiJson,\n contract: trace.account,\n typeName: trace.name,\n kind: 'action',\n })\n } catch {\n // Не можем декодировать — оставляем data={}; событие всё равно публикуем\n data = {}\n }\n }\n\n // Runtime ABI update: eosio::setabi содержит новый ABI в поле 'abi' (hex-encoded)\n // Сохраняем сразу — последующие события того же блока уже должны использовать новый ABI\n if (trace.account === 'eosio' && trace.name === 'setabi') {\n const contractName = data['account']\n const abiHex = data['abi']\n if (typeof contractName === 'string' && typeof abiHex === 'string' && abiHex.length > 0) {\n await this.abiStore.storeAbi(contractName, blockNum, Buffer.from(abiHex, 'hex'))\n }\n }\n\n const partial: Omit<ActionEvent, 'event_id'> = {\n kind: 'action',\n chain_id: this.chainId,\n block_num: blockNum,\n block_time: blockTime,\n block_id: blockId,\n account: trace.account,\n name: trace.name,\n authorization: [...trace.authorization],\n data,\n action_ordinal: trace.actionOrdinal,\n global_sequence: trace.globalSequence,\n receipt: trace.receipt,\n }\n\n actionEvents.push({ ...partial, event_id: computeEventId(partial) })\n }\n\n // ── Фаза 2: Deltas ────────────────────────────────────────────────────────\n for (const delta of block.deltas) {\n\n // account-дельта: нативная таблица с метаданными аккаунта, включая поле 'abi'.\n // Обрабатывается первой чтобы последующие delta events того же блока уже видели новый ABI.\n if (delta.name === 'account' && delta.present && delta.rowRaw.length > 0) {\n const eosioAbiBytes = await this.abiBootstrapper.ensureAbi('eosio', blockNum)\n const eosioAbiJson = eosioAbiBytes && eosioAbiBytes.length > 0 ? abiToJson(eosioAbiBytes) : '{}'\n try {\n // Декодируем account-строку как тип 'account' в ABI eosio\n const accountData = await this.workerPool.run({\n rawBinary: delta.rowRaw,\n abiJson: eosioAbiJson,\n contract: 'eosio',\n typeName: 'account',\n kind: 'delta',\n })\n const accountName = accountData['name']\n const abiHex = accountData['abi']\n if (typeof accountName === 'string' && typeof abiHex === 'string' && abiHex.length > 0) {\n await this.abiStore.storeAbi(accountName, blockNum, Buffer.from(abiHex, 'hex'))\n }\n } catch { /* Если не удалось декодировать account — пропускаем ABI-обновление */ }\n }\n\n // contract_row: строки пользовательских таблиц — декодируются через пользовательский ABI\n if (delta.name === 'contract_row') {\n if (!delta.code || !delta.scope || !delta.table || !delta.primaryKey) continue\n\n const abiBytes = await this.abiBootstrapper.ensureAbi(delta.code, blockNum)\n const abiJson = abiBytes && abiBytes.length > 0 ? abiToJson(abiBytes) : '{}'\n\n let value: Record<string, unknown> = {}\n if (delta.rowRaw.length > 0) {\n try {\n value = await this.workerPool.run({\n rawBinary: delta.rowRaw,\n abiJson,\n contract: delta.code,\n typeName: delta.table,\n kind: 'delta',\n })\n } catch {\n value = {}\n }\n }\n\n const partial: Omit<DeltaEvent, 'event_id'> = {\n kind: 'delta',\n chain_id: this.chainId,\n block_num: blockNum,\n block_time: blockTime,\n block_id: blockId,\n code: delta.code,\n scope: delta.scope,\n table: delta.table,\n primary_key: delta.primaryKey,\n value,\n present: delta.present,\n }\n\n deltaEvents.push({ ...partial, event_id: computeEventId(partial) })\n // continue: нативная проверка ниже не нужна для contract_row\n continue\n }\n\n // Нативные таблицы (permission, account, resource_limits, …):\n // десериализуются через chainClient, который знает нативные ABI из ship-reader.\n if (isNativeTableName(delta.name)) {\n try {\n const native = this.chainClient.deserializeNativeDelta(delta)\n const partial: Omit<NativeDeltaEvent, 'event_id'> = {\n kind: 'native-delta',\n chain_id: this.chainId,\n block_num: blockNum,\n block_time: blockTime,\n block_id: blockId,\n table: native.table,\n lookup_key: native.lookup_key,\n data: native.data,\n present: native.present,\n }\n nativeDeltaEvents.push({ ...partial, event_id: computeEventId(partial) })\n } catch {\n // Ошибки в отдельных нативных дельтах не должны прерывать весь блок\n }\n }\n }\n\n // Возвращаем события в порядке: сначала actions, затем deltas, затем native deltas\n return [...actionEvents, ...deltaEvents, ...nativeDeltaEvents]\n }\n\n /** Число заданий, ожидающих обработки (в очереди + текущее). */\n get pendingCount(): number {\n return this.queue.size + this.queue.pending\n }\n\n /** Ждёт завершения всех задач в очереди (вызывается при graceful shutdown). */\n onIdle(): Promise<void> {\n return this.queue.onIdle()\n }\n}\n","/**\n * Детерминированные идентификаторы событий.\n *\n * event_id — строка, однозначно идентифицирующая событие без обращения к базе данных.\n * Свойства:\n * - Детерминированный: одни и те же входные данные → один и тот же ID.\n * - Fork-safe: события из параллельных форков одного блока отличаются\n * первыми 16 hex-символами block_id.\n * - Stateless: вычисляется в worker-потоке без каких-либо side effects.\n *\n * Форматы по типам (подробнее: docs/event-id-semantics.md):\n * action: chain:a:block_num:blockId[0..16]:global_sequence\n * delta: chain:d:block_num:blockId[0..16]:code:scope:table:primary_key\n * native-delta: chain:n:block_num:blockId[0..16]:table:lookup_key\n * fork: chain:f:forked_from_block:newHeadId[0..16]\n */\n\nimport type { ActionEvent, DeltaEvent, NativeDeltaEvent, ForkEvent, ParserEvent } from '../types.js'\n\ntype ActionWithoutId = Omit<ActionEvent, 'event_id'>\ntype DeltaWithoutId = Omit<DeltaEvent, 'event_id'>\ntype NativeDeltaWithoutId = Omit<NativeDeltaEvent, 'event_id'>\ntype ForkWithoutId = Omit<ForkEvent, 'event_id'>\n\n/** Объединение всех типов событий до присвоения event_id. */\nexport type EventWithoutId = ActionWithoutId | DeltaWithoutId | NativeDeltaWithoutId | ForkWithoutId\n\n/**\n * Вычисляет event_id по полям события (без поля event_id).\n *\n * Принимает событие без event_id, чтобы исключить возможность рекурсии.\n * Параметр blockId обрезается до 16 символов: это первые 8 байт, содержащих\n * номер блока, что делает ID читаемым, но достаточным для уникальности в рамках форка.\n */\nexport function computeEventId(event: EventWithoutId): string {\n // Первые 16 hex-символов block_id содержат enough entropy для fork-различия\n const blockIdShort = (blockId: string) => blockId.slice(0, 16)\n\n if (event.kind === 'action') {\n // global_sequence — монотонный счётчик действий в цепи, уникален в пределах цепи\n return `${event.chain_id}:a:${event.block_num}:${blockIdShort(event.block_id)}:${event.global_sequence}`\n }\n if (event.kind === 'delta') {\n // Комбинация code+scope+table+primary_key уникально идентифицирует строку таблицы\n return `${event.chain_id}:d:${event.block_num}:${blockIdShort(event.block_id)}:${event.code}:${event.scope}:${event.table}:${event.primary_key}`\n }\n if (event.kind === 'native-delta') {\n // lookup_key — натуральный PK нативной таблицы (например owner:name для permission)\n return `${event.chain_id}:n:${event.block_num}:${blockIdShort(event.block_id)}:${event.table}:${event.lookup_key}`\n }\n if (event.kind === 'fork') {\n // fork ID привязан к forked_from_block, а не к новому block_num — чтобы два\n // форка с одинаковой глубиной откати имели разные ID\n return `${event.chain_id}:f:${event.forked_from_block}:${blockIdShort(event.new_head_block_id)}`\n }\n\n // Exhaustiveness check: TS сообщит об ошибке при добавлении нового kind без обработки\n const _exhaustive: never = event\n return _exhaustive\n}\n\n/**\n * Удобная обёртка — принимает готовое событие (с event_id),\n * пересчитывает его ID (полезно для верификации целостности).\n */\nexport function computeEventIdFromComplete(event: ParserEvent): string {\n return computeEventId(event as EventWithoutId)\n}\n","/**\n * Фоновый supervisor для периодической очистки основного стрима (XTRIM).\n *\n * Проблема: события XADD добавляются непрерывно, и без очистки стрим\n * будет расти бесконечно, занимая память Redis.\n *\n * Стратегия MINID:\n * Вместо хранения фиксированного числа записей (MAXLEN), мы сохраняем все\n * записи, которые ещё не подтверждены (pending) хотя бы одной consumer group.\n * minId = min(lastDeliveredId всех групп с pending > 0).\n * XTRIM stream MINID minId удаляет всё с ID < minId.\n *\n * Это гарантирует, что ни один consumer не потеряет сообщения при trim:\n * группа с отставанием «тормозит» trim, пока не догонит.\n *\n * Таймер unref'ится чтобы не мешать graceful shutdown Node.js процесса.\n */\n\nimport type { RedisStore } from '../ports/RedisStore.js'\n\nexport interface XtrimSupervisorOpts {\n redis: RedisStore\n /** Имя стрима для очистки (обычно ce:parser:<chainId>:events). */\n stream: string\n /** Интервал между trim-циклами в мс. По умолчанию 60 000 (1 минута). */\n intervalMs?: number\n}\n\nexport class XtrimSupervisor {\n private timer: ReturnType<typeof setInterval> | null = null\n private readonly redis: RedisStore\n private readonly stream: string\n private readonly intervalMs: number\n\n constructor(opts: XtrimSupervisorOpts) {\n this.redis = opts.redis\n this.stream = opts.stream\n this.intervalMs = opts.intervalMs ?? 60_000\n }\n\n /** Запускает периодический trim. Идемпотентен — повторный вызов игнорируется. */\n start(): void {\n if (this.timer) return\n this.timer = setInterval(() => {\n void this.trim()\n }, this.intervalMs)\n // unref: таймер не удерживает процесс от завершения\n this.timer.unref?.()\n }\n\n /** Останавливает trim-цикл (вызывается при graceful shutdown). */\n stop(): void {\n if (this.timer) {\n clearInterval(this.timer)\n this.timer = null\n }\n }\n\n /**\n * Один цикл очистки:\n * 1. Получаем список consumer groups через XINFO GROUPS.\n * 2. Фильтруем группы у которых есть pending сообщения (pending > 0).\n * 3. Находим минимальный lastDeliveredId среди таких групп.\n * 4. XTRIM stream MINID minId — удаляем всё старее этого ID.\n *\n * Если pending-групп нет — trim не делается (всё подтверждено).\n * Если стрим не существует или XInfo бросает — тихо игнорируем (best-effort).\n */\n private async trim(): Promise<void> {\n try {\n const groups = await this.redis.xinfoGroups(this.stream)\n if (!groups || groups.length === 0) return\n\n // Trim только по группам с pending: не трогаем сообщения которые ещё не доставлены\n const pendingGroups = groups.filter(g => g.pending > 0)\n if (pendingGroups.length === 0) return\n\n // Наименьший lastDeliveredId = самый отстающий consumer\n const minId = pendingGroups\n .map(g => g.lastDeliveredId)\n .reduce((a, b) => (a < b ? a : b))\n\n if (minId) await this.redis.xtrim(this.stream, minId)\n } catch {\n // XTRIM — best-effort: ошибки не должны влиять на основной поток обработки\n }\n }\n}\n","/**\n * Детектор микрофорков блокчейна.\n *\n * Алгоритм прост: SHiP нода отправляет блоки последовательно.\n * При нормальной работе номер каждого следующего блока строго больше предыдущего.\n * Если пришёл блок с номером ≤ последнему обработанному — произошёл форк.\n *\n * При обнаружении форка:\n * 1. Создаём ForkEvent с forked_from_block = lastBlockNum.\n * 2. Устанавливаем lastBlockNum = новый blockNum (форкнутая ветка теперь актуальна).\n * 3. Возвращаем ForkEvent — он будет опубликован первым в пакете событий блока.\n *\n * Потребители должны откатить своё состояние для всех блоков > forked_from_block.\n * Паттерн отката: docs/disaster-recovery.md → Scenario 4.\n */\n\nimport { computeEventId } from '../events/eventId.js'\nimport type { ForkEvent } from '../types.js'\n\nexport class ForkDetector {\n /** -1 означает «ещё не видели ни одного блока». */\n private lastBlockNum = -1\n private chainId: string\n\n constructor(chainId: string) {\n this.chainId = chainId\n }\n\n /**\n * Проверяет, является ли входящий блок форком.\n * Должен вызываться один раз перед обработкой каждого блока.\n *\n * @returns ForkEvent если обнаружен форк, null при нормальной последовательности.\n */\n check(blockNum: number, blockId: string): ForkEvent | null {\n let event: ForkEvent | null = null\n\n // lastBlockNum >= 0: пропускаем первый блок (нет предыстории для сравнения)\n if (this.lastBlockNum >= 0 && blockNum <= this.lastBlockNum) {\n const partial: Omit<ForkEvent, 'event_id'> = {\n kind: 'fork',\n chain_id: this.chainId,\n forked_from_block: this.lastBlockNum,\n new_head_block_id: blockId,\n }\n event = { ...partial, event_id: computeEventId(partial) }\n }\n\n // Обновляем lastBlockNum независимо от наличия форка:\n // после форка track продолжается с нового blockNum\n this.lastBlockNum = blockNum\n return event\n }\n\n /**\n * Сбрасывает историю (вызывается при переподключении к SHiP-ноде,\n * чтобы не ложно детектировать форк при старте с произвольного блока).\n */\n reset(): void {\n this.lastBlockNum = -1\n }\n}\n","/**\n * Единый реестр Redis-ключей.\n *\n * Все ключи определены в одном месте, чтобы избежать опечаток и упростить\n * поиск по кодовой базе. Полная документация формата — docs/redis-key-taxonomy.md.\n *\n * Префиксы:\n * ce:parser:<chainId>: — Stream-ключи, относящиеся к конкретной цепи.\n * parser: — Hash/ZSET/String ключи с глобальным скоупом.\n */\nexport const RedisKeys = {\n /**\n * Главный поток событий парсера (unified event stream).\n * Тип Redis: Stream. Тримируется XtrimSupervisor'ом.\n * Пример: ce:parser:eos-mainnet:events\n */\n eventsStream: (chainId: string) => `ce:parser:${chainId}:events`,\n\n /**\n * Dead-letter поток для конкретной подписки.\n * Содержит сообщения, которые не смог обработать consumer после N попыток.\n * Тип Redis: Stream.\n * Пример: ce:parser:eos-mainnet:dead:verifier\n */\n deadLetterStream: (chainId: string, subId: string) => `ce:parser:${chainId}:dead:${subId}`,\n\n /**\n * Поток для задания on-demand reparse (зарезервировано для будущего).\n * Тип Redis: Stream.\n */\n reparseStream: (chainId: string, jobId: string) => `ce:parser:${chainId}:reparse:${jobId}`,\n\n /**\n * История версий ABI конкретного контракта.\n * Тип Redis: Sorted Set. Score = block_num, member = base64(rawAbiBytes).\n * При поиске ABI для блока N используется ZREVRANGEBYSCORE … N -inf LIMIT 0 1.\n * Пример: parser:abi:eosio.token\n */\n abiZset: (contract: string) => `parser:abi:${contract}`,\n\n /**\n * Контрольная точка синхронизации парсера (crash-recovery).\n * Тип Redis: Hash. Поля: block_num, block_id, last_updated.\n * При рестарте парсер читает отсюда позицию и продолжает с неё.\n */\n syncHash: (chainId: string) => `parser:sync:${chainId}`,\n\n /**\n * Реестр всех зарегистрированных подписок.\n * Тип Redis: Hash. Ключ поля = subId, значение = JSON-метаданные подписки.\n */\n subsHash: () => `parser:subs`,\n\n /**\n * Счётчики ошибок per-event для конкретной подписки.\n * Тип Redis: Hash. Ключ поля = event_id, значение = число провалов.\n * TTL: 24 часа (обновляется при каждом новом провале).\n * Используется FailureTracker для решения о переводе в dead-letter.\n */\n subFailuresHash: (subId: string) => `parser:sub:${subId}:failures`,\n\n /**\n * Блокировка single-active-consumer для подписки.\n * Тип Redis: String (instanceId держателя блокировки). TTL: 10 с (автопродление).\n * Только один экземпляр consumer-а может быть active; остальные — standby.\n */\n subLock: (subId: string) => `parser:sub:${subId}:lock`,\n\n /**\n * Метаданные задания reparse (зарезервировано для будущего).\n * Тип Redis: Hash.\n */\n reparseJobHash: (jobId: string) => `parser:reparse:${jobId}`,\n} as const\n","/**\n * Хранилище версий ABI в Redis Sorted Set.\n *\n * Проблема: ABI контракта меняется в блоке eosio::setabi. Чтобы корректно\n * декодировать действия и дельты исторических блоков, нужна версия ABI,\n * актуальная именно на момент этого блока.\n *\n * Решение: каждая версия ABI хранится как member в ZSET с score=block_num.\n * Для получения ABI на блок N делается ZREVRANGEBYSCORE key N -inf LIMIT 0 1 —\n * это возвращает самую позднюю версию, появившуюся не позже блока N.\n */\n\nimport type { RedisStore } from '../ports/RedisStore.js'\nimport { RedisKeys } from '../redis/keys.js'\n\nexport class AbiStore {\n constructor(private readonly redis: RedisStore) {}\n\n /**\n * Ищет версию ABI контракта, актуальную на момент blockNum.\n * Использует ZREVRANGEBYSCORE: возвращает последнюю запись со score ≤ blockNum.\n * @returns Байты ABI или null если история пуста для данного контракта.\n */\n async getAbi(contract: string, blockNum: number): Promise<Uint8Array | null> {\n const key = RedisKeys.abiZset(contract)\n // ZREVRANGEBYSCORE key blockNum -inf LIMIT 0 1 — один элемент с максимальным score ≤ blockNum\n const results = await this.redis.zrangeByscoreRev(key, String(blockNum), '-inf')\n if (results.length === 0 || !results[0]) return null\n // Байты хранятся в base64 для совместимости с Redis String-значениями\n return Buffer.from(results[0], 'base64')\n }\n\n /**\n * Сохраняет новую версию ABI, привязывая её к blockNum.\n * Вызывается AbiBootstrapper при первом наблюдении контракта и\n * BlockProcessor'ом при перехвате eosio::setabi / account-дельты.\n */\n async storeAbi(contract: string, blockNum: number, abiBytes: Uint8Array): Promise<void> {\n const key = RedisKeys.abiZset(contract)\n const member = Buffer.from(abiBytes).toString('base64')\n await this.redis.zadd(key, blockNum, member)\n }\n}\n","/**\n * Загрузчик первичного ABI для неизвестных контрактов.\n *\n * Жизненный цикл ABI:\n * 1. Первая встреча с контрактом (observedContracts не содержит его):\n * → смотрим в AbiStore (Redis ZSET).\n * → если не найден — загружаем через chain RPC (getRawAbi) и сохраняем.\n * 2. Все последующие встречи: просто читаем из AbiStore (Redis cache hit).\n * 3. Runtime-обновления ABI (eosio::setabi или account-дельта) выполняются\n * непосредственно в BlockProcessor и обходят этот класс.\n *\n * observedContracts — in-memory Set для оптимизации: если контракт уже\n * проходил через этот экземпляр, гарантированно был инициализирован в Redis.\n */\n\nimport type { ChainClient } from '../ports/ChainClient.js'\nimport { AbiNotFoundError } from '../errors.js'\nimport type { AbiStore } from './AbiStore.js'\n\nexport class AbiBootstrapper {\n /** Контракты, ABI которых уже гарантированно записан в Redis в этом сеансе. */\n private readonly observedContracts = new Set<string>()\n private readonly abiFallback: 'rpc-current' | 'fail'\n\n constructor(\n private readonly chainClient: ChainClient,\n private readonly abiStore: AbiStore,\n opts?: { abiFallback?: 'rpc-current' | 'fail' },\n ) {\n this.abiFallback = opts?.abiFallback ?? 'rpc-current'\n }\n\n /**\n * Гарантирует наличие ABI для контракта в кэше перед декодированием события.\n *\n * Быстрый путь: контракт уже в observedContracts → сразу идём в Redis.\n * Медленный путь: первая встреча → проверяем Redis → если пусто, скачиваем с RPC.\n *\n * @param contract — имя аккаунта-контракта (например 'eosio.token').\n * @param blockNum — номер блока, для которого нужна ABI (для поиска версии).\n * @returns Байты ABI или null если ABI недоступен и abiFallback='rpc-current'.\n * @throws AbiNotFoundError если abiFallback='fail' и ABI не найден.\n */\n async ensureAbi(contract: string, blockNum: number): Promise<Uint8Array | null> {\n // Быстрый путь: контракт уже встречался — ABI точно есть в Redis\n if (this.observedContracts.has(contract)) {\n return this.abiStore.getAbi(contract, blockNum)\n }\n\n // Медленный путь: проверяем Redis (вдруг ABI добавлен ранее или другим процессом)\n const stored = await this.abiStore.getAbi(contract, blockNum)\n if (stored) {\n this.observedContracts.add(contract)\n return stored\n }\n\n // Первая встреча, ABI в Redis не найден — bootstrap через chain RPC\n this.observedContracts.add(contract)\n try {\n const abiBytes = await this.chainClient.getRawAbi(contract)\n await this.abiStore.storeAbi(contract, blockNum, abiBytes)\n return abiBytes\n } catch {\n if (this.abiFallback === 'fail') {\n throw new AbiNotFoundError(contract, blockNum, this.abiFallback)\n }\n // rpc-current: игнорируем ошибку, возвращаем null — декодирование даст {}\n return null\n }\n }\n}\n","import type { ParserOptions } from '../config/index.js'\nimport { fromConfigFile, parseConfig } from '../config/index.js'\nimport { ShipReaderAdapter } from '../adapters/ShipReaderAdapter.js'\nimport { IoRedisStore } from '../adapters/IoRedisStore.js'\nimport { WorkerPool } from '../workers/WorkerPool.js'\nimport { BlockProcessor } from './BlockProcessor.js'\nimport type { XtrimSupervisorOpts } from './XtrimSupervisor.js'\nimport { XtrimSupervisor } from './XtrimSupervisor.js'\nimport { ForkDetector } from './ForkDetector.js'\nimport { RedisKeys } from '../redis/keys.js'\nimport { ChainIdMismatchError } from '../errors.js'\nimport { AbiStore } from '../abi/AbiStore.js'\nimport { AbiBootstrapper } from '../abi/AbiBootstrapper.js'\nimport type { ParserEvent } from '../types.js'\n\nexport class Parser {\n private opts: ParserOptions\n private chainClient: ShipReaderAdapter | null = null\n private redis: IoRedisStore | null = null\n private workerPool: WorkerPool | null = null\n private blockProcessor: BlockProcessor | null = null\n private xtrimSupervisor: XtrimSupervisor | null = null\n private running = false\n private stopSignal = false\n\n constructor(opts: ParserOptions) {\n this.opts = opts\n }\n\n static fromConfigFile(filePath: string): Parser {\n return new Parser(fromConfigFile(filePath))\n }\n\n static fromConfig(raw: unknown): Parser {\n return new Parser(parseConfig(raw))\n }\n\n async start(): Promise<void> {\n this.stopSignal = false\n this.running = true\n\n if (!this.opts.noSignalHandlers) {\n const shutdown = () => void this.stop()\n process.once('SIGTERM', shutdown)\n process.once('SIGINT', shutdown)\n }\n\n this.redis = new IoRedisStore(this.opts.redis)\n await this.redis.connect()\n\n await this.checkRedisPersistence()\n\n this.workerPool = new WorkerPool(this.opts.workerPool?.maxThreads ?? 2)\n\n const shipOpts: { url: string; timeoutMs?: number; chainUrl?: string } = {\n url: this.opts.ship.url,\n }\n if (this.opts.ship.timeoutMs !== undefined) shipOpts.timeoutMs = this.opts.ship.timeoutMs\n if (this.opts.chain?.url !== undefined) shipOpts.chainUrl = this.opts.chain.url\n\n this.chainClient = new ShipReaderAdapter(shipOpts)\n\n const { chainId } = await this.chainClient.connect()\n\n if (this.opts.chain?.id && this.opts.chain.id !== chainId) {\n throw new ChainIdMismatchError(this.opts.chain.id, chainId)\n }\n\n const abiFallback = this.opts.abiFallback ?? 'rpc-current'\n const abiStore = new AbiStore(this.redis)\n const abiBootstrapper = new AbiBootstrapper(this.chainClient, abiStore, { abiFallback })\n\n this.blockProcessor = new BlockProcessor({\n chainId,\n workerPool: this.workerPool,\n abiBootstrapper,\n abiStore,\n chainClient: this.chainClient,\n })\n\n const syncKey = RedisKeys.syncHash(chainId)\n const eventsStream = RedisKeys.eventsStream(chainId)\n\n const lastBlockNum = await this.redis.hget(syncKey, 'block_num')\n const lastBlockId = await this.redis.hget(syncKey, 'block_id')\n\n const havePositions =\n lastBlockNum && lastBlockId\n ? [{ blockNum: Number(lastBlockNum), blockId: lastBlockId }]\n : []\n\n const xtrimOpts: XtrimSupervisorOpts = {\n redis: this.redis,\n stream: eventsStream,\n }\n if (this.opts.xtrim?.intervalMs !== undefined) xtrimOpts.intervalMs = this.opts.xtrim.intervalMs\n this.xtrimSupervisor = new XtrimSupervisor(xtrimOpts)\n\n if (this.opts.xtrim?.enabled !== false) {\n this.xtrimSupervisor.start()\n }\n\n const streamOpts = {\n startBlock: havePositions[0]?.blockNum ?? 0,\n havePositions,\n }\n\n const irreversibleOnly = this.opts.irreversibleOnly ?? false\n const forkDetector = new ForkDetector(chainId)\n\n for await (const block of this.chainClient.streamBlocks(streamOpts)) {\n if (this.stopSignal) break\n\n if (irreversibleOnly && block.thisBlock.blockNum > block.lastIrreversible.blockNum) {\n this.chainClient.ack(1)\n continue\n }\n\n const forkEvent = forkDetector.check(block.thisBlock.blockNum, block.thisBlock.blockId)\n const events: ParserEvent[] = await this.blockProcessor.process(block)\n const toPublish: ParserEvent[] = forkEvent ? [forkEvent, ...events] : events\n\n for (const event of toPublish) {\n await this.redis.xadd(eventsStream, this.eventToFields(event))\n }\n\n await this.redis.hset(syncKey, {\n block_num: String(block.thisBlock.blockNum),\n block_id: block.thisBlock.blockId,\n last_updated: new Date().toISOString(),\n })\n\n this.chainClient.ack(1)\n }\n }\n\n private eventToFields(event: ParserEvent): Record<string, string> {\n // BigInt нельзя передать в JSON.stringify без replacer'а —\n // сериализуем как string; ParserClient при чтении делает обратную конверсию.\n return {\n data: JSON.stringify(event, (_k, v) =>\n typeof v === 'bigint' ? v.toString() : v,\n ),\n }\n }\n\n async stop(): Promise<void> {\n this.stopSignal = true\n\n if (this.blockProcessor) {\n await this.blockProcessor.onIdle()\n }\n\n if (this.chainClient) {\n await this.chainClient.close()\n this.chainClient = null\n }\n\n if (this.workerPool) {\n await this.workerPool.destroy()\n this.workerPool = null\n }\n\n if (this.xtrimSupervisor) {\n this.xtrimSupervisor.stop()\n this.xtrimSupervisor = null\n }\n\n if (this.redis) {\n await this.redis.quit()\n this.redis = null\n }\n\n this.running = false\n }\n\n get isRunning(): boolean {\n return this.running\n }\n\n private async checkRedisPersistence(): Promise<void> {\n const redis = this.redis!\n try {\n const aofResult = await redis.hget('__parser_check__', '__noop__')\n void aofResult\n } catch {\n // non-fatal check failure\n }\n }\n}\n","/**\n * Клиент-потребитель событий парсера.\n *\n * ParserClient — главная точка входа для прикладного кода, который хочет\n * получать события из блокчейна без прямого доступа к Redis или SHiP.\n *\n * Что делает:\n * 1. Регистрирует подписку (метаданные) в Redis Hash (parser:subs).\n * 2. Захватывает distributed lock (single-active-consumer) через SubscriptionLock.\n * 3. Читает события из Redis Stream через RedisConsumer (XREADGROUP).\n * 4. Применяет фильтры (matchFilters) — пропускает нерелевантные события.\n * 5. Доставляет событие через yield.\n * 6. После успешной обработки: XACK + clearFailure.\n * 7. При ошибке в обработчике: инкрементирует счётчик (FailureTracker).\n * При достижении порога (3 ошибки) — переводит в dead-letter stream.\n *\n * Использование:\n * const client = new ParserClient({ … })\n * for await (const event of client.stream()) {\n * await myHandler(event)\n * }\n */\n\nimport { randomUUID } from 'node:crypto'\nimport { hostname } from 'node:os'\nimport { IoRedisStore } from '../adapters/IoRedisStore.js'\nimport { RedisKeys } from '../redis/keys.js'\nimport { SubscriptionLock } from './SubscriptionLock.js'\nimport { RedisConsumer } from './RedisConsumer.js'\nimport { FailureTracker } from './FailureTracker.js'\nimport { matchFilters } from './filters.js'\nimport type { SubscriptionFilter } from './filters.js'\nimport type { SubscriptionLockOptions } from './SubscriptionLock.js'\nimport type { ParserEvent } from '../types.js'\n\nexport interface ParserClientOptions {\n /** Уникальный идентификатор подписки (имя consumer group в Redis Stream). */\n subscriptionId: string\n /** Список фильтров. Пустой = все события. */\n filters?: SubscriptionFilter[]\n /**\n * Стартовая позиция потребления:\n * 'last_known' — '$' (только новые события с момента регистрации)\n * 'head-minus-1000' — приблизительно с 1000 блоков назад\n * number — с указанного block_num (приблизительное преобразование в stream ID)\n */\n startFrom?: 'last_known' | number | 'head-minus-1000'\n redis: { url: string; password?: string; keyPrefix?: string }\n chain: { id: string }\n /** Таймаут ожидания lock'а в мс (для тестов). */\n acquireLockTimeoutMs?: number\n /** Отключить SIGTERM/SIGINT обработчики (для тестов и встроенного использования). */\n noSignalHandlers?: boolean\n}\n\nexport class ParserClient {\n private opts: ParserClientOptions\n private redis: IoRedisStore | null = null\n private lock: SubscriptionLock | null = null\n private consumer: RedisConsumer | null = null\n private failureTracker: FailureTracker | null = null\n /** Уникальный ID этого экземпляра — используется как значение distributed lock'а. */\n private instanceId: string\n private closed = false\n\n constructor(opts: ParserClientOptions) {\n this.opts = opts\n // instanceId = hostname:pid:uuid — уникален даже при нескольких process на одной машине\n this.instanceId = `${hostname()}:${process.pid}:${randomUUID()}`\n }\n\n /**\n * Основной AsyncGenerator: инициализирует подключение и начинает yield событий.\n *\n * Этапы старта:\n * 1. Подключаемся к Redis.\n * 2. Регистрируем подписку в HSET parser:subs.\n * 3. Пытаемся захватить lock; если занят — ждём освобождения.\n * 4. Определяем startId для consumer group.\n * 5. Создаём consumer group (XGROUP CREATE).\n * 6. Читаем события в цикле, фильтруем, yield'им.\n *\n * Генератор завершается при вызове close().\n */\n async *stream(): AsyncGenerator<ParserEvent> {\n this.redis = new IoRedisStore(this.opts.redis)\n await this.redis.connect()\n\n const subId = this.opts.subscriptionId\n const chainId = this.opts.chain.id\n const stream = RedisKeys.eventsStream(chainId)\n const groupName = subId\n\n this.failureTracker = new FailureTracker(this.redis, chainId)\n\n // Регистрация подписки: сохраняем метаданные в Redis Hash для CLI (list-subscriptions)\n await this.redis.hset(RedisKeys.subsHash(), {\n [subId]: JSON.stringify({\n subId,\n filters: this.opts.filters ?? [],\n startFrom: this.opts.startFrom ?? 'last_known',\n registeredAt: new Date().toISOString(),\n }),\n })\n\n // Distributed lock: только один экземпляр может быть active\n const lockOpts: SubscriptionLockOptions = {\n redis: this.redis,\n subId,\n instanceId: this.instanceId,\n }\n if (this.opts.acquireLockTimeoutMs !== undefined) {\n lockOpts.acquireLockTimeoutMs = this.opts.acquireLockTimeoutMs\n }\n this.lock = new SubscriptionLock(lockOpts)\n\n if (!this.opts.noSignalHandlers) {\n const close = () => void this.close()\n process.once('SIGTERM', close)\n process.once('SIGINT', close)\n }\n\n // Захватываем lock или ждём пока предыдущий holder умрёт\n const acquired = await this.lock.acquire()\n if (!acquired) {\n await this.lock.waitForPromotion()\n }\n\n // Определяем startId для consumer group\n const startFrom = this.opts.startFrom ?? 'last_known'\n let startId: string\n\n if (startFrom === 'last_known') {\n startId = '$' // только новые сообщения\n } else if (startFrom === 'head-minus-1000') {\n startId = '0' // упрощённо: с начала стрима\n } else {\n // Числовой block_num: конвертируем в приблизительный stream ID\n startId = `${startFrom}-0`\n }\n\n this.consumer = new RedisConsumer({\n redis: this.redis,\n stream,\n groupName,\n blockMs: 2_000,\n })\n await this.consumer.init(startId)\n\n for await (const msg of this.consumer.read()) {\n if (this.closed) break\n\n const rawData = msg.fields['data']\n if (!rawData) {\n // Сообщение без поля 'data' — некорректное, просто подтверждаем\n await this.consumer.ack(msg.id)\n continue\n }\n\n let event: ParserEvent\n try {\n event = JSON.parse(rawData) as ParserEvent\n // JSON не сохраняет BigInt: global_sequence сериализуется как string\n if (event.kind === 'action' && typeof event.global_sequence === 'string') {\n event = { ...event, global_sequence: BigInt(event.global_sequence as unknown as string) }\n }\n } catch {\n // Невалидный JSON — пропускаем\n await this.consumer.ack(msg.id)\n continue\n }\n\n // Применяем фильтры: если событие не нужно этой подписке — XACK и следующее\n if (!matchFilters(event, this.opts.filters)) {\n await this.consumer.ack(msg.id)\n continue\n }\n\n try {\n // Отдаём событие вызывающему коду\n yield event\n // Успешно обработано: подтверждаем и сбрасываем счётчик ошибок\n await this.consumer.ack(msg.id)\n await this.failureTracker.clearFailure(subId, event.event_id)\n } catch (err) {\n // Обработчик выбросил исключение: инкрементируем счётчик\n const count = await this.failureTracker.recordFailure(subId, event.event_id)\n if (this.failureTracker.shouldDeadLetter(count)) {\n // Порог достигнут: отправляем в dead-letter и подтверждаем\n await this.failureTracker.routeToDeadLetter(\n subId,\n event.event_id,\n msg.fields,\n err instanceof Error ? err.message : String(err),\n )\n await this.consumer.ack(msg.id)\n await this.failureTracker.clearFailure(subId, event.event_id)\n }\n // Иначе: оставляем в PEL — следующий recoverOwnPending повторит доставку\n }\n }\n }\n\n /**\n * Graceful shutdown: останавливает генератор, освобождает lock, закрывает Redis.\n */\n async close(): Promise<void> {\n this.closed = true\n this.consumer?.stop()\n if (this.lock) {\n this.lock.stopHeartbeat()\n await this.lock.release()\n }\n if (this.redis) {\n await this.redis.quit()\n this.redis = null\n }\n }\n\n /** Закрывает клиент и завершает процесс (для использования в SIGINT-обработчике). */\n closeAndExit(): void {\n void (async () => {\n await this.close()\n process.exit(0)\n })()\n }\n}\n","/**\n * Distributed lock для single-active-consumer паттерна.\n *\n * Проблема: несколько экземпляров одного consumer-а запущены одновременно\n * (горизонтальное масштабирование, rolling deploy). Обрабатывать события\n * должен ровно один (active), остальные — в режиме ожидания (standby).\n *\n * Механизм:\n * - Lock реализован как Redis String с TTL 10 секунд.\n * - Значение = instanceId (hostname:pid:uuid) — уникально для каждого процесса.\n * - Active-экземпляр продлевает TTL каждые 3 секунды через heartbeat.\n * - Standby-экземпляры опрашивают каждые 500 мс попытку захватить lock.\n * - Если active-экземпляр упал — его TTL истекает за ≤10 с, standby захватывает lock.\n *\n * Атомарные операции:\n * - setNx: атомарный SET NX PX — захватывает lock.\n * - pexpire (LUA): продлевает TTL только если мы — владелец (не перезаписывает чужой).\n * - luaDel (LUA): удаляет lock только если мы — владелец.\n */\n\nimport type { RedisStore } from '../ports/RedisStore.js'\nimport { RedisKeys } from '../redis/keys.js'\n\nexport type LockState = 'acquiring' | 'active' | 'standby' | 'released'\n\nexport interface SubscriptionLockOptions {\n redis: RedisStore\n /** Идентификатор подписки — определяет имя Redis-ключа. */\n subId: string\n /** Уникальный ID этого экземпляра (hostname:pid:uuid). */\n instanceId: string\n /** Интервал heartbeat в мс. По умолчанию 3000. */\n heartbeatIntervalMs?: number\n /** Таймаут ожидания lock'а в мс. По умолчанию Infinity. */\n acquireLockTimeoutMs?: number\n}\n\n/** TTL lock'а: если heartbeat прекратится — lock освободится через 10 с. */\nconst LOCK_TTL_MS = 10_000\n/** Heartbeat раз в 3 с (меньше TTL / 3 — чтобы не терять lock при задержках). */\nconst HEARTBEAT_MS = 3_000\n/** Standby-экземпляры опрашивают каждые 500 мс. */\nconst STANDBY_POLL_MS = 500\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise(r => setTimeout(r, ms))\n}\n\nexport class SubscriptionLock {\n private redis: RedisStore\n private key: string\n readonly instanceId: string\n private heartbeatMs: number\n private acquireTimeoutMs: number\n private heartbeatTimer: ReturnType<typeof setInterval> | null = null\n private _state: LockState = 'acquiring'\n\n constructor(opts: SubscriptionLockOptions) {\n this.redis = opts.redis\n this.key = RedisKeys.subLock(opts.subId)\n this.instanceId = opts.instanceId\n this.heartbeatMs = opts.heartbeatIntervalMs ?? HEARTBEAT_MS\n this.acquireTimeoutMs = opts.acquireLockTimeoutMs ?? Infinity\n }\n\n /** Текущее состояние: acquiring → active/standby → released. */\n get state(): LockState {\n return this._state\n }\n\n /**\n * Пробует захватить lock одним атомарным SET NX PX.\n * @returns true если захватили (state = active), false если занят (state = standby).\n */\n async acquire(): Promise<boolean> {\n const acquired = await this.redis.setNx(this.key, this.instanceId, LOCK_TTL_MS)\n if (acquired) {\n this._state = 'active'\n this.startHeartbeat()\n } else {\n this._state = 'standby'\n }\n return acquired\n }\n\n /**\n * Блокирует текущий процесс до тех пор пока lock не освободится и мы его захватим.\n * Опрашивает Redis каждые STANDBY_POLL_MS мс.\n * @throws Error если acquireLockTimeoutMs истёк.\n */\n async waitForPromotion(): Promise<void> {\n const deadline =\n this.acquireTimeoutMs === Infinity ? Infinity : Date.now() + this.acquireTimeoutMs\n\n while (true) {\n if (Date.now() > deadline) {\n throw new Error(`Lock acquire timeout after ${this.acquireTimeoutMs}ms`)\n }\n await sleep(STANDBY_POLL_MS)\n const acquired = await this.redis.setNx(this.key, this.instanceId, LOCK_TTL_MS)\n if (acquired) {\n this._state = 'active'\n this.startHeartbeat()\n return\n }\n }\n }\n\n /** Запускает периодическое продление TTL (heartbeat). */\n private startHeartbeat(): void {\n this.heartbeatTimer = setInterval(() => {\n void this.renewHeartbeat()\n }, this.heartbeatMs)\n // unref: heartbeat не мешает завершению процесса\n this.heartbeatTimer.unref?.()\n }\n\n /**\n * Один шаг heartbeat: продлеваем TTL через LUA-скрипт.\n * Если LUA вернул false — кто-то другой захватил lock (race condition после\n * истечения нашего TTL). Переходим в standby.\n */\n private async renewHeartbeat(): Promise<void> {\n const renewed = await this.redis.pexpire(this.key, LOCK_TTL_MS, this.instanceId)\n if (!renewed && this._state === 'active') {\n this.stopHeartbeat()\n this._state = 'standby'\n }\n }\n\n /** Останавливает heartbeat-таймер (без освобождения lock'а). */\n stopHeartbeat(): void {\n if (this.heartbeatTimer) {\n clearInterval(this.heartbeatTimer)\n this.heartbeatTimer = null\n }\n }\n\n /**\n * Освобождает lock: останавливает heartbeat, удаляет ключ через LUA (conditional).\n * После вызова state = 'released'.\n */\n async release(): Promise<void> {\n this.stopHeartbeat()\n await this.redis.luaDel(this.key, this.instanceId)\n this._state = 'released'\n }\n}\n","/**\n * Низкоуровневый Redis Stream consumer (XREADGROUP).\n *\n * Реализует two-phase чтение:\n * Фаза 1 — Recovery: при старте сначала читаем PEL (pending entries list)\n * с id='0'. Это сообщения, которые были доставлены в предыдущей сессии\n * но не подтверждены (XACK). Перечитываем их чтобы не потерять.\n *\n * Фаза 2 — Normal read: читаем новые сообщения с id='>'.\n * BLOCK blockMs: если новых нет — ждём (не-busyloop).\n *\n * Все consumer'ы в одной group читают один и тот же поток, но каждое сообщение\n * доставляется ровно одному consumer'у (группа обеспечивает fan-out).\n *\n * Имя consumer'а фиксировано ('primary') — мы не используем конкурентное чтение\n * внутри одной группы (это решается через single-active-consumer lock).\n */\n\nimport type { RedisStore, StreamMessage } from '../ports/RedisStore.js'\n\n/** Фиксированное имя consumer'а внутри group. */\nexport const CONSUMER_NAME = 'primary'\n\nexport interface RedisConsumerOptions {\n redis: RedisStore\n stream: string\n groupName: string\n /** Время блокировки XREADGROUP в мс. 2000 = ждём 2 с перед следующей попыткой. */\n blockMs?: number\n /** Максимум сообщений за один XREADGROUP вызов. */\n count?: number\n}\n\nexport class RedisConsumer {\n private redis: RedisStore\n private stream: string\n private groupName: string\n private blockMs: number\n private count: number\n private stopped = false\n\n constructor(opts: RedisConsumerOptions) {\n this.redis = opts.redis\n this.stream = opts.stream\n this.groupName = opts.groupName\n this.blockMs = opts.blockMs ?? 2_000\n this.count = opts.count ?? 10\n }\n\n /**\n * XGROUP CREATE stream groupName startId MKSTREAM\n * Создаёт group (или игнорирует если уже существует — BUSYGROUP).\n * startId='$' — читать только новые; startId='0' — с самого начала.\n */\n async init(startId = '$'): Promise<void> {\n await this.redis.xgroupCreate(this.stream, this.groupName, startId)\n }\n\n /**\n * XGROUP SETID — переставляет позицию group (для reset-subscription).\n * Примечание: здесь используется xgroupCreate что идемпотентно;\n * для точного SETID нужен xgroupSetId из RedisStore.\n */\n async setStartId(id: string): Promise<void> {\n await this.redis.xgroupCreate(this.stream, this.groupName, id)\n }\n\n /**\n * Читает PEL (pending entries) с id='0': сообщения, доставленные но не подтверждённые.\n * Вызывается при старте для recovery после крэша.\n */\n async recoverOwnPending(): Promise<StreamMessage[]> {\n return this.redis.xreadGroup(\n this.stream,\n this.groupName,\n CONSUMER_NAME,\n 100,\n 0, // blockMs=0: не блокировать при чтении PEL\n '0', // id='0': PEL\n )\n }\n\n /**\n * Асинхронный генератор сообщений.\n *\n * Порядок:\n * 1. Сначала отдаём все pending (незавершённые из предыдущей сессии).\n * 2. Затем в бесконечном цикле читаем новые (BLOCK 2000 мс).\n *\n * Цикл прерывается при вызове stop().\n * Каждое сообщение нужно подтвердить через ack(msg.id).\n */\n async* read(): AsyncGenerator<StreamMessage> {\n // Фаза 1: recovery — перечитываем незавершённые из прошлой сессии\n const pending = await this.recoverOwnPending()\n for (const msg of pending) {\n yield msg\n }\n\n // Фаза 2: нормальное чтение новых сообщений\n while (!this.stopped) {\n const messages = await this.redis.xreadGroup(\n this.stream,\n this.groupName,\n CONSUMER_NAME,\n this.count,\n this.blockMs,\n '>', // id='>': только непрочитанные новые\n )\n for (const msg of messages) {\n if (this.stopped) return\n yield msg\n }\n }\n }\n\n /** XACK stream groupName id — подтверждает что сообщение обработано. */\n async ack(id: string): Promise<void> {\n await this.redis.xack(this.stream, this.groupName, id)\n }\n\n /** Сигнализирует генератору прекратить чтение (graceful stop). */\n stop(): void {\n this.stopped = true\n }\n}\n","/**\n * Трекер ошибок обработки событий для конкретной подписки.\n *\n * Логика: consumer обрабатывает событие и иногда выбрасывает исключение.\n * Вместо немедленного перевода в dead-letter даём несколько попыток (FAILURE_THRESHOLD = 3).\n * После FAILURE_THRESHOLD провалов событие переводится в dead-letter stream.\n *\n * Счётчики хранятся в Redis Hash (parser:sub:<subId>:failures) с TTL 24 ч.\n * TTL сбрасывается при каждом новом провале — чтобы не накапливать стали счётчики.\n *\n * Почему Redis, а не in-memory: при рестарте consumer'а незакрытые ошибки\n * не теряются и события всё равно попадут в dead-letter при следующей попытке.\n */\n\nimport type { RedisStore } from '../ports/RedisStore.js'\nimport { RedisKeys } from '../redis/keys.js'\n\n/** После 3 ошибок подряд событие уходит в dead-letter. */\nconst FAILURE_THRESHOLD = 3\n/** TTL хэша с счётчиками: 24 часа. Обновляется при каждом новом провале. */\nconst FAILURE_TTL_SECONDS = 86_400\n\nexport class FailureTracker {\n private redis: RedisStore\n private chainId: string\n\n constructor(redis: RedisStore, chainId: string) {\n this.redis = redis\n this.chainId = chainId\n }\n\n /**\n * Инкрементирует счётчик ошибок для eventId и продлевает TTL хэша.\n * @returns Новое значение счётчика (1, 2, 3, …).\n */\n async recordFailure(subId: string, eventId: string): Promise<number> {\n const key = RedisKeys.subFailuresHash(subId)\n const count = await this.redis.hincrby(key, eventId, 1)\n // Продлеваем TTL всего хэша (per-field HEXPIRE доступен только с Redis 7.4+)\n await this.redis.expire(key, FAILURE_TTL_SECONDS)\n return count\n }\n\n /**\n * Возвращает текущий счётчик ошибок для eventId.\n * 0 если счётчик не существует (событие ещё не проваливалось).\n */\n async getFailureCount(subId: string, eventId: string): Promise<number> {\n const key = RedisKeys.subFailuresHash(subId)\n const val = await this.redis.hget(key, eventId)\n return val ? parseInt(val, 10) : 0\n }\n\n /**\n * Проверяет достиг ли счётчик порога для dead-letter.\n * Вызывается после recordFailure: if (shouldDeadLetter(count)) { … }\n */\n shouldDeadLetter(count: number): boolean {\n return count >= FAILURE_THRESHOLD\n }\n\n /**\n * Записывает событие в dead-letter stream с метаданными об ошибке.\n * Dead-letter stream: ce:parser:<chainId>:dead:<subId>\n * Поля записи: data (оригинальный payload), failureCount, lastError, subId.\n */\n async routeToDeadLetter(\n subId: string,\n eventId: string,\n /** Оригинальные fields из StreamMessage (включая поле 'data'). */\n payload: Record<string, string>,\n lastError: string,\n ): Promise<void> {\n const stream = RedisKeys.deadLetterStream(this.chainId, subId)\n await this.redis.xadd(stream, {\n ...payload,\n failureCount: String(FAILURE_THRESHOLD),\n lastError,\n subId,\n })\n }\n\n /**\n * Сбрасывает счётчик ошибок для eventId после успешной обработки.\n * Вызывается после успешного yield события в ParserClient.\n */\n async clearFailure(subId: string, eventId: string): Promise<void> {\n await this.redis.hdel(RedisKeys.subFailuresHash(subId), eventId)\n }\n}\n","/**\n * Фильтры подписки — декларативный DSL для выбора нужных событий.\n *\n * Потребитель задаёт массив SubscriptionFilter при создании ParserClient.\n * matchFilters(event, filters) возвращает true если событие удовлетворяет\n * хотя бы одному фильтру (OR-семантика).\n *\n * Значения полей фильтра:\n * undefined / '*' — совпадает с любым значением (wildcard).\n * строка — точное совпадение.\n *\n * Примеры фильтров:\n * { kind: 'action', account: 'eosio.token', name: 'transfer' }\n * { kind: 'delta', code: 'eosio', table: 'global' }\n * { kind: 'native-delta', table: 'permission' }\n * { kind: 'fork' }\n */\n\nimport type { ParserEvent, ActionEvent, DeltaEvent, NativeDeltaEvent } from '../types.js'\nimport type { NativeTableName } from '@coopenomics/coopos-ship-reader'\n\n/** Фильтр по транзакционным действиям. */\nexport interface ActionFilter<T extends Record<string, unknown> = Record<string, unknown>> {\n kind: 'action'\n /** Аккаунт контракта. undefined = любой. */\n account?: string\n /** Имя действия. undefined = любое. */\n name?: string\n /** Частичное совпадение по полям data. */\n data?: Partial<T>\n}\n\n/** Фильтр по изменениям строк пользовательских таблиц. */\nexport interface DeltaFilter {\n kind: 'delta'\n /** Аккаунт контракта. undefined = любой. */\n code?: string\n /** Имя таблицы. undefined = любая. */\n table?: string\n /** Скоуп. undefined = любой. */\n scope?: string\n}\n\n/** Фильтр по изменениям нативных системных таблиц. */\nexport interface NativeDeltaFilter {\n kind: 'native-delta'\n /** Тип нативной таблицы (permission, account, …). undefined = любая. */\n table?: NativeTableName\n}\n\n/** Фильтр по событиям форка (подписывается на все форки без уточнений). */\nexport interface ForkFilter {\n kind: 'fork'\n}\n\nexport type SubscriptionFilter<T extends Record<string, unknown> = Record<string, unknown>> =\n | ActionFilter<T>\n | DeltaFilter\n | NativeDeltaFilter\n | ForkFilter\n\n/**\n * Wildcard-совпадение: undefined и '*' совпадают с чем угодно,\n * иначе требуется точное строковое совпадение.\n */\nfunction matchesWildcard(value: string, pattern: string | undefined): boolean {\n if (pattern === undefined || pattern === '*') return true\n return value === pattern\n}\n\n/** Проверяет совпадение ActionEvent с ActionFilter. */\nfunction matchAction(event: ActionEvent, filter: ActionFilter): boolean {\n if (!matchesWildcard(event.account, filter.account)) return false\n if (!matchesWildcard(event.name, filter.name)) return false\n // Частичное совпадение по data: все указанные поля должны совпадать\n if (filter.data) {\n for (const [k, v] of Object.entries(filter.data)) {\n if (event.data[k] !== v) return false\n }\n }\n return true\n}\n\n/** Проверяет совпадение DeltaEvent с DeltaFilter. */\nfunction matchDelta(event: DeltaEvent, filter: DeltaFilter): boolean {\n if (!matchesWildcard(event.code, filter.code)) return false\n if (!matchesWildcard(event.table, filter.table)) return false\n if (!matchesWildcard(event.scope, filter.scope)) return false\n return true\n}\n\n/** Проверяет совпадение NativeDeltaEvent с NativeDeltaFilter. */\nfunction matchNativeDelta(event: NativeDeltaEvent, filter: NativeDeltaFilter): boolean {\n return matchesWildcard(event.table, filter.table)\n}\n\n/** Проверяет одно событие против одного фильтра. */\nfunction matchOne(event: ParserEvent, filter: SubscriptionFilter): boolean {\n // Быстрая проверка kind перед детальным сравнением\n if (event.kind !== filter.kind) return false\n if (filter.kind === 'action') return matchAction(event as ActionEvent, filter)\n if (filter.kind === 'delta') return matchDelta(event as DeltaEvent, filter)\n if (filter.kind === 'native-delta') return matchNativeDelta(event as NativeDeltaEvent, filter)\n if (filter.kind === 'fork') return true // ForkFilter — совпадает с любым ForkEvent\n const _exhaustive: never = filter\n return _exhaustive\n}\n\n/**\n * Проверяет событие против набора фильтров (OR-семантика).\n * @returns true если filters пустой/undefined (нет ограничений) или хотя бы один фильтр совпал.\n */\nexport function matchFilters(\n event: ParserEvent,\n filters: SubscriptionFilter[] | undefined,\n): boolean {\n if (!filters || filters.length === 0) return true\n return filters.some(f => matchOne(event, f))\n}\n","export { Parser } from './core/Parser.js'\nexport {\n ParserClient,\n SubscriptionLock,\n RedisConsumer,\n FailureTracker,\n matchFilters,\n CONSUMER_NAME,\n} from './client/index.js'\nexport type {\n ParserClientOptions,\n SubscriptionFilter,\n ActionFilter,\n DeltaFilter,\n NativeDeltaFilter,\n ForkFilter,\n LockState,\n SubscriptionLockOptions,\n} from './client/index.js'\nexport type { StreamMessage, XGroupInfo } from './ports/RedisStore.js'\nexport { BlockProcessor } from './core/BlockProcessor.js'\nexport { XtrimSupervisor } from './core/XtrimSupervisor.js'\nexport { WorkerPool } from './workers/WorkerPool.js'\nexport { IoRedisStore } from './adapters/IoRedisStore.js'\nexport { ShipReaderAdapter } from './adapters/ShipReaderAdapter.js'\nexport { computeEventId } from './events/eventId.js'\nexport { fromConfigFile, parseConfig } from './config/index.js'\nexport { RedisKeys } from './redis/keys.js'\nexport type { ParserOptions } from './config/index.js'\nexport type { ParserEvent, ActionEvent, DeltaEvent, NativeDeltaEvent, ForkEvent } from './types.js'\nexport type {\n NativeTableName,\n NativeRowTypeMap,\n NativePermissionRow,\n NativePermissionLinkRow,\n NativeAccountRow,\n NativeAccountMetadataRow,\n NativeCodeRow,\n NativeContractTableRow,\n NativeKeyValueRow,\n NativeReceivedBlockRow,\n NativeBlockInfoRow,\n NativeResourceLimitsRow,\n NativeResourceLimitsStateRow,\n NativeResourceLimitsConfigRow,\n NativeResourceUsageRow,\n NativeGlobalPropertyRow,\n NativeGeneratedTransactionRow,\n NativeProtocolStateRow,\n NativeFillStatusRow,\n} from '@coopenomics/coopos-ship-reader'\nexport { NATIVE_TABLE_NAMES, isNativeTableName } from '@coopenomics/coopos-ship-reader'\nexport type { ChainClient } from './ports/ChainClient.js'\nexport type { RedisStore } from './ports/RedisStore.js'\nexport {\n ConfigValidationError,\n ConfigSecurityError,\n ChainIdMismatchError,\n NotImplementedError,\n AbiNotFoundError,\n} from './errors.js'\nexport { AbiStore } from './abi/AbiStore.js'\nexport { AbiBootstrapper } from './abi/AbiBootstrapper.js'\nexport { ForkDetector } from './core/ForkDetector.js'\nexport { ReconnectSupervisor } from './core/ReconnectSupervisor.js'\nexport type { ReconnectSupervisorOptions } from './core/ReconnectSupervisor.js'\nexport { createLogger, rootLogger } from './logger.js'\nexport type { Logger, LoggerOptions } from './logger.js'\nexport { createParserMetrics, createClientMetrics } from './metrics/index.js'\nexport type { ParserMetrics, ClientMetrics } from './metrics/index.js'\nexport { HttpServer } from './observability/HttpServer.js'\nexport type { HttpServerOptions, HealthStatus } from './observability/HttpServer.js'\n","/**\n * Supervisor для автоматического переподключения с экспоненциальным backoff.\n *\n * Оборачивает произвольную async-функцию (например подключение к SHiP-ноде)\n * и повторяет её при ошибке с нарастающими паузами между попытками.\n *\n * Стратегия backoff: берём backoffSeconds[attempt-1], зажимая индекс\n * по длине массива (последнее значение используется для всех поздних попыток).\n * По умолчанию: [1, 2, 5, 15, 60] секунд.\n *\n * Если число попыток достигает maxAttempts — вызывается onGiveUp, который\n * по умолчанию пишет в stderr и вызывает process.exit(1).\n * В тестах onGiveUp можно заменить на throw чтобы не завершать процесс.\n */\n\n/** Задержки по умолчанию: 1→2→5→15→60→60→… секунд */\nconst DEFAULT_BACKOFF_SECONDS = [1, 2, 5, 15, 60]\nconst DEFAULT_MAX_ATTEMPTS = 10\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((r) => setTimeout(r, ms))\n}\n\nexport interface ReconnectSupervisorOptions {\n /** Максимальное число попыток до вызова onGiveUp. По умолчанию 10. */\n maxAttempts?: number\n /** Паузы между попытками в секундах. Последний элемент используется для всех поздних попыток. */\n backoffSeconds?: number[]\n /** Вызывается перед каждым повтором с номером попытки и паузой в мс. */\n onAttempt?: (attempt: number, delayMs: number) => void\n /** Вызывается при исчерпании всех попыток. По умолчанию пишет в stderr и process.exit(1). */\n onGiveUp?: (attempts: number) => void\n}\n\nexport class ReconnectSupervisor {\n private maxAttempts: number\n private backoffSeconds: number[]\n private onAttempt: (attempt: number, delayMs: number) => void\n private onGiveUp: (attempts: number) => void\n\n constructor(opts: ReconnectSupervisorOptions = {}) {\n this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS\n this.backoffSeconds = opts.backoffSeconds ?? DEFAULT_BACKOFF_SECONDS\n this.onAttempt = opts.onAttempt ?? (() => undefined)\n this.onGiveUp = opts.onGiveUp ?? ((n) => {\n process.stderr.write(`ReconnectSupervisor: exhausted ${n} attempts, exiting\\n`)\n process.exit(1)\n })\n }\n\n /**\n * Запускает fn и повторяет при исключении с паузами.\n *\n * Псевдокод:\n * loop:\n * try: return await fn()\n * catch: attempt++\n * if attempt >= maxAttempts: onGiveUp(); throw\n * delay = backoffSeconds[min(attempt-1, len-1)] * 1000\n * onAttempt(attempt, delay); sleep(delay)\n *\n * @returns Результат первого успешного вызова fn.\n */\n async run<T>(fn: () => Promise<T>): Promise<T> {\n let attempt = 0\n for (;;) {\n try {\n return await fn()\n } catch (err) {\n attempt++\n if (attempt >= this.maxAttempts) {\n this.onGiveUp(attempt)\n // onGiveUp ожидается вызывает process.exit; в тестах может бросать\n throw err\n }\n // Зажимаем индекс: если попыток больше чем элементов в массиве — используем последний\n const backoffIdx = Math.min(attempt - 1, this.backoffSeconds.length - 1)\n const delayMs = (this.backoffSeconds[backoffIdx] ?? 60) * 1000\n this.onAttempt(attempt, delayMs)\n await sleep(delayMs)\n }\n }\n }\n}\n","/**\n * Фабрика структурированного логгера на базе Pino.\n *\n * Возможности:\n * - JSON-формат (по умолчанию) — удобен для Loki, CloudWatch, ELK.\n * - pino-pretty — красивый вывод при NODE_ENV=development или pretty=true.\n * - Redaction: поля password/token/secret/authorization заменяются '[REDACTED]'.\n * - Корреляционное поле chain_id: все логи одного парсера помечены chain_id.\n *\n * Использование:\n * const log = createLogger({ level: 'debug', chain_id: 'eos-mainnet' })\n * log.info({ block_num: 400_000_000 }, 'Block processed')\n *\n * Дочерний логгер (наследует уровень и base):\n * const childLog = log.child({ component: 'BlockProcessor' })\n */\n\nimport pino from 'pino'\n\n/**\n * Пути для redaction чувствительных данных.\n * Pino заменит значения по этим путям на '[REDACTED]' перед записью в лог.\n * Паттерны *.password и т.д. покрывают вложенные объекты.\n */\nconst REDACT_PATHS = [\n 'password',\n 'token',\n 'secret',\n 'authorization',\n '*.password',\n '*.token',\n '*.secret',\n '*.authorization',\n 'redis.password',\n 'redis.url', // Redis URL может содержать пароль в строке подключения\n]\n\nexport type Logger = pino.Logger\n\nexport interface LoggerOptions {\n /** Минимальный уровень: 'trace'|'debug'|'info'|'warn'|'error'|'fatal'. По умолчанию 'info'. */\n level?: string\n /** Включить pino-pretty (цветной вывод). По умолчанию true при NODE_ENV=development. */\n pretty?: boolean\n /** Если задан — добавляется в base поля всех сообщений. */\n chain_id?: string\n}\n\n/**\n * Создаёт новый логгер с указанными параметрами.\n * Уровень можно переопределить через переменную окружения LOG_LEVEL.\n */\nexport function createLogger(opts: LoggerOptions = {}): Logger {\n const level = opts.level ?? (process.env['LOG_LEVEL'] ?? 'info')\n const pretty = opts.pretty ?? (process.env['NODE_ENV'] === 'development')\n\n // base — поля присутствующие в каждом log-сообщении\n const base: Record<string, string> = {}\n if (opts.chain_id) base['chain_id'] = opts.chain_id\n\n // transport: pino-pretty только если запрошен, иначе stdout (fd=1)\n const transport =\n pretty\n ? { target: 'pino-pretty', options: { colorize: true, translateTime: 'SYS:standard' } }\n : undefined\n\n const logger = pino(\n {\n level,\n redact: { paths: REDACT_PATHS, censor: '[REDACTED]' },\n base,\n },\n transport ? pino.transport(transport) : pino.destination(1),\n )\n\n return logger\n}\n\n/** Дефолтный корневой логгер без chain_id — для быстрого старта. */\nexport const rootLogger: Logger = createLogger()\n","/**\n * Prometheus-метрики для парсера (серверная сторона: SHiP → Redis).\n *\n * Все метрики регистрируются в изолированном Registry — не в default глобальном.\n * Это важно для тестов (каждый тест создаёт свой registry) и для случаев\n * когда парсер запускается вместе с другими Prometheus-экспортёрами в процессе.\n *\n * Использование: createParserMetrics() → объект с counter/gauge/histogram полями.\n * Parser главный класс передаёт их в BlockProcessor, XtrimSupervisor и HttpServer.\n *\n * Метрики можно наблюдать через GET /metrics (если health.enabled + metrics.enabled).\n */\n\nimport {\n Counter,\n Gauge,\n Histogram,\n Registry,\n} from 'prom-client'\n\n/**\n * Интерфейс парсерских метрик.\n * Хранится как поле Parser класса и передаётся в подкомпоненты.\n */\nexport interface ParserMetrics {\n readonly registry: Registry\n /** Счётчик обработанных блоков. Растёт монотонно. Используется для расчёта throughput. */\n blocksProcessedTotal: Counter\n /** Текущее отставание: (head_block_time - current_block_time) в секундах.\n * 0 = в реальном времени. Большие значения → парсер не успевает. */\n indexingLagSeconds: Gauge\n /** Счётчик опубликованных событий по видам (action/delta/native-delta/fork).\n * Позволяет видеть объём трафика каждого типа данных. */\n eventsPublishedTotal: Counter<'kind'>\n /** Счётчик попаданий в кэш ABI (worker pool нашёл ABI без запроса к Redis/Chain). */\n abiCacheHitsTotal: Counter\n /** Счётчик промахов кэша ABI (пришлось читать из Redis или запрашивать Chain API). */\n abiCacheMissesTotal: Counter\n /** Текущая длина Redis events stream. Растущее значение → XTRIM не справляется или отключён. */\n streamLength: Gauge\n /** Счётчик удалённых записей XTRIM. Помогает оценить объём хранимых данных. */\n xtrimmedEntriesTotal: Counter\n /** Гистограмма времени обработки одного блока (секунды).\n * Buckets: 1ms–5s. Всплески → медленный ABI декодинг или Redis перегружен. */\n blockProcessingDuration: Histogram\n /** Текущая глубина очереди Piscina worker pool.\n * Растущая очередь → worker pool не успевает за темпом блоков. */\n workerPoolQueueDepth: Gauge\n /** Счётчик ошибок обработки блоков (исключения в BlockProcessor).\n * В норме должен быть близок к 0. */\n blockProcessingErrors: Counter\n}\n\n/**\n * Создаёт набор парсерских метрик в изолированном Registry.\n *\n * @param prefix — префикс имён метрик. По умолчанию 'parser'.\n * Меняется в тестах и при запуске нескольких инстансов.\n */\nexport function createParserMetrics(prefix = 'parser'): ParserMetrics {\n const registry = new Registry()\n\n const blocksProcessedTotal = new Counter({\n name: `${prefix}_blocks_processed_total`,\n help: 'Total number of blocks processed by the parser',\n registers: [registry],\n })\n\n const indexingLagSeconds = new Gauge({\n name: `${prefix}_indexing_lag_seconds`,\n help: 'Lag between head block time and current block time in seconds',\n registers: [registry],\n })\n\n const eventsPublishedTotal = new Counter<'kind'>({\n name: `${prefix}_events_published_total`,\n help: 'Total events published to Redis stream',\n labelNames: ['kind'],\n registers: [registry],\n })\n\n const abiCacheHitsTotal = new Counter({\n name: `${prefix}_abi_cache_hits_total`,\n help: 'Number of ABI cache hits',\n registers: [registry],\n })\n\n const abiCacheMissesTotal = new Counter({\n name: `${prefix}_abi_cache_misses_total`,\n help: 'Number of ABI cache misses (fetched from chain)',\n registers: [registry],\n })\n\n const streamLength = new Gauge({\n name: `${prefix}_stream_length`,\n help: 'Current length of the Redis events stream',\n registers: [registry],\n })\n\n const xtrimmedEntriesTotal = new Counter({\n name: `${prefix}_xtrimmed_entries_total`,\n help: 'Total number of entries removed by XTRIM',\n registers: [registry],\n })\n\n // Buckets подобраны для типичного диапазона: 1ms (быстрый кэш) → 5s (медленный ABI fetch)\n const blockProcessingDuration = new Histogram({\n name: `${prefix}_block_processing_duration_seconds`,\n help: 'Duration of block processing in seconds',\n buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],\n registers: [registry],\n })\n\n const workerPoolQueueDepth = new Gauge({\n name: `${prefix}_worker_pool_queue_depth`,\n help: 'Current number of tasks queued in the Piscina worker pool',\n registers: [registry],\n })\n\n const blockProcessingErrors = new Counter({\n name: `${prefix}_block_processing_errors_total`,\n help: 'Total number of block processing errors',\n registers: [registry],\n })\n\n return {\n registry,\n blocksProcessedTotal,\n indexingLagSeconds,\n eventsPublishedTotal,\n abiCacheHitsTotal,\n abiCacheMissesTotal,\n streamLength,\n xtrimmedEntriesTotal,\n blockProcessingDuration,\n workerPoolQueueDepth,\n blockProcessingErrors,\n }\n}\n","/**\n * Prometheus-метрики для клиентской стороны парсера (ParserClient / RedisConsumer).\n *\n * Клиентские метрики отражают состояние подписок, а не самого парсера.\n * Каждая метрика имеет label sub_id — для разделения по подпискам в Grafana.\n *\n * Архитектурное решение: отдельный Registry от парсерских метрик позволяет:\n * - Запускать клиент и парсер в одном процессе без коллизий имён.\n * - Тестировать клиентские метрики изолированно.\n * - Скрейпить метрики клиента отдельным Prometheus job (если клиент — отдельный сервис).\n *\n * Использование: createClientMetrics() → передаётся в ParserClient конструктор.\n */\n\nimport {\n Counter,\n Gauge,\n Histogram,\n Registry,\n} from 'prom-client'\n\n/**\n * Интерфейс клиентских метрик.\n * Хранится в ParserClient и передаётся в RedisConsumer и FailureTracker.\n */\nexport interface ClientMetrics {\n readonly registry: Registry\n /** Счётчик ошибок в пользовательском handler по (sub_id, kind).\n * Растущее значение → handler падает, события могут уйти в dead-letter. */\n handlerErrorsTotal: Counter<'sub_id' | 'kind'>\n /** Гистограмма времени выполнения handler по (sub_id, kind).\n * Медленный handler блокирует потребление новых сообщений. */\n handlerDurationSeconds: Histogram<'sub_id' | 'kind'>\n /** Состояние distributed lock по (sub_id, role).\n * 1 = активный лидер, 0 = ожидание/acquiring.\n * Позволяет видеть в Grafana: сколько инстансов борются за лидерство. */\n subscriptionLockState: Gauge<'sub_id' | 'role'>\n /** Счётчик событий, упавших в dead-letter stream по sub_id.\n * Ненулевое значение требует внимания оператора (replay-dead-letter). */\n deadLettersTotal: Counter<'sub_id'>\n /** Счётчик прочитанных сообщений из стрима по sub_id (XREADGROUP).\n * Растёт при нормальной работе — отражает throughput потребления. */\n messagesConsumedTotal: Counter<'sub_id'>\n /** Счётчик подтверждённых сообщений (XACK) по sub_id.\n * Должен быть близок к messagesConsumedTotal. Большой разрыв → PEL копится. */\n messageAcknowledgedTotal: Counter<'sub_id'>\n /** Текущий размер PEL (pending entry list) по sub_id.\n * Растущий PEL → сообщения читаются но не подтверждаются (handler зависает или падает). */\n consumerPendingMessages: Gauge<'sub_id'>\n /** Счётчик событий прошедших через фильтр подписки по (sub_id, kind).\n * Позволяет оценить эффективность фильтрации: отношение к messagesConsumedTotal. */\n filterMatchesTotal: Counter<'sub_id' | 'kind'>\n}\n\n/**\n * Создаёт набор клиентских метрик в изолированном Registry.\n *\n * @param prefix — префикс имён метрик. По умолчанию 'parser_client'.\n */\nexport function createClientMetrics(prefix = 'parser_client'): ClientMetrics {\n const registry = new Registry()\n\n const handlerErrorsTotal = new Counter<'sub_id' | 'kind'>({\n name: `${prefix}_handler_errors_total`,\n help: 'Total errors thrown by subscription event handlers',\n labelNames: ['sub_id', 'kind'],\n registers: [registry],\n })\n\n // Buckets: 1ms–1s — типичный диапазон для обработчиков (DB запрос, HTTP call)\n const handlerDurationSeconds = new Histogram<'sub_id' | 'kind'>({\n name: `${prefix}_handler_duration_seconds`,\n help: 'Duration of subscription handler execution in seconds',\n labelNames: ['sub_id', 'kind'],\n buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1],\n registers: [registry],\n })\n\n const subscriptionLockState = new Gauge<'sub_id' | 'role'>({\n name: `${prefix}_subscription_lock_state`,\n help: 'Lock state gauge: 1=active, 0=standby/acquiring',\n labelNames: ['sub_id', 'role'],\n registers: [registry],\n })\n\n const deadLettersTotal = new Counter<'sub_id'>({\n name: `${prefix}_dead_letters_total`,\n help: 'Total messages routed to dead-letter stream',\n labelNames: ['sub_id'],\n registers: [registry],\n })\n\n const messagesConsumedTotal = new Counter<'sub_id'>({\n name: `${prefix}_messages_consumed_total`,\n help: 'Total messages read from the events stream',\n labelNames: ['sub_id'],\n registers: [registry],\n })\n\n const messageAcknowledgedTotal = new Counter<'sub_id'>({\n name: `${prefix}_messages_acknowledged_total`,\n help: 'Total messages acknowledged (XACK)',\n labelNames: ['sub_id'],\n registers: [registry],\n })\n\n const consumerPendingMessages = new Gauge<'sub_id'>({\n name: `${prefix}_consumer_pending_messages`,\n help: 'Current number of pending (unacknowledged) messages for this consumer',\n labelNames: ['sub_id'],\n registers: [registry],\n })\n\n const filterMatchesTotal = new Counter<'sub_id' | 'kind'>({\n name: `${prefix}_filter_matches_total`,\n help: 'Total events that matched subscription filters',\n labelNames: ['sub_id', 'kind'],\n registers: [registry],\n })\n\n return {\n registry,\n handlerErrorsTotal,\n handlerDurationSeconds,\n subscriptionLockState,\n deadLettersTotal,\n messagesConsumedTotal,\n messageAcknowledgedTotal,\n consumerPendingMessages,\n filterMatchesTotal,\n }\n}\n","/**\n * Минималистичный HTTP-сервер для операционной наблюдаемости.\n *\n * Эндпоинты:\n * GET /health → JSON { status, indexingLagSeconds, lagThresholdSeconds }\n * 200 OK если lag ≤ lagThresholdSeconds, 503 Service Unavailable иначе.\n * Полезно для Kubernetes liveness/readiness probe.\n *\n * GET /metrics → Prometheus text format (Content-Type: text/plain; version=0.0.4).\n * Использует переданный metricsRegistry — обычно парсерские метрики.\n *\n * getLag — callback который возвращает текущее отставание в секундах.\n * Вызывающий код (Parser) обновляет это значение после обработки каждого блока.\n *\n * Использование:\n * const server = new HttpServer({ port: 9090, getLag: () => lagGauge.value, metricsRegistry: reg })\n * await server.start()\n * // при shutdown:\n * await server.stop()\n */\n\nimport { createServer, type IncomingMessage, type ServerResponse } from 'node:http'\nimport type { Registry } from 'prom-client'\n\n/** Тело ответа /health. */\nexport interface HealthStatus {\n status: 'ok' | 'degraded'\n /** Текущее отставание в секундах на момент запроса. */\n indexingLagSeconds: number\n /** Порог, выше которого статус становится 'degraded'. */\n lagThresholdSeconds: number\n}\n\nexport interface HttpServerOptions {\n /** Порт HTTP-сервера. По умолчанию 9090. */\n port?: number\n /** Порог lag в секундах для статуса degraded. По умолчанию 60. */\n lagThresholdSeconds?: number\n /** Функция возвращающая актуальное значение отставания. */\n getLag: () => number\n /** Реестр Prometheus-метрик для /metrics эндпоинта. */\n metricsRegistry: Registry\n}\n\nexport class HttpServer {\n private server: ReturnType<typeof createServer>\n private port: number\n private lagThresholdSeconds: number\n private getLag: () => number\n private metricsRegistry: Registry\n\n constructor(opts: HttpServerOptions) {\n this.port = opts.port ?? 9090\n this.lagThresholdSeconds = opts.lagThresholdSeconds ?? 60\n this.getLag = opts.getLag\n this.metricsRegistry = opts.metricsRegistry\n this.server = createServer((req, res) => void this.handle(req, res))\n }\n\n /** Запускает HTTP-сервер, разрешает Promise после успешного bind. */\n start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server.listen(this.port, () => resolve())\n this.server.once('error', reject)\n })\n }\n\n /**\n * Останавливает сервер и ждёт закрытия всех соединений.\n * Вызывается при graceful shutdown парсера.\n */\n stop(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.server.close((err) => (err ? reject(err) : resolve()))\n })\n }\n\n /** Диспетчеризация HTTP-запросов по URL. */\n private async handle(req: IncomingMessage, res: ServerResponse): Promise<void> {\n const url = req.url ?? '/'\n\n if (url === '/health' || url === '/health/') {\n const lagSeconds = this.getLag()\n const degraded = lagSeconds > this.lagThresholdSeconds\n const body: HealthStatus = {\n status: degraded ? 'degraded' : 'ok',\n indexingLagSeconds: lagSeconds,\n lagThresholdSeconds: this.lagThresholdSeconds,\n }\n // 503 при деградации: Kubernetes readiness probe снимет pod из балансировщика\n res.writeHead(degraded ? 503 : 200, { 'Content-Type': 'application/json' })\n res.end(JSON.stringify(body))\n return\n }\n\n if (url === '/metrics' || url === '/metrics/') {\n try {\n const content = await this.metricsRegistry.metrics()\n // Content-Type включает версию формата: text/plain; version=0.0.4; charset=utf-8\n res.writeHead(200, { 'Content-Type': this.metricsRegistry.contentType })\n res.end(content)\n } catch (err) {\n res.writeHead(500)\n res.end('Internal error')\n }\n return\n }\n\n res.writeHead(404)\n res.end('Not found')\n }\n}\n"],"mappings":"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","names":["isNativeTableName","sleep","Counter","Gauge","Histogram","Registry","resolve"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@coopenomics/parser",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Universal EOSIO/Antelope SHiP-to-Redis blockchain indexer (parser)",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Coopenomics contributors",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/coopenomics/parser"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"bin": {
|
|
22
|
+
"parser": "./dist/cli/index.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"NOTICE"
|
|
28
|
+
],
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:unit": "vitest run test/unit",
|
|
37
|
+
"test:integration": "vitest run test/integration"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@coopenomics/coopos-ship-reader": "0.1.0",
|
|
41
|
+
"@wharfkit/antelope": "^1.2.0",
|
|
42
|
+
"ajv": "^8.17.1",
|
|
43
|
+
"commander": "^12.1.0",
|
|
44
|
+
"ioredis": "^5.4.1",
|
|
45
|
+
"p-queue": "^8.0.1",
|
|
46
|
+
"pino": "^9.2.0",
|
|
47
|
+
"pino-pretty": "^13.1.3",
|
|
48
|
+
"piscina": "^4.6.1",
|
|
49
|
+
"prom-client": "^15.1.3",
|
|
50
|
+
"yaml": "^2.4.5"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^20.14.0",
|
|
54
|
+
"@vitest/coverage-v8": "1.6.1",
|
|
55
|
+
"tsup": "^8.1.0",
|
|
56
|
+
"typescript": "^5.4.5",
|
|
57
|
+
"vitest": "^1.6.0"
|
|
58
|
+
},
|
|
59
|
+
"gitHead": "fd7ff5fcaaa67cac4c5ce3772608932128283324"
|
|
60
|
+
}
|