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 uncaughtException or unhandledRejection, writes a crash-<timestamp>.json (error, stack, last parsed message) and crash-<timestamp>.bin (raw compressed ZMQ buffer) to log/, 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:

  1. Looks up the system’s last known tick data — first from an in-memory tickMap (Map keyed by systemAddress), then from persisted data in the database.
  2. Computes a CRC32 of the faction state portion of the message.
  3. Compares against the stored CRC array to detect a change.
  4. 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