From 906470e02e1a3258223e757a6e9f4a87292b6538 Mon Sep 17 00:00:00 2001 From: Eric P Green Date: Mon, 5 Jan 2026 15:12:07 -0500 Subject: [PATCH] Plan for runtime context refactor --- plans/runtime-context-architecture.md | 385 ++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 plans/runtime-context-architecture.md diff --git a/plans/runtime-context-architecture.md b/plans/runtime-context-architecture.md new file mode 100644 index 00000000000..3d80cc488c3 --- /dev/null +++ b/plans/runtime-context-architecture.md @@ -0,0 +1,385 @@ +# Runtime Context Architecture Plan + +## Summary + +This document proposes a new architecture for managing runtime context (host, instanceId, JWT) in the frontend. The new architecture enables multi-runtime support, cleaner component code, and a migration path from Orval/REST to Connect/gRPC. + +## Problem Statement + +### The Immediate Bug (PR #8559) + +Canvas navigation between projects causes errors because: + +1. SvelteKit load functions call `setRuntime()` during navigation +2. The global `runtime` store updates immediately +3. Old components (still mounted) react to the new `instanceId` +4. They attempt to access canvas entities that don't exist for the new instanceId +5. Error occurs before old components unmount + +### The Underlying Architecture Issue + +The current architecture uses a **global mutable store** (`runtime`) that: + +- Is updated from load functions (wrong timing) +- Is read reactively by components (causes race conditions) +- Cannot support multiple runtimes simultaneously +- Mixes concerns (auth, routing, data fetching) + +## Options Considered + +### Option A: Quick Fix (PR #8559) + +- Remove `setRuntime` from load functions +- Set runtime via `RuntimeProvider` component (after old tree unmounts) +- Add `{#key instanceId}` to force component remount +- Add `enabled: !!instanceId` guards on queries + +**Verdict:** Fixes the immediate bug but doesn't address architectural issues. + +### Option B: HTTP Client State (PR #8572) + +- Remove the `runtime` store entirely +- Store host, instanceId, JWT on `httpClient` singleton +- Components call `httpClient.getInstanceId()` (non-reactive) + +**Verdict:** Cleaner than A, but commits to a singleton pattern that doesn't support multi-runtime. Would be a detour if we want multi-runtime later. + +### Option C: Context-Based Architecture with Connect Web + +- Use Svelte context to provide runtime configuration +- Migrate from Orval/REST to Connect/gRPC +- Generate TanStack Query hooks that use context +- Support multiple runtimes by nesting providers + +**Verdict:** Recommended. Solves the immediate problem, enables multi-runtime, and aligns with the desired migration to Connect/gRPC. + +## Recommended Architecture + +### Core Concepts + +``` +┌─────────────────────────────────────────────────────────┐ +│ RuntimeProvider │ +│ - Creates Connect transport with host + auth │ +│ - Sets transport in Svelte context │ +│ - Children only render when transport is ready │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ use{Service}() Factory Hook │ +│ - Calls getContext() to get transport │ +│ - Creates Connect client │ +│ - Returns query/mutation creators │ +└─────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Component │ +│ const { createGetExploreQuery } = useRuntimeService()│ +│ const query = createGetExploreQuery({ name }); │ +└─────────────────────────────────────────────────────────┘ +``` + +### RuntimeProvider + +```svelte + + + +{#if host && instanceId} + +{/if} +``` + +### Generated Service Hook + +```typescript +// Generated: useRuntimeService.ts +import { getContext } from 'svelte'; +import { createClient } from '@connectrpc/connect'; +import { createQuery, createMutation } from '@tanstack/svelte-query'; +import { RuntimeService } from '../proto/gen/rill/runtime/v1/api_connect'; +import type { Transport } from '@connectrpc/connect'; + +interface RuntimeContext { + transport: Transport; + instanceId: string; +} + +export function useRuntimeService() { + const { transport, instanceId } = getContext('runtime'); + const client = createClient(RuntimeService, transport); + + return { + instanceId, + + createGetExploreQuery: ( + params: { name: string }, + options?: { query?: CreateQueryOptions } + ) => createQuery({ + queryKey: ['RuntimeService', 'getExplore', instanceId, params], + queryFn: () => client.getExplore({ instanceId, ...params }), + enabled: !!instanceId, + ...options?.query, + }), + + createListResourcesQuery: ( + params: { kind?: string }, + options?: { query?: CreateQueryOptions } + ) => createQuery({ + queryKey: ['RuntimeService', 'listResources', instanceId, params], + queryFn: () => client.listResources({ instanceId, ...params }), + enabled: !!instanceId, + ...options?.query, + }), + + // ... other RPCs + }; +} + +// For use in load functions (explicit transport) +export function createRuntimeServiceClient(transport: Transport) { + return createClient(RuntimeService, transport); +} +``` + +### Component Usage + +```svelte + + +{#if $exploreQuery.isLoading} + +{:else if $exploreQuery.data} + +{/if} +``` + +### Load Function Usage + +```typescript +// +layout.ts +export async function load({ params }) { + // Fetch runtime config (host, jwt) from admin API or parent + const runtimeConfig = await fetchProjectRuntime(params.org, params.project); + + return { + runtime: runtimeConfig, // Passed to RuntimeProvider via data prop + }; +} +``` + +```svelte + + + + + + +``` + +### Load Function Data Fetching + +When load functions need to fetch data: + +```typescript +// +page.ts +export async function load({ params, parent }) { + const { runtime } = await parent(); + + const transport = createConnectTransport({ + baseUrl: runtime.host, + interceptors: runtime.jwt ? [authInterceptor(runtime.jwt)] : [], + }); + + const client = createRuntimeServiceClient(transport); + const explore = await client.getExplore({ + instanceId: runtime.instanceId, + name: params.exploreName + }); + + return { explore }; +} +``` + +## Multi-Runtime Support + +The context-based architecture naturally supports multiple runtimes: + +```svelte + +
+ + + + + + + +
+``` + +Each subtree uses its own transport and instanceId. Components don't need to know which runtime they're in. + +## Code Generator + +### Input + +The generator reads from `*_connect.ts` files produced by `protoc-gen-es`: + +```typescript +// proto/gen/rill/runtime/v1/api_connect.ts +export const RuntimeService = { + typeName: "rill.runtime.v1.RuntimeService", + methods: { + getExplore: { + name: "GetExplore", + I: GetExploreRequest, + O: GetExploreResponse, + kind: MethodKind.Unary, + }, + // ... + } +}; +``` + +### Output + +For each service, generates: + +1. `use{Service}.ts` - Factory hook for components (context-based) +2. Query key generators for cache management +3. Type exports for request/response + +### Generator Scope + +Estimated ~300-500 lines of TypeScript. Responsibilities: + +- Parse service definitions from `*_connect.ts` +- Determine query vs mutation (unary GETs → query, others → mutation) +- Generate TanStack Query wrappers with proper typing +- Generate query key factories + +## Migration Strategy + +### Phase 0: Immediate Fix (Now) + +Merge PR #8559's quick fix to unblock the Canvas navigation bug. This is compatible with the long-term architecture. + +### Phase 1: Infrastructure (1-2 weeks) + +1. Create `RuntimeProvider` component +2. Write the code generator +3. Generate hooks for `LocalService` (already using Connect) +4. Validate pattern works end-to-end + +### Phase 2: Incremental Migration + +Migrate RPCs incrementally, not all at once: + +1. **Per-RPC migration:** Generate Connect hook for one RPC, update components, verify +2. **Coexistence:** Old Orval hooks and new Connect hooks can coexist +3. **Priority order:** + - Start with low-traffic RPCs to validate + - Then high-pain RPCs (ones causing issues) + - Leave rarely-used RPCs for last + +### Phase 3: Cleanup + +Once a service is fully migrated: + +1. Remove Orval-generated code for that service +2. Update Orval config to exclude migrated services +3. Eventually remove Orval dependency entirely + +## JWT Handling + +### Refresh Flow + +```typescript +// RuntimeProvider handles JWT refresh + +``` + +The interceptor can detect expiring JWTs and trigger refresh before requests fail. + +### User Impersonation + +```typescript +// Switch to viewing as another user +async function impersonateUser(userId: string) { + const newJwt = await adminService.getImpersonationToken(userId); + // Update RuntimeProvider props → new transport created → queries refetch +} +``` + +Since transport is recreated when props change, queries automatically use the new auth context. + +## Open Questions + +1. **Query key namespacing:** Should query keys include a version or hash to handle proto schema changes? + +2. **Streaming RPCs:** How should server-streaming RPCs (like `WatchResources`) integrate with TanStack Query? + +3. **Error handling:** Should the generator produce error type mappings from Connect errors to application errors? + +4. **Caching strategy:** Should we generate cache update helpers for common patterns (optimistic updates, cache invalidation)? + +## Appendix: Comparison with Current Architecture + +| Aspect | Current (Orval + Global Store) | Proposed (Connect + Context) | +|--------|-------------------------------|------------------------------| +| Runtime config | Global mutable store | Svelte context per subtree | +| Client generation | Orval from OpenAPI | Custom generator from protobuf | +| Protocol | REST/HTTP | Connect (gRPC-compatible) | +| Multi-runtime | Not supported | Supported via nested providers | +| Type safety | Generated from OpenAPI | Generated from protobuf | +| Load function support | Problematic (caused bug) | Supported with explicit client | +| Migration | N/A | Incremental, per-RPC | + +## References + +- [PR #8559: Canvas navigation fix](https://github.com/rilldata/rill/pull/8559) +- [PR #8572: Refactor instanceId handling](https://github.com/rilldata/rill/pull/8572) +- [Connect Web documentation](https://connectrpc.com/docs/web/getting-started/) +- [TanStack Query Svelte](https://tanstack.com/query/latest/docs/framework/svelte/overview) +- [Connect-Query (React reference)](https://github.com/connectrpc/connect-query-es)