Skip to main content

webview-trpc-messaging

Implements tRPC-based communication between VS Code extension host and React webviews. Use when creating new webview procedures (queries, mutations, subscriptions), adding a new webview router, wiring up a webview controller, using the tRPC client from React components, applying telemetry middleware (telemetryMiddlewareBody), or supporting AbortSignal-based cancellation in webview operations.

소스 정보

저장소
microsoft/vscode-documentdb
최근 소스 활동
2026년 7월 15일 09:56
감지된 SKILL.md 언어
영어
스타
33
포크
22

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
webview-trpc-messaging
description
Implements tRPC-based communication between VS Code extension host and React webviews. Use when creating new webview procedures (queries, mutations, subscriptions), adding a new webview router, wiring up a webview controller, using the tRPC client from React components, applying telemetry middleware (telemetryMiddlewareBody), or supporting AbortSignal-based cancellation in webview operations.
# Webview tRPC Messaging Type-safe RPC communication between the VS Code extension host (server) and React webviews (client) using tRPC. ## Architecture Overview ``` React Webview (client) Extension Host (server) ───────────────────── ────────────────────── useTrpcClient() hook WebviewController └─ createTRPCClient └─ setupTrpc() └─ vscodeLink ──── postMessage ────► ├─ callerFactory(appRouter) (send/onReceive) ◄─────┤ └─ procedure(input) └─ abort/subscription.stop ``` **Key files** (read as needed for implementation details): | File | Purpose | | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `@microsoft/vscode-ext-webview` (shared) | tRPC init via `initWebviewTrpc`, `publicProcedure`, `router`, `BaseRouterContext` | | `@microsoft/vscode-ext-webview/host` (telemetry) | `telemetryMiddlewareBody`, `ProcedureLogger`, `TelemetryRunner` (consumer builds `publicProcedureWithTelemetry`) | | `src/webviews/_integration/trpc.ts` | Consumer tRPC instance: `publicProcedureWithTelemetry`, the DocumentDB `TelemetryRunner`, and the `RpcEnrichment` shape it contributes to `ctx.actionContext` | | `src/webviews/_integration/appRouter.ts` | Root router + `publicProcedureWithTelemetry` wiring + DocumentDB `BaseRouterContext` | | `src/webviews/_integration/configuration.ts` | Consumer-owned knobs (telemetry namespace, bundle layout, dev-server host) | | `@microsoft/vscode-ext-webview/host` (WebviewController) | `WebviewController` + `openWebview` factory: WebviewPanel lifecycle, tRPC dispatcher (queries, mutations, subscriptions, abort) | | `src/webviews/_integration/openAppWebview.ts` | DocumentDB factory preset that pre-fills router + bundle layout (`openAppWebview`) | | `src/webviews/_integration/useTrpcClient.ts` | React hook providing the tRPC client (pre-typed against `AppRouter`) | | `@microsoft/vscode-ext-webview/webview` (vscodeLink) | Custom tRPC link bridging `postMessage` transport | ## Creating a New Router Each webview maintains its own router. Follow this pattern: ### 1. Define the router context Extend `BaseRouterContext` with view-specific fields: ```typescript // src/webviews/documentdb/myView/myViewRouter.ts import { type BaseRouterContext } from '../../_integration/appRouter'; export type RouterContext = BaseRouterContext & { clusterId: string; viewId: string; databaseName: string; // add view-specific fields }; ``` ### 2. Define procedures ```typescript import { z } from 'zod'; import { publicProcedure, publicProcedureWithTelemetry, router, type WithTelemetry, } from '../../_integration/appRouter'; import { type RouterContext } from './myViewRouter'; export const myViewRouter = router({ // Query with telemetry (preferred for operations that touch external services) getData: publicProcedureWithTelemetry.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => { const myCtx = ctx as WithTelemetry<RouterContext>; // Instrumented procedure: myCtx.actionContext (the full IActionContext) is present. myCtx.actionContext.telemetry.properties.itemId = input.id; // myCtx.signal is the AbortSignal for cancellation return { data: 'result' }; }), // Mutation without telemetry (rare, use for fire-and-forget) doAction: publicProcedure.input(z.string()).mutation(({ input }) => { // lightweight operation }), }); ``` ### 3. Register in appRouter ```typescript // src/webviews/_integration/appRouter.ts import { myViewRouter } from '../../documentdb/myView/myViewRouter'; export const appRouter = router({ common: commonRouter, mongoClusters: { documentView: documentViewRouter, collectionView: collectionViewRouter, myView: myViewRouter, // <-- add here }, }); ``` ### 4. Create the controller Construction-only panels are opened with a factory function that builds the config + router context and calls the `openAppWebview` preset (which pre-fills the app router, caller factory, and bundle layout): ```typescript // src/webviews/documentdb/myView/myViewController.ts import * as vscode from 'vscode'; import { API } from '../../../DocumentDBExperiences'; import { type AppWebviewController, openAppWebview } from '../../_integration/openAppWebview'; import { type RouterContext } from './myViewRouter'; export function openMyViewPanel(initialData: MyViewConfig): AppWebviewController<MyViewConfig> { const title = `${initialData.databaseName}`; const trpcContext: RouterContext = { dbExperience: API.DocumentDB, webviewName: 'myView', clusterId: initialData.clusterId, viewId: initialData.viewId, databaseName: initialData.databaseName, }; return openAppWebview({ title, webviewName: 'myView', config: initialData, context: trpcContext, }); } ``` The returned `AppWebviewController` handle exposes `panel`, `onDisposed`, `revealToForeground`, `isDisposed`, and `dispose`. Genuinely stateful panels may still extend `WebviewController` from `@microsoft/vscode-ext-webview/host` directly instead of using the factory. > **Important:** The `webviewName` field passed to `openAppWebview` is the > **registry key** (`viewType`, must match a key in `WebviewRegistry`, e.g. > `collectionView`). The `webviewName` in the tRPC context is a **telemetry > label** used in telemetry event names. These may be the same string but serve > different purposes -- do not confuse them. ### 5. Register in WebviewRegistry Add your React component to the registry. The key must match the `webviewName` passed to `openAppWebview` (`viewType`). The `WebviewName` type (exported from the same file) ensures compile-time validation of webview names. ```typescript // src/webviews/_integration/WebviewRegistry.ts import { MyView } from '../../documentdb/myView/MyView'; export const WebviewRegistry = { collectionView: CollectionView, documentView: DocumentView, myViewName: MyView, // <-- add your entry } as const; export type WebviewName = keyof typeof WebviewRegistry; ``` ## Telemetry: `publicProcedure` vs `publicProcedureWithTelemetry` | Base | When to use | `ctx.actionContext` | | ------------------------------ | ---------------------------------------------------------------------------- | -------------------------------------------------------- | | `publicProcedure` | Fire-and-forget, no external calls, telemetry reported separately | absent (do not read it) | | `publicProcedureWithTelemetry` | **Default choice.** Any procedure touching DB, network, or user-visible work | Guaranteed, injected by the DocumentDB `TelemetryRunner` | `publicProcedureWithTelemetry` is `publicProcedure.use(telemetryMiddlewareBody(documentDbTelemetryRunner, ...))` (built in `trpc.ts`). The framework's `telemetryMiddlewareBody` delegates to the DocumentDB `TelemetryRunner`, which wraps the call in `callWithTelemetryAndErrorHandling`, contributes the full `IActionContext` to `ctx.actionContext`, auto-generates a telemetry event named `documentDB.rpc.{type}.{path}`, and records errors, duration, and abort status. `actionContext` is **not** a field on the base `RouterContext` — it is an additive enrichment. Narrow to `WithTelemetry<RouterContext>` (`= RouterContext & { actionContext }`) in an instrumented procedure to read it; a plain `publicProcedure` procedure narrows to bare `RouterContext`, so reading `actionContext` there is a compile error instead of a runtime `undefined`. Access telemetry safely: ```typescript const myCtx = ctx as WithTelemetry<RouterContext>; myCtx.actionContext.telemetry.properties.myCustomProp = 'value'; myCtx.actionContext.telemetry.measurements.itemCount = items.length; ``` ## AbortSignal Support Every tRPC operation (query, mutation, subscription) receives its own `AbortController`. Cancellation flows: ``` Client (React) Server (Extension Host) ────────────── ────────────────────── // Queries/Mutations: ac = new AbortController() trpcClient.myProc.query(input, { signal: ac.signal }) ac.abort() → sends 'abort' msg → abortController.abort() → ctx.signal.aborted = true // Subscriptions: sub = trpcClient.mySub.subscribe(...) sub.unsubscribe() → 'subscription.stop' → abortController.abort() ``` ### Using abort in procedures ```typescript // Pass signal to APIs that accept it (MongoDB driver, fetch, etc.) getData: publicProcedureWithTelemetry .input(z.object({ filter: z.record(z.unknown()) })) .query(async ({ input, ctx }) => { const myCtx = ctx as RouterContext; // Option 1: Pass to driver (preferred) const cursor = collection.find(input.filter, { signal: myCtx.signal }); // Option 2: Manual check in loops for (const item of items) { if (myCtx.signal?.aborted) return; await processItem(item); } }), ``` ### Client-side abort ```tsx const trpcClient = useTrpcClient(); const abortControllerRef = useRef<AbortController>(); const runQuery = async () => { abortControllerRef.current?.abort(); // cancel previous const ac = new AbortController(); abortControllerRef.current = ac; const result = await trpcClient.mongoClusters.collectionView.myQuery.query(input, { signal: ac.signal }); }; ``` When `publicProcedureWithTelemetry` detects an aborted signal, the DocumentDB `TelemetryRunner` sets `telemetry.properties.aborted = 'true'` and `result = 'Canceled'` automatically. ## Subscriptions Subscriptions stream multiple values from server to client using async generators: ```typescript // Server (router) streamData: publicProcedureWithTelemetry .input(z.object({ batchSize: z.number() })) .subscription(async function* ({ input, ctx }) { const myCtx = ctx as RouterContext; for (let i = 0; i < total; i += input.batchSize) { if (myCtx.signal?.aborted) return; // check before each yield const batch = await fetchBatch(i, input.batchSize); yield batch; } }), // Client (React) const sub = trpcClient.mongoClusters.myView.streamData.subscribe( { batchSize: 100 }, { onData(batch) { /* handle each batch */ }, onComplete() { /* all done */ }, onError(err) { /* handle error */ }, }, ); // To stop: sub.unsubscribe(); ``` ## Client-Side Hook Usage ```tsx import { useTrpcClient } from '../_integration/useTrpcClient'; import { useConfiguration } from '@microsoft/vscode-ext-webview/react'; export const MyComponent = () => { const trpcClient = useTrpcClient(); const config = useConfiguration<MyViewConfig>(); useEffect(() => { trpcClient.mongoClusters.myView.getData.query({ id: config.documentId }).then(setData); }, []); }; ``` `useConfiguration<T>()` retrieves the initial config passed to `WebviewController` constructor (serialized via `encodeURIComponent(JSON.stringify(...))`). ## Common Pitfalls - **Never use `any`** in procedure context casts — narrow with `ctx as WithTelemetry<RouterContext>` when the procedure reads telemetry (`ctx.actionContext.telemetry`), or `ctx as RouterContext` otherwise - **Always prefer `publicProcedureWithTelemetry`** unless you have a specific reason not to - **Always check `myCtx.signal?.aborted`** in long-running loops — not checking causes wasted work after client cancels
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기