Skip to content

refactor: migrate runtime client from Orval to ConnectRPC with scoped RuntimeClient#8933

Draft
ericpgreen2 wants to merge 44 commits intomainfrom
ericgreen/runtime-context-v2-cleanup
Draft

refactor: migrate runtime client from Orval to ConnectRPC with scoped RuntimeClient#8933
ericpgreen2 wants to merge 44 commits intomainfrom
ericgreen/runtime-context-v2-cleanup

Conversation

@ericpgreen2
Copy link
Contributor

@ericpgreen2 ericpgreen2 commented Feb 26, 2026

Migrate the frontend runtime client from Orval + global store to ConnectRPC + scoped RuntimeClient. See tech design for architecture rationale.

Why. The global runtime store 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:

  • RuntimeClient class wraps ConnectRPC transport, JWT lifecycle, and service clients. Each RuntimeProvider creates a scoped instance in Svelte context; components read it via useRuntimeClient().
  • Custom code generator (scripts/generate-query-hooks.ts) reads ConnectRPC *_connect.ts descriptors and produces TanStack Query hooks with a JSON bridge — consumers keep using Orval-compatible V1* types while proto conversion happens internally.
  • All ~330 consumer files migrated from runtime store imports to useRuntimeClient() + v2 hooks.
  • Deleted: Orval query/mutation hooks (~10k lines), fetchWrapper, http-client, HttpRequestQueue, StreamingQueryBatch, RuntimeProvider (old), runtime-store.
  • Added: ConnectRPC transport with request queue interceptor, RuntimeProvider (v2), RuntimeContextBridge for backward compatibility during migration.

Code samples

Before (Orval + global store):

<script>
  import { runtime } from "@rilldata/web-common/runtime-client/runtime-store";
  import { createRuntimeServiceGetResource } from "@rilldata/web-common/runtime-client";

  $: resource = createRuntimeServiceGetResource($runtime.instanceId, {
    "name.name": name,
    "name.kind": ResourceKind.Alert,
  });
</script>

After (ConnectRPC + scoped client):

<script>
  import { useRuntimeClient } from "@rilldata/web-common/runtime-client/v2";
  import { createRuntimeServiceGetResource } from "@rilldata/web-common/runtime-client";

  const runtimeClient = useRuntimeClient();

  $: resource = createRuntimeServiceGetResource(runtimeClient, {
    name: { name, kind: ResourceKind.Alert },
  });
</script>

Key differences: runtimeClient comes from Svelte context (scoped, not global), the function takes the client object instead of a bare instanceId, request parameters use nested protobuf structure instead of dot-notation, and instanceId is 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 Orval V1* types in their public API and converts to/from proto internally via fromJson()/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

  • Migrate off Orval types. The V1* types in index.schemas.ts are generated from OpenAPI (proto → OpenAPI → TypeScript), losing fidelity on oneof fields and enums. ConnectRPC's protobuf-es generates proper discriminated unions and TypeScript enums. Once this PR lands, we can incrementally replace V1* imports with proto-native types and eventually delete index.schemas.ts and the JSON bridge layer. This would also let us stop running the OpenAPI codegen pipeline entirely.
  • Backend ConnectRPC transport. The backend still serves gRPC-Gateway (REST/JSON). Moving to connect-go would 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-query upstream integration. Our code generator is custom because @connectrpc/connect-query targets React. If upstream adds Svelte support (connectrpc/connect-query-es#324), we could replace our generator. The hook signatures are designed to be compatible.
  • Feature flags per-client. featureFlags is still a module-level singleton. The tech design calls for making it a RuntimeClient property so different runtimes can have different flags.
  • Clean up residual instanceId extractions. A handful of files still destructure instanceId from runtimeClient for non-hook uses (query key construction, SSE connections). These are functional but could be tidied.
  • Infinite query support in code generator. PartitionsTable was manually converted to use createInfiniteQuery since the generator doesn't emit infinite query variants. Adding this to the generator would cover future pagination use cases.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Developed in collaboration with Claude Code

…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
@ericpgreen2 ericpgreen2 force-pushed the ericgreen/runtime-context-v2-feature-migration branch from e2e0ca5 to 2102ddd Compare February 26, 2026 10:34
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.
@ericpgreen2 ericpgreen2 force-pushed the ericgreen/runtime-context-v2-cleanup branch from 33be2b2 to 2192094 Compare February 26, 2026 11:14
@ericpgreen2 ericpgreen2 changed the base branch from ericgreen/runtime-context-v2-feature-migration to main February 26, 2026 11:35
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
@ericpgreen2 ericpgreen2 changed the title refactor: migrate to v2/gen hooks, delete Orval infrastructure and legacy code refactor: migrate runtime context from global store to scoped RuntimeClient Feb 26, 2026
@ericpgreen2 ericpgreen2 changed the title refactor: migrate runtime context from global store to scoped RuntimeClient refactor: migrate from Orval to ConnectRPC with scoped RuntimeClient Feb 26, 2026
@ericpgreen2 ericpgreen2 changed the title refactor: migrate from Orval to ConnectRPC with scoped RuntimeClient refactor: migrate API layer from OpenAPI/REST to ConnectRPC with scoped RuntimeClient Feb 26, 2026
@ericpgreen2 ericpgreen2 changed the title refactor: migrate API layer from OpenAPI/REST to ConnectRPC with scoped RuntimeClient refactor: migrate runtime API layer from OpenAPI/REST to ConnectRPC with scoped RuntimeClient Feb 26, 2026
@ericpgreen2 ericpgreen2 changed the title refactor: migrate runtime API layer from OpenAPI/REST to ConnectRPC with scoped RuntimeClient refactor: migrate runtime client from Orval to ConnectRPC with scoped RuntimeClient Feb 26, 2026
@ericpgreen2 ericpgreen2 self-assigned this Feb 26, 2026
- 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant