Implement Phase 1 tasks 1.1-1.4 (scaffold + core shell + Teltonika framing)

- Project scaffold (Node 22 + TS 5 + pnpm + vitest + ESLint flat config)
- Core shell: TCP server, session loop, adapter registry, types
- Configuration (zod-validated env) and pino logger
- Teltonika adapter: IMEI handshake, frame envelope, CRC-16/IBM,
  codec dispatch registry, DeviceAuthority seam (AllowAllAuthority default)

Codec data parsers (1.5-1.7), Redis publisher (1.8), and downstream
tasks remain. 36 tests covering CRC, framing, handshake, device
authority, config, and core server. typecheck/lint/test/build all clean.
This commit is contained in:
2026-04-30 15:47:26 +02:00
parent c8a5f4cd68
commit 1e9219d14a
35 changed files with 6217 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
import { loadConfig } from './config/load.js';
import { createLogger } from './observability/logger.js';
import { makePublisher } from './core/publish.js';
import { startServer } from './core/server.js';
import { createTeltonikaAdapter } from './adapters/teltonika/index.js';
import { AllowAllAuthority } from './adapters/teltonika/device-authority.js';
import { CodecRegistry } from './adapters/teltonika/codec/registry.js';
import type { Metrics } from './core/types.js';
// -------------------------------------------------------------------------
// Startup: validate config (fail fast on bad env), build logger, boot server
// -------------------------------------------------------------------------
let config;
try {
config = loadConfig();
} catch (err) {
// Config validation failures print a human-readable message and exit 1.
// Logger is not available yet — process.stderr is the only output channel.
process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`);
process.exit(1);
}
const logger = createLogger({
level: config.LOG_LEVEL,
nodeEnv: config.NODE_ENV,
instanceId: config.INSTANCE_ID,
});
logger.info('tcp-ingestion starting');
// Placeholder metrics implementation — replaced in task 1.10
const metrics: Metrics = {
inc: (name, labels) => logger.debug({ metric: name, labels }, 'metric inc'),
observe: (name, value, labels) => logger.debug({ metric: name, value, labels }, 'metric observe'),
};
const publisher = makePublisher(logger);
// Codec registry — empty until tasks 1.51.7 register handlers
const codecRegistry = new CodecRegistry();
const teltonikaAdapter = createTeltonikaAdapter({
port: config.TELTONIKA_PORT,
deviceAuthority: new AllowAllAuthority(),
strictDeviceAuth: config.STRICT_DEVICE_AUTH,
codecRegistry,
});
const ctx = {
publish: publisher,
logger,
metrics,
};
const server = startServer(config.TELTONIKA_PORT, teltonikaAdapter, ctx);
// Graceful shutdown
function shutdown(signal: string): void {
logger.info({ signal }, 'shutdown signal received; closing server');
server.close(() => {
logger.info('server closed; exiting');
process.exit(0);
});
// Force exit after 10s if connections are still open
setTimeout(() => {
logger.warn('forced exit after timeout');
process.exit(1);
}, 10_000).unref();
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));