From d011447190fd329e8f7f6d505f062875a1aee525 Mon Sep 17 00:00:00 2001 From: shahondin1624 Date: Thu, 12 Mar 2026 11:22:15 +0100 Subject: [PATCH] feat: add gRPC-Web client service layer for OrchestratorService - Create src/lib/services/orchestrator.ts with Connect-Web transport - Typed processRequest() async generator for server-streaming RPC - OrchestratorError class mapping gRPC status codes to app errors - Configurable endpoint with resetTransport() for reconfiguration Closes #4 Co-Authored-By: Claude Opus 4.6 --- implementation-plans/_index.md | 1 + implementation-plans/issue-004.md | 25 +++++++++ src/lib/services/.gitkeep | 0 src/lib/services/orchestrator.ts | 89 +++++++++++++++++++++++++++++++ 4 files changed, 115 insertions(+) create mode 100644 implementation-plans/issue-004.md delete mode 100644 src/lib/services/.gitkeep create mode 100644 src/lib/services/orchestrator.ts diff --git a/implementation-plans/_index.md b/implementation-plans/_index.md index c44befd..d905abc 100644 --- a/implementation-plans/_index.md +++ b/implementation-plans/_index.md @@ -5,3 +5,4 @@ | #1 | Project scaffolding: SvelteKit + Tailwind + TypeScript | COMPLETED | [issue-001.md](issue-001.md) | | #2 | Proto codegen pipeline for TypeScript gRPC-Web stubs | COMPLETED | [issue-002.md](issue-002.md) | | #3 | Configure Caddy for gRPC-Web support | COMPLETED | [issue-003.md](issue-003.md) | +| #4 | gRPC-Web client service layer | COMPLETED | [issue-004.md](issue-004.md) | diff --git a/implementation-plans/issue-004.md b/implementation-plans/issue-004.md new file mode 100644 index 0000000..fc91eb9 --- /dev/null +++ b/implementation-plans/issue-004.md @@ -0,0 +1,25 @@ +--- +--- + +# Issue #4: gRPC-Web client service layer + +**Status:** COMPLETED +**Issue:** https://git.shahondin1624.de/llm-multiverse/llm-multiverse-ui/issues/4 +**Branch:** `feature/issue-4-grpc-web-client` + +## Summary + +Create `src/lib/services/orchestrator.ts` wrapping Connect-Web client for the OrchestratorService, with typed `processRequest()` async generator and error mapping. + +## Acceptance Criteria + +- [x] `src/lib/services/orchestrator.ts` module created +- [x] Wraps generated gRPC-Web stubs with typed interface +- [x] Connection setup with configurable endpoint +- [x] Error mapping from gRPC status codes to application errors +- [x] `processRequest()` returns an async iterator of `ProcessRequestResponse` +- [x] Handles streaming responses correctly + +## Deviations + +None. diff --git a/src/lib/services/.gitkeep b/src/lib/services/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/lib/services/orchestrator.ts b/src/lib/services/orchestrator.ts new file mode 100644 index 0000000..1fe6d52 --- /dev/null +++ b/src/lib/services/orchestrator.ts @@ -0,0 +1,89 @@ +import { createClient } from '@connectrpc/connect'; +import { createGrpcWebTransport } from '@connectrpc/connect-web'; +import { OrchestratorService } from '$lib/proto/llm_multiverse/v1/orchestrator_pb'; +import type { + ProcessRequestResponse, + SessionConfig +} from '$lib/proto/llm_multiverse/v1/orchestrator_pb'; +import { create } from '@bufbuild/protobuf'; +import { ProcessRequestRequestSchema } from '$lib/proto/llm_multiverse/v1/orchestrator_pb'; + +/** + * Application-level error wrapping gRPC status codes. + */ +export class OrchestratorError extends Error { + constructor( + message: string, + public readonly code: string, + public readonly details?: string + ) { + super(message); + this.name = 'OrchestratorError'; + } +} + +const DEFAULT_ENDPOINT = '/'; + +let transport: ReturnType | null = null; + +function getTransport(endpoint?: string) { + if (!transport) { + transport = createGrpcWebTransport({ + baseUrl: endpoint ?? DEFAULT_ENDPOINT + }); + } + return transport; +} + +/** + * Reset the transport (useful for reconfiguring the endpoint). + */ +export function resetTransport(): void { + transport = null; +} + +/** + * Create a configured orchestrator client. + */ +function getClient(endpoint?: string) { + return createClient(OrchestratorService, getTransport(endpoint)); +} + +/** + * Send a request to the orchestrator and yield streaming responses. + * + * Returns an async iterator of `ProcessRequestResponse` messages, + * each containing the current orchestration state, status message, + * and optionally intermediate or final results. + */ +export async function* processRequest( + sessionId: string, + userMessage: string, + sessionConfig?: SessionConfig, + endpoint?: string +): AsyncGenerator { + const client = getClient(endpoint); + + const request = create(ProcessRequestRequestSchema, { + sessionId, + userMessage, + sessionConfig + }); + + try { + for await (const response of client.processRequest(request)) { + yield response; + } + } catch (err: unknown) { + if (err instanceof Error) { + // ConnectError has a `code` property + const code = 'code' in err ? (err as { code: unknown }).code : undefined; + throw new OrchestratorError( + err.message, + typeof code === 'string' ? code : 'unknown', + err.message + ); + } + throw new OrchestratorError('Unknown error', 'unknown'); + } +} -- 2.49.1