{"_id":"@davincigraph/hedera-event-sync","name":"@davincigraph/hedera-event-sync","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@davincigraph/hedera-event-sync","version":"1.0.0","private":false,"description":"A type-safe, provider-agnostic Hedera Mirror Node event and transaction synchronization engine.","license":"MIT","author":{"name":"DaVinciGraph"},"keywords":["hedera","mirror-node","events","transactions","indexer","sync","typescript"],"repository":{"type":"git","url":"git+https://github.com/DaVinciGraph/hedera-event-sync.git"},"bugs":{"url":"https://github.com/DaVinciGraph/hedera-event-sync/issues"},"homepage":"https://github.com/DaVinciGraph/hedera-event-sync#readme","engines":{"node":">=18"},"publishConfig":{"access":"public"},"sideEffects":false,"type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"},"./types/config":{"types":"./dist/types/config.d.ts","import":"./dist/types/config.js"},"./types/domain":{"types":"./dist/types/domain.d.ts","import":"./dist/types/domain.js"},"./core/logs/LogEventProcessor":{"types":"./dist/core/logs/LogEventProcessor.d.ts","import":"./dist/core/logs/LogEventProcessor.js"}},"scripts":{"typecheck":"tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit","build":"tsup","test":"node ./test/run-tests.mjs","test:coverage":"c8 --all --include=src/**/*.ts --check-coverage --lines=80 --statements=80 --functions=75 --branches=65 npm test","test:e2e":"node ./test/e2e.public-mirror.mjs","test:package":"node ./test/package-smoke.mjs && tsc -p tsconfig.package-test.json","prepack":"npm run typecheck && npm run test:coverage && npm run build && npm run test:package"},"dependencies":{"@davincigraph/hedera-rest-client":"^1.0.0","abitype":"^1.3.0","ox":"^0.14.44","viem":"^2.56.3"},"devDependencies":{"@types/node":"^22.5.4","c8":"10.1.3","esbuild":"0.27.2","tsup":"8.5.1","tsx":"4.23.13","typescript":"5.5.4"},"gitHead":"46e26054bf24f78f92f4eba4a89afac027ff807d","_id":"@davincigraph/hedera-event-sync@1.0.0","_nodeVersion":"22.14.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-PnZLVm8MZfPDfxwFuV3wy/5Wi4Eqeu9t2hEyr5WB2zYj59vZsxj+iBHovVQbW1lHtxVtz2Ghwmt5CLlXSLAWUg==","shasum":"47577b0a684a2804992483524384546463ff5af4","tarball":"https://registry.npmjs.org/@davincigraph/hedera-event-sync/-/hedera-event-sync-1.0.0.tgz","fileCount":24,"unpackedSize":596543,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCEcfFLECMv3apSZBRYzfukZdwnvIznTffLPUl84d8kWwIgXiYewuqsTLPF97BFlG9SqTnK2YZJND+6N5SY6Ey4kas="}]},"_npmUser":{"name":"davincigraph","email":"the.psycho.writer@gmail.com"},"directories":{},"maintainers":[{"name":"davincigraph","email":"the.psycho.writer@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hedera-event-sync_1.0.0_1789071210155_0.7449308057553896"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-10T20:13:29.973Z","1.0.0":"2026-09-10T20:13:30.296Z","modified":"2026-09-10T20:13:30.523Z"},"maintainers":[{"name":"davincigraph","email":"the.psycho.writer@gmail.com"}],"description":"A type-safe, provider-agnostic Hedera Mirror Node event and transaction synchronization engine.","homepage":"https://github.com/DaVinciGraph/hedera-event-sync#readme","keywords":["hedera","mirror-node","events","transactions","indexer","sync","typescript"],"repository":{"type":"git","url":"git+https://github.com/DaVinciGraph/hedera-event-sync.git"},"author":{"name":"DaVinciGraph"},"bugs":{"url":"https://github.com/DaVinciGraph/hedera-event-sync/issues"},"license":"MIT","readme":"# @davincigraph/hedera-event-sync\n\nTurn Hedera contract events and transactions into application work you can track, retry, and resume.\n\n## Why this package exists\n\nSuppose your contract emits a `Transfer` event. Your application needs to save it in a database, update an admin dashboard, and notify an operator if processing keeps failing.\n\nFetching the event from a Mirror Node is only one part of that job. A running application also needs to keep polling, follow pages, remember progress, handle late-indexed records, retry failed work, and explain what it is doing.\n\nHedera Event Sync coordinates that ongoing work. You tell it **what to read**, **how to handle each record**, and **which lifecycle events to observe**.\n\nTypical uses include:\n\n- Maintaining database records derived from contract events.\n- Backfilling historical activity, then continuing to follow new records.\n- Watching transactions involving an account or contract.\n- Showing administrators what is being read, queued, processed, or retried.\n- Sending operational alerts when reading or processing needs attention.\n\nIt is a Node.js library that runs **inside your application**, not a hosted service or a separate worker you must deploy. It has no required database, email service, UI framework, or DaVinciGraph application.\n\n## How the pieces fit together\n\nA synchronizer manages one or more **queries**. Each query has its own reader and processing queue:\n\n```text\nMirror Node → read and prepare records → in-memory queue → your handler\n                    │                          │                │\n                    └──────────────── hooks ────────────────────┘\n                                       │\n                           your logs, database, UI, alerts\n```\n\n| You configure | Its job |\n| --- | --- |\n| **REST client** | Where requests go: provider, network, authentication, rate limits, and failover |\n| **Query** | What to follow: one contract, matching contract-log topics, or an account's transactions; also where to start and when to pause |\n| **Handler** | What your application does with one accepted record, such as updating a database |\n| **Hooks** | What your application does when synchronization reaches a lifecycle point, such as starting an attempt, adding a step, or reaching a failure threshold |\n\nFor contract logs, the reader decodes the event using your ABI and calls the handler's `normalize()` function to prepare its data. The queue then calls `handle()` to perform the application work. Transaction handlers receive the standard Mirror Node transaction record directly; they have no normalization stage.\n\nEach queued record is a **process**. Executing it creates an **attempt**; a failed execution can create another attempt for that same process. A handler can add **steps** to describe its progress. Those steps are diagnostics, not separately executed tasks.\n\nQueries process their queues independently. Within a query, one handler runs at a time; the reader can continue filling the queue while it runs. See the [ordering and replay guarantees](docs/reference.md#delivery-guarantees) for late-indexed records.\n\n### What stays in your application\n\nYou own database connections, business logic, notification clients, and deployment. The engine does not store durable history, send email by itself, or submit transactions to Hedera.\n\nThe [REST client](https://www.npmjs.com/package/@davincigraph/hedera-rest-client) makes Mirror Node requests. **Event Sync uses that client to continuously turn the responses into managed application work.** It supports public Mirror Nodes and compatible private providers or forks configured through that client.\n\n## Install\n\nRequires Node.js 18+ and ESM. TypeScript is optional; TypeScript consumers need version 5.4+.\n\n```bash\nnpm install @davincigraph/hedera-event-sync\n```\n\nThat is the only installation required for the example below. The compatible REST client is already included and re-exported. An application may also declare its own compatible `@davincigraph/hedera-rest-client` 1.x dependency and pass the same configured client to Event Sync.\n\n## Run your first synchronization\n\nThis example follows **ERC-20-style Transfer events from one testnet contract**. Supply a real contract ID with the event declaration shown below. For other contracts, use their actual ABI and matching event handlers; this is not an ERC-721 example.\n\nSave as `sync.mjs`:\n\n```js\nimport { HederaEventSync, HederaRestClient } from \"@davincigraph/hedera-event-sync\";\n\nconst contractId = process.argv[2];\nif (!contractId) throw new Error(\"Pass your testnet contract ID as the first argument\");\n\nconst restClient = new HederaRestClient({\n  defaultProvider: \"public\",\n  defaultNetwork: \"testnet\",\n});\nconst sync = await HederaEventSync.create({ restClient });\n\n// Hooks observe the engine. Install them before starting a query.\nsync.setHooks({\n  onReadCycleCompleted(query, result) {\n    console.log(\"Read cycle\", query.id, {\n      success: result.success,\n      eligibleRecords: result.itemCount,\n    });\n  },\n  onReadFailure(query, error) {\n    console.error(\"Read failed\", query.id, error);\n  },\n  onProcessTryFailed(query, record, attempt, error) {\n    console.error(\"Attempt failed\", query.id, record.sourceKey, attempt.number, error);\n  },\n  onHookError(failure) {\n    console.error(\"Observer failed\", failure.hook, failure.error);\n  },\n});\n\n// Handlers perform application work. This first handler simply prints the event.\nsync.registry.registerLog(\"Transfer\", {\n  normalize(args) {\n    return args;\n  },\n  handle(transfer, context) {\n    console.log(\"Transfer handled\", transfer, context.raw.timestamp);\n    context.addStep?.({ title: \"Transfer printed\", type: \"success\" });\n  },\n});\n\nconst shutdown = () => {\n  void sync.shutdown().catch((error) => {\n    console.error(\"Shutdown failed\", error);\n    process.exitCode = 1;\n  });\n};\nprocess.once(\"SIGINT\", shutdown);\nprocess.once(\"SIGTERM\", shutdown);\n\n// The standard selector for Transfer(address,address,uint256).\nconst transferTopic = \"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef\";\n\nawait sync.addQuery({\n  type: \"Single-Contract-Logs\",\n  id: 1,\n  title: \"Token transfers\",\n  contract: { address: contractId },\n  abi: [{\n    type: \"event\",\n    name: \"Transfer\",\n    anonymous: false,\n    inputs: [\n      { name: \"from\", type: \"address\", indexed: true },\n      { name: \"to\", type: \"address\", indexed: true },\n      { name: \"value\", type: \"uint256\", indexed: false },\n    ],\n  }],\n  params: {\n    topics: [transferTopic],\n    timestamp: `${Math.floor(Date.now() / 1000)}.000000000`,\n    index: 0,\n  },\n  read: {\n    fetch: { restProvider: \"public\", network: \"testnet\" },\n    pauseAfterConsecutiveFailures: 5,\n  },\n  process: { pauseAfterConsecutiveFailures: 5 },\n});\n\nconsole.log(\"Watching transfers. Press Ctrl+C to stop.\");\n```\n\nRun it, replacing `0.0.123` with your contract ID:\n\n```bash\nnode sync.mjs 0.0.123\n```\n\nYou will see read-cycle messages even when no matching transfers are available. This query starts **from now**, not from the beginning of history. The default reader stays 15 seconds behind the local clock and polls about every 30 seconds, with startup delay and jitter, so immediate event output is not expected.\n\nThere is no additional start command: `addQuery()` starts synchronization. Register handlers and install hooks before calling it. `create()` intentionally reuses one process-local singleton by default; later calls do not replace its configuration.\n\nNext, replace the printing handler with your application logic. For typed handlers, other event ABIs, topic filters, and transaction queries, follow [Queries and handlers](docs/queries.md).\n\n## Connect it to your application\n\nThe important distinction is **work that must succeed for a record to be processed** versus **observations about that work**.\n\n| What you need | Where it belongs | Why |\n| --- | --- | --- |\n| Update business data in PostgreSQL, MongoDB, or another store | Handler | A failed write should fail the attempt, remain retryable, and not count as successful processing |\n| Keep an operational history of reads, attempts, and steps | Hooks | History storage can be independent of business processing |\n| Save a restart position | `onProcessedCheckpoint` hook | It supplies a safe processed cursor, rather than the reader's potentially newer position |\n| Show current activity through WebSockets or server-sent events | Read/process/step/status hooks | The UI can receive transitions while work is happening |\n| Send an operator email when failures reach a threshold | Failure-threshold hooks | Notify on a condition that needs attention rather than on every retry |\n| Record metrics, traces, or send observational webhooks | Hooks matching the event of interest | Your monitoring library receives the lifecycle information it needs |\n\n### Database writes belong in handlers when they are the work\n\nReturn or await your database operation from `handle()`. If it throws, the attempt fails and the engine retries according to the query's policy. Keep `normalize()` focused on preparing data: it runs during reading and can run again during overlap replay.\n\nMake writes idempotent. For example, a transfer table can have a unique key using the network, consensus timestamp, and log index. Reprocessing the same record then does not insert another transfer. Your application can use its existing database driver or ORM; Event Sync does not choose one.\n\nThe [integration guide](docs/integrations.md) includes an actual PostgreSQL schema and handler, not just an unspecified `saveToDatabase()` placeholder.\n\n### Hooks connect progress to other services\n\nHooks are callbacks you implement with your own service clients. They can store an audit trail, publish live updates, notify an operator, or combine those purposes.\n\nFor example, this TypeScript factory adapts an application's email-sending function into a processing-failure observer:\n\n```ts\nimport type { SyncHooks } from \"@davincigraph/hedera-event-sync\";\n\nexport function createFailureAlerts(\n  sendEmail: (message: { subject: string; text: string }) => Promise<unknown>,\n): SyncHooks {\n  return {\n    async onProcessConsecutiveFailuresReached(query, record, failures) {\n      await sendEmail({\n        subject: `Processing paused: ${query.title}`,\n        text: `Query ${query.id}, record ${record.sourceKey}: ${failures} consecutive failures.`,\n      });\n    },\n    onHookError(failure) {\n      console.error(\"Observer failed\", failure.hook, failure.error);\n    },\n  };\n}\n```\n\n`sendEmail` is supplied by your application, not by Event Sync. The [integration guide](docs/integrations.md) shows how to connect a real Nodemailer transport, alongside checkpoint storage and live-progress hooks.\n\nYou can install several hook objects together with `sync.setHooks([historyHooks, alertHooks, dashboardHooks])`. These names represent your application's observers. `setHooks()` replaces the current set; it does not append.\n\nBy default, rejected hooks are reported through `onHookError` and synchronization continues. This is useful when an optional history database or notification service is unavailable. Hooks are still awaited, so give external requests appropriate timeouts: `\"continue\"` handles a rejection, not a promise that never settles.\n\nA success hook failing does **not** rerun a handler that already succeeded. If an email, webhook, or downstream task is a required business effect, perform it through the handler or write a durable outbox entry with your database changes. Observer hooks alone do not provide guaranteed external delivery.\n\n### Make progress visible\n\nUse `onProcessStarted` to show an active attempt, `onStepCreated` to append steps as they occur, and success/failure hooks to finish that attempt in your UI. `onQueryStatusChange` distinguishes reading from processing.\n\n`context.addStep()` records a diagnostic immediately; it does not execute the named task or print anything by itself. Await the actual work in your handler and add steps where they describe that work. A fast handler may finish between UI polls; hooks expose its transitions without artificial delays.\n\nSee the [integration guide](docs/integrations.md) for the UI event mapping and [hook reference](docs/reference.md#hook-reference) for every available callback.\n\n## Backfill and resume after a restart\n\nChoose an initial `params.timestamp` and `params.index` for each query:\n\n- **Live-only:** start at the current timestamp, as in the example.\n- **Historical:** start at an earlier cursor. Omitted fields default to timestamp zero and index zero, which starts a backfill.\n- **Restart:** restore both fields from your saved processed checkpoint.\n\nThe engine's queue, history, and checkpoints live in memory. To resume after a restart, your application loads a saved checkpoint, registers its handlers and hooks, and adds the query again with that cursor.\n\nPersist `onProcessedCheckpoint` or `getProcessedCheckpoint(id)`—**not the read cursor**. Reading can be ahead of handlers. A processed checkpoint also deliberately retains an overlap horizon for late-indexed records, so it may stay unchanged across several successful events.\n\nThe [checkpoint recipe](docs/integrations.md) shows saving and loading both fields in PostgreSQL. A checkpoint belongs to the same logical query and network; it is not interchangeable between mainnet and testnet. Restart replay is expected, which is why handlers need idempotent side effects.\n\n## Where to go next\n\n| Goal | Guide |\n| --- | --- |\n| Define typed log handlers, transaction handlers, or multi-contract topic queries | [Queries and handlers](docs/queries.md) |\n| Use a private mirror, credentials, or another network | [Provider configuration](docs/queries.md#providers-and-networks) |\n| Persist business data and checkpoints, send email, or drive a dashboard | [Application integrations](docs/integrations.md) |\n| Tune polling, capacity, retention, and failure behavior | [Configuration reference](docs/reference.md#configuration-reference) |\n| Pause, resume, inspect, replace, or shut down queries | [Runtime management](docs/reference.md#runtime-management) |\n| Understand replay, hook ordering, and lifecycle restrictions | [Guarantees and lifecycle reference](docs/reference.md) |\n| Investigate missing output or paused processing | [Troubleshooting](docs/reference.md#troubleshooting) |\n\nNo settings need to be changed merely to enable third-party integrations. The handlers and hooks are the integration points; your application supplies the services behind them.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-e76a3ef55b0ba943cc5ccd6c38c851f8"}