refactor: migrate runtime client from Orval to ConnectRPC with scoped RuntimeClient#8933
Draft
ericpgreen2 wants to merge 44 commits intomainfrom
Draft
refactor: migrate runtime client from Orval to ConnectRPC with scoped RuntimeClient#8933ericpgreen2 wants to merge 44 commits intomainfrom
RuntimeClient#8933ericpgreen2 wants to merge 44 commits intomainfrom
Conversation
…hase 1)
Introduce the foundation for replacing the global mutable `runtime`
store with scoped Svelte context.
- Generate ConnectRPC TypeScript service descriptors for RuntimeService,
QueryService, and ConnectorService via buf (new buf.gen.runtime.yaml)
- Add RuntimeClient class that encapsulates transport, JWT lifecycle,
and lazy service client creation
- Add RuntimeProvider component that creates a RuntimeClient, sets it in
Svelte context, and bridges to the global store for unmigrated consumers
- Add useRuntimeClient() context helper
- Wire RuntimeProvider into web-admin project layout with {#key} on
host::instanceId for correct re-mounting on project navigation
- Wire RuntimeProvider into web-local root layout
- Extract JWT expiry constants to shared constants.ts
Add a build-time code generator that reads ConnectRPC *_connect.ts service descriptors and produces TanStack Query hooks for Svelte. For each unary query method, generates 4 tiers: raw RPC function, query key factory, query options factory, and convenience createQuery hook. Mutation methods get 3 tiers: raw function, mutation options, and createMutation hook. Output: 59 queries + 28 mutations across QueryService, RuntimeService, and ConnectorService. Streaming methods (watchFiles, watchResources, watchLogs, completeStreaming, queryBatch) are skipped. Generated hooks take RuntimeClient as first argument and inject instanceId automatically, matching the Phase 1 context architecture.
…ase 3)
Update cache invalidation and query matchers to support both the old
URL-path query key format ("/v1/instances/{id}/...") and the new
service/method format ("QueryService", "metricsViewAggregation",
instanceId, request).
This enables incremental migration: as consumers switch from Orval
hooks to v2 generated hooks, both key formats are correctly matched
for invalidation, profiling, metrics view, and component queries.
The generator was producing `PartialMessage<Omit<Request, "instanceId">>`, but `Omit<...>` strips the `Message<T>` constraint that `PartialMessage` requires. Fix by swapping to `Omit<PartialMessage<Request>, "instanceId">`. Also detect at generation time whether each request type actually has an `instanceId` field; methods like `ListInstances` and `IssueDevJWT` don't have it and should not receive auto-injection.
When RuntimeProvider calls setRuntime with no JWT (local dev), the
equality check compared authContext ("user") against current.jwt?.authContext
(undefined), always returning false. This caused the store to emit a new
value on every call, triggering an infinite reactive loop:
RuntimeProvider bridge → store update → layout re-render → bridge again.
Fix by treating JWT as unchanged when both old and new JWT are absent,
regardless of authContext.
Generated hooks now accept/return Orval-compatible types (V1*) in their public API, converting to/from proto internally via fromJson/toJson. This lets consumers switch to v2 hooks without changing their existing type usage (V1Expression, V1Resource, etc.). The generator reads the Orval schemas file at generation time to determine which V1 types exist. Response types always use V1 (100% coverage). Request types use V1 when available (~25%, POST endpoints) and fall back to PartialMessage<ProtoType> for GET endpoints.
The generated hooks now accept a TData type parameter (defaulting to the response type), enabling select transforms that narrow the return type. This mirrors the Orval pattern and avoids forcing consumers to use derived stores as workarounds.
Proto fromJson() rejects undefined field values, but callers may pass them (e.g., TanStack Query's pageParam starts as undefined, or reactive props that resolve asynchronously). Orval's HTTP client silently omitted undefined values; the JSON bridge must do the same.
Proto3 toJson() omits fields with default values (0, "", false) by default. gRPC-Gateway includes them. This caused missing fields like low: 0 in histogram bins, breaking D3 scale computations and rendering blank histograms for columns with data starting near zero.
…st objects
The shallow `stripUndefined` only removed top-level undefined values.
Nested objects like `timeRange: { start: undefined }` passed through
to `fromJson()` which rejects undefined, causing JSON decode errors.
Port the two-level heap request queue as a ConnectRPC interceptor. Maps method names (instead of URLs) to priorities and controls concurrency via the same architecture as HttpRequestQueue.
Migrate 8 files from the global runtime store to useRuntimeClient(): - ConnectorExplorer, ConnectorEntry, TableEntry, TableSchema, TableMenuItems, TableWorkspaceHeader, ConnectorRefreshButton - ColumnProfile Exercises all key migration patterns: - $runtime.instanceId → useRuntimeClient() + client.instanceId - Orval createQuery hooks → v2 generated hooks (request object style) - Orval createMutation → v2 generated mutation hooks - Structural type compatibility between proto and Orval response types
Migrate DatabaseExplorer, DatabaseEntry, DatabaseSchemaEntry, TableInspector, and References to use useRuntimeClient() from context. This demonstrates eliminating instanceId prop drilling: child components now get the RuntimeClient from Svelte context instead of receiving instanceId as a prop from parents.
… v2 RuntimeClient - column-profile/queries.ts: all functions accept RuntimeClient instead of instanceId - connectors/selectors.ts: useListDatabaseSchemas, useInfiniteListTables, useGetTable accept RuntimeClient; useIsModelingSupported* functions left as-is (callers out of scope) - Column profile components (NumericProfile, TimestampProfile, VarcharProfile, NestedProfile): replace $runtime with useRuntimeClient() - WorkspaceInspector: replace $runtime with useRuntimeClient(), switch Orval hooks to v2 - Canvas components (CanvasInitialization, KPIProvider, PageEditor): replace $runtime with useRuntimeClient(), switch Orval hooks to v2 - Update already-migrated callers to pass client instead of client.instanceId
Reverts the derived-store workarounds in queries.ts and selectors.ts, using select transforms directly as the v2 code generator now supports generic TData inference.
…ntimeClient Chart providers (batch 1): all 6 providers now accept RuntimeClient instead of Writable<Runtime>, use v2 query hooks, and remove runtime from derived store inputs since the client is a stable object. Canvas leaf components (batch 2): 13 Svelte components migrated from $runtime store to useRuntimeClient() context. CanvasStore extended with runtimeClient field to bridge chart providers in canvas context.
…re stores to v2 Bridge pattern: adds runtimeClient alongside runtime in StateManagers. Migrates validSpecStore and timeRangeSummaryStore to v2 hooks, removing runtime from their derived() dependencies.
…ntimeClient Migrates 5 TS modules that receive StateManagers: timeseries-data-store, totals-data-store, multiple-dimension-queries, dimension-filters selector, and dashboard selectors. Removes ctx.runtime from derived() dependencies and switches to v2 query hooks.
…seRuntimeClient Replaces direct `runtime` store imports with `useRuntimeClient()` across 13 dashboard Svelte components. Uses stable `client.instanceId` instead of reactive `$runtime.instanceId`.
- Add ConnectRPC URL routing to `DashboardFetchMocks` (handles
`/rill.runtime.v1.{Service}/{Method}` requests with proper body
decoding and RFC 3339 timestamp normalization)
- Inject `RuntimeClient` context in test renders that mount components
calling `useRuntimeClient()`
…o v2 RuntimeClient Replace `get(runtime).instanceId` with explicit `instanceId` parameters in tdd-export, pivot-export, and dimension-table-export. Switch pivot-queries from Orval hook to v2 `createQueryServiceMetricsViewAggregation`, threading `runtimeClient` through `PivotDashboardContext`.
… explicit instanceId Add `instanceId` parameter to `deriveInterval()` and `resolveTimeRanges()` instead of reading from the global `runtime` store. Thread `instanceId` through callers: DashboardStateSync, Filters, FiltersForm, canvas TimeState, and explore-mappers utils.
…tore to v2 RuntimeClient - Remove unused `runtime` property from dashboard and canvas StateManagers - Delete dead code: `getValidDashboardsQueryOptions` and `getMetricsViewSchemaOptions` - Migrate 10 canvas Svelte components to `useRuntimeClient()` - Migrate canvas markdown `util.ts` to accept explicit `instanceId` parameter - Migrate 2 explores Svelte components to `useRuntimeClient()`
…move dead code Migrate leaf query option factories, intermediate callers, and module-level singletons to accept `client: RuntimeClient` instead of reading the global `runtime` store. Delete dead code (`getCanvasQueryOptions`, `utils.ts`). Chat context functions updated as necessary follow-through from leaf factory signature changes.
…eClient` Migrate ~100 files across chat, alerts, scheduled reports, file management, entity management, workspaces, sources, models, metrics-views, connectors, explore-mappers, exports, and other features from the global `runtime` store to `useRuntimeClient()` / `RuntimeClient` parameter threading. Key changes: - Add `host` property to `RuntimeClient` class - Convert `canvasChatConfig` to `createCanvasChatConfig(client)` factory - Thread `RuntimeClient` through `Conversation` and `ConversationManager` - Migrate `resource-selectors.ts` query option factories to accept `client` - Migrate chat picker data (models, canvases) to accept `client` - Migrate `connectors/code-utils.ts`, `file-artifact.ts`, `new-files.ts`, `submitAddDataForm.ts`, `explore-mappers/*.ts` to accept instanceId/client - Migrate `vega-embed-options.ts` to accept `RuntimeClient` - Migrate `RillIntakeClient` to accept `host` parameter - All `.svelte` callers updated to use `useRuntimeClient()`
Replace the old `runtime-store` mock with a `RuntimeClient` stub, matching the updated `Conversation` constructor signature.
…meClient - Create `local-runtime-config.ts` with shared `LOCAL_HOST` and `LOCAL_INSTANCE_ID` constants for web-local - Migrate web-local load functions and layout to use constants instead of reading from the runtime store - Add `getJwt` parameter to `SSEFetchClient.start()`, removing its direct import of the runtime store - Thread `RuntimeClient` through `updateDevJWT` (dual-write bridge) - Decouple `local-service.ts` from runtime store via `setLocalServiceHost()` - Replace `Runtime` type import in `selectors.ts` with inline type - Rewrite `query-options.ts` to use `fetch` instead of `httpClient` - Rewrite `open-query.ts` to use `fetch` instead of `httpClient` - Rewrite `getFeatureFlags()` to use `fetch` instead of `httpClient` - Migrate `download-report.ts` to accept `host` via mutation data
…rovider, thread host - Migrate embed layout to v2 RuntimeProvider (was the last consumer of the old one) - Delete old RuntimeProvider.svelte (replaced by v2/RuntimeProvider.svelte) - Thread `host` parameter through `createDownloadReportMutation`, removing its `runtime` store dependency Remaining runtime-store consumers are blocked by the Orval → ConnectRPC migration (http-client.ts backbone, SSE singleton pattern, web-local bridge hooks/layouts/load functions).
…ReportMutation` call - Delete `StreamingQueryBatch.ts` and `fetch-streaming-wrapper.ts` (dead code, confirmed no consumers) - Fix spurious `runtimeClient.host` argument passed to `createDownloadReportMutation()` — leftover from rebase conflict resolution; host is already threaded via `DownloadReportRequest.data`
…ttp-client infrastructure - Migrate column-profile, generateMetricsView, Image.svelte, file-upload, app-store, billing/plans selectors, enhance-citation-links, connectors/selectors, and dashboard-fetch-mocks from legacy http-client/fetchWrapper/manual-clients/runtime-store - Add getPriorityForColumn to v2/request-priorities.ts - Delete web-common/src/runtime-client/gen/runtime-service/ - Delete web-common/src/runtime-client/gen/query-service/ - Delete web-common/src/runtime-client/gen/connector-service/ - Delete web-common/src/runtime-client/runtime-store.ts - Delete web-common/src/runtime-client/http-client.ts - Delete web-common/src/runtime-client/http-request-queue/ - Delete web-common/src/runtime-client/fetchWrapper.ts - Delete web-common/src/runtime-client/manual-clients.ts - Delete web-common/orval.config.ts and remove orval from package.json
…arams, nav bar outside RuntimeProvider - `useDashboardPolicyCheck`: add `enabled: !!filePath` to prevent GetFile with empty path when explore data hasn't loaded yet - `References.svelte`: pass `connector`, `database`, `databaseSchema` to `createQueryServiceTableCardinality` (was sending resource name as tableName) - `TopNavigationBar.svelte`: use `tryUseRuntimeClient()` instead of `useRuntimeClient()` since the nav bar renders at the root layout, outside any RuntimeProvider on org-level pages - Add `tryUseRuntimeClient()` — returns null instead of throwing when no RuntimeProvider ancestor exists
- Fix v2 function call sites passing `instanceId` instead of `RuntimeClient`
- Convert flat request params (`"name.kind"`) to nested proto format (`{ name: { kind } }`)
- Replace `error?.response?.status` with `isHTTPError()` duck-typing
- Fix `canvasName` → `canvas` field name in ResolveCanvas requests
- Update `HTTPError` → `Error` in component prop types and query result types
- Add `as any` casts for V1-to-proto type mismatches (timestamps, expressions, exports)
- Remove unused `instanceId` destructures
TopNavigationBar renders in the root layout ABOVE RuntimeProvider, so Svelte context-based `tryUseRuntimeClient()` always returned null. This broke breadcrumbs (empty visualization list) and crashed canvas pages (CanvasBookmarks called `useRuntimeClient()` which threw). Add `runtimeClientStore` — a module-level writable store that RuntimeProvider populates on mount. TopNavigationBar subscribes reactively. A new `RuntimeContextBridge` component sets Svelte context for child components (bookmarks, state managers) that call `useRuntimeClient()`.
…ately When the SSE server opens a connection (200) but closes the stream immediately (e.g. cloud runtime log endpoint), the retry counter was reset to 0 on every successful "open" event. This meant maxRetryAttempts was never reached, causing an infinite reconnection loop (thousands of requests per minute). Fix: only reset retryAttempts when the connection was stable (open for at least 5 seconds). Short-lived connections now count toward the retry limit and eventually stop with exponential backoff.
The SSE log endpoint on cloud requires authentication. The logs page was connecting without a JWT, so the server returned an empty stream. Two fixes: - SSEConnectionManager.start() now forwards options (including getJwt) to the underlying SSEFetchClient (previously options were stored but never passed) - ProjectLogsPage passes runtimeClient.getJwt() so the SSE fetch includes an Authorization header
- Retarget runtime-client/index.ts barrel from deleted Orval gen to v2/gen - Migrate feature-flags.ts from runtime store subscription to setRuntimeClient() - Migrate citation-url-utils.ts from httpClient to fetch - Update conversation.ts fork call to v2 signature (client, request) - Update conversation.spec.ts fork assertions to match v2 signature
e2e0ca5 to
2102ddd
Compare
Update ~120 files to use RuntimeClient as first argument instead of instanceId string, use nested request objects instead of flat dot-notation keys, and fix proto Timestamp type incompatibilities. Add getLocalRuntimeClient() singleton for web-local SvelteKit load functions.
33be2b2 to
2192094
Compare
Migrate remaining consumers to v2 RuntimeClient signatures: - Replace runtime-store imports with useRuntimeClient() across 25 web-admin files - Replace instanceId string args with RuntimeClient in 58 web-common files - Remove deleted http-client imports from 5 files (ModelWorkspace + 4 column profiles) - Fix web-local canvas/explore pages to use useRuntimeClient() - Fix prettier formatting - Remove stale runtime-store mock from code-utils.spec.ts
RuntimeClient
RuntimeClientRuntimeClient
RuntimeClientRuntimeClient
RuntimeClientRuntimeClient
RuntimeClientRuntimeClient
- Fix missing barrel exports: use `*Mutation(runtimeClient)` pattern for
`ShareConversation`, `UnpackExample`, `UnpackEmpty`, `CreateTrigger`,
and `QueryServiceExport` mutations
- Fix `MapExploreUrlContext` shape: pass `{ client: runtimeClient }` instead
of `{ instanceId }` in AlertMetadata and ReportMetadata
- Fix `getHomeBookmarkExploreState` call: pass `runtimeClient` instead of
`instanceId` in explore page
- Remove dead `export let instanceId` props from 5 components and their
parent call sites now that they use `useRuntimeClient()` internally
- Fix ESLint `no-non-null-asserted-optional-chain` with type assertions
in alert and report selectors
- Fix v2 protobuf request format in RunNowButton: use nested `name` object
instead of dot-notation keys
- Fix error type checking in ExploreEmbed: use `isHTTPError()` guard
- Replace missing `createRuntimeServiceGetModelPartitionsInfinite` with
`createInfiniteQuery` using v2 RPC primitives in PartitionsTable
Clean up unused variables left behind by the v2 RuntimeClient migration (13 ESLint errors) and fix 5 svelte-check type errors from mismatched call signatures.
…web-local Remove the now-unused `useRuntimeClient()` call in NavFile.svelte and replace the `$lib/local-runtime-config` import with a relative path in web-local's +layout.svelte to fix svelte-check resolution.
…annotations
RuntimeProvider's `{#if host && instanceId}` guard treated empty-string
host as invalid, but web-local uses `""` (same-origin) in production
mode. Changed to `{#if instanceId}`.
Also fixed `getAnnotationsForMeasure` call in MetricsTimeSeriesCharts
which passed `instanceId` (string) where `client` (RuntimeClient) was
expected, causing a crash on dashboard load.
…cess during render Children of `FileAndResourceWatcher` render before `onMount` fires, so `fileArtifacts.getFileArtifact()` was creating `FileArtifact` instances with an undefined client. Add `setClient()` and call it in the script block so it runs before children mount.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Migrate the frontend runtime client from Orval + global store to ConnectRPC + scoped
RuntimeClient. See tech design for architecture rationale.Why. The global
runtimestore prevents supporting multiple simultaneous runtimes, which is needed for cloud editing. Orval generates hooks tied to a singleton HTTP client, so the hooks can't be reused across providers. Since we need new code generation anyway, we migrate the transport to ConnectRPC at the same time — something the team has wanted for a long time.What changed:
RuntimeClientclass wraps ConnectRPC transport, JWT lifecycle, and service clients. EachRuntimeProvidercreates a scoped instance in Svelte context; components read it viauseRuntimeClient().scripts/generate-query-hooks.ts) reads ConnectRPC*_connect.tsdescriptors and produces TanStack Query hooks with a JSON bridge — consumers keep using Orval-compatibleV1*types while proto conversion happens internally.runtimestore imports touseRuntimeClient()+ v2 hooks.fetchWrapper,http-client,HttpRequestQueue,StreamingQueryBatch,RuntimeProvider(old),runtime-store.RuntimeProvider(v2),RuntimeContextBridgefor backward compatibility during migration.Code samples
Before (Orval + global store):
After (ConnectRPC + scoped client):
Key differences:
runtimeClientcomes from Svelte context (scoped, not global), the function takes the client object instead of a bareinstanceId, request parameters use nested protobuf structure instead of dot-notation, andinstanceIdis injected automatically by the hook.Orval type schemas
web-common/src/runtime-client/gen/index.schemas.ts(3,367 lines, ~440 types) is intentionally retained. The v2 hooks use a JSON bridge that accepts these OrvalV1*types in their public API and converts to/from proto internally viafromJson()/toJson(). This made the migration tractable — consumers keep their existing type imports unchanged. Migrating to native proto types is future work (see below).Future work
V1*types inindex.schemas.tsare generated from OpenAPI (proto → OpenAPI → TypeScript), losing fidelity ononeoffields and enums. ConnectRPC'sprotobuf-esgenerates proper discriminated unions and TypeScript enums. Once this PR lands, we can incrementally replaceV1*imports with proto-native types and eventually deleteindex.schemas.tsand the JSON bridge layer. This would also let us stop running the OpenAPI codegen pipeline entirely.connect-gowould let us drop the OpenAPI proxy layer, improving performance and simplifying the stack. The frontend is now ready for this — the ConnectRPC transport supports both JSON and binary proto.connect-queryupstream integration. Our code generator is custom because@connectrpc/connect-querytargets React. If upstream adds Svelte support (connectrpc/connect-query-es#324), we could replace our generator. The hook signatures are designed to be compatible.featureFlagsis still a module-level singleton. The tech design calls for making it aRuntimeClientproperty so different runtimes can have different flags.instanceIdextractions. A handful of files still destructureinstanceIdfromruntimeClientfor non-hook uses (query key construction, SSE connections). These are functional but could be tidied.PartitionsTablewas manually converted to usecreateInfiniteQuerysince the generator doesn't emit infinite query variants. Adding this to the generator would cover future pagination use cases.Checklist:
Developed in collaboration with Claude Code