eddn_client
eddn_client
A Node.js application that subscribes to the Elite Dangerous Data Network (EDDN) ZeroMQ feed, processes incoming messages, and dispatches them to a pluggable processor. Two processors are currently implemented: a Tick Detector and a Database Writer.
Background
“The Tick” is a daily process in the Background Simulation (BGS) of the Frontier Developments game Elite Dangerous. During the tick, each populated system undergoes a multi-stage process in which player actions over the preceding 24 hours are applied to update Faction states and influence levels.
Zoy’s Tick Detector monitors the EDDN feed and uses CRC-based change detection on incoming journal messages to identify when individual systems have ticked. The first system-level tick detected (showing a state change) is reported as the Galaxy Tick.
Detected ticks are published via a ZeroMQ Pub/Sub socket for downstream clients. See the companion project tickDetectorClient for client examples and published message formats.
Architecture
EDDN ZeroMQ feed (tcp://eddn.edcd.io:9500)
│
▼
main.mjs ──── unzip (zlib) ──── parseMessage()
│
▼
messageDirector.mjs
(dynamic dispatch)
│
├── processor/tickDetector/tickDetector.mjs (Tick Detector)
└── processor/database/database.mjs (Database Writer)
│
▼
dao/postgresDao.mjs (PostgreSQL via pg)
dao/sqliteDao.mjs (SQLite via sqlite3)
main.mjs
Entry point. Connects to the EDDN ZeroMQ subscriber socket, decompresses each message, parses the JSON payload, and calls processMessage() after applying duplicate detection, game version validation, and software whitelist filtering.
Also hosts:
- Crash reporting — on
uncaughtExceptionorunhandledRejection, writes acrash-<timestamp>.json(error, stack, last parsed message) andcrash-<timestamp>.bin(raw compressed ZMQ buffer) tolog/, logs an alert, and exits cleanly for Docker to restart. - Stats HTTP server — a lightweight Express server exposing runtime stats.
messageDirector.mjs
Dynamically imports the configured processor at startup (via config.processor.name). Discovers exported functions from the processor whose names match a supported EDDN message type and registers them. On each incoming message, calls all registered functions for that message type.
Supported message types include: FSDJump, Location, CarrierJump, Docked, FSSDiscoveryScan, Scan, commodity_3, outfitting_2, shipyard_2, and others.
Processors
Processors are the pluggable business logic layer. Each processor exports an initialise() function and one or more message-handling functions named to match EDDN event types.
tickDetector (processor/tickDetector/tickDetector.mjs)
The primary processor. For each qualifying message:
- Looks up the system’s last known tick data — first from an in-memory
tickMap(Map keyed bysystemAddress), then from persisted data in the database. - Computes a CRC32 of the faction state portion of the message.
- Compares against the stored CRC array to detect a change.
- If a change is detected, records the tick and publishes it via the ZeroMQ publisher socket.
The tickMap is loaded from the database on startup and periodically reloaded. A cooldown period (configurable, default 22 hours) prevents false detections.
Publishes on ZeroMQ topics: Heartbeat, SystemTick, GalaxyTick, FactionChanges, FactionExpandedFrom.
ZMQ message payloads
All published messages are deflate-compressed (use zlib.inflateRaw or zlib.inflate to decompress).
SystemTick / GalaxyTick (same shape; GalaxyTick is the first StatePass tick after the cooldown window):
{
"system": "Lave",
"systemAddress": "5031721931314",
"isColony": false,
"schema": "notYetImplemented",
"timeGapMins": "42.3",
"timestamp": "2025-04-01T18:42:00Z",
"dayCount": 3,
"metrics": {
"tickPass": "State",
"stateChange": true,
"infChange": false,
"infStates": false,
"cmfInf": 0.4521,
"cmfName": "Pilots Federation Local Branch",
"cmfHasExpanded": false,
"cmfInfDrop": 0.0,
"cmfExpansionTax": false,
"conflictEnded": false,
"factionChanges": { "retreated": [], "arrived": [] }
}
}
isColony: true = player-colonised system; false = pre-colonisation (legacy) system. Derived from resources/dta_sys_legacy.csv loaded at startup. null if the system address was absent in the source message (rare).
FactionChanges (published when factions retreat from or arrive in a system on a tick):
{
"system": "Lave",
"systemAddress": "5031721931314",
"isColony": false,
"timestamp": "2025-04-01T18:42:00Z",
"factionChanges": { "retreated": ["Old Faction"], "arrived": ["New Faction"] }
}
FactionExpandedFrom (published when the CMF’s influence drop indicates an expansion tax):
{
"system": "Lave",
"systemAddress": "5031721931314",
"isColony": false,
"timestamp": "2025-04-01T18:42:00Z",
"faction": "Pilots Federation Local Branch",
"InfDrop": 0.0423,
"hasExpanded": true
}
Heartbeat (published every 5 minutes):
{
"status": "online",
"timestamp": "2025-04-01T18:42:00Z",
"lastGalaxyTick": "2025-03-31T19:15:00Z",
"version": "0.4.5-beta-td",
"info": "topics and message contents subject to change"
}
database (processor/database/database.mjs)
A simpler processor that writes incoming EDDN messages to the database for archival/analysis. Uses the same DAO abstraction as the tick detector.
DAOs
The DAO layer provides a consistent interface (executeSelect, executeDML, executeDDL) over different database backends. The active DAO is configured via config.dao.name and dynamically imported at startup.
| DAO | File | Backend |
|---|---|---|
| PostgreSQL | dao/postgresDao.mjs |
pg connection pool |
| SQLite | dao/sqliteDao.mjs |
sqlite3 |
All methods use pool.query() / equivalent — no ORM.
SQL modules
SQL statements are separated from DAO logic into provider-specific modules, configured via config.processor.sql:
| Module | Backend |
|---|---|
processor/tickDetector/pgSql.mjs |
PostgreSQL |
processor/tickDetector/liteSql.mjs |
SQLite |
processor/database/pgSql.mjs |
PostgreSQL (database processor) |
Configuration
All configuration is in src/config/config.mjs. Key settings:
| Setting | Purpose |
|---|---|
config.processor.name |
Selects the active processor (tickDetector/tickDetector.mjs or database/database.mjs) |
config.processor.sql |
Selects the SQL module (pgSql.mjs or liteSql.mjs) |
config.dao.name |
Selects the DAO (postgresDao.mjs or sqliteDao.mjs) |
config.tickdetector.publisherURL |
ZeroMQ bind URL for the tick publisher |
config.tickdetector.tickCooldownHours |
Minimum hours between galaxy tick detections |
config.softwareWhitelist |
Map of trusted EDDN software publishers and minimum versions |
config.server |
Environment label used in startup log (VPS-Live, NAS-Standby, etc.) |
Running
npm install
node src/main.mjs
Docker
docker build -t eddn_client .
docker run -d --restart=always eddn_client
The base image is node:22-bookworm-slim. Docker restart=always is the intended recovery mechanism — the application deliberately calls process.exit(1) after writing a crash report.
Logging
Winston is used for logging with daily rotating file transport and an optional Discord webhook transport for error-level and above alerts. Log files are written to the log/ directory.
Database Schema
The tick detector uses the ed_tick PostgreSQL schema:
| Table | Purpose |
|---|---|
dta_tick_data |
Per-system tick data persistence (tickMap backing store) |
res_tick |
All detected system ticks |
res_galaxy_tick |
Detected galaxy ticks (one per day expected) |
Project Structure
.
├── appdata/
│ └── tickdetector/ SQLite database (if using SQLite DAO)
├── log/ Winston log files and crash reports
├── public/ HTTP static assets
├── resources/ Message templates, schema references, dta_sys_legacy.csv
├── src/
│ ├── config/ config.mjs
│ ├── dao/ postgresDao.mjs, sqliteDao.mjs
│ └── processor/
│ ├── database/ Database writer processor + SQL
│ ├── prototype/ Skeleton for new processors
│ └── tickDetector/ Tick detector processor + SQL + cooldown logic
└── Dockerfile