@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/dist/index.js ADDED
@@ -0,0 +1,1773 @@
1
+ // src/config/index.ts
2
+ import { readFileSync } from "fs";
3
+ import { parse as parseYaml } from "yaml";
4
+
5
+ // src/errors.ts
6
+ var ConfigValidationError = class extends Error {
7
+ cause;
8
+ constructor(message, cause) {
9
+ super(message);
10
+ this.name = "ConfigValidationError";
11
+ this.cause = cause;
12
+ }
13
+ };
14
+ var ConfigSecurityError = class extends Error {
15
+ constructor(message) {
16
+ super(message);
17
+ this.name = "ConfigSecurityError";
18
+ }
19
+ };
20
+ var NotImplementedError = class extends Error {
21
+ constructor(method) {
22
+ super(`${method} is not implemented`);
23
+ this.name = "NotImplementedError";
24
+ }
25
+ };
26
+ var ChainIdMismatchError = class extends Error {
27
+ constructor(expected, actual) {
28
+ super(`Chain ID mismatch: expected ${expected}, got ${actual}`);
29
+ this.name = "ChainIdMismatchError";
30
+ }
31
+ };
32
+ var AbiNotFoundError = class extends Error {
33
+ constructor(contract, blockNum, abiFallback) {
34
+ super(`ABI for ${contract} not found at block ${blockNum}, abiFallback=${abiFallback}`);
35
+ this.name = "AbiNotFoundError";
36
+ }
37
+ };
38
+
39
+ // src/config/index.ts
40
+ var PLAIN_SECRET_RE = /redis:\/\/[^$\s]*:[^@$\s]+@/i;
41
+ function interpolateEnv(value) {
42
+ return value.replace(/\$\{([^}]+)\}/g, (_, varName) => {
43
+ return process.env[varName] ?? `\${${varName}}`;
44
+ });
45
+ }
46
+ function interpolateDeep(obj) {
47
+ if (typeof obj === "string") return interpolateEnv(obj);
48
+ if (Array.isArray(obj)) return obj.map(interpolateDeep);
49
+ if (obj !== null && typeof obj === "object") {
50
+ const result = {};
51
+ for (const [k, v] of Object.entries(obj)) {
52
+ result[k] = interpolateDeep(v);
53
+ }
54
+ return result;
55
+ }
56
+ return obj;
57
+ }
58
+ function isObject(v) {
59
+ return v !== null && typeof v === "object" && !Array.isArray(v);
60
+ }
61
+ function validate(raw) {
62
+ const errors = [];
63
+ if (!isObject(raw)) {
64
+ errors.push("(root) must be an object");
65
+ throw new ConfigValidationError(`Config validation failed: ${errors.join("; ")}`);
66
+ }
67
+ if (!isObject(raw["ship"]) || typeof raw["ship"]["url"] !== "string") {
68
+ errors.push("ship.url is required and must be a string");
69
+ }
70
+ if (!isObject(raw["redis"]) || typeof raw["redis"]["url"] !== "string") {
71
+ errors.push("redis.url is required and must be a string");
72
+ }
73
+ const abiFallback = raw["abiFallback"];
74
+ if (abiFallback !== void 0 && abiFallback !== "rpc-current" && abiFallback !== "fail") {
75
+ errors.push('abiFallback must be "rpc-current" or "fail"');
76
+ }
77
+ const deserializer = raw["deserializer"];
78
+ if (deserializer !== void 0 && deserializer !== "wharfkit") {
79
+ errors.push('deserializer must be "wharfkit"');
80
+ }
81
+ if (errors.length > 0) {
82
+ throw new ConfigValidationError(`Config validation failed: ${errors.join("; ")}`);
83
+ }
84
+ return true;
85
+ }
86
+ function checkPlainSecrets(opts) {
87
+ if (PLAIN_SECRET_RE.test(opts.redis.url)) {
88
+ throw new ConfigSecurityError(
89
+ "Secrets must be injected via env variables, not hardcoded in config"
90
+ );
91
+ }
92
+ }
93
+ function parseConfig(raw) {
94
+ const interpolated = interpolateDeep(raw);
95
+ validate(interpolated);
96
+ const opts = interpolated;
97
+ checkPlainSecrets(opts);
98
+ return opts;
99
+ }
100
+ function fromConfigFile(filePath) {
101
+ const text = readFileSync(filePath, "utf8");
102
+ const raw = parseYaml(text);
103
+ return parseConfig(raw);
104
+ }
105
+
106
+ // src/adapters/ShipReaderAdapter.ts
107
+ import { ShipClient } from "@coopenomics/coopos-ship-reader";
108
+ var ShipReaderAdapter = class {
109
+ client;
110
+ /**
111
+ * @param opts.url — WebSocket URL SHiP-ноды, например ws://localhost:29999.
112
+ * @param opts.timeoutMs — таймаут WebSocket-подключения.
113
+ * @param opts.chainUrl — HTTP URL chain API (для getRawAbi / getChainInfo).
114
+ */
115
+ constructor(opts) {
116
+ const shipCfg = { url: opts.url };
117
+ if (opts.timeoutMs !== void 0) shipCfg.timeoutMs = opts.timeoutMs;
118
+ if (opts.chainUrl !== void 0) {
119
+ this.client = new ShipClient({ ship: shipCfg, chain: { url: opts.chainUrl } });
120
+ } else {
121
+ this.client = new ShipClient({ ship: shipCfg });
122
+ }
123
+ }
124
+ /**
125
+ * Устанавливает WebSocket-соединение и выполняет SHiP-рукопожатие.
126
+ * Рукопожатие возвращает chainId — hex-хэш genesis.json.
127
+ */
128
+ async connect() {
129
+ await this.client.connect();
130
+ const { chainId } = await this.client.handshake();
131
+ return { chainId };
132
+ }
133
+ /** Начинает асинхронный поток блоков от указанной позиции. */
134
+ streamBlocks(opts) {
135
+ return this.client.streamBlocks(opts);
136
+ }
137
+ /**
138
+ * ACK n блоков: сигнализирует SHiP-ноде что мы готовы принять ещё.
139
+ * SHiP использует оконное управление потоком: без ACK нода замолчит.
140
+ */
141
+ ack(n) {
142
+ this.client.ack(n);
143
+ }
144
+ /** Закрывает WebSocket. */
145
+ async close() {
146
+ this.client.close();
147
+ }
148
+ /** GET /v1/chain/get_info — возвращает head block, last irreversible и т.д. */
149
+ getChainInfo() {
150
+ return this.client.getChainInfo();
151
+ }
152
+ /** GET /v1/chain/get_raw_abi — загружает сырые байты ABI для bootstrap. */
153
+ getRawAbi(contract) {
154
+ return this.client.getRawAbi(contract);
155
+ }
156
+ /**
157
+ * Десериализует нативную SHiP-дельту в типизированный объект.
158
+ * Делегируется встроенному deserializer'у ship-reader, который
159
+ * содержит hardcoded-схемы нативных таблиц (permission, account, …).
160
+ */
161
+ deserializeNativeDelta(delta) {
162
+ return this.client.deserializer.deserializeNativeDelta(delta);
163
+ }
164
+ };
165
+
166
+ // src/adapters/IoRedisStore.ts
167
+ var { default: RedisClass } = await import("ioredis");
168
+ var PEXPIRE_LUA = `
169
+ local current = redis.call('GET', KEYS[1])
170
+ if current == ARGV[2] then
171
+ redis.call('PEXPIRE', KEYS[1], ARGV[1])
172
+ return 1
173
+ end
174
+ return 0
175
+ `;
176
+ var DEL_LUA = `
177
+ local current = redis.call('GET', KEYS[1])
178
+ if current == ARGV[1] then
179
+ redis.call('DEL', KEYS[1])
180
+ return 1
181
+ end
182
+ return 0
183
+ `;
184
+ function parseStreamEntries(raw) {
185
+ const messages = [];
186
+ for (const [msgId, rawFields] of raw) {
187
+ const fields = {};
188
+ for (let i = 0; i + 1 < rawFields.length; i += 2) {
189
+ fields[rawFields[i] ?? ""] = rawFields[i + 1] ?? "";
190
+ }
191
+ messages.push({ id: msgId, fields });
192
+ }
193
+ return messages;
194
+ }
195
+ function parseXGroupInfo(raw) {
196
+ let obj;
197
+ if (Array.isArray(raw)) {
198
+ obj = {};
199
+ for (let i = 0; i + 1 < raw.length; i += 2) {
200
+ obj[raw[i]] = raw[i + 1];
201
+ }
202
+ } else {
203
+ obj = raw;
204
+ }
205
+ return {
206
+ name: String(obj["name"] ?? ""),
207
+ pending: Number(obj["pending"] ?? 0),
208
+ // Поле 'last-delivered-id' в Redis (с дефисами), не 'lastDeliveredId'
209
+ lastDeliveredId: String(obj["last-delivered-id"] ?? "0-0"),
210
+ // lag появилось в Redis 7.0; null для старых версий
211
+ lag: obj["lag"] != null ? Number(obj["lag"]) : null,
212
+ consumers: Number(obj["consumers"] ?? 0)
213
+ };
214
+ }
215
+ var IoRedisStore = class {
216
+ /** Прямой доступ к ioredis-клиенту (для тестов и расширения). */
217
+ client;
218
+ constructor(opts) {
219
+ const redisOpts = {
220
+ lazyConnect: true,
221
+ // не подключаться в конструкторе — явный connect()
222
+ enableReadyCheck: true
223
+ // проверять готовность перед командами
224
+ };
225
+ if (opts.password !== void 0) redisOpts.password = opts.password;
226
+ if (opts.keyPrefix !== void 0) redisOpts.keyPrefix = opts.keyPrefix;
227
+ this.client = new RedisClass(opts.url, redisOpts);
228
+ }
229
+ /** Явное подключение — вызывается один раз при старте Parser/ParserClient. */
230
+ async connect() {
231
+ await this.client.connect();
232
+ }
233
+ /** XADD stream * field1 val1 … — возвращает присвоенный entry ID. */
234
+ async xadd(stream, fields) {
235
+ const args = [];
236
+ for (const [k, v] of Object.entries(fields)) args.push(k, v);
237
+ const id = await this.client.xadd(stream, "*", ...args);
238
+ return id ?? "";
239
+ }
240
+ /** XTRIM stream MINID minId — удаляет записи с ID < minId. */
241
+ async xtrim(stream, minId) {
242
+ return this.client.xtrim(stream, "MINID", minId);
243
+ }
244
+ /**
245
+ * XGROUP CREATE stream group startId MKSTREAM
246
+ * MKSTREAM: создаёт стрим если не существует.
247
+ * BUSYGROUP: группа уже существует — это нормально, поглощаем ошибку.
248
+ */
249
+ async xgroupCreate(stream, group, startId) {
250
+ try {
251
+ await this.client.xgroup("CREATE", stream, group, startId, "MKSTREAM");
252
+ } catch (err) {
253
+ if (err instanceof Error && err.message.includes("BUSYGROUP")) return;
254
+ throw err;
255
+ }
256
+ }
257
+ /** XGROUP SETID stream group id — переставляет позицию group в стриме. */
258
+ async xgroupSetId(stream, group, id) {
259
+ await this.client.xgroup("SETID", stream, group, id);
260
+ }
261
+ /** XINFO GROUPS stream → список consumer groups с метриками. */
262
+ async xinfoGroups(stream) {
263
+ const raw = await this.client.xinfo("GROUPS", stream);
264
+ return (raw ?? []).map(parseXGroupInfo);
265
+ }
266
+ /**
267
+ * XREADGROUP GROUP group consumer COUNT count BLOCK blockMs STREAMS stream id
268
+ * id='>' — только новые сообщения.
269
+ * id='0' — PEL (pending): уже доставленные, но не подтверждённые (recovery).
270
+ */
271
+ async xreadGroup(stream, group, consumer, count, blockMs, id) {
272
+ const result = await this.client.xreadgroup(
273
+ "GROUP",
274
+ group,
275
+ consumer,
276
+ "COUNT",
277
+ count,
278
+ "BLOCK",
279
+ blockMs,
280
+ "STREAMS",
281
+ stream,
282
+ id
283
+ );
284
+ if (!result) return [];
285
+ const messages = [];
286
+ for (const [, entries] of result) {
287
+ messages.push(...parseStreamEntries(entries));
288
+ }
289
+ return messages;
290
+ }
291
+ /** XRANGE stream start end COUNT count. */
292
+ async xrange(stream, start, end, count) {
293
+ const raw = await this.client.xrange(stream, start, end, "COUNT", count);
294
+ return parseStreamEntries(raw);
295
+ }
296
+ /** XREVRANGE stream end start COUNT count. */
297
+ async xrevrange(stream, end, start, count) {
298
+ const raw = await this.client.xrevrange(stream, end, start, "COUNT", count);
299
+ return parseStreamEntries(raw);
300
+ }
301
+ /** XLEN stream. */
302
+ async xlen(stream) {
303
+ return this.client.xlen(stream);
304
+ }
305
+ /** XDEL stream id — удаляет запись по ID. */
306
+ async xdel(stream, id) {
307
+ return this.client.xdel(stream, id);
308
+ }
309
+ /** XACK stream group id — убирает из PEL. */
310
+ async xack(stream, group, id) {
311
+ await this.client.xack(stream, group, id);
312
+ }
313
+ /** ZADD key score member. */
314
+ async zadd(key, score, member) {
315
+ await this.client.zadd(key, score, member);
316
+ }
317
+ /**
318
+ * ZREVRANGEBYSCORE key max min LIMIT 0 1
319
+ * Возвращает максимум один элемент с score ≤ max.
320
+ * Используется для поиска ABI: «последняя версия не позже блока N».
321
+ */
322
+ async zrangeByscoreRev(key, max, min) {
323
+ return this.client.zrevrangebyscore(key, max, min, "LIMIT", 0, 1);
324
+ }
325
+ /** ZRANGEBYSCORE key min max LIMIT 0 9999999 — все элементы в диапазоне. */
326
+ async zrangeByScore(key, min, max) {
327
+ return this.client.zrangebyscore(key, min, max, "LIMIT", 0, 9999999);
328
+ }
329
+ /** ZCOUNT key min max. */
330
+ async zcount(key, min, max) {
331
+ return this.client.zcount(key, min, max);
332
+ }
333
+ /** ZREMRANGEBYSCORE key min max → число удалённых. */
334
+ async zremRangeByScore(key, min, max) {
335
+ return this.client.zremrangebyscore(key, min, max);
336
+ }
337
+ /** ZCARD key. */
338
+ async zcard(key) {
339
+ return this.client.zcard(key);
340
+ }
341
+ /** HSET key field1 val1 field2 val2 … */
342
+ async hset(key, fields) {
343
+ const args = [];
344
+ for (const [k, v] of Object.entries(fields)) args.push(k, v);
345
+ if (args.length > 0) await this.client.hset(key, ...args);
346
+ }
347
+ /** HGET key field. */
348
+ async hget(key, field) {
349
+ return this.client.hget(key, field);
350
+ }
351
+ /** HGETALL key → пустой объект если ключ не существует (ioredis возвращает null). */
352
+ async hgetAll(key) {
353
+ const result = await this.client.hgetall(key);
354
+ return result ?? {};
355
+ }
356
+ /** HINCRBY key field increment → новое значение счётчика. */
357
+ async hincrby(key, field, increment) {
358
+ return this.client.hincrby(key, field, increment);
359
+ }
360
+ /** HDEL key field. */
361
+ async hdel(key, field) {
362
+ await this.client.hdel(key, field);
363
+ }
364
+ /**
365
+ * SET key value NX PX pxMs
366
+ * NX: только если не существует. PX: TTL в миллисекундах.
367
+ * Используется для захвата distributed lock'а.
368
+ */
369
+ async setNx(key, value, pxMs) {
370
+ const result = await this.client.set(key, value, "NX", "PX", pxMs);
371
+ return result === "OK";
372
+ }
373
+ /**
374
+ * Выполняет PEXPIRE_LUA: продлевает TTL lock'а только если мы — владелец.
375
+ * Возвращает true если продление прошло успешно.
376
+ */
377
+ async pexpire(key, ms, value) {
378
+ const result = await this.client.eval(PEXPIRE_LUA, 1, key, String(ms), value);
379
+ return result === 1;
380
+ }
381
+ /**
382
+ * Выполняет DEL_LUA: удаляет lock только если мы — владелец.
383
+ * Возвращает true если удаление прошло успешно.
384
+ */
385
+ async luaDel(key, value) {
386
+ const result = await this.client.eval(DEL_LUA, 1, key, value);
387
+ return result === 1;
388
+ }
389
+ /** EXPIRE key seconds. */
390
+ async expire(key, seconds) {
391
+ await this.client.expire(key, seconds);
392
+ }
393
+ /**
394
+ * Полный SCAN по паттерну: итерирует cursor пока не вернётся '0'.
395
+ * @param count — подсказка Redis сколько ключей возвращать за итерацию.
396
+ * @returns Полный список ключей (может быть большим для широких паттернов).
397
+ */
398
+ async scan(pattern, count = 100) {
399
+ const keys = [];
400
+ let cursor = "0";
401
+ do {
402
+ const [nextCursor, batch] = await this.client.scan(cursor, "MATCH", pattern, "COUNT", count);
403
+ keys.push(...batch);
404
+ cursor = nextCursor;
405
+ } while (cursor !== "0");
406
+ return keys;
407
+ }
408
+ /** Закрывает соединение с Redis. */
409
+ async quit() {
410
+ await this.client.quit();
411
+ }
412
+ };
413
+
414
+ // src/workers/WorkerPool.ts
415
+ import { fileURLToPath } from "url";
416
+ import { join, dirname, resolve } from "path";
417
+ import { existsSync } from "fs";
418
+ var { default: PiscinaClass } = await import("piscina");
419
+ var __filename = fileURLToPath(import.meta.url);
420
+ var __dirname = dirname(__filename);
421
+ function resolveWorkerPath() {
422
+ const candidates = [
423
+ // Production: рядом с WorkerPool
424
+ join(__dirname, "deserialize.worker.cjs"),
425
+ // Source mode (tests): относительный путь к dist/
426
+ resolve(__dirname, "../../dist/deserialize.worker.cjs"),
427
+ resolve(__dirname, "../../dist/workers/deserialize.worker.cjs")
428
+ ];
429
+ for (const path of candidates) {
430
+ if (existsSync(path)) return path;
431
+ }
432
+ throw new Error(
433
+ `Could not find deserialize.worker.cjs. Tried: ${candidates.join(", ")}. Run "pnpm build" first if running from source.`
434
+ );
435
+ }
436
+ var WorkerPool = class {
437
+ pool;
438
+ /**
439
+ * @param maxThreads — максимум параллельных worker-потоков.
440
+ * Оптимум зависит от числа CPU и IO-нагрузки. Дефолт 2 подходит
441
+ * для большинства серверов; 4+ потока ускоряют плотные блоки (много actions).
442
+ */
443
+ constructor(maxThreads = 2) {
444
+ this.pool = new PiscinaClass({
445
+ filename: resolveWorkerPath(),
446
+ maxThreads
447
+ });
448
+ }
449
+ /**
450
+ * Запускает десериализацию в одном из свободных worker-потоков.
451
+ * Блокирует промис пока worker не вернёт результат.
452
+ */
453
+ run(task) {
454
+ return this.pool.run(task);
455
+ }
456
+ /**
457
+ * Доля занятых потоков от общего числа (0..1).
458
+ * Полезно для метрики parser_worker_pool_queue_depth.
459
+ */
460
+ get utilization() {
461
+ return this.pool.utilization;
462
+ }
463
+ /** Завершает все worker-потоки (вызывается при остановке парсера). */
464
+ destroy() {
465
+ return this.pool.destroy();
466
+ }
467
+ };
468
+
469
+ // src/core/BlockProcessor.ts
470
+ import PQueue from "p-queue";
471
+ import { ABI, Blob as AntelopeBlob } from "@wharfkit/antelope";
472
+ import { isNativeTableName } from "@coopenomics/coopos-ship-reader";
473
+
474
+ // src/events/eventId.ts
475
+ function computeEventId(event) {
476
+ const blockIdShort = (blockId) => blockId.slice(0, 16);
477
+ if (event.kind === "action") {
478
+ return `${event.chain_id}:a:${event.block_num}:${blockIdShort(event.block_id)}:${event.global_sequence}`;
479
+ }
480
+ if (event.kind === "delta") {
481
+ return `${event.chain_id}:d:${event.block_num}:${blockIdShort(event.block_id)}:${event.code}:${event.scope}:${event.table}:${event.primary_key}`;
482
+ }
483
+ if (event.kind === "native-delta") {
484
+ return `${event.chain_id}:n:${event.block_num}:${blockIdShort(event.block_id)}:${event.table}:${event.lookup_key}`;
485
+ }
486
+ if (event.kind === "fork") {
487
+ return `${event.chain_id}:f:${event.forked_from_block}:${blockIdShort(event.new_head_block_id)}`;
488
+ }
489
+ const _exhaustive = event;
490
+ return _exhaustive;
491
+ }
492
+
493
+ // src/core/BlockProcessor.ts
494
+ function abiToJson(bytes) {
495
+ try {
496
+ const base64 = Buffer.from(bytes).toString("base64");
497
+ const abi = ABI.from(AntelopeBlob.from(base64));
498
+ return JSON.stringify(abi.toJSON());
499
+ } catch {
500
+ return "{}";
501
+ }
502
+ }
503
+ var BlockProcessor = class {
504
+ /** Очередь с concurrency=1: только один блок обрабатывается одновременно. */
505
+ queue;
506
+ chainId;
507
+ workerPool;
508
+ abiBootstrapper;
509
+ abiStore;
510
+ chainClient;
511
+ constructor(opts) {
512
+ this.chainId = opts.chainId;
513
+ this.workerPool = opts.workerPool;
514
+ this.abiBootstrapper = opts.abiBootstrapper;
515
+ this.abiStore = opts.abiStore;
516
+ this.chainClient = opts.chainClient;
517
+ this.queue = new PQueue({ concurrency: 1 });
518
+ }
519
+ /**
520
+ * Ставит блок в очередь на обработку.
521
+ * Возвращает Promise который резолвится когда этот конкретный блок обработан.
522
+ * Блоки обрабатываются строго последовательно (concurrency=1).
523
+ */
524
+ process(block) {
525
+ return this.queue.add(() => this.processBlock(block));
526
+ }
527
+ async processBlock(block) {
528
+ const actionEvents = [];
529
+ const deltaEvents = [];
530
+ const nativeDeltaEvents = [];
531
+ const blockNum = block.thisBlock.blockNum;
532
+ const blockId = block.thisBlock.blockId;
533
+ const blockTime = block.traces[0]?.blockTime ?? (/* @__PURE__ */ new Date()).toISOString();
534
+ for (const trace of block.traces) {
535
+ const abiBytes = await this.abiBootstrapper.ensureAbi(trace.account, blockNum);
536
+ const abiJson = abiBytes && abiBytes.length > 0 ? abiToJson(abiBytes) : "{}";
537
+ let data = {};
538
+ if (trace.actRaw.length > 0) {
539
+ try {
540
+ data = await this.workerPool.run({
541
+ rawBinary: trace.actRaw,
542
+ abiJson,
543
+ contract: trace.account,
544
+ typeName: trace.name,
545
+ kind: "action"
546
+ });
547
+ } catch {
548
+ data = {};
549
+ }
550
+ }
551
+ if (trace.account === "eosio" && trace.name === "setabi") {
552
+ const contractName = data["account"];
553
+ const abiHex = data["abi"];
554
+ if (typeof contractName === "string" && typeof abiHex === "string" && abiHex.length > 0) {
555
+ await this.abiStore.storeAbi(contractName, blockNum, Buffer.from(abiHex, "hex"));
556
+ }
557
+ }
558
+ const partial = {
559
+ kind: "action",
560
+ chain_id: this.chainId,
561
+ block_num: blockNum,
562
+ block_time: blockTime,
563
+ block_id: blockId,
564
+ account: trace.account,
565
+ name: trace.name,
566
+ authorization: [...trace.authorization],
567
+ data,
568
+ action_ordinal: trace.actionOrdinal,
569
+ global_sequence: trace.globalSequence,
570
+ receipt: trace.receipt
571
+ };
572
+ actionEvents.push({ ...partial, event_id: computeEventId(partial) });
573
+ }
574
+ for (const delta of block.deltas) {
575
+ if (delta.name === "account" && delta.present && delta.rowRaw.length > 0) {
576
+ const eosioAbiBytes = await this.abiBootstrapper.ensureAbi("eosio", blockNum);
577
+ const eosioAbiJson = eosioAbiBytes && eosioAbiBytes.length > 0 ? abiToJson(eosioAbiBytes) : "{}";
578
+ try {
579
+ const accountData = await this.workerPool.run({
580
+ rawBinary: delta.rowRaw,
581
+ abiJson: eosioAbiJson,
582
+ contract: "eosio",
583
+ typeName: "account",
584
+ kind: "delta"
585
+ });
586
+ const accountName = accountData["name"];
587
+ const abiHex = accountData["abi"];
588
+ if (typeof accountName === "string" && typeof abiHex === "string" && abiHex.length > 0) {
589
+ await this.abiStore.storeAbi(accountName, blockNum, Buffer.from(abiHex, "hex"));
590
+ }
591
+ } catch {
592
+ }
593
+ }
594
+ if (delta.name === "contract_row") {
595
+ if (!delta.code || !delta.scope || !delta.table || !delta.primaryKey) continue;
596
+ const abiBytes = await this.abiBootstrapper.ensureAbi(delta.code, blockNum);
597
+ const abiJson = abiBytes && abiBytes.length > 0 ? abiToJson(abiBytes) : "{}";
598
+ let value = {};
599
+ if (delta.rowRaw.length > 0) {
600
+ try {
601
+ value = await this.workerPool.run({
602
+ rawBinary: delta.rowRaw,
603
+ abiJson,
604
+ contract: delta.code,
605
+ typeName: delta.table,
606
+ kind: "delta"
607
+ });
608
+ } catch {
609
+ value = {};
610
+ }
611
+ }
612
+ const partial = {
613
+ kind: "delta",
614
+ chain_id: this.chainId,
615
+ block_num: blockNum,
616
+ block_time: blockTime,
617
+ block_id: blockId,
618
+ code: delta.code,
619
+ scope: delta.scope,
620
+ table: delta.table,
621
+ primary_key: delta.primaryKey,
622
+ value,
623
+ present: delta.present
624
+ };
625
+ deltaEvents.push({ ...partial, event_id: computeEventId(partial) });
626
+ continue;
627
+ }
628
+ if (isNativeTableName(delta.name)) {
629
+ try {
630
+ const native = this.chainClient.deserializeNativeDelta(delta);
631
+ const partial = {
632
+ kind: "native-delta",
633
+ chain_id: this.chainId,
634
+ block_num: blockNum,
635
+ block_time: blockTime,
636
+ block_id: blockId,
637
+ table: native.table,
638
+ lookup_key: native.lookup_key,
639
+ data: native.data,
640
+ present: native.present
641
+ };
642
+ nativeDeltaEvents.push({ ...partial, event_id: computeEventId(partial) });
643
+ } catch {
644
+ }
645
+ }
646
+ }
647
+ return [...actionEvents, ...deltaEvents, ...nativeDeltaEvents];
648
+ }
649
+ /** Число заданий, ожидающих обработки (в очереди + текущее). */
650
+ get pendingCount() {
651
+ return this.queue.size + this.queue.pending;
652
+ }
653
+ /** Ждёт завершения всех задач в очереди (вызывается при graceful shutdown). */
654
+ onIdle() {
655
+ return this.queue.onIdle();
656
+ }
657
+ };
658
+
659
+ // src/core/XtrimSupervisor.ts
660
+ var XtrimSupervisor = class {
661
+ timer = null;
662
+ redis;
663
+ stream;
664
+ intervalMs;
665
+ constructor(opts) {
666
+ this.redis = opts.redis;
667
+ this.stream = opts.stream;
668
+ this.intervalMs = opts.intervalMs ?? 6e4;
669
+ }
670
+ /** Запускает периодический trim. Идемпотентен — повторный вызов игнорируется. */
671
+ start() {
672
+ if (this.timer) return;
673
+ this.timer = setInterval(() => {
674
+ void this.trim();
675
+ }, this.intervalMs);
676
+ this.timer.unref?.();
677
+ }
678
+ /** Останавливает trim-цикл (вызывается при graceful shutdown). */
679
+ stop() {
680
+ if (this.timer) {
681
+ clearInterval(this.timer);
682
+ this.timer = null;
683
+ }
684
+ }
685
+ /**
686
+ * Один цикл очистки:
687
+ * 1. Получаем список consumer groups через XINFO GROUPS.
688
+ * 2. Фильтруем группы у которых есть pending сообщения (pending > 0).
689
+ * 3. Находим минимальный lastDeliveredId среди таких групп.
690
+ * 4. XTRIM stream MINID minId — удаляем всё старее этого ID.
691
+ *
692
+ * Если pending-групп нет — trim не делается (всё подтверждено).
693
+ * Если стрим не существует или XInfo бросает — тихо игнорируем (best-effort).
694
+ */
695
+ async trim() {
696
+ try {
697
+ const groups = await this.redis.xinfoGroups(this.stream);
698
+ if (!groups || groups.length === 0) return;
699
+ const pendingGroups = groups.filter((g) => g.pending > 0);
700
+ if (pendingGroups.length === 0) return;
701
+ const minId = pendingGroups.map((g) => g.lastDeliveredId).reduce((a, b) => a < b ? a : b);
702
+ if (minId) await this.redis.xtrim(this.stream, minId);
703
+ } catch {
704
+ }
705
+ }
706
+ };
707
+
708
+ // src/core/ForkDetector.ts
709
+ var ForkDetector = class {
710
+ /** -1 означает «ещё не видели ни одного блока». */
711
+ lastBlockNum = -1;
712
+ chainId;
713
+ constructor(chainId) {
714
+ this.chainId = chainId;
715
+ }
716
+ /**
717
+ * Проверяет, является ли входящий блок форком.
718
+ * Должен вызываться один раз перед обработкой каждого блока.
719
+ *
720
+ * @returns ForkEvent если обнаружен форк, null при нормальной последовательности.
721
+ */
722
+ check(blockNum, blockId) {
723
+ let event = null;
724
+ if (this.lastBlockNum >= 0 && blockNum <= this.lastBlockNum) {
725
+ const partial = {
726
+ kind: "fork",
727
+ chain_id: this.chainId,
728
+ forked_from_block: this.lastBlockNum,
729
+ new_head_block_id: blockId
730
+ };
731
+ event = { ...partial, event_id: computeEventId(partial) };
732
+ }
733
+ this.lastBlockNum = blockNum;
734
+ return event;
735
+ }
736
+ /**
737
+ * Сбрасывает историю (вызывается при переподключении к SHiP-ноде,
738
+ * чтобы не ложно детектировать форк при старте с произвольного блока).
739
+ */
740
+ reset() {
741
+ this.lastBlockNum = -1;
742
+ }
743
+ };
744
+
745
+ // src/redis/keys.ts
746
+ var RedisKeys = {
747
+ /**
748
+ * Главный поток событий парсера (unified event stream).
749
+ * Тип Redis: Stream. Тримируется XtrimSupervisor'ом.
750
+ * Пример: ce:parser:eos-mainnet:events
751
+ */
752
+ eventsStream: (chainId) => `ce:parser:${chainId}:events`,
753
+ /**
754
+ * Dead-letter поток для конкретной подписки.
755
+ * Содержит сообщения, которые не смог обработать consumer после N попыток.
756
+ * Тип Redis: Stream.
757
+ * Пример: ce:parser:eos-mainnet:dead:verifier
758
+ */
759
+ deadLetterStream: (chainId, subId) => `ce:parser:${chainId}:dead:${subId}`,
760
+ /**
761
+ * Поток для задания on-demand reparse (зарезервировано для будущего).
762
+ * Тип Redis: Stream.
763
+ */
764
+ reparseStream: (chainId, jobId) => `ce:parser:${chainId}:reparse:${jobId}`,
765
+ /**
766
+ * История версий ABI конкретного контракта.
767
+ * Тип Redis: Sorted Set. Score = block_num, member = base64(rawAbiBytes).
768
+ * При поиске ABI для блока N используется ZREVRANGEBYSCORE … N -inf LIMIT 0 1.
769
+ * Пример: parser:abi:eosio.token
770
+ */
771
+ abiZset: (contract) => `parser:abi:${contract}`,
772
+ /**
773
+ * Контрольная точка синхронизации парсера (crash-recovery).
774
+ * Тип Redis: Hash. Поля: block_num, block_id, last_updated.
775
+ * При рестарте парсер читает отсюда позицию и продолжает с неё.
776
+ */
777
+ syncHash: (chainId) => `parser:sync:${chainId}`,
778
+ /**
779
+ * Реестр всех зарегистрированных подписок.
780
+ * Тип Redis: Hash. Ключ поля = subId, значение = JSON-метаданные подписки.
781
+ */
782
+ subsHash: () => `parser:subs`,
783
+ /**
784
+ * Счётчики ошибок per-event для конкретной подписки.
785
+ * Тип Redis: Hash. Ключ поля = event_id, значение = число провалов.
786
+ * TTL: 24 часа (обновляется при каждом новом провале).
787
+ * Используется FailureTracker для решения о переводе в dead-letter.
788
+ */
789
+ subFailuresHash: (subId) => `parser:sub:${subId}:failures`,
790
+ /**
791
+ * Блокировка single-active-consumer для подписки.
792
+ * Тип Redis: String (instanceId держателя блокировки). TTL: 10 с (автопродление).
793
+ * Только один экземпляр consumer-а может быть active; остальные — standby.
794
+ */
795
+ subLock: (subId) => `parser:sub:${subId}:lock`,
796
+ /**
797
+ * Метаданные задания reparse (зарезервировано для будущего).
798
+ * Тип Redis: Hash.
799
+ */
800
+ reparseJobHash: (jobId) => `parser:reparse:${jobId}`
801
+ };
802
+
803
+ // src/abi/AbiStore.ts
804
+ var AbiStore = class {
805
+ constructor(redis) {
806
+ this.redis = redis;
807
+ }
808
+ redis;
809
+ /**
810
+ * Ищет версию ABI контракта, актуальную на момент blockNum.
811
+ * Использует ZREVRANGEBYSCORE: возвращает последнюю запись со score ≤ blockNum.
812
+ * @returns Байты ABI или null если история пуста для данного контракта.
813
+ */
814
+ async getAbi(contract, blockNum) {
815
+ const key = RedisKeys.abiZset(contract);
816
+ const results = await this.redis.zrangeByscoreRev(key, String(blockNum), "-inf");
817
+ if (results.length === 0 || !results[0]) return null;
818
+ return Buffer.from(results[0], "base64");
819
+ }
820
+ /**
821
+ * Сохраняет новую версию ABI, привязывая её к blockNum.
822
+ * Вызывается AbiBootstrapper при первом наблюдении контракта и
823
+ * BlockProcessor'ом при перехвате eosio::setabi / account-дельты.
824
+ */
825
+ async storeAbi(contract, blockNum, abiBytes) {
826
+ const key = RedisKeys.abiZset(contract);
827
+ const member = Buffer.from(abiBytes).toString("base64");
828
+ await this.redis.zadd(key, blockNum, member);
829
+ }
830
+ };
831
+
832
+ // src/abi/AbiBootstrapper.ts
833
+ var AbiBootstrapper = class {
834
+ constructor(chainClient, abiStore, opts) {
835
+ this.chainClient = chainClient;
836
+ this.abiStore = abiStore;
837
+ this.abiFallback = opts?.abiFallback ?? "rpc-current";
838
+ }
839
+ chainClient;
840
+ abiStore;
841
+ /** Контракты, ABI которых уже гарантированно записан в Redis в этом сеансе. */
842
+ observedContracts = /* @__PURE__ */ new Set();
843
+ abiFallback;
844
+ /**
845
+ * Гарантирует наличие ABI для контракта в кэше перед декодированием события.
846
+ *
847
+ * Быстрый путь: контракт уже в observedContracts → сразу идём в Redis.
848
+ * Медленный путь: первая встреча → проверяем Redis → если пусто, скачиваем с RPC.
849
+ *
850
+ * @param contract — имя аккаунта-контракта (например 'eosio.token').
851
+ * @param blockNum — номер блока, для которого нужна ABI (для поиска версии).
852
+ * @returns Байты ABI или null если ABI недоступен и abiFallback='rpc-current'.
853
+ * @throws AbiNotFoundError если abiFallback='fail' и ABI не найден.
854
+ */
855
+ async ensureAbi(contract, blockNum) {
856
+ if (this.observedContracts.has(contract)) {
857
+ return this.abiStore.getAbi(contract, blockNum);
858
+ }
859
+ const stored = await this.abiStore.getAbi(contract, blockNum);
860
+ if (stored) {
861
+ this.observedContracts.add(contract);
862
+ return stored;
863
+ }
864
+ this.observedContracts.add(contract);
865
+ try {
866
+ const abiBytes = await this.chainClient.getRawAbi(contract);
867
+ await this.abiStore.storeAbi(contract, blockNum, abiBytes);
868
+ return abiBytes;
869
+ } catch {
870
+ if (this.abiFallback === "fail") {
871
+ throw new AbiNotFoundError(contract, blockNum, this.abiFallback);
872
+ }
873
+ return null;
874
+ }
875
+ }
876
+ };
877
+
878
+ // src/core/Parser.ts
879
+ var Parser = class _Parser {
880
+ opts;
881
+ chainClient = null;
882
+ redis = null;
883
+ workerPool = null;
884
+ blockProcessor = null;
885
+ xtrimSupervisor = null;
886
+ running = false;
887
+ stopSignal = false;
888
+ constructor(opts) {
889
+ this.opts = opts;
890
+ }
891
+ static fromConfigFile(filePath) {
892
+ return new _Parser(fromConfigFile(filePath));
893
+ }
894
+ static fromConfig(raw) {
895
+ return new _Parser(parseConfig(raw));
896
+ }
897
+ async start() {
898
+ this.stopSignal = false;
899
+ this.running = true;
900
+ if (!this.opts.noSignalHandlers) {
901
+ const shutdown = () => void this.stop();
902
+ process.once("SIGTERM", shutdown);
903
+ process.once("SIGINT", shutdown);
904
+ }
905
+ this.redis = new IoRedisStore(this.opts.redis);
906
+ await this.redis.connect();
907
+ await this.checkRedisPersistence();
908
+ this.workerPool = new WorkerPool(this.opts.workerPool?.maxThreads ?? 2);
909
+ const shipOpts = {
910
+ url: this.opts.ship.url
911
+ };
912
+ if (this.opts.ship.timeoutMs !== void 0) shipOpts.timeoutMs = this.opts.ship.timeoutMs;
913
+ if (this.opts.chain?.url !== void 0) shipOpts.chainUrl = this.opts.chain.url;
914
+ this.chainClient = new ShipReaderAdapter(shipOpts);
915
+ const { chainId } = await this.chainClient.connect();
916
+ if (this.opts.chain?.id && this.opts.chain.id !== chainId) {
917
+ throw new ChainIdMismatchError(this.opts.chain.id, chainId);
918
+ }
919
+ const abiFallback = this.opts.abiFallback ?? "rpc-current";
920
+ const abiStore = new AbiStore(this.redis);
921
+ const abiBootstrapper = new AbiBootstrapper(this.chainClient, abiStore, { abiFallback });
922
+ this.blockProcessor = new BlockProcessor({
923
+ chainId,
924
+ workerPool: this.workerPool,
925
+ abiBootstrapper,
926
+ abiStore,
927
+ chainClient: this.chainClient
928
+ });
929
+ const syncKey = RedisKeys.syncHash(chainId);
930
+ const eventsStream = RedisKeys.eventsStream(chainId);
931
+ const lastBlockNum = await this.redis.hget(syncKey, "block_num");
932
+ const lastBlockId = await this.redis.hget(syncKey, "block_id");
933
+ const havePositions = lastBlockNum && lastBlockId ? [{ blockNum: Number(lastBlockNum), blockId: lastBlockId }] : [];
934
+ const xtrimOpts = {
935
+ redis: this.redis,
936
+ stream: eventsStream
937
+ };
938
+ if (this.opts.xtrim?.intervalMs !== void 0) xtrimOpts.intervalMs = this.opts.xtrim.intervalMs;
939
+ this.xtrimSupervisor = new XtrimSupervisor(xtrimOpts);
940
+ if (this.opts.xtrim?.enabled !== false) {
941
+ this.xtrimSupervisor.start();
942
+ }
943
+ const streamOpts = {
944
+ startBlock: havePositions[0]?.blockNum ?? 0,
945
+ havePositions
946
+ };
947
+ const irreversibleOnly = this.opts.irreversibleOnly ?? false;
948
+ const forkDetector = new ForkDetector(chainId);
949
+ for await (const block of this.chainClient.streamBlocks(streamOpts)) {
950
+ if (this.stopSignal) break;
951
+ if (irreversibleOnly && block.thisBlock.blockNum > block.lastIrreversible.blockNum) {
952
+ this.chainClient.ack(1);
953
+ continue;
954
+ }
955
+ const forkEvent = forkDetector.check(block.thisBlock.blockNum, block.thisBlock.blockId);
956
+ const events = await this.blockProcessor.process(block);
957
+ const toPublish = forkEvent ? [forkEvent, ...events] : events;
958
+ for (const event of toPublish) {
959
+ await this.redis.xadd(eventsStream, this.eventToFields(event));
960
+ }
961
+ await this.redis.hset(syncKey, {
962
+ block_num: String(block.thisBlock.blockNum),
963
+ block_id: block.thisBlock.blockId,
964
+ last_updated: (/* @__PURE__ */ new Date()).toISOString()
965
+ });
966
+ this.chainClient.ack(1);
967
+ }
968
+ }
969
+ eventToFields(event) {
970
+ return {
971
+ data: JSON.stringify(
972
+ event,
973
+ (_k, v) => typeof v === "bigint" ? v.toString() : v
974
+ )
975
+ };
976
+ }
977
+ async stop() {
978
+ this.stopSignal = true;
979
+ if (this.blockProcessor) {
980
+ await this.blockProcessor.onIdle();
981
+ }
982
+ if (this.chainClient) {
983
+ await this.chainClient.close();
984
+ this.chainClient = null;
985
+ }
986
+ if (this.workerPool) {
987
+ await this.workerPool.destroy();
988
+ this.workerPool = null;
989
+ }
990
+ if (this.xtrimSupervisor) {
991
+ this.xtrimSupervisor.stop();
992
+ this.xtrimSupervisor = null;
993
+ }
994
+ if (this.redis) {
995
+ await this.redis.quit();
996
+ this.redis = null;
997
+ }
998
+ this.running = false;
999
+ }
1000
+ get isRunning() {
1001
+ return this.running;
1002
+ }
1003
+ async checkRedisPersistence() {
1004
+ const redis = this.redis;
1005
+ try {
1006
+ const aofResult = await redis.hget("__parser_check__", "__noop__");
1007
+ void aofResult;
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ };
1012
+
1013
+ // src/client/ParserClient.ts
1014
+ import { randomUUID } from "crypto";
1015
+ import { hostname } from "os";
1016
+
1017
+ // src/client/SubscriptionLock.ts
1018
+ var LOCK_TTL_MS = 1e4;
1019
+ var HEARTBEAT_MS = 3e3;
1020
+ var STANDBY_POLL_MS = 500;
1021
+ function sleep(ms) {
1022
+ return new Promise((r) => setTimeout(r, ms));
1023
+ }
1024
+ var SubscriptionLock = class {
1025
+ redis;
1026
+ key;
1027
+ instanceId;
1028
+ heartbeatMs;
1029
+ acquireTimeoutMs;
1030
+ heartbeatTimer = null;
1031
+ _state = "acquiring";
1032
+ constructor(opts) {
1033
+ this.redis = opts.redis;
1034
+ this.key = RedisKeys.subLock(opts.subId);
1035
+ this.instanceId = opts.instanceId;
1036
+ this.heartbeatMs = opts.heartbeatIntervalMs ?? HEARTBEAT_MS;
1037
+ this.acquireTimeoutMs = opts.acquireLockTimeoutMs ?? Infinity;
1038
+ }
1039
+ /** Текущее состояние: acquiring → active/standby → released. */
1040
+ get state() {
1041
+ return this._state;
1042
+ }
1043
+ /**
1044
+ * Пробует захватить lock одним атомарным SET NX PX.
1045
+ * @returns true если захватили (state = active), false если занят (state = standby).
1046
+ */
1047
+ async acquire() {
1048
+ const acquired = await this.redis.setNx(this.key, this.instanceId, LOCK_TTL_MS);
1049
+ if (acquired) {
1050
+ this._state = "active";
1051
+ this.startHeartbeat();
1052
+ } else {
1053
+ this._state = "standby";
1054
+ }
1055
+ return acquired;
1056
+ }
1057
+ /**
1058
+ * Блокирует текущий процесс до тех пор пока lock не освободится и мы его захватим.
1059
+ * Опрашивает Redis каждые STANDBY_POLL_MS мс.
1060
+ * @throws Error если acquireLockTimeoutMs истёк.
1061
+ */
1062
+ async waitForPromotion() {
1063
+ const deadline = this.acquireTimeoutMs === Infinity ? Infinity : Date.now() + this.acquireTimeoutMs;
1064
+ while (true) {
1065
+ if (Date.now() > deadline) {
1066
+ throw new Error(`Lock acquire timeout after ${this.acquireTimeoutMs}ms`);
1067
+ }
1068
+ await sleep(STANDBY_POLL_MS);
1069
+ const acquired = await this.redis.setNx(this.key, this.instanceId, LOCK_TTL_MS);
1070
+ if (acquired) {
1071
+ this._state = "active";
1072
+ this.startHeartbeat();
1073
+ return;
1074
+ }
1075
+ }
1076
+ }
1077
+ /** Запускает периодическое продление TTL (heartbeat). */
1078
+ startHeartbeat() {
1079
+ this.heartbeatTimer = setInterval(() => {
1080
+ void this.renewHeartbeat();
1081
+ }, this.heartbeatMs);
1082
+ this.heartbeatTimer.unref?.();
1083
+ }
1084
+ /**
1085
+ * Один шаг heartbeat: продлеваем TTL через LUA-скрипт.
1086
+ * Если LUA вернул false — кто-то другой захватил lock (race condition после
1087
+ * истечения нашего TTL). Переходим в standby.
1088
+ */
1089
+ async renewHeartbeat() {
1090
+ const renewed = await this.redis.pexpire(this.key, LOCK_TTL_MS, this.instanceId);
1091
+ if (!renewed && this._state === "active") {
1092
+ this.stopHeartbeat();
1093
+ this._state = "standby";
1094
+ }
1095
+ }
1096
+ /** Останавливает heartbeat-таймер (без освобождения lock'а). */
1097
+ stopHeartbeat() {
1098
+ if (this.heartbeatTimer) {
1099
+ clearInterval(this.heartbeatTimer);
1100
+ this.heartbeatTimer = null;
1101
+ }
1102
+ }
1103
+ /**
1104
+ * Освобождает lock: останавливает heartbeat, удаляет ключ через LUA (conditional).
1105
+ * После вызова state = 'released'.
1106
+ */
1107
+ async release() {
1108
+ this.stopHeartbeat();
1109
+ await this.redis.luaDel(this.key, this.instanceId);
1110
+ this._state = "released";
1111
+ }
1112
+ };
1113
+
1114
+ // src/client/RedisConsumer.ts
1115
+ var CONSUMER_NAME = "primary";
1116
+ var RedisConsumer = class {
1117
+ redis;
1118
+ stream;
1119
+ groupName;
1120
+ blockMs;
1121
+ count;
1122
+ stopped = false;
1123
+ constructor(opts) {
1124
+ this.redis = opts.redis;
1125
+ this.stream = opts.stream;
1126
+ this.groupName = opts.groupName;
1127
+ this.blockMs = opts.blockMs ?? 2e3;
1128
+ this.count = opts.count ?? 10;
1129
+ }
1130
+ /**
1131
+ * XGROUP CREATE stream groupName startId MKSTREAM
1132
+ * Создаёт group (или игнорирует если уже существует — BUSYGROUP).
1133
+ * startId='$' — читать только новые; startId='0' — с самого начала.
1134
+ */
1135
+ async init(startId = "$") {
1136
+ await this.redis.xgroupCreate(this.stream, this.groupName, startId);
1137
+ }
1138
+ /**
1139
+ * XGROUP SETID — переставляет позицию group (для reset-subscription).
1140
+ * Примечание: здесь используется xgroupCreate что идемпотентно;
1141
+ * для точного SETID нужен xgroupSetId из RedisStore.
1142
+ */
1143
+ async setStartId(id) {
1144
+ await this.redis.xgroupCreate(this.stream, this.groupName, id);
1145
+ }
1146
+ /**
1147
+ * Читает PEL (pending entries) с id='0': сообщения, доставленные но не подтверждённые.
1148
+ * Вызывается при старте для recovery после крэша.
1149
+ */
1150
+ async recoverOwnPending() {
1151
+ return this.redis.xreadGroup(
1152
+ this.stream,
1153
+ this.groupName,
1154
+ CONSUMER_NAME,
1155
+ 100,
1156
+ 0,
1157
+ // blockMs=0: не блокировать при чтении PEL
1158
+ "0"
1159
+ // id='0': PEL
1160
+ );
1161
+ }
1162
+ /**
1163
+ * Асинхронный генератор сообщений.
1164
+ *
1165
+ * Порядок:
1166
+ * 1. Сначала отдаём все pending (незавершённые из предыдущей сессии).
1167
+ * 2. Затем в бесконечном цикле читаем новые (BLOCK 2000 мс).
1168
+ *
1169
+ * Цикл прерывается при вызове stop().
1170
+ * Каждое сообщение нужно подтвердить через ack(msg.id).
1171
+ */
1172
+ async *read() {
1173
+ const pending = await this.recoverOwnPending();
1174
+ for (const msg of pending) {
1175
+ yield msg;
1176
+ }
1177
+ while (!this.stopped) {
1178
+ const messages = await this.redis.xreadGroup(
1179
+ this.stream,
1180
+ this.groupName,
1181
+ CONSUMER_NAME,
1182
+ this.count,
1183
+ this.blockMs,
1184
+ ">"
1185
+ // id='>': только непрочитанные новые
1186
+ );
1187
+ for (const msg of messages) {
1188
+ if (this.stopped) return;
1189
+ yield msg;
1190
+ }
1191
+ }
1192
+ }
1193
+ /** XACK stream groupName id — подтверждает что сообщение обработано. */
1194
+ async ack(id) {
1195
+ await this.redis.xack(this.stream, this.groupName, id);
1196
+ }
1197
+ /** Сигнализирует генератору прекратить чтение (graceful stop). */
1198
+ stop() {
1199
+ this.stopped = true;
1200
+ }
1201
+ };
1202
+
1203
+ // src/client/FailureTracker.ts
1204
+ var FAILURE_THRESHOLD = 3;
1205
+ var FAILURE_TTL_SECONDS = 86400;
1206
+ var FailureTracker = class {
1207
+ redis;
1208
+ chainId;
1209
+ constructor(redis, chainId) {
1210
+ this.redis = redis;
1211
+ this.chainId = chainId;
1212
+ }
1213
+ /**
1214
+ * Инкрементирует счётчик ошибок для eventId и продлевает TTL хэша.
1215
+ * @returns Новое значение счётчика (1, 2, 3, …).
1216
+ */
1217
+ async recordFailure(subId, eventId) {
1218
+ const key = RedisKeys.subFailuresHash(subId);
1219
+ const count = await this.redis.hincrby(key, eventId, 1);
1220
+ await this.redis.expire(key, FAILURE_TTL_SECONDS);
1221
+ return count;
1222
+ }
1223
+ /**
1224
+ * Возвращает текущий счётчик ошибок для eventId.
1225
+ * 0 если счётчик не существует (событие ещё не проваливалось).
1226
+ */
1227
+ async getFailureCount(subId, eventId) {
1228
+ const key = RedisKeys.subFailuresHash(subId);
1229
+ const val = await this.redis.hget(key, eventId);
1230
+ return val ? parseInt(val, 10) : 0;
1231
+ }
1232
+ /**
1233
+ * Проверяет достиг ли счётчик порога для dead-letter.
1234
+ * Вызывается после recordFailure: if (shouldDeadLetter(count)) { … }
1235
+ */
1236
+ shouldDeadLetter(count) {
1237
+ return count >= FAILURE_THRESHOLD;
1238
+ }
1239
+ /**
1240
+ * Записывает событие в dead-letter stream с метаданными об ошибке.
1241
+ * Dead-letter stream: ce:parser:<chainId>:dead:<subId>
1242
+ * Поля записи: data (оригинальный payload), failureCount, lastError, subId.
1243
+ */
1244
+ async routeToDeadLetter(subId, eventId, payload, lastError) {
1245
+ const stream = RedisKeys.deadLetterStream(this.chainId, subId);
1246
+ await this.redis.xadd(stream, {
1247
+ ...payload,
1248
+ failureCount: String(FAILURE_THRESHOLD),
1249
+ lastError,
1250
+ subId
1251
+ });
1252
+ }
1253
+ /**
1254
+ * Сбрасывает счётчик ошибок для eventId после успешной обработки.
1255
+ * Вызывается после успешного yield события в ParserClient.
1256
+ */
1257
+ async clearFailure(subId, eventId) {
1258
+ await this.redis.hdel(RedisKeys.subFailuresHash(subId), eventId);
1259
+ }
1260
+ };
1261
+
1262
+ // src/client/filters.ts
1263
+ function matchesWildcard(value, pattern) {
1264
+ if (pattern === void 0 || pattern === "*") return true;
1265
+ return value === pattern;
1266
+ }
1267
+ function matchAction(event, filter) {
1268
+ if (!matchesWildcard(event.account, filter.account)) return false;
1269
+ if (!matchesWildcard(event.name, filter.name)) return false;
1270
+ if (filter.data) {
1271
+ for (const [k, v] of Object.entries(filter.data)) {
1272
+ if (event.data[k] !== v) return false;
1273
+ }
1274
+ }
1275
+ return true;
1276
+ }
1277
+ function matchDelta(event, filter) {
1278
+ if (!matchesWildcard(event.code, filter.code)) return false;
1279
+ if (!matchesWildcard(event.table, filter.table)) return false;
1280
+ if (!matchesWildcard(event.scope, filter.scope)) return false;
1281
+ return true;
1282
+ }
1283
+ function matchNativeDelta(event, filter) {
1284
+ return matchesWildcard(event.table, filter.table);
1285
+ }
1286
+ function matchOne(event, filter) {
1287
+ if (event.kind !== filter.kind) return false;
1288
+ if (filter.kind === "action") return matchAction(event, filter);
1289
+ if (filter.kind === "delta") return matchDelta(event, filter);
1290
+ if (filter.kind === "native-delta") return matchNativeDelta(event, filter);
1291
+ if (filter.kind === "fork") return true;
1292
+ const _exhaustive = filter;
1293
+ return _exhaustive;
1294
+ }
1295
+ function matchFilters(event, filters) {
1296
+ if (!filters || filters.length === 0) return true;
1297
+ return filters.some((f) => matchOne(event, f));
1298
+ }
1299
+
1300
+ // src/client/ParserClient.ts
1301
+ var ParserClient = class {
1302
+ opts;
1303
+ redis = null;
1304
+ lock = null;
1305
+ consumer = null;
1306
+ failureTracker = null;
1307
+ /** Уникальный ID этого экземпляра — используется как значение distributed lock'а. */
1308
+ instanceId;
1309
+ closed = false;
1310
+ constructor(opts) {
1311
+ this.opts = opts;
1312
+ this.instanceId = `${hostname()}:${process.pid}:${randomUUID()}`;
1313
+ }
1314
+ /**
1315
+ * Основной AsyncGenerator: инициализирует подключение и начинает yield событий.
1316
+ *
1317
+ * Этапы старта:
1318
+ * 1. Подключаемся к Redis.
1319
+ * 2. Регистрируем подписку в HSET parser:subs.
1320
+ * 3. Пытаемся захватить lock; если занят — ждём освобождения.
1321
+ * 4. Определяем startId для consumer group.
1322
+ * 5. Создаём consumer group (XGROUP CREATE).
1323
+ * 6. Читаем события в цикле, фильтруем, yield'им.
1324
+ *
1325
+ * Генератор завершается при вызове close().
1326
+ */
1327
+ async *stream() {
1328
+ this.redis = new IoRedisStore(this.opts.redis);
1329
+ await this.redis.connect();
1330
+ const subId = this.opts.subscriptionId;
1331
+ const chainId = this.opts.chain.id;
1332
+ const stream = RedisKeys.eventsStream(chainId);
1333
+ const groupName = subId;
1334
+ this.failureTracker = new FailureTracker(this.redis, chainId);
1335
+ await this.redis.hset(RedisKeys.subsHash(), {
1336
+ [subId]: JSON.stringify({
1337
+ subId,
1338
+ filters: this.opts.filters ?? [],
1339
+ startFrom: this.opts.startFrom ?? "last_known",
1340
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString()
1341
+ })
1342
+ });
1343
+ const lockOpts = {
1344
+ redis: this.redis,
1345
+ subId,
1346
+ instanceId: this.instanceId
1347
+ };
1348
+ if (this.opts.acquireLockTimeoutMs !== void 0) {
1349
+ lockOpts.acquireLockTimeoutMs = this.opts.acquireLockTimeoutMs;
1350
+ }
1351
+ this.lock = new SubscriptionLock(lockOpts);
1352
+ if (!this.opts.noSignalHandlers) {
1353
+ const close = () => void this.close();
1354
+ process.once("SIGTERM", close);
1355
+ process.once("SIGINT", close);
1356
+ }
1357
+ const acquired = await this.lock.acquire();
1358
+ if (!acquired) {
1359
+ await this.lock.waitForPromotion();
1360
+ }
1361
+ const startFrom = this.opts.startFrom ?? "last_known";
1362
+ let startId;
1363
+ if (startFrom === "last_known") {
1364
+ startId = "$";
1365
+ } else if (startFrom === "head-minus-1000") {
1366
+ startId = "0";
1367
+ } else {
1368
+ startId = `${startFrom}-0`;
1369
+ }
1370
+ this.consumer = new RedisConsumer({
1371
+ redis: this.redis,
1372
+ stream,
1373
+ groupName,
1374
+ blockMs: 2e3
1375
+ });
1376
+ await this.consumer.init(startId);
1377
+ for await (const msg of this.consumer.read()) {
1378
+ if (this.closed) break;
1379
+ const rawData = msg.fields["data"];
1380
+ if (!rawData) {
1381
+ await this.consumer.ack(msg.id);
1382
+ continue;
1383
+ }
1384
+ let event;
1385
+ try {
1386
+ event = JSON.parse(rawData);
1387
+ if (event.kind === "action" && typeof event.global_sequence === "string") {
1388
+ event = { ...event, global_sequence: BigInt(event.global_sequence) };
1389
+ }
1390
+ } catch {
1391
+ await this.consumer.ack(msg.id);
1392
+ continue;
1393
+ }
1394
+ if (!matchFilters(event, this.opts.filters)) {
1395
+ await this.consumer.ack(msg.id);
1396
+ continue;
1397
+ }
1398
+ try {
1399
+ yield event;
1400
+ await this.consumer.ack(msg.id);
1401
+ await this.failureTracker.clearFailure(subId, event.event_id);
1402
+ } catch (err) {
1403
+ const count = await this.failureTracker.recordFailure(subId, event.event_id);
1404
+ if (this.failureTracker.shouldDeadLetter(count)) {
1405
+ await this.failureTracker.routeToDeadLetter(
1406
+ subId,
1407
+ event.event_id,
1408
+ msg.fields,
1409
+ err instanceof Error ? err.message : String(err)
1410
+ );
1411
+ await this.consumer.ack(msg.id);
1412
+ await this.failureTracker.clearFailure(subId, event.event_id);
1413
+ }
1414
+ }
1415
+ }
1416
+ }
1417
+ /**
1418
+ * Graceful shutdown: останавливает генератор, освобождает lock, закрывает Redis.
1419
+ */
1420
+ async close() {
1421
+ this.closed = true;
1422
+ this.consumer?.stop();
1423
+ if (this.lock) {
1424
+ this.lock.stopHeartbeat();
1425
+ await this.lock.release();
1426
+ }
1427
+ if (this.redis) {
1428
+ await this.redis.quit();
1429
+ this.redis = null;
1430
+ }
1431
+ }
1432
+ /** Закрывает клиент и завершает процесс (для использования в SIGINT-обработчике). */
1433
+ closeAndExit() {
1434
+ void (async () => {
1435
+ await this.close();
1436
+ process.exit(0);
1437
+ })();
1438
+ }
1439
+ };
1440
+
1441
+ // src/index.ts
1442
+ import { NATIVE_TABLE_NAMES, isNativeTableName as isNativeTableName2 } from "@coopenomics/coopos-ship-reader";
1443
+
1444
+ // src/core/ReconnectSupervisor.ts
1445
+ var DEFAULT_BACKOFF_SECONDS = [1, 2, 5, 15, 60];
1446
+ var DEFAULT_MAX_ATTEMPTS = 10;
1447
+ function sleep2(ms) {
1448
+ return new Promise((r) => setTimeout(r, ms));
1449
+ }
1450
+ var ReconnectSupervisor = class {
1451
+ maxAttempts;
1452
+ backoffSeconds;
1453
+ onAttempt;
1454
+ onGiveUp;
1455
+ constructor(opts = {}) {
1456
+ this.maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1457
+ this.backoffSeconds = opts.backoffSeconds ?? DEFAULT_BACKOFF_SECONDS;
1458
+ this.onAttempt = opts.onAttempt ?? (() => void 0);
1459
+ this.onGiveUp = opts.onGiveUp ?? ((n) => {
1460
+ process.stderr.write(`ReconnectSupervisor: exhausted ${n} attempts, exiting
1461
+ `);
1462
+ process.exit(1);
1463
+ });
1464
+ }
1465
+ /**
1466
+ * Запускает fn и повторяет при исключении с паузами.
1467
+ *
1468
+ * Псевдокод:
1469
+ * loop:
1470
+ * try: return await fn()
1471
+ * catch: attempt++
1472
+ * if attempt >= maxAttempts: onGiveUp(); throw
1473
+ * delay = backoffSeconds[min(attempt-1, len-1)] * 1000
1474
+ * onAttempt(attempt, delay); sleep(delay)
1475
+ *
1476
+ * @returns Результат первого успешного вызова fn.
1477
+ */
1478
+ async run(fn) {
1479
+ let attempt = 0;
1480
+ for (; ; ) {
1481
+ try {
1482
+ return await fn();
1483
+ } catch (err) {
1484
+ attempt++;
1485
+ if (attempt >= this.maxAttempts) {
1486
+ this.onGiveUp(attempt);
1487
+ throw err;
1488
+ }
1489
+ const backoffIdx = Math.min(attempt - 1, this.backoffSeconds.length - 1);
1490
+ const delayMs = (this.backoffSeconds[backoffIdx] ?? 60) * 1e3;
1491
+ this.onAttempt(attempt, delayMs);
1492
+ await sleep2(delayMs);
1493
+ }
1494
+ }
1495
+ }
1496
+ };
1497
+
1498
+ // src/logger.ts
1499
+ import pino from "pino";
1500
+ var REDACT_PATHS = [
1501
+ "password",
1502
+ "token",
1503
+ "secret",
1504
+ "authorization",
1505
+ "*.password",
1506
+ "*.token",
1507
+ "*.secret",
1508
+ "*.authorization",
1509
+ "redis.password",
1510
+ "redis.url"
1511
+ // Redis URL может содержать пароль в строке подключения
1512
+ ];
1513
+ function createLogger(opts = {}) {
1514
+ const level = opts.level ?? (process.env["LOG_LEVEL"] ?? "info");
1515
+ const pretty = opts.pretty ?? process.env["NODE_ENV"] === "development";
1516
+ const base = {};
1517
+ if (opts.chain_id) base["chain_id"] = opts.chain_id;
1518
+ const transport = pretty ? { target: "pino-pretty", options: { colorize: true, translateTime: "SYS:standard" } } : void 0;
1519
+ const logger = pino(
1520
+ {
1521
+ level,
1522
+ redact: { paths: REDACT_PATHS, censor: "[REDACTED]" },
1523
+ base
1524
+ },
1525
+ transport ? pino.transport(transport) : pino.destination(1)
1526
+ );
1527
+ return logger;
1528
+ }
1529
+ var rootLogger = createLogger();
1530
+
1531
+ // src/metrics/parserMetrics.ts
1532
+ import {
1533
+ Counter,
1534
+ Gauge,
1535
+ Histogram,
1536
+ Registry
1537
+ } from "prom-client";
1538
+ function createParserMetrics(prefix = "parser") {
1539
+ const registry = new Registry();
1540
+ const blocksProcessedTotal = new Counter({
1541
+ name: `${prefix}_blocks_processed_total`,
1542
+ help: "Total number of blocks processed by the parser",
1543
+ registers: [registry]
1544
+ });
1545
+ const indexingLagSeconds = new Gauge({
1546
+ name: `${prefix}_indexing_lag_seconds`,
1547
+ help: "Lag between head block time and current block time in seconds",
1548
+ registers: [registry]
1549
+ });
1550
+ const eventsPublishedTotal = new Counter({
1551
+ name: `${prefix}_events_published_total`,
1552
+ help: "Total events published to Redis stream",
1553
+ labelNames: ["kind"],
1554
+ registers: [registry]
1555
+ });
1556
+ const abiCacheHitsTotal = new Counter({
1557
+ name: `${prefix}_abi_cache_hits_total`,
1558
+ help: "Number of ABI cache hits",
1559
+ registers: [registry]
1560
+ });
1561
+ const abiCacheMissesTotal = new Counter({
1562
+ name: `${prefix}_abi_cache_misses_total`,
1563
+ help: "Number of ABI cache misses (fetched from chain)",
1564
+ registers: [registry]
1565
+ });
1566
+ const streamLength = new Gauge({
1567
+ name: `${prefix}_stream_length`,
1568
+ help: "Current length of the Redis events stream",
1569
+ registers: [registry]
1570
+ });
1571
+ const xtrimmedEntriesTotal = new Counter({
1572
+ name: `${prefix}_xtrimmed_entries_total`,
1573
+ help: "Total number of entries removed by XTRIM",
1574
+ registers: [registry]
1575
+ });
1576
+ const blockProcessingDuration = new Histogram({
1577
+ name: `${prefix}_block_processing_duration_seconds`,
1578
+ help: "Duration of block processing in seconds",
1579
+ buckets: [1e-3, 5e-3, 0.01, 0.05, 0.1, 0.5, 1, 5],
1580
+ registers: [registry]
1581
+ });
1582
+ const workerPoolQueueDepth = new Gauge({
1583
+ name: `${prefix}_worker_pool_queue_depth`,
1584
+ help: "Current number of tasks queued in the Piscina worker pool",
1585
+ registers: [registry]
1586
+ });
1587
+ const blockProcessingErrors = new Counter({
1588
+ name: `${prefix}_block_processing_errors_total`,
1589
+ help: "Total number of block processing errors",
1590
+ registers: [registry]
1591
+ });
1592
+ return {
1593
+ registry,
1594
+ blocksProcessedTotal,
1595
+ indexingLagSeconds,
1596
+ eventsPublishedTotal,
1597
+ abiCacheHitsTotal,
1598
+ abiCacheMissesTotal,
1599
+ streamLength,
1600
+ xtrimmedEntriesTotal,
1601
+ blockProcessingDuration,
1602
+ workerPoolQueueDepth,
1603
+ blockProcessingErrors
1604
+ };
1605
+ }
1606
+
1607
+ // src/metrics/clientMetrics.ts
1608
+ import {
1609
+ Counter as Counter2,
1610
+ Gauge as Gauge2,
1611
+ Histogram as Histogram2,
1612
+ Registry as Registry2
1613
+ } from "prom-client";
1614
+ function createClientMetrics(prefix = "parser_client") {
1615
+ const registry = new Registry2();
1616
+ const handlerErrorsTotal = new Counter2({
1617
+ name: `${prefix}_handler_errors_total`,
1618
+ help: "Total errors thrown by subscription event handlers",
1619
+ labelNames: ["sub_id", "kind"],
1620
+ registers: [registry]
1621
+ });
1622
+ const handlerDurationSeconds = new Histogram2({
1623
+ name: `${prefix}_handler_duration_seconds`,
1624
+ help: "Duration of subscription handler execution in seconds",
1625
+ labelNames: ["sub_id", "kind"],
1626
+ buckets: [1e-3, 5e-3, 0.01, 0.05, 0.1, 0.5, 1],
1627
+ registers: [registry]
1628
+ });
1629
+ const subscriptionLockState = new Gauge2({
1630
+ name: `${prefix}_subscription_lock_state`,
1631
+ help: "Lock state gauge: 1=active, 0=standby/acquiring",
1632
+ labelNames: ["sub_id", "role"],
1633
+ registers: [registry]
1634
+ });
1635
+ const deadLettersTotal = new Counter2({
1636
+ name: `${prefix}_dead_letters_total`,
1637
+ help: "Total messages routed to dead-letter stream",
1638
+ labelNames: ["sub_id"],
1639
+ registers: [registry]
1640
+ });
1641
+ const messagesConsumedTotal = new Counter2({
1642
+ name: `${prefix}_messages_consumed_total`,
1643
+ help: "Total messages read from the events stream",
1644
+ labelNames: ["sub_id"],
1645
+ registers: [registry]
1646
+ });
1647
+ const messageAcknowledgedTotal = new Counter2({
1648
+ name: `${prefix}_messages_acknowledged_total`,
1649
+ help: "Total messages acknowledged (XACK)",
1650
+ labelNames: ["sub_id"],
1651
+ registers: [registry]
1652
+ });
1653
+ const consumerPendingMessages = new Gauge2({
1654
+ name: `${prefix}_consumer_pending_messages`,
1655
+ help: "Current number of pending (unacknowledged) messages for this consumer",
1656
+ labelNames: ["sub_id"],
1657
+ registers: [registry]
1658
+ });
1659
+ const filterMatchesTotal = new Counter2({
1660
+ name: `${prefix}_filter_matches_total`,
1661
+ help: "Total events that matched subscription filters",
1662
+ labelNames: ["sub_id", "kind"],
1663
+ registers: [registry]
1664
+ });
1665
+ return {
1666
+ registry,
1667
+ handlerErrorsTotal,
1668
+ handlerDurationSeconds,
1669
+ subscriptionLockState,
1670
+ deadLettersTotal,
1671
+ messagesConsumedTotal,
1672
+ messageAcknowledgedTotal,
1673
+ consumerPendingMessages,
1674
+ filterMatchesTotal
1675
+ };
1676
+ }
1677
+
1678
+ // src/observability/HttpServer.ts
1679
+ import { createServer } from "http";
1680
+ var HttpServer = class {
1681
+ server;
1682
+ port;
1683
+ lagThresholdSeconds;
1684
+ getLag;
1685
+ metricsRegistry;
1686
+ constructor(opts) {
1687
+ this.port = opts.port ?? 9090;
1688
+ this.lagThresholdSeconds = opts.lagThresholdSeconds ?? 60;
1689
+ this.getLag = opts.getLag;
1690
+ this.metricsRegistry = opts.metricsRegistry;
1691
+ this.server = createServer((req, res) => void this.handle(req, res));
1692
+ }
1693
+ /** Запускает HTTP-сервер, разрешает Promise после успешного bind. */
1694
+ start() {
1695
+ return new Promise((resolve2, reject) => {
1696
+ this.server.listen(this.port, () => resolve2());
1697
+ this.server.once("error", reject);
1698
+ });
1699
+ }
1700
+ /**
1701
+ * Останавливает сервер и ждёт закрытия всех соединений.
1702
+ * Вызывается при graceful shutdown парсера.
1703
+ */
1704
+ stop() {
1705
+ return new Promise((resolve2, reject) => {
1706
+ this.server.close((err) => err ? reject(err) : resolve2());
1707
+ });
1708
+ }
1709
+ /** Диспетчеризация HTTP-запросов по URL. */
1710
+ async handle(req, res) {
1711
+ const url = req.url ?? "/";
1712
+ if (url === "/health" || url === "/health/") {
1713
+ const lagSeconds = this.getLag();
1714
+ const degraded = lagSeconds > this.lagThresholdSeconds;
1715
+ const body = {
1716
+ status: degraded ? "degraded" : "ok",
1717
+ indexingLagSeconds: lagSeconds,
1718
+ lagThresholdSeconds: this.lagThresholdSeconds
1719
+ };
1720
+ res.writeHead(degraded ? 503 : 200, { "Content-Type": "application/json" });
1721
+ res.end(JSON.stringify(body));
1722
+ return;
1723
+ }
1724
+ if (url === "/metrics" || url === "/metrics/") {
1725
+ try {
1726
+ const content = await this.metricsRegistry.metrics();
1727
+ res.writeHead(200, { "Content-Type": this.metricsRegistry.contentType });
1728
+ res.end(content);
1729
+ } catch (err) {
1730
+ res.writeHead(500);
1731
+ res.end("Internal error");
1732
+ }
1733
+ return;
1734
+ }
1735
+ res.writeHead(404);
1736
+ res.end("Not found");
1737
+ }
1738
+ };
1739
+ export {
1740
+ AbiBootstrapper,
1741
+ AbiNotFoundError,
1742
+ AbiStore,
1743
+ BlockProcessor,
1744
+ CONSUMER_NAME,
1745
+ ChainIdMismatchError,
1746
+ ConfigSecurityError,
1747
+ ConfigValidationError,
1748
+ FailureTracker,
1749
+ ForkDetector,
1750
+ HttpServer,
1751
+ IoRedisStore,
1752
+ NATIVE_TABLE_NAMES,
1753
+ NotImplementedError,
1754
+ Parser,
1755
+ ParserClient,
1756
+ ReconnectSupervisor,
1757
+ RedisConsumer,
1758
+ RedisKeys,
1759
+ ShipReaderAdapter,
1760
+ SubscriptionLock,
1761
+ WorkerPool,
1762
+ XtrimSupervisor,
1763
+ computeEventId,
1764
+ createClientMetrics,
1765
+ createLogger,
1766
+ createParserMetrics,
1767
+ fromConfigFile,
1768
+ isNativeTableName2 as isNativeTableName,
1769
+ matchFilters,
1770
+ parseConfig,
1771
+ rootLogger
1772
+ };
1773
+ //# sourceMappingURL=index.js.map