@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.
@@ -0,0 +1,1520 @@
1
+ import { ActionAuthorization, ActionReceipt, NativeTableName, GetBlocksOptions, ShipBlock, ChainInfo, ShipDelta, NativeDeltaEvent as NativeDeltaEvent$1 } from '@coopenomics/coopos-ship-reader';
2
+ export { NATIVE_TABLE_NAMES, NativeAccountMetadataRow, NativeAccountRow, NativeBlockInfoRow, NativeCodeRow, NativeContractTableRow, NativeFillStatusRow, NativeGeneratedTransactionRow, NativeGlobalPropertyRow, NativeKeyValueRow, NativePermissionLinkRow, NativePermissionRow, NativeProtocolStateRow, NativeReceivedBlockRow, NativeResourceLimitsConfigRow, NativeResourceLimitsRow, NativeResourceLimitsStateRow, NativeResourceUsageRow, NativeRowTypeMap, NativeTableName, isNativeTableName } from '@coopenomics/coopos-ship-reader';
3
+ import pino from 'pino';
4
+ import { Registry, Counter, Gauge, Histogram } from 'prom-client';
5
+
6
+ /**
7
+ * Загрузка и валидация конфигурации парсера.
8
+ *
9
+ * Поддерживаемые форматы: YAML файл (fromConfigFile) или уже разобранный объект (parseConfig).
10
+ *
11
+ * Конвейер обработки:
12
+ * 1. Чтение YAML → parseYaml → raw object.
13
+ * 2. interpolateDeep: рекурсивно заменяет ${VAR} → process.env[VAR].
14
+ * Если переменная не задана — оставляем плейсхолдер (не ломаем конфиг, но validate упадёт
15
+ * если это обязательное поле).
16
+ * 3. validate: проверяет обязательные поля (ship.url, redis.url) и enum-значения.
17
+ * 4. checkPlainSecrets: запрещает хардкодированные пароли в Redis URL.
18
+ * redis://:hardcoded-pass@host → ConfigSecurityError.
19
+ * redis://${REDIS_PASSWORD}@host → OK (это плейсхолдер, не секрет).
20
+ *
21
+ * Почему env-интерполяция важна: операторы хранят конфиг в git без секретов,
22
+ * инжектируя их через переменные среды в Kubernetes / Docker. Формат ${VAR} — стандарт.
23
+ */
24
+ /**
25
+ * Все настройки парсера в одном объекте.
26
+ * Передаётся в конструктор Parser и ParserClient.
27
+ * Все поля кроме ship и redis — опциональны (имеют дефолты в соответствующих модулях).
28
+ */
29
+ interface ParserOptions {
30
+ /** SHiP WebSocket соединение. timeoutMs по умолчанию 10000. */
31
+ ship: {
32
+ url: string;
33
+ timeoutMs?: number;
34
+ };
35
+ /** Chain API для ABI fallback (abiFallback: 'rpc-current'). Опционален. */
36
+ chain?: {
37
+ url?: string;
38
+ id?: string;
39
+ };
40
+ /** Redis подключение. keyPrefix добавляет namespace к ключам (полезно при shared Redis). */
41
+ redis: {
42
+ url: string;
43
+ password?: string;
44
+ keyPrefix?: string;
45
+ };
46
+ /** Piscina worker pool для десериализации. maxThreads по умолчанию = CPU count / 2. */
47
+ workerPool?: {
48
+ maxThreads?: number;
49
+ };
50
+ /** Поведение при отсутствии ABI: 'rpc-current' = попробовать Chain API, 'fail' = ошибка. */
51
+ abiFallback?: 'rpc-current' | 'fail';
52
+ /** XtrimSupervisor: интервал проверки и включение/отключение автообрезки стрима. */
53
+ xtrim?: {
54
+ intervalMs?: number;
55
+ enabled?: boolean;
56
+ };
57
+ /** ReconnectSupervisor: максимум попыток и backoff-таблица в секундах. */
58
+ reconnect?: {
59
+ maxAttempts?: number;
60
+ backoffSeconds?: number[];
61
+ };
62
+ /** Десериализатор ABI-данных. Единственный вариант — 'wharfkit'. */
63
+ deserializer?: 'wharfkit';
64
+ /** Pino logger настройки. pretty=true включает pino-pretty (для разработки). */
65
+ logger?: {
66
+ level?: string;
67
+ pretty?: boolean;
68
+ };
69
+ /** HTTP /health endpoint. Kubernetes liveness/readiness probe. */
70
+ health?: {
71
+ enabled?: boolean;
72
+ port?: number;
73
+ lagThresholdSeconds?: number;
74
+ };
75
+ /** HTTP /metrics endpoint для Prometheus. */
76
+ metrics?: {
77
+ enabled?: boolean;
78
+ port?: number;
79
+ };
80
+ /** Обрабатывать только irreversible блоки (block_num <= lastIrreversible). */
81
+ irreversibleOnly?: boolean;
82
+ /** Не устанавливать SIGTERM/SIGINT обработчики. Используется в тестах. */
83
+ noSignalHandlers?: boolean;
84
+ }
85
+ /**
86
+ * Парсит и валидирует конфиг из уже разобранного объекта (результат parseYaml или тест).
87
+ * Применяет env-интерполяцию, валидацию, проверку безопасности.
88
+ */
89
+ declare function parseConfig(raw: unknown): ParserOptions;
90
+ /**
91
+ * Читает YAML файл по пути и возвращает валидированные ParserOptions.
92
+ * Основная точка входа для CLI команд и пользовательского кода.
93
+ * Выбрасывает ConfigValidationError/ConfigSecurityError/Error при любых проблемах.
94
+ */
95
+ declare function fromConfigFile(filePath: string): ParserOptions;
96
+
97
+ declare class Parser {
98
+ private opts;
99
+ private chainClient;
100
+ private redis;
101
+ private workerPool;
102
+ private blockProcessor;
103
+ private xtrimSupervisor;
104
+ private running;
105
+ private stopSignal;
106
+ constructor(opts: ParserOptions);
107
+ static fromConfigFile(filePath: string): Parser;
108
+ static fromConfig(raw: unknown): Parser;
109
+ start(): Promise<void>;
110
+ private eventToFields;
111
+ stop(): Promise<void>;
112
+ get isRunning(): boolean;
113
+ private checkRedisPersistence;
114
+ }
115
+
116
+ /**
117
+ * Публичные типы событий, которые производит парсер и потребляет ParserClient.
118
+ *
119
+ * Каждое событие несёт `kind`-дискриминант для type-narrowing в switch/if,
120
+ * а также `event_id` — детерминированный идентификатор, вычисляемый из
121
+ * полей события (см. events/eventId.ts). Один и тот же event_id всегда
122
+ * означает одно и то же событие, что позволяет идемпотентно обрабатывать
123
+ * повторные доставки.
124
+ */
125
+
126
+ /** Событие вызова смарт-контракта (inline action). */
127
+ interface ActionEvent {
128
+ kind: 'action';
129
+ /** Уникальный детерминированный ID: chain:a:block_num:blockId[0..16]:global_sequence */
130
+ event_id: string;
131
+ chain_id: string;
132
+ block_num: number;
133
+ /** ISO-8601 время блока из трассировки транзакции. */
134
+ block_time: string;
135
+ block_id: string;
136
+ /** Аккаунт-владелец контракта (account). */
137
+ account: string;
138
+ /** Имя действия (action name). */
139
+ name: string;
140
+ authorization: ActionAuthorization[];
141
+ /** Декодированные ABI-поля действия. Пустой объект если ABI недоступен. */
142
+ data: Record<string, unknown>;
143
+ /** Порядковый номер действия внутри транзакции (1-based). */
144
+ action_ordinal: number;
145
+ /** Глобальная уникальная последовательность — монотонный счётчик действий в цепи. */
146
+ global_sequence: bigint;
147
+ /** Квитанция об исполнении; null если нет трассировки. */
148
+ receipt: ActionReceipt | null;
149
+ }
150
+ /** Изменение строки в пользовательской таблице смарт-контракта (contract_row delta). */
151
+ interface DeltaEvent {
152
+ kind: 'delta';
153
+ /** Уникальный ID: chain:d:block_num:blockId[0..16]:code:scope:table:primary_key */
154
+ event_id: string;
155
+ chain_id: string;
156
+ block_num: number;
157
+ block_time: string;
158
+ block_id: string;
159
+ /** Аккаунт контракта-владельца таблицы. */
160
+ code: string;
161
+ /** Скоуп: обычно аккаунт, с которым связана строка. */
162
+ scope: string;
163
+ table: string;
164
+ /** Первичный ключ строки (строковое представление). */
165
+ primary_key: string;
166
+ /** Декодированные ABI-поля строки. */
167
+ value: Record<string, unknown>;
168
+ /** true — строка создана/обновлена; false — удалена. */
169
+ present: boolean;
170
+ }
171
+ /**
172
+ * Изменение нативной (системной) строки SHiP-дельты — permission, account,
173
+ * resource_limits и другие типы из ship-reader/native-tables.
174
+ *
175
+ * Параметр T позволяет сузить тип данных: NativeDeltaEvent<NativePermissionRow>.
176
+ */
177
+ interface NativeDeltaEvent<T = Record<string, unknown>> {
178
+ kind: 'native-delta';
179
+ /** Уникальный ID: chain:n:block_num:blockId[0..16]:table:lookup_key */
180
+ event_id: string;
181
+ chain_id: string;
182
+ block_num: number;
183
+ block_time: string;
184
+ block_id: string;
185
+ /** Имя нативной таблицы, например 'permission', 'account', 'resource_limits'. */
186
+ table: string;
187
+ /** Натуральный первичный ключ строки (зависит от типа таблицы). */
188
+ lookup_key: string;
189
+ /** Десериализованные данные строки. */
190
+ data: T;
191
+ /** true — строка создана/обновлена; false — удалена. */
192
+ present: boolean;
193
+ }
194
+ /**
195
+ * Сигнал о микрофорке блокчейна.
196
+ *
197
+ * Публикуется первым в пакете событий того блока, где обнаружен форк.
198
+ * Потребитель должен откатить своё состояние для всех блоков > forked_from_block.
199
+ * Подробнее: docs/event-id-semantics.md.
200
+ */
201
+ interface ForkEvent {
202
+ kind: 'fork';
203
+ /** Уникальный ID: chain:f:forked_from_block:newHeadBlockId[0..16] */
204
+ event_id: string;
205
+ chain_id: string;
206
+ /** Последний безопасный блок (до него откатываться не нужно). */
207
+ forked_from_block: number;
208
+ /** Block ID нового head — отличается от предыдущей версии того же номера. */
209
+ new_head_block_id: string;
210
+ }
211
+ /** Дискриминантное объединение всех типов событий парсера. */
212
+ type ParserEvent = ActionEvent | DeltaEvent | NativeDeltaEvent | ForkEvent;
213
+
214
+ /**
215
+ * Фильтры подписки — декларативный DSL для выбора нужных событий.
216
+ *
217
+ * Потребитель задаёт массив SubscriptionFilter при создании ParserClient.
218
+ * matchFilters(event, filters) возвращает true если событие удовлетворяет
219
+ * хотя бы одному фильтру (OR-семантика).
220
+ *
221
+ * Значения полей фильтра:
222
+ * undefined / '*' — совпадает с любым значением (wildcard).
223
+ * строка — точное совпадение.
224
+ *
225
+ * Примеры фильтров:
226
+ * { kind: 'action', account: 'eosio.token', name: 'transfer' }
227
+ * { kind: 'delta', code: 'eosio', table: 'global' }
228
+ * { kind: 'native-delta', table: 'permission' }
229
+ * { kind: 'fork' }
230
+ */
231
+
232
+ /** Фильтр по транзакционным действиям. */
233
+ interface ActionFilter<T extends Record<string, unknown> = Record<string, unknown>> {
234
+ kind: 'action';
235
+ /** Аккаунт контракта. undefined = любой. */
236
+ account?: string;
237
+ /** Имя действия. undefined = любое. */
238
+ name?: string;
239
+ /** Частичное совпадение по полям data. */
240
+ data?: Partial<T>;
241
+ }
242
+ /** Фильтр по изменениям строк пользовательских таблиц. */
243
+ interface DeltaFilter {
244
+ kind: 'delta';
245
+ /** Аккаунт контракта. undefined = любой. */
246
+ code?: string;
247
+ /** Имя таблицы. undefined = любая. */
248
+ table?: string;
249
+ /** Скоуп. undefined = любой. */
250
+ scope?: string;
251
+ }
252
+ /** Фильтр по изменениям нативных системных таблиц. */
253
+ interface NativeDeltaFilter {
254
+ kind: 'native-delta';
255
+ /** Тип нативной таблицы (permission, account, …). undefined = любая. */
256
+ table?: NativeTableName;
257
+ }
258
+ /** Фильтр по событиям форка (подписывается на все форки без уточнений). */
259
+ interface ForkFilter {
260
+ kind: 'fork';
261
+ }
262
+ type SubscriptionFilter<T extends Record<string, unknown> = Record<string, unknown>> = ActionFilter<T> | DeltaFilter | NativeDeltaFilter | ForkFilter;
263
+ /**
264
+ * Проверяет событие против набора фильтров (OR-семантика).
265
+ * @returns true если filters пустой/undefined (нет ограничений) или хотя бы один фильтр совпал.
266
+ */
267
+ declare function matchFilters(event: ParserEvent, filters: SubscriptionFilter[] | undefined): boolean;
268
+
269
+ /**
270
+ * Клиент-потребитель событий парсера.
271
+ *
272
+ * ParserClient — главная точка входа для прикладного кода, который хочет
273
+ * получать события из блокчейна без прямого доступа к Redis или SHiP.
274
+ *
275
+ * Что делает:
276
+ * 1. Регистрирует подписку (метаданные) в Redis Hash (parser:subs).
277
+ * 2. Захватывает distributed lock (single-active-consumer) через SubscriptionLock.
278
+ * 3. Читает события из Redis Stream через RedisConsumer (XREADGROUP).
279
+ * 4. Применяет фильтры (matchFilters) — пропускает нерелевантные события.
280
+ * 5. Доставляет событие через yield.
281
+ * 6. После успешной обработки: XACK + clearFailure.
282
+ * 7. При ошибке в обработчике: инкрементирует счётчик (FailureTracker).
283
+ * При достижении порога (3 ошибки) — переводит в dead-letter stream.
284
+ *
285
+ * Использование:
286
+ * const client = new ParserClient({ … })
287
+ * for await (const event of client.stream()) {
288
+ * await myHandler(event)
289
+ * }
290
+ */
291
+
292
+ interface ParserClientOptions {
293
+ /** Уникальный идентификатор подписки (имя consumer group в Redis Stream). */
294
+ subscriptionId: string;
295
+ /** Список фильтров. Пустой = все события. */
296
+ filters?: SubscriptionFilter[];
297
+ /**
298
+ * Стартовая позиция потребления:
299
+ * 'last_known' — '$' (только новые события с момента регистрации)
300
+ * 'head-minus-1000' — приблизительно с 1000 блоков назад
301
+ * number — с указанного block_num (приблизительное преобразование в stream ID)
302
+ */
303
+ startFrom?: 'last_known' | number | 'head-minus-1000';
304
+ redis: {
305
+ url: string;
306
+ password?: string;
307
+ keyPrefix?: string;
308
+ };
309
+ chain: {
310
+ id: string;
311
+ };
312
+ /** Таймаут ожидания lock'а в мс (для тестов). */
313
+ acquireLockTimeoutMs?: number;
314
+ /** Отключить SIGTERM/SIGINT обработчики (для тестов и встроенного использования). */
315
+ noSignalHandlers?: boolean;
316
+ }
317
+ declare class ParserClient {
318
+ private opts;
319
+ private redis;
320
+ private lock;
321
+ private consumer;
322
+ private failureTracker;
323
+ /** Уникальный ID этого экземпляра — используется как значение distributed lock'а. */
324
+ private instanceId;
325
+ private closed;
326
+ constructor(opts: ParserClientOptions);
327
+ /**
328
+ * Основной AsyncGenerator: инициализирует подключение и начинает yield событий.
329
+ *
330
+ * Этапы старта:
331
+ * 1. Подключаемся к Redis.
332
+ * 2. Регистрируем подписку в HSET parser:subs.
333
+ * 3. Пытаемся захватить lock; если занят — ждём освобождения.
334
+ * 4. Определяем startId для consumer group.
335
+ * 5. Создаём consumer group (XGROUP CREATE).
336
+ * 6. Читаем события в цикле, фильтруем, yield'им.
337
+ *
338
+ * Генератор завершается при вызове close().
339
+ */
340
+ stream(): AsyncGenerator<ParserEvent>;
341
+ /**
342
+ * Graceful shutdown: останавливает генератор, освобождает lock, закрывает Redis.
343
+ */
344
+ close(): Promise<void>;
345
+ /** Закрывает клиент и завершает процесс (для использования в SIGINT-обработчике). */
346
+ closeAndExit(): void;
347
+ }
348
+
349
+ /**
350
+ * Порт хранилища Redis — абстракция над Redis-командами.
351
+ *
352
+ * Порт (интерфейс) изолирует бизнес-логику от конкретного Redis-клиента.
353
+ * Единственная реализация — IoRedisStore (adapters/IoRedisStore.ts).
354
+ * В тестах используются vi.fn()-моки, соответствующие этому интерфейсу.
355
+ *
356
+ * Именование методов следует camelCase-версии Redis-команд:
357
+ * XADD → xadd, ZRANGEBYSCORE → zrangeByScore и т.д.
358
+ */
359
+ /** Одно сообщение Redis Stream, возвращаемое XRANGE / XREADGROUP. */
360
+ interface StreamMessage {
361
+ /** Redis Stream entry ID в формате <milliseconds>-<sequence>. */
362
+ id: string;
363
+ /** Пары поле→значение записи. */
364
+ fields: Record<string, string>;
365
+ }
366
+ /** Информация о consumer group из XINFO GROUPS. */
367
+ interface XGroupInfo {
368
+ name: string;
369
+ /** Число сообщений в PEL (pending entries list) — доставлено, но не подтверждено. */
370
+ pending: number;
371
+ /** ID последнего доставленного сообщения. */
372
+ lastDeliveredId: string;
373
+ /** Отставание group от head стрима (null для Redis < 7.0). */
374
+ lag: number | null;
375
+ /** Число зарегистрированных consumers в group. */
376
+ consumers: number;
377
+ }
378
+ interface RedisStore {
379
+ /** XADD stream * field value … → возвращает присвоенный entry ID. */
380
+ xadd(stream: string, fields: Record<string, string>): Promise<string>;
381
+ /** XTRIM stream MINID minId — удаляет записи с ID < minId. */
382
+ xtrim(stream: string, minId: string): Promise<number>;
383
+ /**
384
+ * XGROUP CREATE stream group startId MKSTREAM
385
+ * Идемпотентен: BUSYGROUP ошибка поглощается — группа уже существует.
386
+ */
387
+ xgroupCreate(stream: string, group: string, startId: string): Promise<void>;
388
+ /** XGROUP SETID stream group id — перемещает позицию group. */
389
+ xgroupSetId(stream: string, group: string, id: string): Promise<void>;
390
+ /** XINFO GROUPS stream → список consumer groups с метриками. */
391
+ xinfoGroups(stream: string): Promise<XGroupInfo[]>;
392
+ /**
393
+ * XREADGROUP GROUP group consumer COUNT count BLOCK blockMs STREAMS stream id
394
+ * id='>' — читать новые; id='0' — читать PEL (pending, для recovery).
395
+ */
396
+ xreadGroup(stream: string, group: string, consumer: string, count: number, blockMs: number, id: string): Promise<StreamMessage[]>;
397
+ /** XRANGE stream start end COUNT count — диапазон по ID в прямом порядке. */
398
+ xrange(stream: string, start: string, end: string, count: number): Promise<StreamMessage[]>;
399
+ /** XREVRANGE stream end start COUNT count — диапазон в обратном порядке. */
400
+ xrevrange(stream: string, end: string, start: string, count: number): Promise<StreamMessage[]>;
401
+ /** XLEN stream — число записей в стриме. */
402
+ xlen(stream: string): Promise<number>;
403
+ /** XDEL stream id — удаляет запись по ID. */
404
+ xdel(stream: string, id: string): Promise<number>;
405
+ /** XACK stream group id — подтверждает обработку, убирает из PEL. */
406
+ xack(stream: string, group: string, id: string): Promise<void>;
407
+ /** ZADD key score member. */
408
+ zadd(key: string, score: number, member: string): Promise<void>;
409
+ /**
410
+ * ZREVRANGEBYSCORE key max min LIMIT 0 1
411
+ * Используется для поиска «последней версии ABI на момент блока N»:
412
+ * max=N, min='-inf', возвращает первый (наиболее свежий) элемент.
413
+ */
414
+ zrangeByscoreRev(key: string, max: string, min: string): Promise<string[]>;
415
+ /** ZRANGEBYSCORE key min max LIMIT 0 ∞. */
416
+ zrangeByScore(key: string, min: string, max: string): Promise<string[]>;
417
+ /** ZCOUNT key min max. */
418
+ zcount(key: string, min: string, max: string): Promise<number>;
419
+ /** ZREMRANGEBYSCORE key min max → число удалённых элементов. */
420
+ zremRangeByScore(key: string, min: string, max: string): Promise<number>;
421
+ /** ZCARD key — мощность множества. */
422
+ zcard(key: string): Promise<number>;
423
+ /** HSET key field1 value1 field2 value2 … */
424
+ hset(key: string, fields: Record<string, string>): Promise<void>;
425
+ /** HGET key field → null если поле или ключ не существует. */
426
+ hget(key: string, field: string): Promise<string | null>;
427
+ /** HGETALL key → пустой объект если ключ не существует. */
428
+ hgetAll(key: string): Promise<Record<string, string>>;
429
+ /** HINCRBY key field increment → новое значение. */
430
+ hincrby(key: string, field: string, increment: number): Promise<number>;
431
+ /** HDEL key field. */
432
+ hdel(key: string, field: string): Promise<void>;
433
+ /**
434
+ * SET key value NX PX pxMs — атомарный conditional set.
435
+ * Возвращает true если ключ был успешно создан (не существовал).
436
+ * Используется для захвата distributed lock'а.
437
+ */
438
+ setNx(key: string, value: string, pxMs: number): Promise<boolean>;
439
+ /**
440
+ * Lua-скрипт: PEXPIRE key ms IF GET(key) == value.
441
+ * Продлевает TTL lock'а только если мы — текущий владелец.
442
+ * Атомарность необходима: между GET и PEXPIRE не должно быть race condition.
443
+ */
444
+ pexpire(key: string, ms: number, value: string): Promise<boolean>;
445
+ /**
446
+ * Lua-скрипт: DEL key IF GET(key) == value.
447
+ * Удаляет lock только если мы — его владелец.
448
+ * Защищает от случайного сброса чужого lock'а (если наш TTL истёк).
449
+ */
450
+ luaDel(key: string, value: string): Promise<boolean>;
451
+ /** EXPIRE key seconds. */
452
+ expire(key: string, seconds: number): Promise<void>;
453
+ /**
454
+ * SCAN-итерация по паттерну: продолжает пока cursor != '0'.
455
+ * Возвращает полный список ключей, соответствующих pattern.
456
+ * @param count — подсказка Redis сколько ключей возвращать за итерацию (не гарантия).
457
+ */
458
+ scan(pattern: string, count?: number): Promise<string[]>;
459
+ /** Закрыть соединение с Redis. */
460
+ quit(): Promise<void>;
461
+ }
462
+
463
+ /**
464
+ * Distributed lock для single-active-consumer паттерна.
465
+ *
466
+ * Проблема: несколько экземпляров одного consumer-а запущены одновременно
467
+ * (горизонтальное масштабирование, rolling deploy). Обрабатывать события
468
+ * должен ровно один (active), остальные — в режиме ожидания (standby).
469
+ *
470
+ * Механизм:
471
+ * - Lock реализован как Redis String с TTL 10 секунд.
472
+ * - Значение = instanceId (hostname:pid:uuid) — уникально для каждого процесса.
473
+ * - Active-экземпляр продлевает TTL каждые 3 секунды через heartbeat.
474
+ * - Standby-экземпляры опрашивают каждые 500 мс попытку захватить lock.
475
+ * - Если active-экземпляр упал — его TTL истекает за ≤10 с, standby захватывает lock.
476
+ *
477
+ * Атомарные операции:
478
+ * - setNx: атомарный SET NX PX — захватывает lock.
479
+ * - pexpire (LUA): продлевает TTL только если мы — владелец (не перезаписывает чужой).
480
+ * - luaDel (LUA): удаляет lock только если мы — владелец.
481
+ */
482
+
483
+ type LockState = 'acquiring' | 'active' | 'standby' | 'released';
484
+ interface SubscriptionLockOptions {
485
+ redis: RedisStore;
486
+ /** Идентификатор подписки — определяет имя Redis-ключа. */
487
+ subId: string;
488
+ /** Уникальный ID этого экземпляра (hostname:pid:uuid). */
489
+ instanceId: string;
490
+ /** Интервал heartbeat в мс. По умолчанию 3000. */
491
+ heartbeatIntervalMs?: number;
492
+ /** Таймаут ожидания lock'а в мс. По умолчанию Infinity. */
493
+ acquireLockTimeoutMs?: number;
494
+ }
495
+ declare class SubscriptionLock {
496
+ private redis;
497
+ private key;
498
+ readonly instanceId: string;
499
+ private heartbeatMs;
500
+ private acquireTimeoutMs;
501
+ private heartbeatTimer;
502
+ private _state;
503
+ constructor(opts: SubscriptionLockOptions);
504
+ /** Текущее состояние: acquiring → active/standby → released. */
505
+ get state(): LockState;
506
+ /**
507
+ * Пробует захватить lock одним атомарным SET NX PX.
508
+ * @returns true если захватили (state = active), false если занят (state = standby).
509
+ */
510
+ acquire(): Promise<boolean>;
511
+ /**
512
+ * Блокирует текущий процесс до тех пор пока lock не освободится и мы его захватим.
513
+ * Опрашивает Redis каждые STANDBY_POLL_MS мс.
514
+ * @throws Error если acquireLockTimeoutMs истёк.
515
+ */
516
+ waitForPromotion(): Promise<void>;
517
+ /** Запускает периодическое продление TTL (heartbeat). */
518
+ private startHeartbeat;
519
+ /**
520
+ * Один шаг heartbeat: продлеваем TTL через LUA-скрипт.
521
+ * Если LUA вернул false — кто-то другой захватил lock (race condition после
522
+ * истечения нашего TTL). Переходим в standby.
523
+ */
524
+ private renewHeartbeat;
525
+ /** Останавливает heartbeat-таймер (без освобождения lock'а). */
526
+ stopHeartbeat(): void;
527
+ /**
528
+ * Освобождает lock: останавливает heartbeat, удаляет ключ через LUA (conditional).
529
+ * После вызова state = 'released'.
530
+ */
531
+ release(): Promise<void>;
532
+ }
533
+
534
+ /**
535
+ * Низкоуровневый Redis Stream consumer (XREADGROUP).
536
+ *
537
+ * Реализует two-phase чтение:
538
+ * Фаза 1 — Recovery: при старте сначала читаем PEL (pending entries list)
539
+ * с id='0'. Это сообщения, которые были доставлены в предыдущей сессии
540
+ * но не подтверждены (XACK). Перечитываем их чтобы не потерять.
541
+ *
542
+ * Фаза 2 — Normal read: читаем новые сообщения с id='>'.
543
+ * BLOCK blockMs: если новых нет — ждём (не-busyloop).
544
+ *
545
+ * Все consumer'ы в одной group читают один и тот же поток, но каждое сообщение
546
+ * доставляется ровно одному consumer'у (группа обеспечивает fan-out).
547
+ *
548
+ * Имя consumer'а фиксировано ('primary') — мы не используем конкурентное чтение
549
+ * внутри одной группы (это решается через single-active-consumer lock).
550
+ */
551
+
552
+ /** Фиксированное имя consumer'а внутри group. */
553
+ declare const CONSUMER_NAME = "primary";
554
+ interface RedisConsumerOptions {
555
+ redis: RedisStore;
556
+ stream: string;
557
+ groupName: string;
558
+ /** Время блокировки XREADGROUP в мс. 2000 = ждём 2 с перед следующей попыткой. */
559
+ blockMs?: number;
560
+ /** Максимум сообщений за один XREADGROUP вызов. */
561
+ count?: number;
562
+ }
563
+ declare class RedisConsumer {
564
+ private redis;
565
+ private stream;
566
+ private groupName;
567
+ private blockMs;
568
+ private count;
569
+ private stopped;
570
+ constructor(opts: RedisConsumerOptions);
571
+ /**
572
+ * XGROUP CREATE stream groupName startId MKSTREAM
573
+ * Создаёт group (или игнорирует если уже существует — BUSYGROUP).
574
+ * startId='$' — читать только новые; startId='0' — с самого начала.
575
+ */
576
+ init(startId?: string): Promise<void>;
577
+ /**
578
+ * XGROUP SETID — переставляет позицию group (для reset-subscription).
579
+ * Примечание: здесь используется xgroupCreate что идемпотентно;
580
+ * для точного SETID нужен xgroupSetId из RedisStore.
581
+ */
582
+ setStartId(id: string): Promise<void>;
583
+ /**
584
+ * Читает PEL (pending entries) с id='0': сообщения, доставленные но не подтверждённые.
585
+ * Вызывается при старте для recovery после крэша.
586
+ */
587
+ recoverOwnPending(): Promise<StreamMessage[]>;
588
+ /**
589
+ * Асинхронный генератор сообщений.
590
+ *
591
+ * Порядок:
592
+ * 1. Сначала отдаём все pending (незавершённые из предыдущей сессии).
593
+ * 2. Затем в бесконечном цикле читаем новые (BLOCK 2000 мс).
594
+ *
595
+ * Цикл прерывается при вызове stop().
596
+ * Каждое сообщение нужно подтвердить через ack(msg.id).
597
+ */
598
+ read(): AsyncGenerator<StreamMessage>;
599
+ /** XACK stream groupName id — подтверждает что сообщение обработано. */
600
+ ack(id: string): Promise<void>;
601
+ /** Сигнализирует генератору прекратить чтение (graceful stop). */
602
+ stop(): void;
603
+ }
604
+
605
+ /**
606
+ * Трекер ошибок обработки событий для конкретной подписки.
607
+ *
608
+ * Логика: consumer обрабатывает событие и иногда выбрасывает исключение.
609
+ * Вместо немедленного перевода в dead-letter даём несколько попыток (FAILURE_THRESHOLD = 3).
610
+ * После FAILURE_THRESHOLD провалов событие переводится в dead-letter stream.
611
+ *
612
+ * Счётчики хранятся в Redis Hash (parser:sub:<subId>:failures) с TTL 24 ч.
613
+ * TTL сбрасывается при каждом новом провале — чтобы не накапливать стали счётчики.
614
+ *
615
+ * Почему Redis, а не in-memory: при рестарте consumer'а незакрытые ошибки
616
+ * не теряются и события всё равно попадут в dead-letter при следующей попытке.
617
+ */
618
+
619
+ declare class FailureTracker {
620
+ private redis;
621
+ private chainId;
622
+ constructor(redis: RedisStore, chainId: string);
623
+ /**
624
+ * Инкрементирует счётчик ошибок для eventId и продлевает TTL хэша.
625
+ * @returns Новое значение счётчика (1, 2, 3, …).
626
+ */
627
+ recordFailure(subId: string, eventId: string): Promise<number>;
628
+ /**
629
+ * Возвращает текущий счётчик ошибок для eventId.
630
+ * 0 если счётчик не существует (событие ещё не проваливалось).
631
+ */
632
+ getFailureCount(subId: string, eventId: string): Promise<number>;
633
+ /**
634
+ * Проверяет достиг ли счётчик порога для dead-letter.
635
+ * Вызывается после recordFailure: if (shouldDeadLetter(count)) { … }
636
+ */
637
+ shouldDeadLetter(count: number): boolean;
638
+ /**
639
+ * Записывает событие в dead-letter stream с метаданными об ошибке.
640
+ * Dead-letter stream: ce:parser:<chainId>:dead:<subId>
641
+ * Поля записи: data (оригинальный payload), failureCount, lastError, subId.
642
+ */
643
+ routeToDeadLetter(subId: string, eventId: string,
644
+ /** Оригинальные fields из StreamMessage (включая поле 'data'). */
645
+ payload: Record<string, string>, lastError: string): Promise<void>;
646
+ /**
647
+ * Сбрасывает счётчик ошибок для eventId после успешной обработки.
648
+ * Вызывается после успешного yield события в ParserClient.
649
+ */
650
+ clearFailure(subId: string, eventId: string): Promise<void>;
651
+ }
652
+
653
+ /**
654
+ * Пул worker-потоков для CPU-интенсивной ABI-десериализации.
655
+ *
656
+ * Использует Piscina — высокопроизводительный worker pool для Node.js.
657
+ * Каждый worker держит собственный in-memory ABI-кэш, поэтому повторные
658
+ * задания с одним и тем же abiJson не перепарсивают его.
659
+ */
660
+ interface DeserializeTask {
661
+ /** Сырые байты action data или table row для декодирования. */
662
+ rawBinary: Uint8Array;
663
+ /** JSON-представление ABI нужного контракта. */
664
+ abiJson: string;
665
+ contract: string;
666
+ /** Имя типа в ABI для декодирования. */
667
+ typeName: string;
668
+ kind: 'action' | 'delta';
669
+ }
670
+ declare class WorkerPool {
671
+ private pool;
672
+ /**
673
+ * @param maxThreads — максимум параллельных worker-потоков.
674
+ * Оптимум зависит от числа CPU и IO-нагрузки. Дефолт 2 подходит
675
+ * для большинства серверов; 4+ потока ускоряют плотные блоки (много actions).
676
+ */
677
+ constructor(maxThreads?: number);
678
+ /**
679
+ * Запускает десериализацию в одном из свободных worker-потоков.
680
+ * Блокирует промис пока worker не вернёт результат.
681
+ */
682
+ run(task: DeserializeTask): Promise<Record<string, unknown>>;
683
+ /**
684
+ * Доля занятых потоков от общего числа (0..1).
685
+ * Полезно для метрики parser_worker_pool_queue_depth.
686
+ */
687
+ get utilization(): number;
688
+ /** Завершает все worker-потоки (вызывается при остановке парсера). */
689
+ destroy(): Promise<void>;
690
+ }
691
+
692
+ /**
693
+ * Порт блокчейн-клиента — абстракция над SHiP WebSocket-соединением.
694
+ *
695
+ * Позволяет подменять реализацию в тестах (mock) не затрагивая
696
+ * бизнес-логику BlockProcessor и Parser.
697
+ * Единственная реализация — ShipReaderAdapter (adapters/ShipReaderAdapter.ts).
698
+ */
699
+
700
+ interface ChainClient {
701
+ /**
702
+ * Устанавливает WebSocket-соединение с SHiP-нодой и выполняет рукопожатие.
703
+ * @returns chainId — hex-идентификатор блокчейна из genesis.json.
704
+ */
705
+ connect(): Promise<{
706
+ chainId: string;
707
+ }>;
708
+ /**
709
+ * Возвращает AsyncIterable блоков начиная с позиции opts.startBlock.
710
+ * Метод блокирующий по своей природе — итерация останавливается только
711
+ * при закрытии соединения или брейке цикла.
712
+ */
713
+ streamBlocks(opts: GetBlocksOptions): AsyncIterable<ShipBlock>;
714
+ /**
715
+ * Отправляет ACK-подтверждение SHiP-ноде: «я обработал n блоков, присылай следующие».
716
+ * Без ACK нода прекратит отправку новых блоков (flow control).
717
+ */
718
+ ack(n: number): void;
719
+ /** Закрывает WebSocket-соединение. */
720
+ close(): Promise<void>;
721
+ /** Запрашивает chain_id и last_irreversible через chain RPC (get_info). */
722
+ getChainInfo(): Promise<ChainInfo>;
723
+ /**
724
+ * Загружает сырые байты ABI контракта через chain RPC (get_raw_abi).
725
+ * Вызывается только при первом появлении контракта (bootstrap), дальше
726
+ * ABI берётся из Redis-кэша (AbiStore).
727
+ */
728
+ getRawAbi(contract: string): Promise<Uint8Array>;
729
+ /**
730
+ * Десериализует нативную SHiP-дельту (permission, account и т.д.)
731
+ * в типизированный объект.
732
+ * Делегируется ship-reader'у, который знает структуру нативных таблиц.
733
+ */
734
+ deserializeNativeDelta(delta: ShipDelta): NativeDeltaEvent$1;
735
+ }
736
+
737
+ /**
738
+ * Хранилище версий ABI в Redis Sorted Set.
739
+ *
740
+ * Проблема: ABI контракта меняется в блоке eosio::setabi. Чтобы корректно
741
+ * декодировать действия и дельты исторических блоков, нужна версия ABI,
742
+ * актуальная именно на момент этого блока.
743
+ *
744
+ * Решение: каждая версия ABI хранится как member в ZSET с score=block_num.
745
+ * Для получения ABI на блок N делается ZREVRANGEBYSCORE key N -inf LIMIT 0 1 —
746
+ * это возвращает самую позднюю версию, появившуюся не позже блока N.
747
+ */
748
+
749
+ declare class AbiStore {
750
+ private readonly redis;
751
+ constructor(redis: RedisStore);
752
+ /**
753
+ * Ищет версию ABI контракта, актуальную на момент blockNum.
754
+ * Использует ZREVRANGEBYSCORE: возвращает последнюю запись со score ≤ blockNum.
755
+ * @returns Байты ABI или null если история пуста для данного контракта.
756
+ */
757
+ getAbi(contract: string, blockNum: number): Promise<Uint8Array | null>;
758
+ /**
759
+ * Сохраняет новую версию ABI, привязывая её к blockNum.
760
+ * Вызывается AbiBootstrapper при первом наблюдении контракта и
761
+ * BlockProcessor'ом при перехвате eosio::setabi / account-дельты.
762
+ */
763
+ storeAbi(contract: string, blockNum: number, abiBytes: Uint8Array): Promise<void>;
764
+ }
765
+
766
+ /**
767
+ * Загрузчик первичного ABI для неизвестных контрактов.
768
+ *
769
+ * Жизненный цикл ABI:
770
+ * 1. Первая встреча с контрактом (observedContracts не содержит его):
771
+ * → смотрим в AbiStore (Redis ZSET).
772
+ * → если не найден — загружаем через chain RPC (getRawAbi) и сохраняем.
773
+ * 2. Все последующие встречи: просто читаем из AbiStore (Redis cache hit).
774
+ * 3. Runtime-обновления ABI (eosio::setabi или account-дельта) выполняются
775
+ * непосредственно в BlockProcessor и обходят этот класс.
776
+ *
777
+ * observedContracts — in-memory Set для оптимизации: если контракт уже
778
+ * проходил через этот экземпляр, гарантированно был инициализирован в Redis.
779
+ */
780
+
781
+ declare class AbiBootstrapper {
782
+ private readonly chainClient;
783
+ private readonly abiStore;
784
+ /** Контракты, ABI которых уже гарантированно записан в Redis в этом сеансе. */
785
+ private readonly observedContracts;
786
+ private readonly abiFallback;
787
+ constructor(chainClient: ChainClient, abiStore: AbiStore, opts?: {
788
+ abiFallback?: 'rpc-current' | 'fail';
789
+ });
790
+ /**
791
+ * Гарантирует наличие ABI для контракта в кэше перед декодированием события.
792
+ *
793
+ * Быстрый путь: контракт уже в observedContracts → сразу идём в Redis.
794
+ * Медленный путь: первая встреча → проверяем Redis → если пусто, скачиваем с RPC.
795
+ *
796
+ * @param contract — имя аккаунта-контракта (например 'eosio.token').
797
+ * @param blockNum — номер блока, для которого нужна ABI (для поиска версии).
798
+ * @returns Байты ABI или null если ABI недоступен и abiFallback='rpc-current'.
799
+ * @throws AbiNotFoundError если abiFallback='fail' и ABI не найден.
800
+ */
801
+ ensureAbi(contract: string, blockNum: number): Promise<Uint8Array | null>;
802
+ }
803
+
804
+ /**
805
+ * Обработчик одного блока SHiP → список ParserEvent.
806
+ *
807
+ * Архитектура:
808
+ * BlockProcessor получает ShipBlock и возвращает массив событий в порядке:
809
+ * [ActionEvents..., DeltaEvents..., NativeDeltaEvents...]
810
+ *
811
+ * Три фазы обработки:
812
+ * 1. Traces (транзакционные трассировки) → ActionEvent[]
813
+ * - Для каждого trace: получить ABI → десериализовать action data в worker'е.
814
+ * - Особый случай eosio::setabi: извлекаем новый ABI и сохраняем в AbiStore.
815
+ *
816
+ * 2. Deltas → DeltaEvent[] + NativeDeltaEvent[]
817
+ * - account-дельта: содержит обновлённый ABI контракта → сохраняем в AbiStore.
818
+ * - contract_row: строка пользовательской таблицы → DeltaEvent (ABI-декодирование).
819
+ * - нативные таблицы (isNativeTableName): NativeDeltaEvent через chainClient.
820
+ *
821
+ * p-queue с concurrency=1 гарантирует последовательную обработку блоков:
822
+ * блок N+1 не начнёт обрабатываться пока не завершится блок N.
823
+ */
824
+
825
+ interface BlockProcessorOptions {
826
+ /** Идентификатор цепи — проставляется в каждое событие. */
827
+ chainId: string;
828
+ /** Пул потоков для CPU-интенсивной ABI-десериализации. */
829
+ workerPool: WorkerPool;
830
+ /** Загрузчик/кэш ABI: обеспечивает ABI перед каждым декодированием. */
831
+ abiBootstrapper: AbiBootstrapper;
832
+ /** Прямой доступ к Redis-кэшу ABI для runtime-обновлений (setabi). */
833
+ abiStore: AbiStore;
834
+ /** Блокчейн-клиент для десериализации нативных дельт. */
835
+ chainClient: ChainClient;
836
+ }
837
+ declare class BlockProcessor {
838
+ /** Очередь с concurrency=1: только один блок обрабатывается одновременно. */
839
+ private queue;
840
+ private chainId;
841
+ private workerPool;
842
+ private abiBootstrapper;
843
+ private abiStore;
844
+ private chainClient;
845
+ constructor(opts: BlockProcessorOptions);
846
+ /**
847
+ * Ставит блок в очередь на обработку.
848
+ * Возвращает Promise который резолвится когда этот конкретный блок обработан.
849
+ * Блоки обрабатываются строго последовательно (concurrency=1).
850
+ */
851
+ process(block: ShipBlock): Promise<ParserEvent[]>;
852
+ private processBlock;
853
+ /** Число заданий, ожидающих обработки (в очереди + текущее). */
854
+ get pendingCount(): number;
855
+ /** Ждёт завершения всех задач в очереди (вызывается при graceful shutdown). */
856
+ onIdle(): Promise<void>;
857
+ }
858
+
859
+ /**
860
+ * Фоновый supervisor для периодической очистки основного стрима (XTRIM).
861
+ *
862
+ * Проблема: события XADD добавляются непрерывно, и без очистки стрим
863
+ * будет расти бесконечно, занимая память Redis.
864
+ *
865
+ * Стратегия MINID:
866
+ * Вместо хранения фиксированного числа записей (MAXLEN), мы сохраняем все
867
+ * записи, которые ещё не подтверждены (pending) хотя бы одной consumer group.
868
+ * minId = min(lastDeliveredId всех групп с pending > 0).
869
+ * XTRIM stream MINID minId удаляет всё с ID < minId.
870
+ *
871
+ * Это гарантирует, что ни один consumer не потеряет сообщения при trim:
872
+ * группа с отставанием «тормозит» trim, пока не догонит.
873
+ *
874
+ * Таймер unref'ится чтобы не мешать graceful shutdown Node.js процесса.
875
+ */
876
+
877
+ interface XtrimSupervisorOpts {
878
+ redis: RedisStore;
879
+ /** Имя стрима для очистки (обычно ce:parser:<chainId>:events). */
880
+ stream: string;
881
+ /** Интервал между trim-циклами в мс. По умолчанию 60 000 (1 минута). */
882
+ intervalMs?: number;
883
+ }
884
+ declare class XtrimSupervisor {
885
+ private timer;
886
+ private readonly redis;
887
+ private readonly stream;
888
+ private readonly intervalMs;
889
+ constructor(opts: XtrimSupervisorOpts);
890
+ /** Запускает периодический trim. Идемпотентен — повторный вызов игнорируется. */
891
+ start(): void;
892
+ /** Останавливает trim-цикл (вызывается при graceful shutdown). */
893
+ stop(): void;
894
+ /**
895
+ * Один цикл очистки:
896
+ * 1. Получаем список consumer groups через XINFO GROUPS.
897
+ * 2. Фильтруем группы у которых есть pending сообщения (pending > 0).
898
+ * 3. Находим минимальный lastDeliveredId среди таких групп.
899
+ * 4. XTRIM stream MINID minId — удаляем всё старее этого ID.
900
+ *
901
+ * Если pending-групп нет — trim не делается (всё подтверждено).
902
+ * Если стрим не существует или XInfo бросает — тихо игнорируем (best-effort).
903
+ */
904
+ private trim;
905
+ }
906
+
907
+ /**
908
+ * Адаптер Redis — реализует интерфейс RedisStore через ioredis.
909
+ *
910
+ * Ioredis не имеет поля "exports" в package.json, поэтому для NodeNext
911
+ * resolution используется динамический import() с явным приведением типа.
912
+ *
913
+ * Два Lua-скрипта реализуют атомарные операции для distributed lock:
914
+ *
915
+ * PEXPIRE_LUA — условное продление TTL:
916
+ * «Продли TTL ключа key на ms миллисекунд, но только если его текущее
917
+ * значение равно value (т.е. мы — владельцы lock'а)».
918
+ * Атомарность важна: без неё возможен race condition между GET и PEXPIRE.
919
+ *
920
+ * DEL_LUA — условное удаление:
921
+ * «Удали ключ key, но только если его текущее значение равно value».
922
+ * Защищает от случайного удаления lock'а другого процесса, если наш TTL истёк.
923
+ */
924
+
925
+ /**
926
+ * Минимальный интерфейс ioredis-клиента с только нужными командами.
927
+ * Сигнатуры точно соответствуют тому что возвращает ioredis — без обёрток.
928
+ */
929
+ interface IRedisClient {
930
+ connect(): Promise<void>;
931
+ xadd(stream: string, id: string, ...args: string[]): Promise<string | null>;
932
+ xtrim(stream: string, strategy: string, threshold: string): Promise<number>;
933
+ xgroup(action: string, stream: string, group: string, id: string, mkstream?: string): Promise<unknown>;
934
+ xinfo(subcommand: string, key: string): Promise<unknown>;
935
+ xreadgroup(group: string, groupName: string, consumerName: string, count: string, countVal: number, block: string, blockMs: number, streams: string, stream: string, id: string): Promise<Array<[string, Array<[string, string[]]>]> | null>;
936
+ xrange(key: string, start: string, end: string, count: string, countVal: number): Promise<Array<[string, string[]]>>;
937
+ xrevrange(key: string, end: string, start: string, count: string, countVal: number): Promise<Array<[string, string[]]>>;
938
+ xlen(key: string): Promise<number>;
939
+ xdel(key: string, ...ids: string[]): Promise<number>;
940
+ xack(stream: string, group: string, id: string): Promise<number>;
941
+ zadd(key: string, score: number, member: string): Promise<number>;
942
+ zrangebyscore(key: string, min: string, max: string, limit: string, offset: number, count: number): Promise<string[]>;
943
+ zrevrangebyscore(key: string, max: string, min: string, limit: string, offset: number, count: number): Promise<string[]>;
944
+ zcount(key: string, min: string, max: string): Promise<number>;
945
+ zremrangebyscore(key: string, min: string, max: string): Promise<number>;
946
+ zcard(key: string): Promise<number>;
947
+ hset(key: string, ...args: string[]): Promise<number>;
948
+ hget(key: string, field: string): Promise<string | null>;
949
+ hgetall(key: string): Promise<Record<string, string> | null>;
950
+ hincrby(key: string, field: string, increment: number): Promise<number>;
951
+ hdel(key: string, ...fields: string[]): Promise<number>;
952
+ set(key: string, value: string, nx: string, px: string, ms: number): Promise<string | null>;
953
+ eval(script: string, numkeys: number, ...args: string[]): Promise<unknown>;
954
+ expire(key: string, seconds: number): Promise<number>;
955
+ scan(cursor: string, match: string, pattern: string, count: string, countVal: number): Promise<[string, string[]]>;
956
+ quit(): Promise<string>;
957
+ }
958
+ declare class IoRedisStore implements RedisStore {
959
+ /** Прямой доступ к ioredis-клиенту (для тестов и расширения). */
960
+ readonly client: IRedisClient;
961
+ constructor(opts: {
962
+ url: string;
963
+ password?: string;
964
+ keyPrefix?: string;
965
+ });
966
+ /** Явное подключение — вызывается один раз при старте Parser/ParserClient. */
967
+ connect(): Promise<void>;
968
+ /** XADD stream * field1 val1 … — возвращает присвоенный entry ID. */
969
+ xadd(stream: string, fields: Record<string, string>): Promise<string>;
970
+ /** XTRIM stream MINID minId — удаляет записи с ID < minId. */
971
+ xtrim(stream: string, minId: string): Promise<number>;
972
+ /**
973
+ * XGROUP CREATE stream group startId MKSTREAM
974
+ * MKSTREAM: создаёт стрим если не существует.
975
+ * BUSYGROUP: группа уже существует — это нормально, поглощаем ошибку.
976
+ */
977
+ xgroupCreate(stream: string, group: string, startId: string): Promise<void>;
978
+ /** XGROUP SETID stream group id — переставляет позицию group в стриме. */
979
+ xgroupSetId(stream: string, group: string, id: string): Promise<void>;
980
+ /** XINFO GROUPS stream → список consumer groups с метриками. */
981
+ xinfoGroups(stream: string): Promise<XGroupInfo[]>;
982
+ /**
983
+ * XREADGROUP GROUP group consumer COUNT count BLOCK blockMs STREAMS stream id
984
+ * id='>' — только новые сообщения.
985
+ * id='0' — PEL (pending): уже доставленные, но не подтверждённые (recovery).
986
+ */
987
+ xreadGroup(stream: string, group: string, consumer: string, count: number, blockMs: number, id: string): Promise<StreamMessage[]>;
988
+ /** XRANGE stream start end COUNT count. */
989
+ xrange(stream: string, start: string, end: string, count: number): Promise<StreamMessage[]>;
990
+ /** XREVRANGE stream end start COUNT count. */
991
+ xrevrange(stream: string, end: string, start: string, count: number): Promise<StreamMessage[]>;
992
+ /** XLEN stream. */
993
+ xlen(stream: string): Promise<number>;
994
+ /** XDEL stream id — удаляет запись по ID. */
995
+ xdel(stream: string, id: string): Promise<number>;
996
+ /** XACK stream group id — убирает из PEL. */
997
+ xack(stream: string, group: string, id: string): Promise<void>;
998
+ /** ZADD key score member. */
999
+ zadd(key: string, score: number, member: string): Promise<void>;
1000
+ /**
1001
+ * ZREVRANGEBYSCORE key max min LIMIT 0 1
1002
+ * Возвращает максимум один элемент с score ≤ max.
1003
+ * Используется для поиска ABI: «последняя версия не позже блока N».
1004
+ */
1005
+ zrangeByscoreRev(key: string, max: string, min: string): Promise<string[]>;
1006
+ /** ZRANGEBYSCORE key min max LIMIT 0 9999999 — все элементы в диапазоне. */
1007
+ zrangeByScore(key: string, min: string, max: string): Promise<string[]>;
1008
+ /** ZCOUNT key min max. */
1009
+ zcount(key: string, min: string, max: string): Promise<number>;
1010
+ /** ZREMRANGEBYSCORE key min max → число удалённых. */
1011
+ zremRangeByScore(key: string, min: string, max: string): Promise<number>;
1012
+ /** ZCARD key. */
1013
+ zcard(key: string): Promise<number>;
1014
+ /** HSET key field1 val1 field2 val2 … */
1015
+ hset(key: string, fields: Record<string, string>): Promise<void>;
1016
+ /** HGET key field. */
1017
+ hget(key: string, field: string): Promise<string | null>;
1018
+ /** HGETALL key → пустой объект если ключ не существует (ioredis возвращает null). */
1019
+ hgetAll(key: string): Promise<Record<string, string>>;
1020
+ /** HINCRBY key field increment → новое значение счётчика. */
1021
+ hincrby(key: string, field: string, increment: number): Promise<number>;
1022
+ /** HDEL key field. */
1023
+ hdel(key: string, field: string): Promise<void>;
1024
+ /**
1025
+ * SET key value NX PX pxMs
1026
+ * NX: только если не существует. PX: TTL в миллисекундах.
1027
+ * Используется для захвата distributed lock'а.
1028
+ */
1029
+ setNx(key: string, value: string, pxMs: number): Promise<boolean>;
1030
+ /**
1031
+ * Выполняет PEXPIRE_LUA: продлевает TTL lock'а только если мы — владелец.
1032
+ * Возвращает true если продление прошло успешно.
1033
+ */
1034
+ pexpire(key: string, ms: number, value: string): Promise<boolean>;
1035
+ /**
1036
+ * Выполняет DEL_LUA: удаляет lock только если мы — владелец.
1037
+ * Возвращает true если удаление прошло успешно.
1038
+ */
1039
+ luaDel(key: string, value: string): Promise<boolean>;
1040
+ /** EXPIRE key seconds. */
1041
+ expire(key: string, seconds: number): Promise<void>;
1042
+ /**
1043
+ * Полный SCAN по паттерну: итерирует cursor пока не вернётся '0'.
1044
+ * @param count — подсказка Redis сколько ключей возвращать за итерацию.
1045
+ * @returns Полный список ключей (может быть большим для широких паттернов).
1046
+ */
1047
+ scan(pattern: string, count?: number): Promise<string[]>;
1048
+ /** Закрывает соединение с Redis. */
1049
+ quit(): Promise<void>;
1050
+ }
1051
+
1052
+ /**
1053
+ * Адаптер SHiP-клиента — реализует порт ChainClient через ShipClient из ship-reader.
1054
+ *
1055
+ * Роль: изолирует бизнес-логику (BlockProcessor, Parser) от деталей
1056
+ * WebSocket-протокола SHiP и специфики @coopenomics/coopos-ship-reader.
1057
+ *
1058
+ * Жизненный цикл:
1059
+ * 1. new ShipReaderAdapter(opts) — конструируем клиент
1060
+ * 2. connect() — WebSocket handshake, получаем chainId
1061
+ * 3. streamBlocks(opts) — начинаем принимать блоки
1062
+ * 4. ack(1) — после каждого блока подтверждаем получение
1063
+ * 5. close() — завершаем соединение
1064
+ */
1065
+
1066
+ declare class ShipReaderAdapter implements ChainClient {
1067
+ private client;
1068
+ /**
1069
+ * @param opts.url — WebSocket URL SHiP-ноды, например ws://localhost:29999.
1070
+ * @param opts.timeoutMs — таймаут WebSocket-подключения.
1071
+ * @param opts.chainUrl — HTTP URL chain API (для getRawAbi / getChainInfo).
1072
+ */
1073
+ constructor(opts: {
1074
+ url: string;
1075
+ timeoutMs?: number;
1076
+ chainUrl?: string;
1077
+ });
1078
+ /**
1079
+ * Устанавливает WebSocket-соединение и выполняет SHiP-рукопожатие.
1080
+ * Рукопожатие возвращает chainId — hex-хэш genesis.json.
1081
+ */
1082
+ connect(): Promise<{
1083
+ chainId: string;
1084
+ }>;
1085
+ /** Начинает асинхронный поток блоков от указанной позиции. */
1086
+ streamBlocks(opts: GetBlocksOptions): AsyncIterable<ShipBlock>;
1087
+ /**
1088
+ * ACK n блоков: сигнализирует SHiP-ноде что мы готовы принять ещё.
1089
+ * SHiP использует оконное управление потоком: без ACK нода замолчит.
1090
+ */
1091
+ ack(n: number): void;
1092
+ /** Закрывает WebSocket. */
1093
+ close(): Promise<void>;
1094
+ /** GET /v1/chain/get_info — возвращает head block, last irreversible и т.д. */
1095
+ getChainInfo(): Promise<ChainInfo>;
1096
+ /** GET /v1/chain/get_raw_abi — загружает сырые байты ABI для bootstrap. */
1097
+ getRawAbi(contract: string): Promise<Uint8Array>;
1098
+ /**
1099
+ * Десериализует нативную SHiP-дельту в типизированный объект.
1100
+ * Делегируется встроенному deserializer'у ship-reader, который
1101
+ * содержит hardcoded-схемы нативных таблиц (permission, account, …).
1102
+ */
1103
+ deserializeNativeDelta(delta: ShipDelta): NativeDeltaEvent$1;
1104
+ }
1105
+
1106
+ /**
1107
+ * Детерминированные идентификаторы событий.
1108
+ *
1109
+ * event_id — строка, однозначно идентифицирующая событие без обращения к базе данных.
1110
+ * Свойства:
1111
+ * - Детерминированный: одни и те же входные данные → один и тот же ID.
1112
+ * - Fork-safe: события из параллельных форков одного блока отличаются
1113
+ * первыми 16 hex-символами block_id.
1114
+ * - Stateless: вычисляется в worker-потоке без каких-либо side effects.
1115
+ *
1116
+ * Форматы по типам (подробнее: docs/event-id-semantics.md):
1117
+ * action: chain:a:block_num:blockId[0..16]:global_sequence
1118
+ * delta: chain:d:block_num:blockId[0..16]:code:scope:table:primary_key
1119
+ * native-delta: chain:n:block_num:blockId[0..16]:table:lookup_key
1120
+ * fork: chain:f:forked_from_block:newHeadId[0..16]
1121
+ */
1122
+
1123
+ type ActionWithoutId = Omit<ActionEvent, 'event_id'>;
1124
+ type DeltaWithoutId = Omit<DeltaEvent, 'event_id'>;
1125
+ type NativeDeltaWithoutId = Omit<NativeDeltaEvent, 'event_id'>;
1126
+ type ForkWithoutId = Omit<ForkEvent, 'event_id'>;
1127
+ /** Объединение всех типов событий до присвоения event_id. */
1128
+ type EventWithoutId = ActionWithoutId | DeltaWithoutId | NativeDeltaWithoutId | ForkWithoutId;
1129
+ /**
1130
+ * Вычисляет event_id по полям события (без поля event_id).
1131
+ *
1132
+ * Принимает событие без event_id, чтобы исключить возможность рекурсии.
1133
+ * Параметр blockId обрезается до 16 символов: это первые 8 байт, содержащих
1134
+ * номер блока, что делает ID читаемым, но достаточным для уникальности в рамках форка.
1135
+ */
1136
+ declare function computeEventId(event: EventWithoutId): string;
1137
+
1138
+ /**
1139
+ * Единый реестр Redis-ключей.
1140
+ *
1141
+ * Все ключи определены в одном месте, чтобы избежать опечаток и упростить
1142
+ * поиск по кодовой базе. Полная документация формата — docs/redis-key-taxonomy.md.
1143
+ *
1144
+ * Префиксы:
1145
+ * ce:parser:<chainId>: — Stream-ключи, относящиеся к конкретной цепи.
1146
+ * parser: — Hash/ZSET/String ключи с глобальным скоупом.
1147
+ */
1148
+ declare const RedisKeys: {
1149
+ /**
1150
+ * Главный поток событий парсера (unified event stream).
1151
+ * Тип Redis: Stream. Тримируется XtrimSupervisor'ом.
1152
+ * Пример: ce:parser:eos-mainnet:events
1153
+ */
1154
+ readonly eventsStream: (chainId: string) => string;
1155
+ /**
1156
+ * Dead-letter поток для конкретной подписки.
1157
+ * Содержит сообщения, которые не смог обработать consumer после N попыток.
1158
+ * Тип Redis: Stream.
1159
+ * Пример: ce:parser:eos-mainnet:dead:verifier
1160
+ */
1161
+ readonly deadLetterStream: (chainId: string, subId: string) => string;
1162
+ /**
1163
+ * Поток для задания on-demand reparse (зарезервировано для будущего).
1164
+ * Тип Redis: Stream.
1165
+ */
1166
+ readonly reparseStream: (chainId: string, jobId: string) => string;
1167
+ /**
1168
+ * История версий ABI конкретного контракта.
1169
+ * Тип Redis: Sorted Set. Score = block_num, member = base64(rawAbiBytes).
1170
+ * При поиске ABI для блока N используется ZREVRANGEBYSCORE … N -inf LIMIT 0 1.
1171
+ * Пример: parser:abi:eosio.token
1172
+ */
1173
+ readonly abiZset: (contract: string) => string;
1174
+ /**
1175
+ * Контрольная точка синхронизации парсера (crash-recovery).
1176
+ * Тип Redis: Hash. Поля: block_num, block_id, last_updated.
1177
+ * При рестарте парсер читает отсюда позицию и продолжает с неё.
1178
+ */
1179
+ readonly syncHash: (chainId: string) => string;
1180
+ /**
1181
+ * Реестр всех зарегистрированных подписок.
1182
+ * Тип Redis: Hash. Ключ поля = subId, значение = JSON-метаданные подписки.
1183
+ */
1184
+ readonly subsHash: () => string;
1185
+ /**
1186
+ * Счётчики ошибок per-event для конкретной подписки.
1187
+ * Тип Redis: Hash. Ключ поля = event_id, значение = число провалов.
1188
+ * TTL: 24 часа (обновляется при каждом новом провале).
1189
+ * Используется FailureTracker для решения о переводе в dead-letter.
1190
+ */
1191
+ readonly subFailuresHash: (subId: string) => string;
1192
+ /**
1193
+ * Блокировка single-active-consumer для подписки.
1194
+ * Тип Redis: String (instanceId держателя блокировки). TTL: 10 с (автопродление).
1195
+ * Только один экземпляр consumer-а может быть active; остальные — standby.
1196
+ */
1197
+ readonly subLock: (subId: string) => string;
1198
+ /**
1199
+ * Метаданные задания reparse (зарезервировано для будущего).
1200
+ * Тип Redis: Hash.
1201
+ */
1202
+ readonly reparseJobHash: (jobId: string) => string;
1203
+ };
1204
+
1205
+ /**
1206
+ * Доменные ошибки пакета.
1207
+ *
1208
+ * Каждый класс расширяет Error и устанавливает `name`, чтобы стек-трейсы
1209
+ * содержали читаемое имя вместо просто "Error".
1210
+ */
1211
+ /** Конфигурационный YAML не прошёл структурную валидацию. */
1212
+ declare class ConfigValidationError extends Error {
1213
+ readonly cause?: unknown;
1214
+ constructor(message: string, cause?: unknown);
1215
+ }
1216
+ /**
1217
+ * Секреты обнаружены прямо в конфигурационном файле (например пароль Redis
1218
+ * захардкожен в URL). Правильный способ — переменные окружения ${VAR}.
1219
+ */
1220
+ declare class ConfigSecurityError extends Error {
1221
+ constructor(message: string);
1222
+ }
1223
+ /** Метод интерфейса объявлен, но не реализован в данном адаптере. */
1224
+ declare class NotImplementedError extends Error {
1225
+ constructor(method: string);
1226
+ }
1227
+ /**
1228
+ * Chain ID в конфиге не совпал с реальным ID цепи, полученным из SHiP-рукопожатия.
1229
+ * Защищает от случайного подключения к неверной ноде.
1230
+ */
1231
+ declare class ChainIdMismatchError extends Error {
1232
+ constructor(expected: string, actual: string);
1233
+ }
1234
+ /**
1235
+ * ABI для указанного контракта не найден ни в Redis-кэше, ни по RPC,
1236
+ * а конфигурация abiFallback='fail' запрещает продолжение без него.
1237
+ */
1238
+ declare class AbiNotFoundError extends Error {
1239
+ constructor(contract: string, blockNum: number, abiFallback: string);
1240
+ }
1241
+
1242
+ /**
1243
+ * Детектор микрофорков блокчейна.
1244
+ *
1245
+ * Алгоритм прост: SHiP нода отправляет блоки последовательно.
1246
+ * При нормальной работе номер каждого следующего блока строго больше предыдущего.
1247
+ * Если пришёл блок с номером ≤ последнему обработанному — произошёл форк.
1248
+ *
1249
+ * При обнаружении форка:
1250
+ * 1. Создаём ForkEvent с forked_from_block = lastBlockNum.
1251
+ * 2. Устанавливаем lastBlockNum = новый blockNum (форкнутая ветка теперь актуальна).
1252
+ * 3. Возвращаем ForkEvent — он будет опубликован первым в пакете событий блока.
1253
+ *
1254
+ * Потребители должны откатить своё состояние для всех блоков > forked_from_block.
1255
+ * Паттерн отката: docs/disaster-recovery.md → Scenario 4.
1256
+ */
1257
+
1258
+ declare class ForkDetector {
1259
+ /** -1 означает «ещё не видели ни одного блока». */
1260
+ private lastBlockNum;
1261
+ private chainId;
1262
+ constructor(chainId: string);
1263
+ /**
1264
+ * Проверяет, является ли входящий блок форком.
1265
+ * Должен вызываться один раз перед обработкой каждого блока.
1266
+ *
1267
+ * @returns ForkEvent если обнаружен форк, null при нормальной последовательности.
1268
+ */
1269
+ check(blockNum: number, blockId: string): ForkEvent | null;
1270
+ /**
1271
+ * Сбрасывает историю (вызывается при переподключении к SHiP-ноде,
1272
+ * чтобы не ложно детектировать форк при старте с произвольного блока).
1273
+ */
1274
+ reset(): void;
1275
+ }
1276
+
1277
+ /**
1278
+ * Supervisor для автоматического переподключения с экспоненциальным backoff.
1279
+ *
1280
+ * Оборачивает произвольную async-функцию (например подключение к SHiP-ноде)
1281
+ * и повторяет её при ошибке с нарастающими паузами между попытками.
1282
+ *
1283
+ * Стратегия backoff: берём backoffSeconds[attempt-1], зажимая индекс
1284
+ * по длине массива (последнее значение используется для всех поздних попыток).
1285
+ * По умолчанию: [1, 2, 5, 15, 60] секунд.
1286
+ *
1287
+ * Если число попыток достигает maxAttempts — вызывается onGiveUp, который
1288
+ * по умолчанию пишет в stderr и вызывает process.exit(1).
1289
+ * В тестах onGiveUp можно заменить на throw чтобы не завершать процесс.
1290
+ */
1291
+ interface ReconnectSupervisorOptions {
1292
+ /** Максимальное число попыток до вызова onGiveUp. По умолчанию 10. */
1293
+ maxAttempts?: number;
1294
+ /** Паузы между попытками в секундах. Последний элемент используется для всех поздних попыток. */
1295
+ backoffSeconds?: number[];
1296
+ /** Вызывается перед каждым повтором с номером попытки и паузой в мс. */
1297
+ onAttempt?: (attempt: number, delayMs: number) => void;
1298
+ /** Вызывается при исчерпании всех попыток. По умолчанию пишет в stderr и process.exit(1). */
1299
+ onGiveUp?: (attempts: number) => void;
1300
+ }
1301
+ declare class ReconnectSupervisor {
1302
+ private maxAttempts;
1303
+ private backoffSeconds;
1304
+ private onAttempt;
1305
+ private onGiveUp;
1306
+ constructor(opts?: ReconnectSupervisorOptions);
1307
+ /**
1308
+ * Запускает fn и повторяет при исключении с паузами.
1309
+ *
1310
+ * Псевдокод:
1311
+ * loop:
1312
+ * try: return await fn()
1313
+ * catch: attempt++
1314
+ * if attempt >= maxAttempts: onGiveUp(); throw
1315
+ * delay = backoffSeconds[min(attempt-1, len-1)] * 1000
1316
+ * onAttempt(attempt, delay); sleep(delay)
1317
+ *
1318
+ * @returns Результат первого успешного вызова fn.
1319
+ */
1320
+ run<T>(fn: () => Promise<T>): Promise<T>;
1321
+ }
1322
+
1323
+ /**
1324
+ * Фабрика структурированного логгера на базе Pino.
1325
+ *
1326
+ * Возможности:
1327
+ * - JSON-формат (по умолчанию) — удобен для Loki, CloudWatch, ELK.
1328
+ * - pino-pretty — красивый вывод при NODE_ENV=development или pretty=true.
1329
+ * - Redaction: поля password/token/secret/authorization заменяются '[REDACTED]'.
1330
+ * - Корреляционное поле chain_id: все логи одного парсера помечены chain_id.
1331
+ *
1332
+ * Использование:
1333
+ * const log = createLogger({ level: 'debug', chain_id: 'eos-mainnet' })
1334
+ * log.info({ block_num: 400_000_000 }, 'Block processed')
1335
+ *
1336
+ * Дочерний логгер (наследует уровень и base):
1337
+ * const childLog = log.child({ component: 'BlockProcessor' })
1338
+ */
1339
+
1340
+ type Logger = pino.Logger;
1341
+ interface LoggerOptions {
1342
+ /** Минимальный уровень: 'trace'|'debug'|'info'|'warn'|'error'|'fatal'. По умолчанию 'info'. */
1343
+ level?: string;
1344
+ /** Включить pino-pretty (цветной вывод). По умолчанию true при NODE_ENV=development. */
1345
+ pretty?: boolean;
1346
+ /** Если задан — добавляется в base поля всех сообщений. */
1347
+ chain_id?: string;
1348
+ }
1349
+ /**
1350
+ * Создаёт новый логгер с указанными параметрами.
1351
+ * Уровень можно переопределить через переменную окружения LOG_LEVEL.
1352
+ */
1353
+ declare function createLogger(opts?: LoggerOptions): Logger;
1354
+ /** Дефолтный корневой логгер без chain_id — для быстрого старта. */
1355
+ declare const rootLogger: Logger;
1356
+
1357
+ /**
1358
+ * Prometheus-метрики для парсера (серверная сторона: SHiP → Redis).
1359
+ *
1360
+ * Все метрики регистрируются в изолированном Registry — не в default глобальном.
1361
+ * Это важно для тестов (каждый тест создаёт свой registry) и для случаев
1362
+ * когда парсер запускается вместе с другими Prometheus-экспортёрами в процессе.
1363
+ *
1364
+ * Использование: createParserMetrics() → объект с counter/gauge/histogram полями.
1365
+ * Parser главный класс передаёт их в BlockProcessor, XtrimSupervisor и HttpServer.
1366
+ *
1367
+ * Метрики можно наблюдать через GET /metrics (если health.enabled + metrics.enabled).
1368
+ */
1369
+
1370
+ /**
1371
+ * Интерфейс парсерских метрик.
1372
+ * Хранится как поле Parser класса и передаётся в подкомпоненты.
1373
+ */
1374
+ interface ParserMetrics {
1375
+ readonly registry: Registry;
1376
+ /** Счётчик обработанных блоков. Растёт монотонно. Используется для расчёта throughput. */
1377
+ blocksProcessedTotal: Counter;
1378
+ /** Текущее отставание: (head_block_time - current_block_time) в секундах.
1379
+ * 0 = в реальном времени. Большие значения → парсер не успевает. */
1380
+ indexingLagSeconds: Gauge;
1381
+ /** Счётчик опубликованных событий по видам (action/delta/native-delta/fork).
1382
+ * Позволяет видеть объём трафика каждого типа данных. */
1383
+ eventsPublishedTotal: Counter<'kind'>;
1384
+ /** Счётчик попаданий в кэш ABI (worker pool нашёл ABI без запроса к Redis/Chain). */
1385
+ abiCacheHitsTotal: Counter;
1386
+ /** Счётчик промахов кэша ABI (пришлось читать из Redis или запрашивать Chain API). */
1387
+ abiCacheMissesTotal: Counter;
1388
+ /** Текущая длина Redis events stream. Растущее значение → XTRIM не справляется или отключён. */
1389
+ streamLength: Gauge;
1390
+ /** Счётчик удалённых записей XTRIM. Помогает оценить объём хранимых данных. */
1391
+ xtrimmedEntriesTotal: Counter;
1392
+ /** Гистограмма времени обработки одного блока (секунды).
1393
+ * Buckets: 1ms–5s. Всплески → медленный ABI декодинг или Redis перегружен. */
1394
+ blockProcessingDuration: Histogram;
1395
+ /** Текущая глубина очереди Piscina worker pool.
1396
+ * Растущая очередь → worker pool не успевает за темпом блоков. */
1397
+ workerPoolQueueDepth: Gauge;
1398
+ /** Счётчик ошибок обработки блоков (исключения в BlockProcessor).
1399
+ * В норме должен быть близок к 0. */
1400
+ blockProcessingErrors: Counter;
1401
+ }
1402
+ /**
1403
+ * Создаёт набор парсерских метрик в изолированном Registry.
1404
+ *
1405
+ * @param prefix — префикс имён метрик. По умолчанию 'parser'.
1406
+ * Меняется в тестах и при запуске нескольких инстансов.
1407
+ */
1408
+ declare function createParserMetrics(prefix?: string): ParserMetrics;
1409
+
1410
+ /**
1411
+ * Prometheus-метрики для клиентской стороны парсера (ParserClient / RedisConsumer).
1412
+ *
1413
+ * Клиентские метрики отражают состояние подписок, а не самого парсера.
1414
+ * Каждая метрика имеет label sub_id — для разделения по подпискам в Grafana.
1415
+ *
1416
+ * Архитектурное решение: отдельный Registry от парсерских метрик позволяет:
1417
+ * - Запускать клиент и парсер в одном процессе без коллизий имён.
1418
+ * - Тестировать клиентские метрики изолированно.
1419
+ * - Скрейпить метрики клиента отдельным Prometheus job (если клиент — отдельный сервис).
1420
+ *
1421
+ * Использование: createClientMetrics() → передаётся в ParserClient конструктор.
1422
+ */
1423
+
1424
+ /**
1425
+ * Интерфейс клиентских метрик.
1426
+ * Хранится в ParserClient и передаётся в RedisConsumer и FailureTracker.
1427
+ */
1428
+ interface ClientMetrics {
1429
+ readonly registry: Registry;
1430
+ /** Счётчик ошибок в пользовательском handler по (sub_id, kind).
1431
+ * Растущее значение → handler падает, события могут уйти в dead-letter. */
1432
+ handlerErrorsTotal: Counter<'sub_id' | 'kind'>;
1433
+ /** Гистограмма времени выполнения handler по (sub_id, kind).
1434
+ * Медленный handler блокирует потребление новых сообщений. */
1435
+ handlerDurationSeconds: Histogram<'sub_id' | 'kind'>;
1436
+ /** Состояние distributed lock по (sub_id, role).
1437
+ * 1 = активный лидер, 0 = ожидание/acquiring.
1438
+ * Позволяет видеть в Grafana: сколько инстансов борются за лидерство. */
1439
+ subscriptionLockState: Gauge<'sub_id' | 'role'>;
1440
+ /** Счётчик событий, упавших в dead-letter stream по sub_id.
1441
+ * Ненулевое значение требует внимания оператора (replay-dead-letter). */
1442
+ deadLettersTotal: Counter<'sub_id'>;
1443
+ /** Счётчик прочитанных сообщений из стрима по sub_id (XREADGROUP).
1444
+ * Растёт при нормальной работе — отражает throughput потребления. */
1445
+ messagesConsumedTotal: Counter<'sub_id'>;
1446
+ /** Счётчик подтверждённых сообщений (XACK) по sub_id.
1447
+ * Должен быть близок к messagesConsumedTotal. Большой разрыв → PEL копится. */
1448
+ messageAcknowledgedTotal: Counter<'sub_id'>;
1449
+ /** Текущий размер PEL (pending entry list) по sub_id.
1450
+ * Растущий PEL → сообщения читаются но не подтверждаются (handler зависает или падает). */
1451
+ consumerPendingMessages: Gauge<'sub_id'>;
1452
+ /** Счётчик событий прошедших через фильтр подписки по (sub_id, kind).
1453
+ * Позволяет оценить эффективность фильтрации: отношение к messagesConsumedTotal. */
1454
+ filterMatchesTotal: Counter<'sub_id' | 'kind'>;
1455
+ }
1456
+ /**
1457
+ * Создаёт набор клиентских метрик в изолированном Registry.
1458
+ *
1459
+ * @param prefix — префикс имён метрик. По умолчанию 'parser_client'.
1460
+ */
1461
+ declare function createClientMetrics(prefix?: string): ClientMetrics;
1462
+
1463
+ /**
1464
+ * Минималистичный HTTP-сервер для операционной наблюдаемости.
1465
+ *
1466
+ * Эндпоинты:
1467
+ * GET /health → JSON { status, indexingLagSeconds, lagThresholdSeconds }
1468
+ * 200 OK если lag ≤ lagThresholdSeconds, 503 Service Unavailable иначе.
1469
+ * Полезно для Kubernetes liveness/readiness probe.
1470
+ *
1471
+ * GET /metrics → Prometheus text format (Content-Type: text/plain; version=0.0.4).
1472
+ * Использует переданный metricsRegistry — обычно парсерские метрики.
1473
+ *
1474
+ * getLag — callback который возвращает текущее отставание в секундах.
1475
+ * Вызывающий код (Parser) обновляет это значение после обработки каждого блока.
1476
+ *
1477
+ * Использование:
1478
+ * const server = new HttpServer({ port: 9090, getLag: () => lagGauge.value, metricsRegistry: reg })
1479
+ * await server.start()
1480
+ * // при shutdown:
1481
+ * await server.stop()
1482
+ */
1483
+
1484
+ /** Тело ответа /health. */
1485
+ interface HealthStatus {
1486
+ status: 'ok' | 'degraded';
1487
+ /** Текущее отставание в секундах на момент запроса. */
1488
+ indexingLagSeconds: number;
1489
+ /** Порог, выше которого статус становится 'degraded'. */
1490
+ lagThresholdSeconds: number;
1491
+ }
1492
+ interface HttpServerOptions {
1493
+ /** Порт HTTP-сервера. По умолчанию 9090. */
1494
+ port?: number;
1495
+ /** Порог lag в секундах для статуса degraded. По умолчанию 60. */
1496
+ lagThresholdSeconds?: number;
1497
+ /** Функция возвращающая актуальное значение отставания. */
1498
+ getLag: () => number;
1499
+ /** Реестр Prometheus-метрик для /metrics эндпоинта. */
1500
+ metricsRegistry: Registry;
1501
+ }
1502
+ declare class HttpServer {
1503
+ private server;
1504
+ private port;
1505
+ private lagThresholdSeconds;
1506
+ private getLag;
1507
+ private metricsRegistry;
1508
+ constructor(opts: HttpServerOptions);
1509
+ /** Запускает HTTP-сервер, разрешает Promise после успешного bind. */
1510
+ start(): Promise<void>;
1511
+ /**
1512
+ * Останавливает сервер и ждёт закрытия всех соединений.
1513
+ * Вызывается при graceful shutdown парсера.
1514
+ */
1515
+ stop(): Promise<void>;
1516
+ /** Диспетчеризация HTTP-запросов по URL. */
1517
+ private handle;
1518
+ }
1519
+
1520
+ export { AbiBootstrapper, AbiNotFoundError, AbiStore, type ActionEvent, type ActionFilter, BlockProcessor, CONSUMER_NAME, type ChainClient, ChainIdMismatchError, type ClientMetrics, ConfigSecurityError, ConfigValidationError, type DeltaEvent, type DeltaFilter, FailureTracker, ForkDetector, type ForkEvent, type ForkFilter, type HealthStatus, HttpServer, type HttpServerOptions, IoRedisStore, type LockState, type Logger, type LoggerOptions, type NativeDeltaEvent, type NativeDeltaFilter, NotImplementedError, Parser, ParserClient, type ParserClientOptions, type ParserEvent, type ParserMetrics, type ParserOptions, ReconnectSupervisor, type ReconnectSupervisorOptions, RedisConsumer, RedisKeys, type RedisStore, ShipReaderAdapter, type StreamMessage, type SubscriptionFilter, SubscriptionLock, type SubscriptionLockOptions, WorkerPool, type XGroupInfo, XtrimSupervisor, computeEventId, createClientMetrics, createLogger, createParserMetrics, fromConfigFile, matchFilters, parseConfig, rootLogger };