Skip to content

Implement call lifecycle, realtime fan-out and all bonus items#8

Open
crydafan wants to merge 42 commits into
development-voycelink:mainfrom
crydafan:main
Open

Implement call lifecycle, realtime fan-out and all bonus items#8
crydafan wants to merge 42 commits into
development-voycelink:mainfrom
crydafan:main

Conversation

@crydafan
Copy link
Copy Markdown

What changed

Core implementation

call-service — event processing, persistence, pub/sub

  • State machine (resolveNextStatus) enforces the allowed transition graph and throws InvalidTransitionError on illegal moves
  • Five event handlers (call_initiatedcall_routedcall_answeredcall_holdcall_ended) apply business rules (SLA flag at >30 s wait, hold limit at >60 s, short-call flag at <10 s, reroute flag at >15 s routing time) and attach them as metadata on the persisted event
  • Drizzle ORM repositories for calls and events; DB access is fully behind interfaces, making the service layer independently testable

realtime-service — Redis subscriber + Socket.io fan-out

  • Subscribes to call-status-updates channel; on each message it emits to a room named after the callId so only clients watching that call receive the update

frontend — real data, real-time

  • useCalls fetches via React Query and subscribes each visible call to its Socket.io room; status updates patch the cache in place without a refetch
  • Loading and error states are handled in the hook

Bonus items

Item What was done
Pagination GET /api/calls accepts ?page=&limit= (default 20, max 100); returns { data, total, page, limit, totalPages }; PaginatedResult<T> generic lives in contracts so frontend types stay in sync
Retry / dead-letter Publisher retries up to 3× with exponential backoff (50 ms, 100 ms); on final failure logs to stderr and returns — the DB write is already committed and is the source of truth, so we degrade gracefully rather than rolling back
Idempotent ingestion All five handlers call findEventByTypeForCall before doing any work and return the existing event on duplicate delivery
Dockerfiles One per service under infra/docker/
Contracts Merged index.js + index.d.ts into a single index.ts; types are derived from Zod schemas via z.infer<> so they can never diverge
Observability Request logger middleware (TIMESTAMP METHOD path → STATUS (Xms) on every response); /health enriched with uptime and timestamp
Dev compose pnpm infra:dev builds a shared image (installs all deps, pre-builds contracts), then mounts each service's source directory; nodemon + ts-node hot-reloads the Node services, Next.js runs next dev; CHOKIDAR_USEPOLLING / WATCHPACK_POLLING enabled for reliable watching on Mac/Windows Docker Desktop

Testing

  • Unit tests (CallService.test.ts) cover all five event handlers, the state machine, business rules, idempotency, and error paths — everything isolated from infrastructure via mocks
  • Route tests (events.test.ts) test the HTTP layer end-to-end via supertest with callService mocked at the service boundary: 201, 400 (Zod), 401, 404, 422, 500 — no DB or Redis needed

Tradeoffs and assumptions

  • Idempotency scope: call_routed is deduplicated by event type, meaning a retry returns the original routing rather than allowing re-assignment. For a real system we'd probably want "latest route wins" semantics with a different dedup key.
  • Publisher failure mode: swallowing after 3 retries keeps the API available when Redis is degraded, but the dashboard goes stale. A proper fix is a failed_publishes outbox table drained by a background job, skipped here in favour of simplicity.
  • Pagination count query: two sequential queries (count + paginated select) rather than a window function. Simpler to read; fine at this scale, but a single query with COUNT(*) OVER () would be more efficient on large tables.
  • Contracts pre-built in dev image: changing packages/contracts/index.ts requires docker compose build to take effect; only service src/ directories hot-reload. Acceptable given how rarely the contract layer changes.

What I'd do next

  • Persistent dead-letter queue: outbox table + background re-publisher for Redis failures
  • Structured logging: replace console.log with pino, add a request correlation ID header
  • E2E tests: Playwright against the full stack for the dashboard golden path
  • Rate limiting: on POST /api/events to protect against replay floods

crydafan added 30 commits May 12, 2026 18:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant