RPCs (Remote Procedure Calls)
RPCs (Remote Procedure Calls) in Pikku allow you to call one function from within another function. This is useful for code reuse, creating helper functions, and building complex workflows by composing smaller functions.
How RPCs Work
Any pikkuFunc
, pikkuSessionlessFunc
, or pikkuVoidFunc
can be called as an RPC from within another function using the rpc.invoke()
method. The RPC system automatically handles:
- Type safety: Full TypeScript support for inputs and outputs
- Session management: Sessions are passed through to the called function
- Service injection: All services are available to the called function
- Recursion protection: Built-in depth tracking to prevent infinite loops
Basic RPC Usage
Creating Functions That Can Be Called as RPCs
Any Pikku function can be invoked as an RPC:
import { pikkuFunc, pikkuSessionlessFunc } from '#pikku/pikku-types.gen.js'
// This function can be called as an RPC
export const calculateTax = pikkuSessionlessFunc<
{ amount: number; rate: number },
{ tax: number; total: number }
>(async ({ logger }, { amount, rate }) => {
logger.info(`Calculating tax for amount: ${amount}`)
const tax = amount * rate
const total = amount + tax
return { tax, total }
})
// This function can also be called as an RPC
export const validateUser = pikkuFunc<
{ userId: string },
{ isValid: boolean; user?: { name: string; email: string } }
>(async ({ todos, logger }, { userId }, session) => {
logger.info(`Validating user: ${userId}`)
const user = await todos.getUserById(userId)
if (!user) {
return { isValid: false }
}
return {
isValid: true,
user: { name: user.name, email: user.email }
}
})
Calling Functions as RPCs
Use the rpc
service to invoke other functions:
export const processOrder = pikkuFunc<
{ userId: string; items: Array<{ price: number }> },
{ success: boolean; total: number; tax: number }
>(async ({ rpc, logger }, { userId, items }, session) => {
logger.info(`Processing order for user: ${userId}`)
// First, validate the user (RPC call)
const userValidation = await rpc?.invoke('validateUser', { userId })
if (!userValidation?.isValid) {
throw new Error('Invalid user')
}
// Calculate totals
const subtotal = items.reduce((sum, item) => sum + item.price, 0)
// Calculate tax (RPC call)
const taxResult = await rpc?.invoke('calculateTax', {
amount: subtotal,
rate: 0.08
})
logger.info(`Order processed: ${taxResult?.total}`)
return {
success: true,
total: taxResult?.total || subtotal,
tax: taxResult?.tax || 0
}
})
Advanced RPC Patterns
RPC Depth Tracking
Pikku automatically tracks RPC call depth to prevent infinite recursion:
export const recursiveFunction = pikkuSessionlessFunc<
{ count: number },
{ result: number }
>(async ({ rpc, logger }, { count }) => {
logger.info(`RPC depth: ${rpc?.depth || 0}`)
// Prevent infinite recursion
if (rpc?.depth && rpc.depth >= 5) {
return { result: count }
}
if (count > 0) {
// Recursive RPC call
const result = await rpc?.invoke('recursiveFunction', {
count: count - 1
})
return { result: result?.result || 0 }
}
return { result: count }
})
Error Handling in RPCs
RPC calls can throw errors just like regular function calls:
export const safeRPCCall = pikkuSessionlessFunc<
{ userId: string },
{ success: boolean; error?: string }
>(async ({ rpc, logger }, { userId }) => {
try {
const result = await rpc?.invoke('validateUser', { userId })
if (result?.isValid) {
return { success: true }
} else {
return { success: false, error: 'User validation failed' }
}
} catch (error) {
logger.error('RPC call failed:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
})
Conditional RPC Calls
You can conditionally call RPCs based on business logic:
export const smartProcessor = pikkuFunc<
{ data: any; useAdvanced: boolean },
{ result: any }
>(async ({ rpc, logger }, { data, useAdvanced }, session) => {
if (useAdvanced) {
// Call advanced processing function
const result = await rpc?.invoke('advancedProcessor', { data })
return { result }
} else {
// Call simple processing function
const result = await rpc?.invoke('simpleProcessor', { data })
return { result }
}
})
RPC Service Injection
The rpc
service is automatically injected into functions that need it. Make sure to destructure it in your function parameters:
// ✅ Correct: Destructure rpc from services
export const myFunction = pikkuSessionlessFunc(async ({ rpc, logger }) => {
const result = await rpc?.invoke('otherFunction', {})
return result
})
// ❌ Incorrect: Don't use services.rpc
export const myFunction = pikkuSessionlessFunc(async (services) => {
const result = await services.rpc?.invoke('otherFunction', {})
return result
})
Best Practices
1. Use Optional Chaining
Always use optional chaining when calling rpc?.invoke()
since the RPC service might not be available in all contexts:
const result = await rpc?.invoke('functionName', data)
2. Handle RPC Failures Gracefully
RPC calls can fail, so always handle potential errors:
try {
const result = await rpc?.invoke('functionName', data)
// Handle success
} catch (error) {
// Handle failure
logger.error('RPC call failed:', error)
}
3. Keep RPC Functions Pure
Design RPC functions to be pure and reusable:
// ✅ Good: Pure, reusable function
export const calculateDiscount = pikkuSessionlessFunc<
{ amount: number; discountRate: number },
{ discount: number; finalAmount: number }
>(async ({ logger }, { amount, discountRate }) => {
const discount = amount * discountRate
return { discount, finalAmount: amount - discount }
})
// ❌ Avoid: Functions with side effects that are hard to reuse
export const processPaymentAndSendEmail = pikkuFunc<...>(async ({ rpc, email }, data, session) => {
// This does too many things and is hard to test/reuse
})
4. Use Type Safety
Always specify input and output types for your RPC functions:
// ✅ Good: Fully typed
export const typedFunction = pikkuSessionlessFunc<
{ input: string },
{ output: number }
>(async ({ logger }, { input }) => {
return { output: input.length }
})
Common Use Cases
1. Data Validation
export const validateAndProcess = pikkuFunc<...>(async ({ rpc }, data, session) => {
// Validate input data
const validation = await rpc?.invoke('validateInput', data)
if (!validation?.isValid) {
throw new Error('Invalid input')
}
// Process the validated data
return await rpc?.invoke('processData', validation.cleanedData)
})
2. Code Reuse Across Routes
// Shared business logic
export const getUserProfile = pikkuFunc<...>(async ({ todos }, { userId }) => {
return await todos.getProfile(userId)
})
// Multiple HTTP routes can use the same logic
export const publicProfile = pikkuSessionlessFunc(async ({ rpc }, { userId }) => {
return await rpc?.invoke('getUserProfile', { userId })
})
export const privateProfile = pikkuFunc(async ({ rpc }, { userId }, session) => {
// Add session-specific logic
const profile = await rpc?.invoke('getUserProfile', { userId })
return { ...profile, isOwner: profile.userId === session.userId }
})
3. Complex Workflows
export const completeOrderWorkflow = pikkuFunc<...>(async ({ rpc }, orderData, session) => {
// Step 1: Validate order
await rpc?.invoke('validateOrder', orderData)
// Step 2: Calculate totals
const totals = await rpc?.invoke('calculateOrderTotals', orderData)
// Step 3: Process payment
const payment = await rpc?.invoke('processPayment', {
amount: totals.total,
userId: session.userId
})
// Step 4: Create order record
return await rpc?.invoke('createOrderRecord', {
...orderData,
...totals,
paymentId: payment.id
})
})
Summary
RPCs in Pikku provide a powerful way to compose functions and create reusable business logic. By using rpc.invoke()
, you can call any Pikku function from within another function while maintaining type safety and proper error handling.
Remember to always use optional chaining, handle errors gracefully, and design your RPC functions to be pure and reusable.