pyRPC
DemoBlogChangelogDocs
← Back to Blog

Inside the Interactive Demo Sandbox

·8 min read

The pyrpc playground at /demo lets you write Python server code with @rpc and @model decorators, then call those procedures from TypeScript — all in the browser, with real-time autocomplete and type checking, no server required.

This post explains how the sandbox works under the hood, the key design decisions that shaped it, and how it compares to the actual pyrpc implementation (v0.1.0-alpha.1 across all packages).


Architecture Overview

The playground processes user code in three phases:

  1. Type Generation — Python server code is parsed client-side (regex-based) to extract @model class fields and @rpc function signatures, then converted to TypeScript declarations.
  2. Monaco Integration — The generated types are injected into Monaco Editor as virtual files at /node_modules/@pyrpc/types/index.d.ts, enabling real-time autocomplete and type errors on client.* calls.
  3. Sandbox Execution — When the user clicks Run, client calls are extracted via regex, dispatched to a local API endpoint that parses the server code to construct mock return values, and the results are fed through a simulated console.log.

Phase 1: Client-Side Type Generation

The type pipeline lives in docs/lib/parsePythonTypes.ts. Three functions form the core:

  • parseServerCode(code) — Uses regex to find @model classes and @rpc functions. Extracts field names and types for models, parameter names/types and return type for procedures.
  • introspectionToTypes(schema) — Converts the parsed schema into TypeScript declaration strings. Python types like int become number, list[X] becomes X[], model names reference the generated interfaces.
  • validateServerCode(code) — Detects unknown decorators (e.g. @procedure), missing return type annotations, and empty model classes.

The output is a TypeScript source string containing export interface User { ... } and export interface Types { get_user(id: number): Promise<User>; ... }.


Phase 2: Monaco Editor Integration

The critical design decision was how to feed these types into Monaco so that TypeScript's compiler can see them. Monaco provides setExtraLibs for declaring ambient types, but we found that files added via setExtraLibs are not visible to TypeScript's module resolution — import type { Types } from "@pyrpc/types" would fail to resolve.

The solution: use monaco.editor.createModel() to create actual editor models at the exact file paths TypeScript expects:

  • /node_modules/@pyrpc/types/index.d.ts — Generated type declarations (updated live as server code changes)
  • /node_modules/@pyrpc/client/index.d.ts — Static client SDK stubs (createClient, PyRPCClient, etc.)

The client editor model is set with path="/model.ts" so that TypeScript's NodeJs module resolution finds the /node_modules/... models. After updating the types model, a no-op edit (pushEditOperations) forces the TypeScript worker to re-evaluate the program, updating diagnostics and completion data immediately — at compile time, not after clicking Run.

The Omit<PyRPCClient, 'rpc'> type in the stubs hides the internal .rpc property from autocomplete, so client. shows only user-defined methods.


Phase 3: Mock Sandbox Execution

When the user clicks Run, the playground:

  1. Extracts client callsparseClientCalls uses a global matchAll regex to find every client.method(...) call in the client code.
  2. Dispatches to mock API — A lightweight proxy (createSandboxClient) sends each call as a POST to /api/sandbox/rpc with the server code in a header.
  3. Parses server code — The mock endpoint re-parses the @rpc function signatures and @model fields, then builds mock return values:
  • For model return types: creates an object with default-typed fields (e.g., str becomes "Sample fieldName"), overrides with matching parameter values, then overrides with return value literals parsed from the function body (e.g. return User(id=id, name="Core User") sets name to "Core User").
  • For primitive return types: returns sensible defaults (int = 42, str = "mock_result", any = computed from input).

Finally, simulateConsoleLogs processes each console.log(...) call — supporting template literals (Hello ${name}), typeof() expressions, dot-path resolution (user.name), and proper handling of nested parentheses — and displays the output in a theme-aware terminal.


Live Validation

The server editor validates code on every keystroke via a useEffect that calls validateServerCode. Detected issues are surfaced as Monaco error markers (red underlines) and also block the Start Server button:

  • Unknown decorators — Any @name that isn't @rpc or @model triggers an error.
  • Missing return type@rpc def foo() without a return type annotation is flagged.
  • Empty model — A @model class with no typed fields is flagged.

This validation mirrors what pyrpc-core would enforce at runtime, but shifted to edit-time for immediate feedback.


Comparison with the Real pyrpc (v0.1.0-alpha.1)

The sandbox simulates pyrpc's behaviour, but there are fundamental differences between the demo and the real framework:

Type System

AspectSandboxReal pyrpc
Type parsingClient-side regexPython AST via pyrpc-codegen
Supported typesint, str, bool, float, list[X], dict[K,V], Optional[X], Union[X,Y]Full Python type system via Pydantic TypeAdapter
Type validationNone (skip)Pydantic TypeAdapter.validate_python() on every call
ModelsParsed fields onlyPydantic dataclasses with full validation, defaults, nesting

Execution Model

AspectSandboxReal pyrpc
RuntimeNone (mock results)Python — ASGI/Flask/FastAPI server
Procedure executionRegex-parsed signature + mock defaultsActual Python function call with Pydantic-validated params
Return valuesConstructed from return literals in bodyActual function return value, validated by TypeAdapter
TransportNext.js API route (/api/sandbox/rpc)PyRPCAsgiApp / mounted on FastAPI/Flask
Adapter patternTemplate shows mount_fastapi(app) but ignoredmount_fastapi(app) registers actual HTTP routes

Protocol & Error Handling

AspectSandboxReal pyrpc
ProtocolJSON-RPC 2.0-like (simplified)JSON-RPC 2.0 with request/response envelopes
Error codesBasic HTTP + -32600/-32601/-32603Full JSON-RPC 2.0 error codes + Pydantic validation errors
IntrospectionClient-side regex (sync, instant)Server-side GET /rpc endpoint returning full schema

Client SDK

AspectSandboxReal pyrpc
ImplementationInline Proxy + fetch in page.tsx@pyrpc/client npm package with full Proxy-based API
Parameter passingSingle arg = positional; object = namedSingle non-array object = named params; array = positional

Key Design Decisions

createModel over setExtraLibs

setExtraLibs files don't participate in module resolution. By creating real editor models at the resolved file paths, TypeScript's NodeJs resolution finds them correctly when the user types import type { Types } from "@pyrpc/types".

No Piston API

The original design used Piston (a remote code execution API) to run Python in a sandboxed container. Piston requires authentication and adds latency. The regex-based mock approach is instant, offline-capable, and sufficient for a type-first demo.

Return value literal parsing

Rather than hardcoding return values per template, the sandbox parses the actual return statement (return User(id=id, name="Core User")) to extract literal values. This makes it work with any user-defined server code without special-casing templates.

No external npm dependencies for the sandbox

The sandbox avoids depending on @pyrpc/client at build time by inlining a minimal proxy. This prevents resolution issues during Vercel deployment where workspace packages aren't available. The proxy mirrors the real client's parameter handling (positional vs named) so the demo accurately reflects the real developer experience.