[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:
@@ -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
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
[](https://github.com/coopenomics/parser/actions/workflows/ci.yml)
|
[](https://github.com/coopenomics/parser/actions/workflows/ci.yml)
|
||||||
[](https://www.npmjs.com/package/@coopenomics/parser)
|
[](https://www.npmjs.com/package/@coopenomics/parser2)
|
||||||
[](LICENSE)
|
[](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 опубликует.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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**
|
||||||
|
|||||||
@@ -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 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
+3
-3
@@ -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 })
|
||||||
+4
-4
@@ -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
-4
@@ -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).
|
||||||
+2
-2
@@ -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(
|
||||||
+2
-2
@@ -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
|
||||||
+1
-1
@@ -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
|
||||||
+2
-2
@@ -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
|
||||||
+1
-1
@@ -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
|
||||||
+1
-1
@@ -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)
|
||||||
+7
-7
@@ -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')
|
||||||
})
|
})
|
||||||
|
|
||||||
+2
-2
@@ -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' }],
|
||||||
+7
-7
@@ -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)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
+23
-23
@@ -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)', () => {
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -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:*
|
||||||
|
|||||||
Reference in New Issue
Block a user