# Task 1.2 — Core shell & framing types **Phase:** 1 — Inbound telemetry **Status:** 🟩 Done — landed in commit `1e9219d` **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 }`. - `AdapterContext` interface: `{ publish: (p: Position) => Promise; 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`. 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`. 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; // 0–360 speed: number; // km/h, 0 may mean "GPS invalid" — caller preserves verbatim satellites: number; priority: 0 | 1 | 2; // Low | High | Panic attributes: Record; }; ``` 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.)