Implement Phase 1 task 1.8 (Redis Streams publisher + main wiring)

- Bounded in-memory queue (default 10000); overflow throws PublishOverflowError
  so the framing layer skips ACK and the device retransmits.
- Background worker drains via XADD with MAXLEN ~ approximate trimming.
- JSON serialization with sentinel encoding for bigint/Buffer/Date; correctly
  handles Buffer.prototype.toJSON firing before the replacer.
- AdapterContext.publish(position, codec) with codec-label closure at dispatch
  in adapters/teltonika/index.ts; zero changes to the three codec parsers.
- connectRedis with retry-on-startup; main.ts wires the full pipeline.
- installGracefulShutdown stubbed (full hardening in task 1.12).
- 19 new tests (17 unit + 2 Docker-conditional integration). Total 81 passing.
This commit is contained in:
2026-04-30 16:37:51 +02:00
parent 381287bacc
commit c33c7a4f6b
12 changed files with 1896 additions and 55 deletions
+30 -1
View File
@@ -1,5 +1,6 @@
import type * as net from 'node:net';
import type { Adapter, AdapterContext } from '../../core/types.js';
import type { CodecLabel } from '../../core/publish.js';
import type { DeviceAuthority } from './device-authority.js';
import { AllowAllAuthority } from './device-authority.js';
import { readImeiHandshake, HandshakeError } from './handshake.js';
@@ -9,6 +10,25 @@ import { codec8Handler } from './codec/data/codec8.js';
import { codec8eHandler } from './codec/data/codec8e.js';
import { codec16Handler } from './codec/data/codec16.js';
/**
* Maps numeric codec IDs (as seen in the frame header) to their canonical
* string labels used in the Redis Stream record.
*
* Codec label plumbing decision (task 1.8):
* Option A: change CodecHandlerContext.publish signature to accept a codec label.
* - Pro: most direct.
* - Con: ripples into all three codec parser files + registry type.
* Option B (chosen): wrap ctx.publish in a closure at the dispatch site in
* index.ts. The handler still calls ctx.publish(position) unchanged; the
* wrapper captures frame.codecId, resolves it to a label, and forwards to
* Publisher.publish(position, codec). Zero changes to parsers or registry.
*/
const CODEC_ID_TO_LABEL: ReadonlyMap<number, CodecLabel> = new Map([
[0x08, '8'],
[0x8e, '8E'],
[0x10, '16'],
]);
export type TeltonikaAdapterOptions = {
readonly port: number;
readonly deviceAuthority?: DeviceAuthority;
@@ -154,11 +174,20 @@ export function createTeltonikaAdapter(options: TeltonikaAdapterOptions): Adapte
return;
}
// Resolve the codec label from the numeric ID so the publisher can
// include it as a top-level Redis Stream field. Fall back to the hex
// string if somehow an unregistered ID slipped past the registry guard
// (defensive — should not happen given the unknown-codec drop above).
const codecLabel = CODEC_ID_TO_LABEL.get(frame.codecId) ?? ('8' as CodecLabel);
let result: { recordCount: number };
try {
result = await handler.handle(frame.payload, {
imei,
publish: ctx.publish,
// Wrap AdapterContext.publish(position, codec) into the codec-
// handler-facing (position) => Promise<void> shape. The codec
// parsers are unaware of the label; it is captured here at dispatch.
publish: (position) => ctx.publish(position, codecLabel),
logger: sessionLogger,
});
} catch (handlerErr) {