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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user