Files
tcp-ingestion/.planning/phase-1-telemetry/02-core-shell.md
T
julian c8a5f4cd68 Add Phase 1 and Phase 2 planning documents
ROADMAP plus granular task files per phase. Phase 1 (12 tasks + 1.13
device authority) covers Codec 8/8E/16 telemetry ingestion; Phase 2
(6 tasks) covers Codec 12/14 outbound commands; Phase 3 enumerates
deferred items.
2026-04-30 15:50:49 +02:00

90 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 1.2 — Core shell & framing types
**Phase:** 1 — Inbound telemetry
**Status:** ⬜ Not started
**Depends on:** 1.1
**Wiki refs:** `docs/wiki/concepts/protocol-adapter.md`, `docs/wiki/concepts/codec-dispatch.md`, `docs/wiki/concepts/position-record.md`
## Goal
Build the vendor-agnostic shell: TCP server bootstrap, per-socket session loop, and the type/registry definitions that adapters plug into. **No Teltonika-specific code in this task.**
## Deliverables
- `src/core/types.ts`:
- `Position` type matching the [[position-record]] shape exactly.
- `Adapter` interface: `{ name: string; ports: number[]; handleSession(socket: net.Socket, ctx: AdapterContext): Promise<void> }`.
- `AdapterContext` interface: `{ publish: (p: Position) => Promise<void>; logger: Logger; metrics: Metrics }` — a narrow contract giving adapters what they need without leaking shell internals.
- `src/core/registry.ts`:
- `AdapterRegistry` class (or simple module) holding `Map<port, Adapter>`. Methods: `register(adapter)`, `get(port)`.
- This is the *adapter* registry. The *codec* registry (per-vendor, in Teltonika's case) is internal to the adapter — it lives in `src/adapters/teltonika/` (task 1.4).
- `src/core/session.ts`:
- `runSession(socket, adapter, ctx)` that wraps `adapter.handleSession` with:
- Initial socket configuration (`setNoDelay`, `setKeepAlive` with a sane delay, e.g. 60s).
- Standard error handling: `error`, `close`, `end` events all logged at `debug` level with the connection's remote address.
- A `try { await handleSession() } catch (e) { logger.warn(e) }` that ensures the socket is destroyed if the handler throws.
- Crucially, `runSession` does *not* know about IMEI, framing, or codecs — those are entirely the adapter's business.
- `src/core/server.ts`:
- `startServer(port, adapter, ctx)` returning a closable handle. Uses `net.createServer((socket) => runSession(socket, adapter, ctx))`.
- Logs server bind, accepts, and close events.
- `src/core/publish.ts`:
- Stub `publishPosition(position)` returning `Promise<void>`. Real implementation lands in task 1.8. For now, it accepts a `Position` and logs at debug. The shape should already match what task 1.8 will produce so `Adapter` types stabilize early.
## Specification
### Vendor-agnostic discipline (re-stated)
**`src/core/` must not import from `src/adapters/` — ever.** This is enforced by ESLint with `eslint-plugin-import`'s `no-restricted-paths` rule. Add the rule in this task; failure to follow it should be a CI error.
```js
// in eslint.config.js
'import/no-restricted-paths': ['error', {
zones: [{ target: './src/core', from: './src/adapters' }]
}]
```
Adapters can import from `core/`; the reverse is forbidden.
### TCP socket settings
- `socket.setNoDelay(true)` — disable Nagle so ACKs are not batched. We're sending small ACKs; latency matters more than packet count.
- `socket.setKeepAlive(true, 60_000)` — TCP keepalive with 60s probe. Defends against idle NAT timeouts; safe because devices already retransmit on disconnect.
- No `socket.setTimeout()` at the shell level. The protocol does not specify per-frame timing; idle sockets are fine. Adapters can impose timeouts if their protocol demands.
### Position type
Mirror [[position-record]] precisely:
```ts
export type Position = {
device_id: string;
timestamp: Date;
latitude: number;
longitude: number;
altitude: number;
angle: number; // 0360
speed: number; // km/h, 0 may mean "GPS invalid" — caller preserves verbatim
satellites: number;
priority: 0 | 1 | 2; // Low | High | Panic
attributes: Record<string, number | bigint | Buffer>;
};
```
Use `Date` not `number` for `timestamp` — the value is a `Date` from the moment it leaves Ingestion, downstream is responsible for serialization choice.
## Acceptance criteria
- [ ] `pnpm typecheck` and `pnpm lint` pass.
- [ ] `src/core/server.ts` can be imported and `startServer` returns a `net.Server` that listens on a configurable port.
- [ ] A trivial test (in `test/core/server.test.ts`) starts a server with a stub adapter, opens a TCP client, and verifies the adapter's `handleSession` is invoked with a real socket.
- [ ] ESLint enforces the no-restricted-paths rule — verified by adding a temporary import-from-adapter into `src/core/server.ts` and confirming the lint error, then removing it.
## Risks / open questions
- The `AdapterContext`'s metrics interface is sketched but not fully specified until task 1.10. Make a minimal placeholder (`{ inc: (name, labels?) => void; observe: ... }`) and tighten in 1.10.
- `Buffer` in `Position.attributes` requires JSON serialization handling at the publish boundary (task 1.8). Decide there: base64-encode buffers, or serialize via msgpack. Recommendation: base64 with a sentinel in the JSON, e.g. `{ "_b64": "..." }`. Defer the decision to task 1.8 and revisit if simpler options surface.
## Done
(Fill in once complete.)