Skip to content

Latest commit

 

History

History
214 lines (171 loc) · 7.63 KB

File metadata and controls

214 lines (171 loc) · 7.63 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

Development Commands

  • bun install - Install dependencies (use bun, not npm)
  • bun run index.ts <path> - Generate RPC code from interface definitions
  • bun run index.ts <path> --watch - Watch mode for automatic regeneration

CLI Usage Examples

  • bun run index.ts ./examples/00-full-app/pkg/rpc/define.ts - Generate from full app example (complete working application)
  • bun run index.ts ./examples/01-basic/define.ts - Generate from basic example (simple interfaces)
  • bun run index.ts ./examples/00-full-app/pkg/rpc/define.ts --package-name "my-rpc" --timeout 3000

Architecture

This is a TypeScript code generator for Socket.IO RPC packages. The tool generates type-safe client-server communication code from interface definitions.

Core Components

Main Generator (index.ts)

  • CLI entry point using commander
  • Core generation logic using ts-morph AST manipulation
  • Extracts function signatures from ClientFunctions and ServerFunctions interfaces
  • Generates client.generated.ts, server.generated.ts, and types.generated.ts files

Key Generation Process

  1. Parse input TypeScript file containing interface definitions
  2. Extract function signatures from ClientFunctions and ServerFunctions interfaces
  3. Generate bidirectional RPC functions:
    • Client functions call server methods
    • Server functions call client methods
    • Handler functions set up event listeners
  4. Generate factory functions (createRpcClient, createRpcServer) for ergonomic API
  5. Generate error handling with RpcError type
  6. Output complete package with TypeScript declarations

Generated Code Structure

  • Factory functions - createRpcClient() / createRpcServer() for ergonomic API
  • Client/Server interfaces - RpcClient, RpcServer with .handle, .server/.client, .dispose()
  • Error handling - Built-in RpcError type and isRpcError() guard
  • Type safety - Full TypeScript support with generated type imports

Interface Requirements

  • Must define ClientFunctions and ServerFunctions interfaces
  • Do NOT use Promise in interface return types (automatically wrapped)
  • Use void for fire-and-forget functions
  • Non-void functions automatically get acknowledgment handling and timeout support

Ergonomic API Usage (Recommended)

The generator creates createRpcClient() and createRpcServer() factory functions that provide a clean API with automatic cleanup.

Client Side:

import { createRpcClient } from './rpc/client.generated';

const rpc = createRpcClient(socket);

// Register handlers with rpc.handle.* (for calls FROM server)
rpc.handle.showError(async (error) => {
  console.error('Error:', error);
});

rpc.handle.onProgress(async (current, total) => {
  console.log(`Progress: ${current}/${total}`);
});

// Make RPC calls with rpc.server.* (calls TO server)
const result = await rpc.server.generateText("Hello!");

// Single cleanup call
rpc.dispose();

Server Side:

import { createRpcServer } from './rpc/server.generated';

io.on('connection', (socket) => {
  const rpc = createRpcServer(socket);

  // Register handlers with rpc.handle.* (for calls FROM client)
  rpc.handle.generateText(async (prompt) => {
    // Call client methods via rpc.client.* (calls TO client)
    rpc.client.showError(new Error("Something happened"));
    return "Generated: " + prompt;
  });

  // Cleanup on disconnect
  socket.on('disconnect', () => rpc.dispose());
});

Vue 3 Integration

<script setup lang="ts">
import { onBeforeUnmount } from 'vue';
import { socket } from './socket';
import { createRpcClient } from './rpc/client.generated';

const rpc = createRpcClient(socket);

// Register handlers - no manual tracking needed
rpc.handle.showError(async (error) => {
  console.error('Error:', error);
});

rpc.handle.onProgress(async (current, total) => {
  console.log(`Progress: ${current}/${total}`);
});

// Single cleanup call handles everything
onBeforeUnmount(() => rpc.dispose());
</script>

React Integration

import { useEffect, useRef } from 'react';
import { socket } from './socket';
import { createRpcClient, RpcClient } from './rpc/client.generated';

function MyComponent() {
  const rpcRef = useRef<RpcClient>();

  useEffect(() => {
    const rpc = createRpcClient(socket);
    rpcRef.current = rpc;

    rpc.handle.showError(async (error) => {
      console.error('Error:', error);
    });

    return () => rpc.dispose();
  }, []);

  return <div>My Component</div>;
}

API Structure

// RpcClient interface
interface RpcClient {
  handle: {
    // Register handlers for server-to-client calls. Returns an unsubscribe function.
    // Re-registering the same name replaces the previous handler.
    showError: (handler: (error: Error) => Promise<void>) => UnsubscribeFunction;
    askQuestion: (handler: (question: string) => Promise<string>) => UnsubscribeFunction;
    // ...
  };
  server: {
    // Call server methods. `opts` carries timeout / AbortSignal / volatile.
    generateText: (prompt: string, opts?: RpcCallOptions) => Promise<string | RpcError>;
    // ...
  };
  socket: Socket;        // Underlying socket
  connected: boolean;    // Whether the socket is currently connected
  onConnect(handler: () => void): UnsubscribeFunction;            // re-sync on (re)connect
  onDisconnect(handler: (reason: string) => void): UnsubscribeFunction;
  onReconnect(handler: (attempt: number) => void): UnsubscribeFunction;
  disposed: boolean;     // Whether disposed
  dispose(): void;       // Cleanup all handlers
}

// RpcServer interface
interface RpcServer {
  handle: {
    // Register handlers for client-to-server calls. Returns an unsubscribe function.
    generateText: (handler: (prompt: string) => Promise<string>) => UnsubscribeFunction;
    // ...
  };
  client: {
    // Call client methods. `opts` carries timeout / AbortSignal / volatile.
    showError: (error: Error, opts?: RpcCallOptions) => void;
    askQuestion: (question: string, opts?: RpcCallOptions) => Promise<string | RpcError>;
    // ...
  };
  socket: Socket;        // Underlying socket
  connected: boolean;    // Whether the socket is currently connected
  onDisconnect(handler: (reason: string) => void): UnsubscribeFunction;
  disposed: boolean;     // Whether disposed
  dispose(): void;       // Cleanup all handlers
}

Error model

  • RpcError is branded with a __rpcError: true field; isRpcError() checks the brand, so a successful result shaped like { message, code } is never misread as an error.
  • Handlers signal failure by throwingthrow rpcError(code, message, data?) for a typed error, or any thrown value (normalized to INTERNAL_ERROR). Do not return RpcError from a handler.
  • Standard codes: TIMEOUT, DISPOSED, DISCONNECTED, ABORTED, INTERNAL_ERROR, INVALID_ARGUMENT.
  • --error-mode throw makes calls reject with the RpcError instead of returning T | RpcError.

Example Structure

pkg/rpc/
├── define.ts              # Interface definitions (input)
├── client.generated.ts    # Generated client RPC (includes createRpcClient)
├── server.generated.ts    # Generated server RPC (includes createRpcServer)
├── types.generated.ts     # Generated types and error handling
├── index.ts              # Package entry point
├── package.json          # Generated package config
└── tsconfig.json         # Generated TypeScript config

The tool automatically infers the output directory from the input file path and generates a complete npm package structure.