RPC Client
Type-safe gRPC clients with DI integration
The defineRpcClient function creates type-safe gRPC clients that integrate seamlessly with JustScale's dependency injection system. Clients automatically get all methods from the contract with full TypeScript inference.
Basic Usage
Define a client by specifying the contract and configuration source:
import { defineRpcClient } from '@justscale/rpc'
import { defineService } from '@justscale/core'
import { GreeterContract } from './greeter.proto'
// Settings service provides configuration
class GreeterSettings extends defineService({
inject: {},
factory: () => ({
address: 'localhost:50051',
timeout: 30_000,
}),
}) {}
// Define the client with injected settings
const GreeterClient = defineRpcClient({
contract: GreeterContract,
inject: { settings: GreeterSettings },
})The client looks for a settings dependency with an address property by default. All methods from the contract become available on the client instance.
Injecting Clients into Services
Once defined, inject the client into your services like any other dependency:
class OrderService extends defineService({
inject: { greeter: GreeterClient },
factory: ({ greeter }) => ({
async processOrder(customerId: string) {
// Fully typed - TypeScript knows SayHello's input/output
const reply = await greeter.SayHello({ name: customerId })
return reply.message
},
async *streamUpdates(orderId: string) {
// Server streaming methods return AsyncIterable
for await (const update of greeter.StreamUpdates({ orderId })) {
yield update
}
},
}),
}) {}Config Factory
For more control over configuration, use the config factory function. This lets you combine multiple injected dependencies to build the client config:
import { EnvService, ServiceDiscovery } from '@justscale/core'
const GreeterClient = defineRpcClient({
contract: GreeterContract,
inject: {
env: EnvService,
discovery: ServiceDiscovery,
},
config: ({ env, discovery }) => ({
// Try service discovery first, fall back to env var
address: discovery.resolve('greeter') ?? env.get('GREETER_ADDR'),
timeout: Number(env.get('GREETER_TIMEOUT') ?? 30_000),
metadata: {
'x-api-key': env.get('GREETER_API_KEY'),
},
}),
})The config factory receives all resolved dependencies and must return an RpcClientConfig object.
Retry Configuration
Configure automatic retries with exponential backoff for transient failures:
const GreeterClient = defineRpcClient({
contract: GreeterContract,
inject: { settings: GreeterSettings },
config: ({ settings }) => ({
address: settings.address,
retry: {
maxAttempts: 3, // Total attempts (default: 3)
initialBackoff: 100, // Initial delay in ms (default: 100)
maxBackoff: 10_000, // Maximum delay in ms (default: 10000)
backoffMultiplier: 2, // Multiplier per retry (default: 2)
retryableStatusCodes: [ // Which codes to retry
14, // UNAVAILABLE
8, // RESOURCE_EXHAUSTED
],
},
}),
})By default, only UNAVAILABLE and RESOURCE_EXHAUSTED status codes trigger retries. Failed retries use exponential backoff with jitter to avoid thundering herd.
Load Balancing
For high availability, configure a resolver and load balancer in the client config:
import {
defineRpcClient,
staticResolver,
roundRobinBalancer,
} from '@justscale/rpc'
import { GreeterContract } from './greeter.proto'
const GreeterClient = defineRpcClient({
contract: GreeterContract,
inject: {},
config: () => ({
// Logical name (passed to resolver)
address: 'greeter-service',
// Resolves the address to multiple endpoints
resolver: staticResolver([
'server1:50051',
'server2:50051',
'server3:50051',
]),
// Distributes requests across endpoints
loadBalancer: roundRobinBalancer(),
}),
})Available load balancers:
import {
roundRobinBalancer,
randomBalancer,
pickFirstBalancer,
weightedRoundRobinBalancer,
} from '@justscale/rpc'
// Round-robin: cycle through addresses in order
roundRobinBalancer()
// Random: pick randomly with equal probability
randomBalancer()
// Pick-first: always use first address, failover on error
pickFirstBalancer()
// Weighted: distribute based on capacity
weightedRoundRobinBalancer(new Map([
['server1:50051', 3], // Gets 3x traffic
['server2:50051', 1], // Gets 1x traffic
]))Available resolvers and custom resolver example:
import { staticResolver, passthroughResolver } from '@justscale/rpc'
// Static list of addresses
staticResolver(['server1:50051', 'server2:50051'])
// Pass-through for single server (uses address directly)
passthroughResolver()
// Custom DNS resolver
const dnsResolver = {
async resolve(target: string) {
const addresses = await dns.resolve4(target)
return addresses.map(ip => `${ip}:50051`)
},
}Load balancers receive success/failure feedback after each request, allowing intelligent routing decisions. The client maintains a connection pool, reusing HTTP/2 sessions for each endpoint.
Full Configuration Reference
All available options for RpcClientConfig:
interface RpcClientConfig {
// Server address (host:port) or logical name for resolver
address: string
// TLS configuration
tls?: {
ca?: Buffer | string // CA certificate
cert?: Buffer | string // Client certificate
key?: Buffer | string // Client private key
insecure?: boolean // Skip verification (dev only!)
}
// Request timeout in ms (default: 30000)
timeout?: number
// Default metadata sent with every request
metadata?: Record<string, string>
// Retry configuration
retry?: {
maxAttempts?: number // default: 3
initialBackoff?: number // default: 100ms
maxBackoff?: number // default: 10000ms
backoffMultiplier?: number // default: 2
retryableStatusCodes?: number[] // default: [UNAVAILABLE, RESOURCE_EXHAUSTED]
}
// Request compression (identity, gzip, deflate, none)
compression?: 'identity' | 'gzip' | 'deflate' | 'none'
// Message size limits (default: 4MB)
maxReceiveMessageSize?: number
maxSendMessageSize?: number
// Address resolver for multi-server deployments
resolver?: AddressResolver
// Load balancer for distributing requests (default: round-robin)
loadBalancer?: LoadBalancer
}Per-Call Options
Override client defaults on a per-call basis:
// Override timeout for a slow operation
const reply = await greeter.SayHello(
{ name: 'World' },
{ timeout: 60_000 }
)
// Add request-specific metadata
const reply = await greeter.SayHello(
{ name: 'World' },
{ metadata: { 'x-request-id': requestId } }
)
// Support cancellation via AbortSignal
const controller = new AbortController()
setTimeout(() => controller.abort(), 5000)
const reply = await greeter.SayHello(
{ name: 'World' },
{ signal: controller.signal }
)Closing Connections
Clean up client connections when done:
// The client instance has a close() method
await greeter.close()
// In a service with cleanup
class OrderService extends defineService({
inject: { greeter: GreeterClient },
factory: ({ greeter }) => ({
// ... methods ...
async [Symbol.asyncDispose]() {
await greeter.close()
},
}),
}) {}