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:
+73
@@ -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.5–1.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'));
|
||||
Reference in New Issue
Block a user