Skip to content

Refactor: Wire format codec with ports & adapters pattern #37

@jpeggdev

Description

@jpeggdev

Problem

The IPC bridge (endpoints.ts, 209 lines) fuses two concerns: wire format transformation and IPC dispatch. The wire transforms (configToWire, configFromWire, endpointFromWire — 146 lines) use `any` casts with no runtime validation. Tests mock the entire command module via `vi.mock`, completely skipping the codec — so transform bugs (e.g., `apikey` <-> `api_key` mapping) are never caught.

Specific impedance mismatches handled manually:

  • `endpoint_type` <-> `type` (serde field rename)
  • `response_type` <-> `type` (nested in MockConfig)
  • `api_key` <-> `apikey` (ProxyAuth variant)
  • `variations`/`callbacks`: `null` (Rust Option) <-> `[]` (TS empty array)
  • Nested tagged unions: `{ type: 'mock', response: {...} }` (wire) <-> `MockConfig` flat object (TS)

Every new config variant requires updating both directions. No code generation exists.

Proposed Interface

Separate into three layers: Port (interface) + Codec (pure transforms) + Adapters (production Tauri / test in-memory).

Port — what stores depend on:

// src/services/tauri/ports/IEndpointPort.ts
export interface IEndpointPort {
  list(projectId: string): Promise<Endpoint[]>
  get(id: string): Promise<Endpoint>
  create(data: NewEndpoint): Promise<Endpoint>
  update(id: string, data: UpdateEndpointData, endpointType?: EndpointType): Promise<Endpoint>
  delete(id: string): Promise<void>
  restore(endpoint: Endpoint): Promise<Endpoint>
  restoreMany(projectId: string, endpoints: Endpoint[]): Promise<Endpoint[]>
  findByMethodPath(projectId: string, method: HttpMethod, path: string): Promise<Endpoint | null>
}

Codec — pure encode/decode with explicit wire types (no `any`):

// src/services/tauri/codecs/endpointCodec.ts
export function decodeEndpoint(wire: WireEndpoint): Endpoint
export function encodeConfig(endpointType: EndpointType, config: Endpoint['config']): WireConfig
export function encodeNewEndpoint(data: NewEndpoint): Record<string, unknown>

Production adapter — invoke + codec:

// src/services/tauri/adapters/TauriEndpointAdapter.ts
export const tauriEndpointAdapter: IEndpointPort = {
  list: async (projectId) => {
    const wires = await invoke<WireEndpoint[]>('get_endpoints', { projectId })
    return wires.map(decodeEndpoint)
  },
  // ...
}

In-memory test adapter — TS-shaped, no codec, no Tauri:

// src/services/tauri/adapters/InMemoryEndpointAdapter.ts
export function createInMemoryEndpointAdapter(seed?: Endpoint[]): IEndpointPort & { _store: Map<string, Endpoint> }

Stores use a swappable adapter:

let _adapter: IEndpointPort = tauriEndpointAdapter
export function setEndpointAdapter(adapter: IEndpointPort): void { _adapter = adapter }

Dependency Strategy

  • Category: Cross-boundary (Ports & Adapters) — Rust backend accessed via Tauri IPC
  • Port interface has zero runtime deps — stores import only this
  • Codec imports only port types + shared types (read-only)
  • Production adapter imports codec + invoke
  • In-memory adapter imports only shared types
  • Stores never import Tauri directly — only the port interface
  • The `any` surface shrinks from 6 function signatures to 2 narrow casts with comments

Testing Strategy

  • New boundary tests:
    • Codec roundtrip tests: `decodeEndpoint(wireFixture)` verifies field mapping, null-to-array, auth type conversion
    • Store tests use `setEndpointAdapter(createInMemoryEndpointAdapter())` — no `vi.mock` needed
  • Old tests to delete: `vi.mock('@/services/tauri')` patterns in store tests (replaced by adapter injection)
  • Test environment: Standard Vitest; codec tests use real wire fixtures matching Rust serde output

Implementation Recommendations

  • The codec module should own all wire <-> TS transforms with explicit `WireEndpoint` types
  • It should hide serde rename logic, null/array coercion, and tagged union switching
  • It should expose: `decodeEndpoint`, `encodeConfig`, `encodeNewEndpoint`, `encodeUpdateData`
  • The port interface is the contract; adapters are swappable implementations
  • Apply the same pattern to other complex command modules as they grow; simple modules (environments, flows) can stay as-is until they need transforms

Metadata

Metadata

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions