-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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