[549-27][@ant] refactor: вернуть имя parser2 обратно (полный реверт 549-20) — существующий @coopenomics/parser@2026.x в mono-ai живёт в продакшене (dicoop/cooparser всё ещё деплоится), параллельный publish @coopenomics/parser@1.0.x из этого репо создавал коллизию ожиданий у потребителей; раздельные имена parser2/parser устраняют путаницу. Папка packages/parser→parser2, пакет @coopenomics/parser→@coopenomics/parser2, docker image dicoop/parser→dicoop/parser2, Redis prefix parser:→parser2: (ce:parser:, abi, sub, sync, subs, reparse), метрики parser_*→parser2_*, bin parser2. Версия bumpnута до 1.0.2 для нового публичного пакета @coopenomics/parser2

This commit is contained in:
coopops
2026-04-23 14:43:22 +00:00
parent aff8a2451d
commit 167c885a70
89 changed files with 135 additions and 135 deletions
+6 -6
View File
@@ -36,7 +36,7 @@ jobs:
with: { node-version: '20', cache: 'pnpm' } with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build - run: pnpm --filter @coopenomics/coopos-ship-reader build
- run: pnpm --filter @coopenomics/parser typecheck - run: pnpm --filter @coopenomics/parser2 typecheck
unit: unit:
name: Unit tests name: Unit tests
@@ -50,7 +50,7 @@ jobs:
with: { node-version: '20', cache: 'pnpm' } with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build - run: pnpm --filter @coopenomics/coopos-ship-reader build
- run: pnpm --filter @coopenomics/parser test:unit -- --reporter=verbose --coverage - run: pnpm --filter @coopenomics/parser2 test:unit -- --reporter=verbose --coverage
- uses: codecov/codecov-action@v4 - uses: codecov/codecov-action@v4
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -79,7 +79,7 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build - run: pnpm --filter @coopenomics/coopos-ship-reader build
# Parser2 build: нужен dist/deserialize.worker.cjs для Piscina worker pool # Parser2 build: нужен dist/deserialize.worker.cjs для Piscina worker pool
- run: pnpm --filter @coopenomics/parser build - run: pnpm --filter @coopenomics/parser2 build
# Запускаем nodeos вручную через docker run: образ dicoop/blockchain:v5.1.0-dev # Запускаем nodeos вручную через docker run: образ dicoop/blockchain:v5.1.0-dev
# не имеет CMD по умолчанию, и GitHub Actions services: не поддерживает # не имеет CMD по умолчанию, и GitHub Actions services: не поддерживает
# передачу команды контейнеру. Монтируем config.ini (SHiP на 8080) и genesis.json. # передачу команды контейнеру. Монтируем config.ini (SHiP на 8080) и genesis.json.
@@ -87,7 +87,7 @@ jobs:
run: | run: |
docker run -d --name blockchain \ docker run -d --name blockchain \
-p 8888:8888 -p 8080:8080 -p 9876:9876 \ -p 8888:8888 -p 8080:8080 -p 9876:9876 \
-v "${GITHUB_WORKSPACE}/packages/parser/test/integration/node-config:/mnt/dev/config" \ -v "${GITHUB_WORKSPACE}/packages/parser2/test/integration/node-config:/mnt/dev/config" \
dicoop/blockchain:v5.1.0-dev \ dicoop/blockchain:v5.1.0-dev \
/bin/bash -c '/usr/local/bin/nodeos -d /mnt/dev/data -p eosio --config-dir /mnt/dev/config --genesis-json /mnt/dev/config/genesis.json --disable-replay-opts' /bin/bash -c '/usr/local/bin/nodeos -d /mnt/dev/data -p eosio --config-dir /mnt/dev/config --genesis-json /mnt/dev/config/genesis.json --disable-replay-opts'
sleep 3 sleep 3
@@ -104,7 +104,7 @@ jobs:
echo "ERROR: Chain API did not become ready. Container logs:" echo "ERROR: Chain API did not become ready. Container logs:"
docker logs blockchain | tail -100 docker logs blockchain | tail -100
exit 1 exit 1
- run: pnpm --filter @coopenomics/parser test:integration - run: pnpm --filter @coopenomics/parser2 test:integration
env: env:
REDIS_URL: redis://localhost:6379 REDIS_URL: redis://localhost:6379
CHAIN_URL: http://localhost:8888 CHAIN_URL: http://localhost:8888
@@ -125,4 +125,4 @@ jobs:
with: { node-version: '20', cache: 'pnpm' } with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build - run: pnpm --filter @coopenomics/coopos-ship-reader build
- run: pnpm --filter @coopenomics/parser build - run: pnpm --filter @coopenomics/parser2 build
+7 -7
View File
@@ -1,7 +1,7 @@
name: Release name: Release
# Запускается при каждом push в main (обычно — merge PR из dev). # Запускается при каждом push в main (обычно — merge PR из dev).
# Если версия в packages/parser/package.json изменилась (не совпадает с # Если версия в packages/parser2/package.json изменилась (не совпадает с
# существующим git-тегом v<version>) — тогда: # существующим git-тегом v<version>) — тогда:
# 1. Публикуем workspace-пакеты в npm # 1. Публикуем workspace-пакеты в npm
# 2. Создаём git-тег v<version> и пушим его # 2. Создаём git-тег v<version> и пушим его
@@ -50,12 +50,12 @@ jobs:
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
# Читаем версию из packages/parser/package.json и проверяем # Читаем версию из packages/parser2/package.json и проверяем
# что тега v<version> ещё не существует. # что тега v<version> ещё не существует.
- name: Check version change - name: Check version change
id: ver id: ver
run: | run: |
VERSION=$(node -p "require('./packages/parser/package.json').version") VERSION=$(node -p "require('./packages/parser2/package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
if git rev-parse "v$VERSION" >/dev/null 2>&1; then if git rev-parse "v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists — nothing to release" echo "Tag v$VERSION already exists — nothing to release"
@@ -69,13 +69,13 @@ jobs:
if: steps.ver.outputs.released == 'true' if: steps.ver.outputs.released == 'true'
run: | run: |
pnpm --filter @coopenomics/coopos-ship-reader build pnpm --filter @coopenomics/coopos-ship-reader build
pnpm --filter @coopenomics/parser build pnpm --filter @coopenomics/parser2 build
# Настраиваем npm auth для lerna/pnpm: NODE_AUTH_TOKEN из setup-node # Настраиваем npm auth для lerna/pnpm: NODE_AUTH_TOKEN из setup-node
# уже прописывает _authToken в .npmrc, но lerna внутри читает из npm cli, # уже прописывает _authToken в .npmrc, но lerna внутри читает из npm cli,
# поэтому дублируем через env NPM_TOKEN → npm_config_//registry.npmjs.org/:_authToken. # поэтому дублируем через env NPM_TOKEN → npm_config_//registry.npmjs.org/:_authToken.
# npm trusted publishing: OIDC через id-token: write, без NPM_TOKEN. # npm trusted publishing: OIDC через id-token: write, без NPM_TOKEN.
# Настраивается на стороне npm: packages/coopenomics-parser/settings/access # Настраивается на стороне npm: packages/coopenomics-parser2/settings/access
# → Trusted Publisher → GitHub Actions (repo=coopenomics/parser, # → Trusted Publisher → GitHub Actions (repo=coopenomics/parser,
# workflow=.github/workflows/release.yml, environment=""). # workflow=.github/workflows/release.yml, environment="").
# Provenance включён — sigstore подпишет каждый publish. # Provenance включён — sigstore подпишет каждый publish.
@@ -115,7 +115,7 @@ jobs:
with: { node-version: '20', cache: 'pnpm' } with: { node-version: '20', cache: 'pnpm' }
- run: pnpm install --frozen-lockfile - run: pnpm install --frozen-lockfile
- run: pnpm --filter @coopenomics/coopos-ship-reader build - run: pnpm --filter @coopenomics/coopos-ship-reader build
- run: pnpm --filter @coopenomics/parser build - run: pnpm --filter @coopenomics/parser2 build
- uses: docker/setup-buildx-action@v3 - uses: docker/setup-buildx-action@v3
# Docker Hub auth: требует secrets DOCKERHUB_USERNAME + DOCKERHUB_TOKEN # Docker Hub auth: требует secrets DOCKERHUB_USERNAME + DOCKERHUB_TOKEN
@@ -128,7 +128,7 @@ jobs:
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: dicoop/parser images: dicoop/parser2
tags: | tags: |
type=semver,pattern={{version}},value=v${{ needs.release.outputs.version }} type=semver,pattern={{version}},value=v${{ needs.release.outputs.version }}
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.release.outputs.version }} type=semver,pattern={{major}}.{{minor}},value=v${{ needs.release.outputs.version }}
+17 -17
View File
@@ -3,7 +3,7 @@
Универсальный индексер блокчейнов EOSIO / Antelope: читает блоки из State History Plugin (SHiP) по WebSocket, декодирует actions и дельты таблиц с учётом исторических ABI, публикует унифицированный поток событий в Redis Stream. Потребители получают события через `ParserClient` с single-active-consumer lock'ом, recovery после сбоев и dead-letter для poison-messages. Универсальный индексер блокчейнов EOSIO / Antelope: читает блоки из State History Plugin (SHiP) по WebSocket, декодирует actions и дельты таблиц с учётом исторических ABI, публикует унифицированный поток событий в Redis Stream. Потребители получают события через `ParserClient` с single-active-consumer lock'ом, recovery после сбоев и dead-letter для poison-messages.
[![CI](https://github.com/coopenomics/parser/actions/workflows/ci.yml/badge.svg)](https://github.com/coopenomics/parser/actions/workflows/ci.yml) [![CI](https://github.com/coopenomics/parser/actions/workflows/ci.yml/badge.svg)](https://github.com/coopenomics/parser/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/@coopenomics/parser.svg)](https://www.npmjs.com/package/@coopenomics/parser) [![npm](https://img.shields.io/npm/v/@coopenomics/parser2.svg)](https://www.npmjs.com/package/@coopenomics/parser2)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
## Зачем ## Зачем
@@ -14,7 +14,7 @@ SHiP отдаёт сырые бинарные блоки — чтобы прев
| Пакет | Описание | npm | | Пакет | Описание | npm |
|:---|:---|:---| |:---|:---|:---|
| [`@coopenomics/parser`](packages/parser) | Ядро индексера (Parser) + подписочный клиент (ParserClient), CLI, observability | [`@coopenomics/parser`](https://www.npmjs.com/package/@coopenomics/parser) | | [`@coopenomics/parser2`](packages/parser2) | Ядро индексера (Parser) + подписочный клиент (ParserClient), CLI, observability | [`@coopenomics/parser2`](https://www.npmjs.com/package/@coopenomics/parser2) |
| [`@coopenomics/coopos-ship-reader`](packages/ship-reader) | Низкоуровневый WebSocket SHiP клиент с поддержкой 24 нативных системных таблиц | [`@coopenomics/coopos-ship-reader`](https://www.npmjs.com/package/@coopenomics/coopos-ship-reader) | | [`@coopenomics/coopos-ship-reader`](packages/ship-reader) | Низкоуровневый WebSocket SHiP клиент с поддержкой 24 нативных системных таблиц | [`@coopenomics/coopos-ship-reader`](https://www.npmjs.com/package/@coopenomics/coopos-ship-reader) |
## Быстрый старт ## Быстрый старт
@@ -22,7 +22,7 @@ SHiP отдаёт сырые бинарные блоки — чтобы прев
### 1. Установка ### 1. Установка
```bash ```bash
pnpm add @coopenomics/parser pnpm add @coopenomics/parser2
# или если используете только SHiP-клиент без Redis-пайплайна: # или если используете только SHiP-клиент без Redis-пайплайна:
pnpm add @coopenomics/coopos-ship-reader pnpm add @coopenomics/coopos-ship-reader
``` ```
@@ -66,12 +66,12 @@ metrics:
parser start --config parser.config.yaml parser start --config parser.config.yaml
``` ```
Парсер подключится к SHiP, начнёт читать блоки с последней checkpoint-позиции (или с head если запускается впервые), и публиковать события в Redis stream `ce:parser:<chainId>:events`. Парсер подключится к SHiP, начнёт читать блоки с последней checkpoint-позиции (или с head если запускается впервые), и публиковать события в Redis stream `ce:parser2:<chainId>:events`.
### 3. Подписка на события из приложения ### 3. Подписка на события из приложения
```typescript ```typescript
import { ParserClient } from '@coopenomics/parser' import { ParserClient } from '@coopenomics/parser2'
const client = new ParserClient({ const client = new ParserClient({
subscriptionId: 'my-app', subscriptionId: 'my-app',
@@ -118,12 +118,12 @@ parser abi-prune --keep-last 10 --all-contracts
## Docker ## Docker
Публичный образ: [`dicoop/parser`](https://hub.docker.com/r/dicoop/parser) — multi-arch (`linux/amd64`, `linux/arm64`). Публичный образ: [`dicoop/parser2`](https://hub.docker.com/r/dicoop/parser2) — multi-arch (`linux/amd64`, `linux/arm64`).
```bash ```bash
docker pull dicoop/parser:latest docker pull dicoop/parser2:latest
# или конкретная версия # или конкретная версия
docker pull dicoop/parser:1.0.1 docker pull dicoop/parser2:1.0.1
``` ```
### Минимальный запуск ### Минимальный запуск
@@ -134,7 +134,7 @@ docker pull dicoop/parser:1.0.1
docker run --rm \ docker run --rm \
-v $(pwd)/parser.config.yaml:/app/parser.config.yaml:ro \ -v $(pwd)/parser.config.yaml:/app/parser.config.yaml:ro \
--network host \ --network host \
dicoop/parser:latest \ dicoop/parser2:latest \
start --config /app/parser.config.yaml start --config /app/parser.config.yaml
``` ```
@@ -156,7 +156,7 @@ services:
- "6379:6379" - "6379:6379"
parser: parser:
image: dicoop/parser:1.0.1 image: dicoop/parser2:1.0.1
depends_on: depends_on:
- redis - redis
volumes: volumes:
@@ -210,7 +210,7 @@ irreversibleOnly: false # читать head-блоки (false) или жда
# Посмотреть подписки в прод-Redis # Посмотреть подписки в прод-Redis
docker run --rm --network host \ docker run --rm --network host \
-e REDIS_URL=redis://localhost:6379 \ -e REDIS_URL=redis://localhost:6379 \
dicoop/parser:latest \ dicoop/parser2:latest \
list-subscriptions list-subscriptions
``` ```
@@ -247,7 +247,7 @@ docker run --rm --network host \
``` ```
. .
├── packages/ ├── packages/
│ ├── parser/ # @coopenomics/parser — ядро + CLI + ParserClient │ ├── parser/ # @coopenomics/parser2 — ядро + CLI + ParserClient
│ └── ship-reader/ # @coopenomics/coopos-ship-reader — SHiP WS клиент │ └── ship-reader/ # @coopenomics/coopos-ship-reader — SHiP WS клиент
├── docs/ # redis taxonomy, disaster recovery ├── docs/ # redis taxonomy, disaster recovery
├── examples/ # пример verifier-like подписчика ├── examples/ # пример verifier-like подписчика
@@ -264,19 +264,19 @@ pnpm install
```bash ```bash
pnpm build # build всех пакетов pnpm build # build всех пакетов
pnpm --filter @coopenomics/parser build # только parser pnpm --filter @coopenomics/parser2 build # только parser
``` ```
### Тесты ### Тесты
```bash ```bash
pnpm test # unit во всех пакетах pnpm test # unit во всех пакетах
pnpm --filter @coopenomics/parser test:unit pnpm --filter @coopenomics/parser2 test:unit
pnpm --filter @coopenomics/parser test:integration # нужен Docker для блокчейн-ноды pnpm --filter @coopenomics/parser2 test:integration # нужен Docker для блокчейн-ноды
``` ```
**Текущее покрытие:** **Текущее покрытие:**
- `@coopenomics/parser`: 81% statements / 74% functions (205 unit-тестов) - `@coopenomics/parser2`: 81% statements / 74% functions (205 unit-тестов)
- `@coopenomics/coopos-ship-reader`: 81% statements / 86% functions (51 unit-тест) - `@coopenomics/coopos-ship-reader`: 81% statements / 86% functions (51 unit-тест)
### Бенчмарк ### Бенчмарк
@@ -308,7 +308,7 @@ pnpm release
# 5. GitHub Actions release.yml автоматически: # 5. GitHub Actions release.yml автоматически:
# • lerna publish from-package — опубликует в npm только пакеты с новой версией # • lerna publish from-package — опубликует в npm только пакеты с новой версией
# • changelogithub — создаст GitHub Release с changelog # • changelogithub — создаст GitHub Release с changelog
# • docker build-push — опубликует образ в docker.io/dicoop/parser:<version> # • docker build-push — опубликует образ в docker.io/dicoop/parser2:<version>
``` ```
Альтернативно, если lerna version не использовалась — можно вручную поднять версию в `packages/*/package.json`, смёрджить в main, и workflow опубликует. Альтернативно, если lerna version не использовалась — можно вручную поднять версию в `packages/*/package.json`, смёрджить в main, и workflow опубликует.
+7 -7
View File
@@ -1,12 +1,12 @@
# Disaster Recovery Runbook # Disaster Recovery Runbook
This document covers recovery procedures for common failure scenarios involving `@coopenomics/parser`. This document covers recovery procedures for common failure scenarios involving `@coopenomics/parser2`.
--- ---
## Scenario 1 — Redis data loss (RDB/AOF corruption or total loss) ## Scenario 1 — Redis data loss (RDB/AOF corruption or total loss)
**Symptoms:** Parser starts but emits no events; `redis-cli HGET parser:sync:<chainId> block_num` returns nil. **Symptoms:** Parser starts but emits no events; `redis-cli HGET parser2:sync:<chainId> block_num` returns nil.
**Impact:** The sync checkpoint is lost. The parser will restart from block 0 (or the last known position in config). Consumers will re-process old events — their handlers must be idempotent. **Impact:** The sync checkpoint is lost. The parser will restart from block 0 (or the last known position in config). Consumers will re-process old events — their handlers must be idempotent.
@@ -19,7 +19,7 @@ This document covers recovery procedures for common failure scenarios involving
2. **Decide start block.** If you know the last safely processed block, set it manually: 2. **Decide start block.** If you know the last safely processed block, set it manually:
```bash ```bash
redis-cli HSET parser:sync:<chainId> block_num <N> block_id <ID> last_updated $(date -u +%FT%TZ) redis-cli HSET parser2:sync:<chainId> block_num <N> block_id <ID> last_updated $(date -u +%FT%TZ)
``` ```
If unknown, leave empty and the parser will replay from 0. If unknown, leave empty and the parser will replay from 0.
@@ -30,7 +30,7 @@ This document covers recovery procedures for common failure scenarios involving
4. **Restart the parser.** 4. **Restart the parser.**
5. **Monitor lag** via `/health` endpoint or `parser_indexing_lag_seconds` Prometheus metric. 5. **Monitor lag** via `/health` endpoint or `parser2_indexing_lag_seconds` Prometheus metric.
--- ---
@@ -48,7 +48,7 @@ Simply restart the parser. The sync hash guarantees replay from the last complet
## Scenario 3 — Consumer handler failures → dead-letter accumulation ## Scenario 3 — Consumer handler failures → dead-letter accumulation
**Symptoms:** `ce:parser:<chainId>:dead:<subId>` stream grows; `parser_client_dead_letters_total` counter rising. **Symptoms:** `ce:parser2:<chainId>:dead:<subId>` stream grows; `parser2_client_dead_letters_total` counter rising.
### Recovery steps ### Recovery steps
@@ -128,6 +128,6 @@ The parser will automatically re-fetch the ABI from the chain RPC on the next bl
|---|---| |---|---|
| Parser lag | `curl http://localhost:9090/health` | | Parser lag | `curl http://localhost:9090/health` |
| Prometheus metrics | `curl http://localhost:9090/metrics` | | Prometheus metrics | `curl http://localhost:9090/metrics` |
| Stream length | `redis-cli XLEN ce:parser:<chainId>:events` | | Stream length | `redis-cli XLEN ce:parser2:<chainId>:events` |
| Sync position | `redis-cli HGETALL parser:sync:<chainId>` | | Sync position | `redis-cli HGETALL parser2:sync:<chainId>` |
| Dead letters | `parser list-dead-letters --chain <chainId> --all` | | Dead letters | `parser list-dead-letters --chain <chainId> --all` |
+11 -11
View File
@@ -1,21 +1,21 @@
# Redis Key Taxonomy # Redis Key Taxonomy
All keys used by `@coopenomics/parser` follow a consistent namespace scheme. All keys used by `@coopenomics/parser2` follow a consistent namespace scheme.
Keys produced by the **parser (indexer)** use the prefix `ce:parser:` or `parser:`. Keys produced by the **parser (indexer)** use the prefix `ce:parser2:` or `parser:`.
## Key catalogue ## Key catalogue
| Key pattern | Type | TTL | Produced by | Consumed by | Description | | Key pattern | Type | TTL | Produced by | Consumed by | Description |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| `ce:parser:{chainId}:events` | Stream | ∞ (XTRIM) | Parser | ParserClient | Unified event stream: all `action`, `delta`, `native-delta`, `fork` events | | `ce:parser2:{chainId}:events` | Stream | ∞ (XTRIM) | Parser | ParserClient | Unified event stream: all `action`, `delta`, `native-delta`, `fork` events |
| `ce:parser:{chainId}:dead:{subId}` | Stream | ∞ | FailureTracker | CLI / admin | Dead-letter stream for a specific subscription; entries include `data`, `failureCount`, `lastError`, `subId` | | `ce:parser2:{chainId}:dead:{subId}` | Stream | ∞ | FailureTracker | CLI / admin | Dead-letter stream for a specific subscription; entries include `data`, `failureCount`, `lastError`, `subId` |
| `ce:parser:{chainId}:reparse:{jobId}` | Stream | ∞ | CLI (future) | Parser | On-demand reparse job stream | | `ce:parser2:{chainId}:reparse:{jobId}` | Stream | ∞ | CLI (future) | Parser | On-demand reparse job stream |
| `parser:abi:{contract}` | Sorted Set | ∞ | AbiStore | BlockProcessor / AbiBootstrapper | ABI version history; score = block_num, member = base64-encoded raw ABI bytes | | `parser2:abi:{contract}` | Sorted Set | ∞ | AbiStore | BlockProcessor / AbiBootstrapper | ABI version history; score = block_num, member = base64-encoded raw ABI bytes |
| `parser:sync:{chainId}` | Hash | ∞ | Parser | Parser (crash-recovery) | Sync checkpoint: `block_num`, `block_id`, `last_updated` | | `parser2:sync:{chainId}` | Hash | ∞ | Parser | Parser (crash-recovery) | Sync checkpoint: `block_num`, `block_id`, `last_updated` |
| `parser:subs` | Hash | ∞ | ParserClient | CLI (list-subscriptions) | Subscription registry; field = subId, value = JSON metadata | | `parser2:subs` | Hash | ∞ | ParserClient | CLI (list-subscriptions) | Subscription registry; field = subId, value = JSON metadata |
| `parser:sub:{subId}:failures` | Hash | 24 h | FailureTracker | FailureTracker / CLI | Per-event failure counter; field = event_id, value = count | | `parser2:sub:{subId}:failures` | Hash | 24 h | FailureTracker | FailureTracker / CLI | Per-event failure counter; field = event_id, value = count |
| `parser:sub:{subId}:lock` | String | 10 s (auto-renew) | SubscriptionLock | SubscriptionLock | Active-standby lock; value = instanceId | | `parser2:sub:{subId}:lock` | String | 10 s (auto-renew) | SubscriptionLock | SubscriptionLock | Active-standby lock; value = instanceId |
| `parser:reparse:{jobId}` | Hash | ∞ | CLI (future) | Parser | Reparse job metadata | | `parser2:reparse:{jobId}` | Hash | ∞ | CLI (future) | Parser | Reparse job metadata |
## Naming rules ## Naming rules
+1 -1
View File
@@ -22,7 +22,7 @@ pnpm install
**Step 2 — Build the package** **Step 2 — Build the package**
```bash ```bash
pnpm --filter @coopenomics/parser build pnpm --filter @coopenomics/parser2 build
``` ```
**Step 3 — Configure** **Step 3 — Configure**
+2 -2
View File
@@ -8,8 +8,8 @@
* See README.md for the 5-step startup guide. * See README.md for the 5-step startup guide.
*/ */
import { Parser, ParserClient } from '@coopenomics/parser' import { Parser, ParserClient } from '@coopenomics/parser2'
import type { NativeDeltaEvent, NativePermissionRow } from '@coopenomics/parser' import type { NativeDeltaEvent, NativePermissionRow } from '@coopenomics/parser2'
// ── Configuration ──────────────────────────────────────────────────────────── // ── Configuration ────────────────────────────────────────────────────────────
+1 -1
View File
@@ -7,7 +7,7 @@
"start": "tsx index.ts" "start": "tsx index.ts"
}, },
"dependencies": { "dependencies": {
"@coopenomics/parser": "workspace:*" "@coopenomics/parser2": "workspace:*"
}, },
"devDependencies": { "devDependencies": {
"tsx": "^4.15.0" "tsx": "^4.15.0"
@@ -1,6 +1,6 @@
{ {
"name": "@coopenomics/parser", "name": "@coopenomics/parser2",
"version": "1.0.1", "version": "1.0.2",
"description": "Universal EOSIO/Antelope SHiP-to-Redis blockchain indexer (parser)", "description": "Universal EOSIO/Antelope SHiP-to-Redis blockchain indexer (parser)",
"license": "MIT", "license": "MIT",
"author": "Coopenomics contributors", "author": "Coopenomics contributors",
@@ -19,7 +19,7 @@
"module": "./dist/index.js", "module": "./dist/index.js",
"types": "./dist/index.d.ts", "types": "./dist/index.d.ts",
"bin": { "bin": {
"parser": "./dist/cli/index.js" "parser2": "./dist/cli/index.js"
}, },
"files": [ "files": [
"dist", "dist",
@@ -60,7 +60,7 @@ async function pruneContract(
* @param contract имя контракта (null если allContracts=true). * @param contract имя контракта (null если allContracts=true).
* @param olderThan block_num: удалить версии с block_num < olderThan. * @param olderThan block_num: удалить версии с block_num < olderThan.
* @param dryRun только показать, не удалять. * @param dryRun только показать, не удалять.
* @param allContracts обработать все контракты в Redis (SCAN parser:abi:*). * @param allContracts обработать все контракты в Redis (SCAN parser2:abi:*).
*/ */
export async function abiPrune( export async function abiPrune(
redis: RedisStore, redis: RedisStore,
@@ -75,7 +75,7 @@ export async function abiPrune(
if (allContracts) { if (allContracts) {
// SCAN по паттерну: находим все ABI-ключи во всех контрактах // SCAN по паттерну: находим все ABI-ключи во всех контрактах
const keys = await redis.scan('parser:abi:*') const keys = await redis.scan('parser2:abi:*')
if (keys.length === 0) { if (keys.length === 0) {
console.log('No ABI history found.') console.log('No ABI history found.')
return return
@@ -83,7 +83,7 @@ export async function abiPrune(
const rows: Array<{ contract: string; pruned: number; remaining: number }> = [] const rows: Array<{ contract: string; pruned: number; remaining: number }> = []
for (const key of keys) { for (const key of keys) {
const name = key.replace(/^parser:abi:/, '') const name = key.replace(/^parser2:abi:/, '')
try { try {
const result = await pruneContract(redis, name, olderThan, dryRun) const result = await pruneContract(redis, name, olderThan, dryRun)
rows.push({ contract: name, pruned: dryRun ? result.remaining : result.pruned, remaining: result.remaining }) rows.push({ contract: name, pruned: dryRun ? result.remaining : result.pruned, remaining: result.remaining })
@@ -3,14 +3,14 @@
* *
* Выводит содержимое dead-letter stream(ов) в табличном или JSON формате. * Выводит содержимое dead-letter stream(ов) в табличном или JSON формате.
* *
* Dead-letter stream: ce:parser:<chainId>:dead:<subId> * Dead-letter stream: ce:parser2:<chainId>:dead:<subId>
* Каждая запись содержит поля: * Каждая запись содержит поля:
* data оригинальный JSON ParserEvent * data оригинальный JSON ParserEvent
* failureCount число провалов (обычно = FAILURE_THRESHOLD = 3) * failureCount число провалов (обычно = FAILURE_THRESHOLD = 3)
* lastError текст последнего исключения * lastError текст последнего исключения
* subId идентификатор подписки-владельца * subId идентификатор подписки-владельца
* *
* Режим --all: сканирует Redis по паттерну ce:parser:<chainId>:dead:* * Режим --all: сканирует Redis по паттерну ce:parser2:<chainId>:dead:*
* и показывает все dead-letter стримы для цепи. * и показывает все dead-letter стримы для цепи.
* *
* Режим --json: выводит JSON-массив объектов с разобранными полями * Режим --json: выводит JSON-массив объектов с разобранными полями
@@ -105,10 +105,10 @@ export async function listDeadLetters(
let streams: Array<{ key: string; subId: string }> = [] let streams: Array<{ key: string; subId: string }> = []
if (all) { if (all) {
// SCAN по паттерну: ce:parser:<chainId>:dead:* // SCAN по паттерну: ce:parser2:<chainId>:dead:*
const pattern = RedisKeys.deadLetterStream(chainId, '*') const pattern = RedisKeys.deadLetterStream(chainId, '*')
const keys = await redis.scan(pattern) const keys = await redis.scan(pattern)
const prefix = `ce:parser:${chainId}:dead:` const prefix = `ce:parser2:${chainId}:dead:`
streams = keys.map(k => ({ key: k, subId: k.slice(prefix.length) })) streams = keys.map(k => ({ key: k, subId: k.slice(prefix.length) }))
if (streams.length === 0) { if (streams.length === 0) {
console.log('No dead-letter streams found.') console.log('No dead-letter streams found.')
@@ -4,10 +4,10 @@
* Показывает зарегистрированные подписки и их текущее состояние в consumer group. * Показывает зарегистрированные подписки и их текущее состояние в consumer group.
* *
* Источник данных два ключа Redis: * Источник данных два ключа Redis:
* 1. parser:subs HASH: subId JSON с метаданными подписки (filters, startFrom, registeredAt). * 1. parser2:subs HASH: subId JSON с метаданными подписки (filters, startFrom, registeredAt).
* Записывается при вызове ParserClient.subscribe(), сохраняется между перезапусками. * Записывается при вызове ParserClient.subscribe(), сохраняется между перезапусками.
* *
* 2. ce:parser:<chainId>:events Redis Stream с consumer groups. * 2. ce:parser2:<chainId>:events Redis Stream с consumer groups.
* XINFO GROUPS даёт для каждой группы: pending count, lag, last-delivered-id. * XINFO GROUPS даёт для каждой группы: pending count, lag, last-delivered-id.
* Если стрим ещё не создан (парсер не запускался) xinfoGroups выбросит ошибку; * Если стрим ещё не создан (парсер не запускался) xinfoGroups выбросит ошибку;
* мы перехватываем её и показываем все подписки как «not started». * мы перехватываем её и показываем все подписки как «not started».
@@ -26,7 +26,7 @@ import type { RedisStore } from '../../ports/RedisStore.js'
import { RedisKeys } from '../../redis/keys.js' import { RedisKeys } from '../../redis/keys.js'
/** /**
* Метаданные подписки, хранимые в Redis HASH parser:subs. * Метаданные подписки, хранимые в Redis HASH parser2:subs.
* Сохраняются при registerSubscription() и используются для восстановления после рестарта. * Сохраняются при registerSubscription() и используются для восстановления после рестарта.
*/ */
interface SubMeta { interface SubMeta {
@@ -81,7 +81,7 @@ function formatFilters(filters: Array<Record<string, string>>): string {
* Основная функция команды list-subscriptions. * Основная функция команды list-subscriptions.
* *
* Алгоритм: * Алгоритм:
* 1. HGETALL parser:subs все зарегистрированные подписки. * 1. HGETALL parser2:subs все зарегистрированные подписки.
* 2. XINFO GROUPS <stream> runtime-статистика consumer groups (может упасть если стрим не создан). * 2. XINFO GROUPS <stream> runtime-статистика consumer groups (может упасть если стрим не создан).
* 3. JOIN по subId: дополняем каждую подписку данными из consumer group. * 3. JOIN по subId: дополняем каждую подписку данными из consumer group.
* 4. Вывод: JSON-массив или ASCII-таблица с выравниванием. * 4. Вывод: JSON-массив или ASCII-таблица с выравниванием.
@@ -1,5 +1,5 @@
/** /**
* CLI точка входа для @coopenomics/parser. * CLI точка входа для @coopenomics/parser2.
* *
* Инструмент командной строки `parser` предоставляет набор операционных команд * Инструмент командной строки `parser` предоставляет набор операционных команд
* для управления парсером без его перезапуска: * для управления парсером без его перезапуска:
@@ -32,7 +32,7 @@ const program = new Command()
program program
.name('parser') .name('parser')
.description('@coopenomics/parser — universal EOSIO/Antelope blockchain indexer') .description('@coopenomics/parser2 — universal EOSIO/Antelope blockchain indexer')
.version('0.1.0') .version('0.1.0')
/** /**
@@ -103,7 +103,7 @@ program
/** /**
* Команда `list-subscriptions`: показывает все зарегистрированные подписки. * Команда `list-subscriptions`: показывает все зарегистрированные подписки.
* Читает из parser:subs HASH и объединяет с данными XINFO GROUPS. * Читает из parser2:subs HASH и объединяет с данными XINFO GROUPS.
* Полезна для мониторинга: видно pending, lag и last-delivered-id каждой группы. * Полезна для мониторинга: видно pending, lag и last-delivered-id каждой группы.
*/ */
program program
@@ -165,7 +165,7 @@ program
* Команда `abi-prune`: удаляет устаревшие версии ABI из Redis ZSET. * Команда `abi-prune`: удаляет устаревшие версии ABI из Redis ZSET.
* Без периодической очистки активные контракты накапливают сотни версий. * Без периодической очистки активные контракты накапливают сотни версий.
* --older-than <block> удалить версии с block_num < этого значения. * --older-than <block> удалить версии с block_num < этого значения.
* --all-contracts SCAN по parser:abi:* и применить к каждому контракту. * --all-contracts SCAN по parser2:abi:* и применить к каждому контракту.
* --dry-run показать количество версий для удаления без изменений. * --dry-run показать количество версий для удаления без изменений.
*/ */
program program
@@ -196,7 +196,7 @@ program
/** /**
* Команда `list-dead-letters`: инспектирует dead-letter stream(ы). * Команда `list-dead-letters`: инспектирует dead-letter stream(ы).
* Dead-letter stream: ce:parser:<chainId>:dead:<subId> * Dead-letter stream: ce:parser2:<chainId>:dead:<subId>
* Содержит события которые handler не смог обработать 3 раза подряд. * Содержит события которые handler не смог обработать 3 раза подряд.
* --all сканировать все dead-letter стримы для цепи (SCAN dead:*). * --all сканировать все dead-letter стримы для цепи (SCAN dead:*).
* --limit максимум записей за вызов (пагинация через --from). * --limit максимум записей за вызов (пагинация через --from).
@@ -5,7 +5,7 @@
* Вместо немедленного перевода в dead-letter даём несколько попыток (FAILURE_THRESHOLD = 3). * Вместо немедленного перевода в dead-letter даём несколько попыток (FAILURE_THRESHOLD = 3).
* После FAILURE_THRESHOLD провалов событие переводится в dead-letter stream. * После FAILURE_THRESHOLD провалов событие переводится в dead-letter stream.
* *
* Счётчики хранятся в Redis Hash (parser:sub:<subId>:failures) с TTL 24 ч. * Счётчики хранятся в Redis Hash (parser2:sub:<subId>:failures) с TTL 24 ч.
* TTL сбрасывается при каждом новом провале чтобы не накапливать стали счётчики. * TTL сбрасывается при каждом новом провале чтобы не накапливать стали счётчики.
* *
* Почему Redis, а не in-memory: при рестарте consumer'а незакрытые ошибки * Почему Redis, а не in-memory: при рестарте consumer'а незакрытые ошибки
@@ -61,7 +61,7 @@ export class FailureTracker {
/** /**
* Записывает событие в dead-letter stream с метаданными об ошибке. * Записывает событие в dead-letter stream с метаданными об ошибке.
* Dead-letter stream: ce:parser:<chainId>:dead:<subId> * Dead-letter stream: ce:parser2:<chainId>:dead:<subId>
* Поля записи: data (оригинальный payload), failureCount, lastError, subId. * Поля записи: data (оригинальный payload), failureCount, lastError, subId.
*/ */
async routeToDeadLetter( async routeToDeadLetter(
@@ -5,7 +5,7 @@
* получать события из блокчейна без прямого доступа к Redis или SHiP. * получать события из блокчейна без прямого доступа к Redis или SHiP.
* *
* Что делает: * Что делает:
* 1. Регистрирует подписку (метаданные) в Redis Hash (parser:subs). * 1. Регистрирует подписку (метаданные) в Redis Hash (parser2:subs).
* 2. Захватывает distributed lock (single-active-consumer) через SubscriptionLock. * 2. Захватывает distributed lock (single-active-consumer) через SubscriptionLock.
* 3. Читает события из Redis Stream через RedisConsumer (XREADGROUP). * 3. Читает события из Redis Stream через RedisConsumer (XREADGROUP).
* 4. Применяет фильтры (matchFilters) пропускает нерелевантные события. * 4. Применяет фильтры (matchFilters) пропускает нерелевантные события.
@@ -74,7 +74,7 @@ export class ParserClient {
* *
* Этапы старта: * Этапы старта:
* 1. Подключаемся к Redis. * 1. Подключаемся к Redis.
* 2. Регистрируем подписку в HSET parser:subs. * 2. Регистрируем подписку в HSET parser2:subs.
* 3. Пытаемся захватить lock; если занят ждём освобождения. * 3. Пытаемся захватить lock; если занят ждём освобождения.
* 4. Определяем startId для consumer group. * 4. Определяем startId для consumer group.
* 5. Создаём consumer group (XGROUP CREATE). * 5. Создаём consumer group (XGROUP CREATE).
@@ -181,7 +181,7 @@ export class Parser {
private async checkRedisPersistence(): Promise<void> { private async checkRedisPersistence(): Promise<void> {
const redis = this.redis! const redis = this.redis!
try { try {
const aofResult = await redis.hget('__parser_check__', '__noop__') const aofResult = await redis.hget('__parser2_check__', '__noop__')
void aofResult void aofResult
} catch { } catch {
// non-fatal check failure // non-fatal check failure
@@ -20,7 +20,7 @@ import type { RedisStore } from '../ports/RedisStore.js'
export interface XtrimSupervisorOpts { export interface XtrimSupervisorOpts {
redis: RedisStore redis: RedisStore
/** Имя стрима для очистки (обычно ce:parser:<chainId>:events). */ /** Имя стрима для очистки (обычно ce:parser2:<chainId>:events). */
stream: string stream: string
/** Интервал между trim-циклами в мс. По умолчанию 60 000 (1 минута). */ /** Интервал между trim-циклами в мс. По умолчанию 60 000 (1 минута). */
intervalMs?: number intervalMs?: number
@@ -55,9 +55,9 @@ export interface ClientMetrics {
/** /**
* Создаёт набор клиентских метрик в изолированном Registry. * Создаёт набор клиентских метрик в изолированном Registry.
* *
* @param prefix префикс имён метрик. По умолчанию 'parser_client'. * @param prefix префикс имён метрик. По умолчанию 'parser2_client'.
*/ */
export function createClientMetrics(prefix = 'parser_client'): ClientMetrics { export function createClientMetrics(prefix = 'parser2_client'): ClientMetrics {
const registry = new Registry() const registry = new Registry()
const handlerErrorsTotal = new Counter<'sub_id' | 'kind'>({ const handlerErrorsTotal = new Counter<'sub_id' | 'kind'>({
@@ -5,51 +5,51 @@
* поиск по кодовой базе. Полная документация формата docs/redis-key-taxonomy.md. * поиск по кодовой базе. Полная документация формата docs/redis-key-taxonomy.md.
* *
* Префиксы: * Префиксы:
* ce:parser:<chainId>: Stream-ключи, относящиеся к конкретной цепи. * ce:parser2:<chainId>: Stream-ключи, относящиеся к конкретной цепи.
* parser: Hash/ZSET/String ключи с глобальным скоупом. * parser: Hash/ZSET/String ключи с глобальным скоупом.
*/ */
export const RedisKeys = { export const RedisKeys = {
/** /**
* Главный поток событий парсера (unified event stream). * Главный поток событий парсера (unified event stream).
* Тип Redis: Stream. Тримируется XtrimSupervisor'ом. * Тип Redis: Stream. Тримируется XtrimSupervisor'ом.
* Пример: ce:parser:eos-mainnet:events * Пример: ce:parser2:eos-mainnet:events
*/ */
eventsStream: (chainId: string) => `ce:parser:${chainId}:events`, eventsStream: (chainId: string) => `ce:parser2:${chainId}:events`,
/** /**
* Dead-letter поток для конкретной подписки. * Dead-letter поток для конкретной подписки.
* Содержит сообщения, которые не смог обработать consumer после N попыток. * Содержит сообщения, которые не смог обработать consumer после N попыток.
* Тип Redis: Stream. * Тип Redis: Stream.
* Пример: ce:parser:eos-mainnet:dead:verifier * Пример: ce:parser2:eos-mainnet:dead:verifier
*/ */
deadLetterStream: (chainId: string, subId: string) => `ce:parser:${chainId}:dead:${subId}`, deadLetterStream: (chainId: string, subId: string) => `ce:parser2:${chainId}:dead:${subId}`,
/** /**
* Поток для задания on-demand reparse (зарезервировано для будущего). * Поток для задания on-demand reparse (зарезервировано для будущего).
* Тип Redis: Stream. * Тип Redis: Stream.
*/ */
reparseStream: (chainId: string, jobId: string) => `ce:parser:${chainId}:reparse:${jobId}`, reparseStream: (chainId: string, jobId: string) => `ce:parser2:${chainId}:reparse:${jobId}`,
/** /**
* История версий ABI конкретного контракта. * История версий ABI конкретного контракта.
* Тип Redis: Sorted Set. Score = block_num, member = base64(rawAbiBytes). * Тип Redis: Sorted Set. Score = block_num, member = base64(rawAbiBytes).
* При поиске ABI для блока N используется ZREVRANGEBYSCORE N -inf LIMIT 0 1. * При поиске ABI для блока N используется ZREVRANGEBYSCORE N -inf LIMIT 0 1.
* Пример: parser:abi:eosio.token * Пример: parser2:abi:eosio.token
*/ */
abiZset: (contract: string) => `parser:abi:${contract}`, abiZset: (contract: string) => `parser2:abi:${contract}`,
/** /**
* Контрольная точка синхронизации парсера (crash-recovery). * Контрольная точка синхронизации парсера (crash-recovery).
* Тип Redis: Hash. Поля: block_num, block_id, last_updated. * Тип Redis: Hash. Поля: block_num, block_id, last_updated.
* При рестарте парсер читает отсюда позицию и продолжает с неё. * При рестарте парсер читает отсюда позицию и продолжает с неё.
*/ */
syncHash: (chainId: string) => `parser:sync:${chainId}`, syncHash: (chainId: string) => `parser2:sync:${chainId}`,
/** /**
* Реестр всех зарегистрированных подписок. * Реестр всех зарегистрированных подписок.
* Тип Redis: Hash. Ключ поля = subId, значение = JSON-метаданные подписки. * Тип Redis: Hash. Ключ поля = subId, значение = JSON-метаданные подписки.
*/ */
subsHash: () => `parser:subs`, subsHash: () => `parser2:subs`,
/** /**
* Счётчики ошибок per-event для конкретной подписки. * Счётчики ошибок per-event для конкретной подписки.
@@ -57,18 +57,18 @@ export const RedisKeys = {
* TTL: 24 часа (обновляется при каждом новом провале). * TTL: 24 часа (обновляется при каждом новом провале).
* Используется FailureTracker для решения о переводе в dead-letter. * Используется FailureTracker для решения о переводе в dead-letter.
*/ */
subFailuresHash: (subId: string) => `parser:sub:${subId}:failures`, subFailuresHash: (subId: string) => `parser2:sub:${subId}:failures`,
/** /**
* Блокировка single-active-consumer для подписки. * Блокировка single-active-consumer для подписки.
* Тип Redis: String (instanceId держателя блокировки). TTL: 10 с (автопродление). * Тип Redis: String (instanceId держателя блокировки). TTL: 10 с (автопродление).
* Только один экземпляр consumer-а может быть active; остальные standby. * Только один экземпляр consumer-а может быть active; остальные standby.
*/ */
subLock: (subId: string) => `parser:sub:${subId}:lock`, subLock: (subId: string) => `parser2:sub:${subId}:lock`,
/** /**
* Метаданные задания reparse (зарезервировано для будущего). * Метаданные задания reparse (зарезервировано для будущего).
* Тип Redis: Hash. * Тип Redis: Hash.
*/ */
reparseJobHash: (jobId: string) => `parser:reparse:${jobId}`, reparseJobHash: (jobId: string) => `parser2:reparse:${jobId}`,
} as const } as const
@@ -85,7 +85,7 @@ export class WorkerPool {
/** /**
* Доля занятых потоков от общего числа (0..1). * Доля занятых потоков от общего числа (0..1).
* Полезно для метрики parser_worker_pool_queue_depth. * Полезно для метрики parser2_worker_pool_queue_depth.
*/ */
get utilization(): number { get utilization(): number {
return this.pool.utilization return this.pool.utilization
@@ -116,7 +116,7 @@ describe('listDeadLetters — --all flag', () => {
}) })
it('calls scan with correct pattern for --all', async () => { it('calls scan with correct pattern for --all', async () => {
const redis = makeRedis({ scanResult: ['ce:parser:mainnet:dead:sub1'] }) const redis = makeRedis({ scanResult: ['ce:parser2:mainnet:dead:sub1'] })
vi.spyOn(redis, 'xlen').mockResolvedValue(0) vi.spyOn(redis, 'xlen').mockResolvedValue(0)
vi.spyOn(redis, 'xrange').mockResolvedValue([]) vi.spyOn(redis, 'xrange').mockResolvedValue([])
const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined) const spy = vi.spyOn(console, 'log').mockImplementation(() => undefined)
@@ -38,23 +38,23 @@ describe('IoRedisStore', () => {
}) })
it('zadd calls redis.zadd with score and member', async () => { it('zadd calls redis.zadd with score and member', async () => {
await store.zadd('parser:abi:eosio', 100, '{"version":"eosio::abi/1.0"}') await store.zadd('parser2:abi:eosio', 100, '{"version":"eosio::abi/1.0"}')
expect(store.client.zadd).toHaveBeenCalledWith('parser:abi:eosio', 100, '{"version":"eosio::abi/1.0"}') expect(store.client.zadd).toHaveBeenCalledWith('parser2:abi:eosio', 100, '{"version":"eosio::abi/1.0"}')
}) })
it('zrangeByscoreRev calls redis.zrevrangebyscore with max first, then min', async () => { it('zrangeByscoreRev calls redis.zrevrangebyscore with max first, then min', async () => {
const result = await store.zrangeByscoreRev('parser:abi:eosio', '250', '-inf') const result = await store.zrangeByscoreRev('parser2:abi:eosio', '250', '-inf')
expect(result).toHaveLength(1) expect(result).toHaveLength(1)
expect(store.client.zrevrangebyscore).toHaveBeenCalledWith('parser:abi:eosio', '250', '-inf', 'LIMIT', 0, 1) expect(store.client.zrevrangebyscore).toHaveBeenCalledWith('parser2:abi:eosio', '250', '-inf', 'LIMIT', 0, 1)
}) })
it('hset calls redis.hset with flattened args', async () => { it('hset calls redis.hset with flattened args', async () => {
await store.hset('parser:sync:mychain', { block_num: '100', block_id: 'abc' }) await store.hset('parser2:sync:mychain', { block_num: '100', block_id: 'abc' })
expect(store.client.hset).toHaveBeenCalledWith('parser:sync:mychain', 'block_num', '100', 'block_id', 'abc') expect(store.client.hset).toHaveBeenCalledWith('parser2:sync:mychain', 'block_num', '100', 'block_id', 'abc')
}) })
it('hget returns value', async () => { it('hget returns value', async () => {
const val = await store.hget('parser:sync:mychain', 'block_num') const val = await store.hget('parser2:sync:mychain', 'block_num')
expect(val).toBe('100') expect(val).toBe('100')
}) })
@@ -6,7 +6,7 @@
* streams/locks и отслеживаем ack. * streams/locks и отслеживаем ack.
* *
* Покрываем: * Покрываем:
* - Регистрация подписки в parser:subs * - Регистрация подписки в parser2:subs
* - Определение startId по startFrom * - Определение startId по startFrom
* - Ожидание промотации если lock занят * - Ожидание промотации если lock занят
* - Применение фильтров: не-matching события XACK'аются молча * - Применение фильтров: не-matching события XACK'аются молча
@@ -185,7 +185,7 @@ describe('ParserClient — subscription registration', () => {
beforeEach(() => { fakeRedisInstances.length = 0 }) beforeEach(() => { fakeRedisInstances.length = 0 })
afterEach(() => { vi.clearAllMocks() }) afterEach(() => { vi.clearAllMocks() })
it('registers subscription metadata in parser:subs hash on stream() start', async () => { it('registers subscription metadata in parser2:subs hash on stream() start', async () => {
const client = new ParserClient({ const client = new ParserClient({
subscriptionId: 'my-sub', subscriptionId: 'my-sub',
filters: [{ kind: 'action', account: 'eosio.token' }], filters: [{ kind: 'action', account: 'eosio.token' }],
@@ -17,36 +17,36 @@ describe('createParserMetrics', () => {
}) })
it('registry contains exactly 10 metrics', async () => { it('registry contains exactly 10 metrics', async () => {
const m = createParserMetrics('test_parser_count') const m = createParserMetrics('test_parser2_count')
const all = await m.registry.getMetricsAsJSON() const all = await m.registry.getMetricsAsJSON()
expect(all.length).toBe(10) expect(all.length).toBe(10)
}) })
it('blocksProcessedTotal increments', async () => { it('blocksProcessedTotal increments', async () => {
const m = createParserMetrics('test_parser_inc') const m = createParserMetrics('test_parser2_inc')
m.blocksProcessedTotal.inc() m.blocksProcessedTotal.inc()
m.blocksProcessedTotal.inc() m.blocksProcessedTotal.inc()
const all = await m.registry.getMetricsAsJSON() const all = await m.registry.getMetricsAsJSON()
const metric = all.find(x => x.name === 'test_parser_inc_blocks_processed_total') const metric = all.find(x => x.name === 'test_parser2_inc_blocks_processed_total')
expect(metric?.values[0]?.value).toBe(2) expect(metric?.values[0]?.value).toBe(2)
}) })
it('eventsPublishedTotal supports kind label', async () => { it('eventsPublishedTotal supports kind label', async () => {
const m = createParserMetrics('test_parser_labels') const m = createParserMetrics('test_parser2_labels')
m.eventsPublishedTotal.inc({ kind: 'action' }) m.eventsPublishedTotal.inc({ kind: 'action' })
m.eventsPublishedTotal.inc({ kind: 'delta' }) m.eventsPublishedTotal.inc({ kind: 'delta' })
m.eventsPublishedTotal.inc({ kind: 'action' }) m.eventsPublishedTotal.inc({ kind: 'action' })
const all = await m.registry.getMetricsAsJSON() const all = await m.registry.getMetricsAsJSON()
const metric = all.find(x => x.name === 'test_parser_labels_events_published_total') const metric = all.find(x => x.name === 'test_parser2_labels_events_published_total')
const actions = metric?.values.find(v => v.labels['kind'] === 'action') const actions = metric?.values.find(v => v.labels['kind'] === 'action')
expect(actions?.value).toBe(2) expect(actions?.value).toBe(2)
}) })
it('indexingLagSeconds can be set to a float', async () => { it('indexingLagSeconds can be set to a float', async () => {
const m = createParserMetrics('test_parser_lag') const m = createParserMetrics('test_parser2_lag')
m.indexingLagSeconds.set(3.5) m.indexingLagSeconds.set(3.5)
const all = await m.registry.getMetricsAsJSON() const all = await m.registry.getMetricsAsJSON()
const metric = all.find(x => x.name === 'test_parser_lag_indexing_lag_seconds') const metric = all.find(x => x.name === 'test_parser2_lag_indexing_lag_seconds')
expect(metric?.values[0]?.value).toBe(3.5) expect(metric?.values[0]?.value).toBe(3.5)
}) })
}) })
@@ -9,65 +9,65 @@ import { describe, it, expect } from 'vitest'
import { RedisKeys } from '../../src/redis/keys.js' import { RedisKeys } from '../../src/redis/keys.js'
describe('RedisKeys — event streams', () => { describe('RedisKeys — event streams', () => {
it('eventsStream embeds chainId in ce:parser:<chainId>:events', () => { it('eventsStream embeds chainId in ce:parser2:<chainId>:events', () => {
expect(RedisKeys.eventsStream('eos-mainnet')).toBe('ce:parser:eos-mainnet:events') expect(RedisKeys.eventsStream('eos-mainnet')).toBe('ce:parser2:eos-mainnet:events')
expect(RedisKeys.eventsStream('telos-test')).toBe('ce:parser:telos-test:events') expect(RedisKeys.eventsStream('telos-test')).toBe('ce:parser2:telos-test:events')
}) })
it('deadLetterStream includes both chainId and subId', () => { it('deadLetterStream includes both chainId and subId', () => {
expect(RedisKeys.deadLetterStream('eos', 'verifier')) expect(RedisKeys.deadLetterStream('eos', 'verifier'))
.toBe('ce:parser:eos:dead:verifier') .toBe('ce:parser2:eos:dead:verifier')
}) })
it('reparseStream includes chainId and jobId', () => { it('reparseStream includes chainId and jobId', () => {
expect(RedisKeys.reparseStream('eos', 'job-42')) expect(RedisKeys.reparseStream('eos', 'job-42'))
.toBe('ce:parser:eos:reparse:job-42') .toBe('ce:parser2:eos:reparse:job-42')
}) })
}) })
describe('RedisKeys — ABI and sync', () => { describe('RedisKeys — ABI and sync', () => {
it('abiZset is namespaced by contract name', () => { it('abiZset is namespaced by contract name', () => {
expect(RedisKeys.abiZset('eosio.token')).toBe('parser:abi:eosio.token') expect(RedisKeys.abiZset('eosio.token')).toBe('parser2:abi:eosio.token')
expect(RedisKeys.abiZset('eosio')).toBe('parser:abi:eosio') expect(RedisKeys.abiZset('eosio')).toBe('parser2:abi:eosio')
}) })
it('syncHash is keyed by chainId only', () => { it('syncHash is keyed by chainId only', () => {
expect(RedisKeys.syncHash('eos-mainnet')).toBe('parser:sync:eos-mainnet') expect(RedisKeys.syncHash('eos-mainnet')).toBe('parser2:sync:eos-mainnet')
}) })
}) })
describe('RedisKeys — subscriptions', () => { describe('RedisKeys — subscriptions', () => {
it('subsHash is a global registry (no params)', () => { it('subsHash is a global registry (no params)', () => {
expect(RedisKeys.subsHash()).toBe('parser:subs') expect(RedisKeys.subsHash()).toBe('parser2:subs')
}) })
it('subFailuresHash is keyed by subId', () => { it('subFailuresHash is keyed by subId', () => {
expect(RedisKeys.subFailuresHash('verifier')).toBe('parser:sub:verifier:failures') expect(RedisKeys.subFailuresHash('verifier')).toBe('parser2:sub:verifier:failures')
}) })
it('subLock is keyed by subId', () => { it('subLock is keyed by subId', () => {
expect(RedisKeys.subLock('verifier')).toBe('parser:sub:verifier:lock') expect(RedisKeys.subLock('verifier')).toBe('parser2:sub:verifier:lock')
}) })
it('reparseJobHash is keyed by jobId', () => { it('reparseJobHash is keyed by jobId', () => {
expect(RedisKeys.reparseJobHash('job-42')).toBe('parser:reparse:job-42') expect(RedisKeys.reparseJobHash('job-42')).toBe('parser2:reparse:job-42')
}) })
}) })
describe('RedisKeys — prefix namespacing invariants', () => { describe('RedisKeys — prefix namespacing invariants', () => {
it('all stream keys start with ce:parser:', () => { it('all stream keys start with ce:parser2:', () => {
expect(RedisKeys.eventsStream('x')).toMatch(/^ce:parser:/) expect(RedisKeys.eventsStream('x')).toMatch(/^ce:parser2:/)
expect(RedisKeys.deadLetterStream('x', 's')).toMatch(/^ce:parser:/) expect(RedisKeys.deadLetterStream('x', 's')).toMatch(/^ce:parser2:/)
expect(RedisKeys.reparseStream('x', 'j')).toMatch(/^ce:parser:/) expect(RedisKeys.reparseStream('x', 'j')).toMatch(/^ce:parser2:/)
}) })
it('all non-stream keys start with parser:', () => { it('all non-stream keys start with parser2:', () => {
expect(RedisKeys.abiZset('x')).toMatch(/^parser:/) expect(RedisKeys.abiZset('x')).toMatch(/^parser2:/)
expect(RedisKeys.syncHash('x')).toMatch(/^parser:/) expect(RedisKeys.syncHash('x')).toMatch(/^parser2:/)
expect(RedisKeys.subsHash()).toMatch(/^parser:/) expect(RedisKeys.subsHash()).toMatch(/^parser2:/)
expect(RedisKeys.subFailuresHash('x')).toMatch(/^parser:/) expect(RedisKeys.subFailuresHash('x')).toMatch(/^parser2:/)
expect(RedisKeys.subLock('x')).toMatch(/^parser:/) expect(RedisKeys.subLock('x')).toMatch(/^parser2:/)
expect(RedisKeys.reparseJobHash('x')).toMatch(/^parser:/) expect(RedisKeys.reparseJobHash('x')).toMatch(/^parser2:/)
}) })
it('keys for different chains are disjoint (no collision)', () => { it('keys for different chains are disjoint (no collision)', () => {
+1 -1
View File
@@ -2,7 +2,7 @@
Clean-room SHiP WebSocket client for EOSIO/Antelope blockchains. Streams blockchain blocks, deserializes actions and deltas via `@wharfkit/antelope`, with support for all 24 native system table delta types. Clean-room SHiP WebSocket client for EOSIO/Antelope blockchains. Streams blockchain blocks, deserializes actions and deltas via `@wharfkit/antelope`, with support for all 24 native system table delta types.
Primary consumer: [`@coopenomics/parser`](https://github.com/coopenomics/parser). Primary consumer: [`@coopenomics/parser2`](https://github.com/coopenomics/parser).
## Installation ## Installation
+1 -1
View File
@@ -15,7 +15,7 @@ importers:
specifier: ^5.4.5 specifier: ^5.4.5
version: 5.9.3 version: 5.9.3
packages/parser: packages/parser2:
dependencies: dependencies:
'@coopenomics/coopos-ship-reader': '@coopenomics/coopos-ship-reader':
specifier: workspace:* specifier: workspace:*