Property Access
kkrpc supports direct property access and mutation across RPC boundaries using JavaScript Proxy objects. This enables you to read and write remote object properties as if they were local, with full TypeScript support.
Features
- Property Getters: Access remote properties with
await api.property
- Property Setters: Set remote properties with
api.property = value
- Nested Properties: Deep property access with dot notation
api.nested.deep.property
- Type Safety: Full TypeScript inference and IDE autocompletion
- Async/Await Support: Seamless integration with async/await syntax
How It Works
Property access is implemented using JavaScript Proxy objects with custom get and set traps:
- Property Access: When you access a property like
api.counter
, a proxy returns another proxy - Await Trigger: Using
await api.counter
triggers the proxy’s “then” handler - RPC Message: This sends a “get” type message to the remote endpoint
- Property Setting: Direct assignment like
api.counter = 42
triggers the proxy’s set trap - Remote Execution: The remote endpoint handles get/set operations on the actual object
Basic Usage
API Definition
// Define your API interface with propertiesexport interface API { // Methods add(a: number, b: number): Promise<number>
// Simple properties counter: number status: string
// Nested objects config: { theme: string language: string notifications: { enabled: boolean sound: boolean } }
// Mixed properties and methods database: { connectionCount: number connect(): Promise<void> query(sql: string): Promise<any[]> }}
// Implementationexport const apiImplementation: API = { add: async (a, b) => a + b,
counter: 0, status: "ready",
config: { theme: "light", language: "en", notifications: { enabled: true, sound: false } },
database: { connectionCount: 0, connect: async () => { // Connection logic }, query: async (sql) => { // Query logic return [] } }}
Property Access Examples
const api = rpc.getAPI<API>()
// Property getters (always use await for remote properties)const currentCount = await api.counterconst theme = await api.config.themeconst notificationsEnabled = await api.config.notifications.enabledconst dbConnections = await api.database.connectionCount
console.log(currentCount) // 0console.log(theme) // "light"console.log(notificationsEnabled) // trueconsole.log(dbConnections) // 0
// Property setters (direct assignment)api.counter = 42api.config.theme = "dark"api.config.notifications.sound = trueapi.database.connectionCount = 5
// Verify changesconsole.log(await api.counter) // 42console.log(await api.config.theme) // "dark"console.log(await api.config.notifications.sound) // trueconsole.log(await api.database.connectionCount) // 5
Mixing Properties and Methods
// Call methods normallyconst sum = await api.add(10, 20)await api.database.connect()const results = await api.database.query("SELECT * FROM users")
// Access properties with awaitconst status = await api.statusconst connectionCount = await api.database.connectionCount
// Set properties directlyapi.status = "connected"api.database.connectionCount = 10
Advanced Usage
Dynamic Property Updates
// Increment counter remotelyconst current = await api.counterapi.counter = current + 1
// Toggle boolean propertiesconst enabled = await api.config.notifications.enabledapi.config.notifications.enabled = !enabled
// Update nested objectsconst config = await api.configapi.config = { ...config, theme: config.theme === "light" ? "dark" : "light"}
Property Validation
// You can add validation in your API implementationexport const apiImplementation: API = { _counter: 0,
get counter() { return this._counter },
set counter(value: number) { if (typeof value !== 'number' || value < 0) { throw new Error('Counter must be a non-negative number') } this._counter = value }}
// Client side error handlingtry { api.counter = -5 // This will throw an error} catch (error) { console.error('Invalid counter value:', error.message)}
Reactive Property Updates
// API with property change notificationsexport const apiImplementation = { _config: { theme: "light", language: "en" }, _listeners: new Set<(config: any) => void>(),
get config() { return this._config },
set config(newConfig) { this._config = { ...newConfig } // Notify all listeners this._listeners.forEach(listener => listener(this._config)) },
onConfigChange(callback: (config: any) => void) { this._listeners.add(callback) },
offConfigChange(callback: (config: any) => void) { this._listeners.delete(callback) }}
// Client sideapi.onConfigChange((newConfig) => { console.log('Config changed:', newConfig)})
api.config = { theme: "dark", language: "es" } // Triggers notification
Type Safety
kkrpc provides full TypeScript support for property access:
interface UserAPI { currentUser: { id: number name: string preferences: { theme: 'light' | 'dark' notifications: boolean } }}
const api = rpc.getAPI<UserAPI>()
// TypeScript knows the exact typesconst userId: number = await api.currentUser.idconst theme: 'light' | 'dark' = await api.currentUser.preferences.theme
// TypeScript prevents invalid assignmentsapi.currentUser.preferences.theme = "invalid" // ❌ TypeScript errorapi.currentUser.preferences.theme = "dark" // ✅ Valid
Performance Considerations
- Batching: Each property access results in a separate RPC call
- Caching: Consider caching frequently accessed properties locally
- Grouping: Access multiple related properties in a single method call when possible
// Less efficient: Multiple RPC callsconst name = await api.user.nameconst email = await api.user.emailconst age = await api.user.age
// More efficient: Single RPC callconst getUserInfo = async () => { return { name: this.user.name, email: this.user.email, age: this.user.age }}
const userInfo = await api.getUserInfo()
Best Practices
- Always use await: Property getters require
await
for remote access - Minimize round trips: Group related property accesses when possible
- Handle errors: Wrap property access in try-catch blocks
- Use TypeScript: Leverage type safety for better development experience
- Validate inputs: Add validation logic in property setters
- Document behavior: Clearly document which properties trigger side effects
Limitations
- Serialization: Properties must be JSON-serializable (or superjson-serializable)
- No Getters/Setters: JavaScript getters/setters on the API object are not directly supported
- Performance: Each property access is a network call
- Atomic Operations: Multiple property updates are not atomic across RPC boundaries
Error Handling
try { const value = await api.someProperty} catch (error) { if (error.name === 'PropertyNotFoundError') { console.log('Property does not exist') } else if (error.name === 'ValidationError') { console.log('Invalid property value') } else { console.log('Network or other error:', error.message) }}
Property access in kkrpc provides a natural and intuitive way to work with remote objects while maintaining type safety and performance considerations.