* Add bump-changes * Add utility tests * Add utility tests * Bump to new version of esbuild and typescript * v0.7.3 * use platform rig 0.7.10 * upgrade: memory engine optimized; change name to (was recommended by Copilot and Onnikov, TODO: CHANGE CLIENT TOO!!!) Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix rate limits bug * Bump versions * Fix lock file * Fix bug in queue cleanup * Add more tests for queue * Add api-test tests * Initial commit * Improve hierarchy + tests Add tests for hierarchy and few performance/memory optimizations. * Add more hierarchy tests * Move from Huly platform repository * Add docker tests setup * Fix test to be executed only once * Add connection tests * Fix package include source files * More tests * Create README.md * Fix pnpm lock * Fix packages publish * Remove broken tests * feat: adjust hulylake client for storage adapter Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix export * Fix publish * Fix message update (#114) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix lang store (#115) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * update hulylake client Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump version Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add hulylake storage adapter Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump version Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix validation issues Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix: do not fail on deseralization error and add logs Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.14 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix collaboration test Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Update prettier and new update-deps script Prettier + svelte support * fix unstable ydoc tests Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add tx ordering middleware * Fix ordering tests * Fix Kafka close of admin * Add tests for measurement and understand overhead * Fix not updated lock file * Fix update-deps * Fix update-deps * Use latest platform-rig * Fix deps * Add rush check to CI * Use latest versions * Bump versions * Fix lock file * validate json patch Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.15 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix merge unit tests Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Script to sync eslint deps * Fix deps * Fix tests * Fix platform-rig detection * Update to latest platform-rig * Update to latest platform rig and core * Bump typescript * Bump typescript * Rollback eslint plugins * Fix lock file * Bump platform-rig * Update to latest platform-rig * update to latest platform-rig * Allow to compile svelte files * Add ui-test component for checking compile * Fix log levels rename compile ui -> compile ui-esbuild * Fix build * Bump esbuild svelte version * Chore: use fixed versions in update-deps Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Chore: commit changes Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Update deps * Add tests for session manager * Fix txOrdering implementation * Bump ordering * Prevent metrics zero values in measure + Fix format svelte files * Revert update-deps script logic * v0.7.19 * update to latest platform-rig * Update deps * Fix pnpm * Session counters * Fix pnpm lock * Add storage client Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump core * Fix pnpm * Get rid of communication dependency * Add copilot memory file * Use proper name for instructions file * Fix instructions * Use domain instead of test name in gauges * Update instructions file * fix front service upload Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * remove incorrect test Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Move packages to huly.core * Move packages to core, since they are not utils * Add global user profile Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Add support for memory limit check * Bump version * Fix pnpm * report more accurate upload progress Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fic validation issues Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix deps Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Move LowLevelStorage to server * Fix linting * Revert "Fix linting" This reverts commit54631d353e. * Revert "Move LowLevelStorage to server" This reverts commitaafb8f6f12. * feature: add regorus engine with permit file Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix one second counters for memory usage * Fix kafka test * use fresh core * Version bump * fix: key parameter added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Fix readme and few author mistakes * Export domain schemas * Bump version * Tests (#117) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * feat: compact compact worker (#4) Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.16 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Add TypeIdentifier Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add change logs Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * rename send -> try_send Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix pnpm lock Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add identifier middleware, bump core Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Add subsciption methods to account client Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix reaction notification (#118) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Improve find methods schemas to convert to valid types Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> * Add change description Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> * Do not transcode while recording Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Open telemetry support Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * use proper content type in multipart upload Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * fix build (#26) Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix peers (#120) Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Bump version Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> * Add ActivityCollaborativeChange Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update pnpm Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Allow to suspend errors on with * Fix pnpm cache * update versions * v0.7.17 for all * v0.7.11 * v0.7.14 * remove arc from worker Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.17 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Rank for attributes Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update pnpm Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix one second counters * Fix withContext and allow pass options * Fix formatting * Use updated deps * Bump versions * Update deps * Update deps to platform.core * add support for textColor mark Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * add support for textStyle mark Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions again Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Fix Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Rework on second timers * fix merge of large blobs feched from s3 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * bump version -> 0.1.18 Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * New subscription methods in account-client * Update lock file Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Send error on find for wrong domain * Suspend connect custom errors events in traces * Bump client * Bump core * update deps * Fix lock file * Sorting for TypeIdentifier Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Bump version Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * add workspace usage info Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Bump versions Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> * Improve pg security perfomance Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix identifier middleware Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Update TxAccessLevel interface Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Allow guest to update its identities Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Add password login locked platform status Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> * Fix Uptrace normalizeMarkdown errors Signed-off-by: Artem Savchenko <armisav@gmail.com> * Add change log Signed-off-by: Artem Savchenko <armisav@gmail.com> * Add txMatch to permission Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * update pnpm lock Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix permission middleware Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix enum sorting Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Enable formatting check Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Enable formatting check * Add change Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Fix Uptrace NaN error Signed-off-by: Artem Savchenko <armisav@gmail.com> * feature: removed actors, improved performance Signed-off-by: Leonid Kaganov <lleo@lleo.me> * feature: ping from server to clients added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * feature: ping from server to clients added Signed-off-by: Leonid Kaganov <lleo@lleo.me> * Compress kafka messages and fix exception in findAll Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump versions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump versions Signed-off-by: Artem Savchenko <armisav@gmail.com> * Rush change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Fix compression param Signed-off-by: Artem Savchenko <armisav@gmail.com> * Trigger change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Clean up Signed-off-by: Artem Savchenko <armisav@gmail.com> * Trigger change Signed-off-by: Artem Savchenko <armisav@gmail.com> * Bump markdown version Signed-off-by: Artem Savchenko <armisav@gmail.com> * Enable sub projects * Fix wrong double symbol scripts * Include foundation packages * Add support for custom exclude filters Add support for custom exclude filters - by Andrey Sobolev - haiodo@gmail.com Signed-off-by: Andrey Sobolev <haiodo@gmail.com> * Bump Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> * Fix Uptrace filter is not a function error Signed-off-by: Artem Savchenko <armisav@gmail.com> * Sync versions Signed-off-by: Andrey Sobolev <haiodo@gmail.com> --------- Signed-off-by: Leonid Kaganov <lleo@lleo.me> Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com> Signed-off-by: Alexey Zinoviev <alexey.zinoviev@xored.com> Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com> Signed-off-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> Signed-off-by: Artem Savchenko <armisav@gmail.com> Signed-off-by: Andrey Sobolev <haiodo@gmail.com> Co-authored-by: Leonid Kaganov <lleo@lleo.me> Co-authored-by: Alexander Onnikov <Alexander.Onnikov@xored.com> Co-authored-by: Alexander Onnikov <Alexander.Onnikov@gmail.com> Co-authored-by: Kristina <kristin.fefelova@gmail.com> Co-authored-by: Alexey Zinoviev <alexey.zinoviev@xored.com> Co-authored-by: Denis Bykhov <bykhov.denis@gmail.com> Co-authored-by: Nikolay Marchuk <nikolay.marchuk@hardcoreeng.com> Co-authored-by: Alexander Onnikov <aonnikov@hardcoreeng.com> Co-authored-by: Artem Savchenko <armisav@gmail.com>
22 KiB
Container Development Guide
Learn how to build robust containers for Huly Virtual Network.
Table of Contents
- Introduction
- Container Basics
- Container Lifecycle
- Implementing Request Handlers
- Event Broadcasting
- State Management
- Error Handling
- Testing Containers
- Best Practices
- Common Patterns
Introduction
Containers are the core building blocks of Huly Network applications. They encapsulate business logic, manage state, and handle client requests. This guide will teach you how to build production-ready containers.
Container Basics
The Container Interface
Every container must implement the Container interface:
import type { Container, ContainerUuid, ClientUuid } from '@hcengineering/network-core'
interface Container {
// Handle requests from clients
request(operation: string, data?: any, clientId?: ClientUuid): Promise<any>
// Health check
ping(): Promise<void>
// Cleanup resources
terminate(): Promise<void>
// Client connection management
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void
disconnect(clientId: ClientUuid): void
// Optional: called when container is removed from network
onTerminated?(): void
}
Minimal Container
Here's the simplest possible container:
export class MinimalContainer implements Container {
constructor(readonly uuid: ContainerUuid) {}
async request(operation: string, data?: any): Promise<any> {
return { message: 'Hello, World!' }
}
async ping(): Promise<void> {}
async terminate(): Promise<void> {}
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {}
disconnect(clientId: ClientUuid): void {}
}
Container with Operations
Most containers handle multiple operations:
export class CalculatorContainer implements Container {
constructor(readonly uuid: ContainerUuid) {}
async request(operation: string, data?: any): Promise<any> {
switch (operation) {
case 'add':
return { result: data.a + data.b }
case 'subtract':
return { result: data.a - data.b }
case 'multiply':
return { result: data.a * data.b }
case 'divide':
if (data.b === 0) {
throw new Error('Division by zero')
}
return { result: data.a / data.b }
default:
throw new Error(`Unknown operation: ${operation}`)
}
}
async ping(): Promise<void> {}
async terminate(): Promise<void> {}
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {}
disconnect(clientId: ClientUuid): void {}
}
Container Lifecycle
Lifecycle Phases
1. Creation → Container factory called
2. Registration → Added to network registry
3. Active → Processing requests
4. Referenced → Clients hold references
5. Idle → No references, countdown started
6. Terminating → terminate() called
7. Removed → Removed from registry
Handling Creation
Containers are created by factory functions:
import type { GetOptions, ContainerUuid } from '@hcengineering/network-core'
import { createNetworkClient } from '@hcengineering/network-client'
const client = createNetworkClient('localhost:3737')
await client.waitConnection(5000)
await client.serveAgent('localhost:3738', {
'my-service': async (options: GetOptions) => {
// Extract creation parameters
const uuid = options.uuid ?? generateUuid()
const userId = options.extra?.userId
const tier = options.labels?.[0] || 'free'
// Create container with parameters
const container = new MyServiceContainer(uuid, userId, tier)
// Initialize if needed
await container.initialize()
// Return container with endpoint
return {
uuid,
container,
endpoint: `myservice://host/${uuid}` as any
}
}
})
Handling Termination
Always clean up resources in terminate():
export class DatabaseContainer implements Container {
private connection?: DatabaseConnection
private cache = new Map<string, any>()
async terminate(): Promise<void> {
console.log(`Terminating container ${this.uuid}`)
// 1. Notify connected clients
await this.notifyShutdown()
// 2. Close external connections
if (this.connection) {
await this.connection.close()
this.connection = undefined
}
// 3. Clear caches
this.cache.clear()
// 4. Cancel any pending operations
this.cancelPendingOperations()
console.log(`Container ${this.uuid} terminated`)
}
// Optional: called after removal from network
onTerminated(): void {
console.log(`Container ${this.uuid} removed from network`)
}
}
Implementing Request Handlers
Request Handler Pattern
Use a switch statement or command pattern:
export class UserServiceContainer implements Container {
private users = new Map<string, User>()
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
console.log(`Operation: ${operation}`, data)
try {
switch (operation) {
case 'createUser':
return await this.createUser(data)
case 'getUser':
return await this.getUser(data.userId)
case 'updateUser':
return await this.updateUser(data.userId, data.updates)
case 'deleteUser':
return await this.deleteUser(data.userId)
case 'listUsers':
return await this.listUsers(data.filter)
default:
return {
success: false,
error: `Unknown operation: ${operation}`,
supportedOperations: ['createUser', 'getUser', 'updateUser', 'deleteUser', 'listUsers']
}
}
} catch (error: any) {
console.error(`Error in ${operation}:`, error)
return {
success: false,
error: error.message
}
}
}
private async createUser(data: any): Promise<any> {
const user: User = {
id: generateId(),
name: data.name,
email: data.email,
createdAt: Date.now()
}
this.users.set(user.id, user)
await this.broadcast({
type: 'userCreated',
user
})
return { success: true, user }
}
private async getUser(userId: string): Promise<any> {
const user = this.users.get(userId)
if (!user) {
return {
success: false,
error: 'User not found'
}
}
return { success: true, user }
}
// ... other methods
}
Async Operations
Handle long-running operations properly:
export class ProcessingContainer implements Container {
private activeJobs = new Map<string, AbortController>()
async request(operation: string, data?: any): Promise<any> {
switch (operation) {
case 'startJob': {
const jobId = generateId()
const controller = new AbortController()
this.activeJobs.set(jobId, controller)
// Start async processing
this.processJob(jobId, data, controller.signal).catch((err) => {
console.error(`Job ${jobId} failed:`, err)
})
return { success: true, jobId }
}
case 'cancelJob': {
const controller = this.activeJobs.get(data.jobId)
if (controller) {
controller.abort()
this.activeJobs.delete(data.jobId)
return { success: true }
}
return { success: false, error: 'Job not found' }
}
case 'getJobStatus': {
const active = this.activeJobs.has(data.jobId)
return { success: true, active }
}
}
}
private async processJob(jobId: string, data: any, signal: AbortSignal): Promise<void> {
try {
for (let i = 0; i < 100; i++) {
if (signal.aborted) {
await this.broadcast({
type: 'jobCancelled',
jobId
})
return
}
// Do work
await this.processChunk(data, i)
// Report progress
await this.broadcast({
type: 'jobProgress',
jobId,
progress: i + 1
})
}
await this.broadcast({
type: 'jobCompleted',
jobId
})
} finally {
this.activeJobs.delete(jobId)
}
}
async terminate(): Promise<void> {
// Cancel all active jobs
for (const [jobId, controller] of this.activeJobs) {
controller.abort()
}
this.activeJobs.clear()
}
}
Event Broadcasting
Broadcasting to Connected Clients
export class ChatRoomContainer implements Container {
private clients = new Map<ClientUuid, (data: any) => Promise<void>>()
private messages: Message[] = []
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {
console.log(`Client ${clientId} connected`)
this.clients.set(clientId, broadcast)
// Send welcome message
broadcast({
type: 'welcome',
message: `Welcome! ${this.clients.size} users online`,
history: this.messages.slice(-10) // Last 10 messages
}).catch((err) => console.error('Failed to send welcome:', err))
}
disconnect(clientId: ClientUuid): void {
console.log(`Client ${clientId} disconnected`)
this.clients.delete(clientId)
// Notify others
this.broadcast({
type: 'userLeft',
clientId,
usersOnline: this.clients.size
}).catch((err) => console.error('Failed to broadcast:', err))
}
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
switch (operation) {
case 'sendMessage': {
const message: Message = {
id: generateId(),
clientId: clientId!,
text: data.text,
timestamp: Date.now()
}
this.messages.push(message)
// Broadcast to all connected clients
await this.broadcast({
type: 'newMessage',
message
})
return { success: true, messageId: message.id }
}
}
}
private async broadcast(event: any): Promise<void> {
const promises = Array.from(this.clients.values()).map((fn) =>
fn(event).catch((err) => console.error('Broadcast error:', err))
)
await Promise.all(promises)
}
async terminate(): Promise<void> {
await this.broadcast({
type: 'roomClosed',
message: 'Chat room is closing'
})
this.clients.clear()
this.messages = []
}
}
Selective Broadcasting
Send events to specific clients:
export class NotificationContainer implements Container {
private subscribers = new Map<
ClientUuid,
{
broadcast: (data: any) => Promise<void>
filter: NotificationFilter
}
>()
connect(clientId: ClientUuid, broadcast: (data: any) => Promise<void>): void {
// Store with default filter
this.subscribers.set(clientId, {
broadcast,
filter: { all: true }
})
}
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
switch (operation) {
case 'subscribe': {
const sub = this.subscribers.get(clientId!)
if (sub) {
sub.filter = data.filter
}
return { success: true }
}
case 'sendNotification': {
await this.sendNotification(data.notification)
return { success: true }
}
}
}
private async sendNotification(notification: Notification): Promise<void> {
const promises: Promise<void>[] = []
for (const [clientId, { broadcast, filter }] of this.subscribers) {
if (this.matchesFilter(notification, filter)) {
promises.push(
broadcast({ type: 'notification', notification }).catch((err) =>
console.error(`Failed to notify ${clientId}:`, err)
)
)
}
}
await Promise.all(promises)
}
private matchesFilter(notification: Notification, filter: NotificationFilter): boolean {
if (filter.all) return true
if (filter.types && !filter.types.includes(notification.type)) return false
if (filter.priority && notification.priority < filter.priority) return false
return true
}
}
State Management
In-Memory State
export class SessionContainer implements Container {
private sessionData = new Map<string, any>()
private lastActivity = Date.now()
private readonly TIMEOUT = 30 * 60 * 1000 // 30 minutes
async request(operation: string, data?: any): Promise<any> {
this.lastActivity = Date.now()
switch (operation) {
case 'set':
this.sessionData.set(data.key, data.value)
return { success: true }
case 'get':
return {
success: true,
value: this.sessionData.get(data.key)
}
case 'getAll':
return {
success: true,
data: Object.fromEntries(this.sessionData)
}
case 'isActive':
const inactive = Date.now() - this.lastActivity
return {
success: true,
active: inactive < this.TIMEOUT
}
}
}
}
Persistent State
export class PersistentContainer implements Container {
private cache = new Map<string, any>()
private db: Database
constructor(readonly uuid: ContainerUuid, private readonly dbPath: string) {}
async initialize(): Promise<void> {
this.db = await openDatabase(this.dbPath)
// Load initial data into cache
const data = await this.db.loadAll()
for (const [key, value] of data) {
this.cache.set(key, value)
}
}
async request(operation: string, data?: any): Promise<any> {
switch (operation) {
case 'set': {
// Update cache
this.cache.set(data.key, data.value)
// Persist to database (async)
this.db.save(data.key, data.value).catch((err) => console.error('Failed to persist:', err))
return { success: true }
}
case 'get': {
// Try cache first
let value = this.cache.get(data.key)
// Fall back to database
if (value === undefined) {
value = await this.db.load(data.key)
if (value !== undefined) {
this.cache.set(data.key, value)
}
}
return { success: true, value }
}
}
}
async terminate(): Promise<void> {
// Flush any pending writes
await this.db.flush()
await this.db.close()
this.cache.clear()
}
}
Error Handling
Graceful Error Handling
export class RobustContainer implements Container {
async request(operation: string, data?: any): Promise<any> {
try {
// Validate input
this.validateRequest(operation, data)
// Process request
const result = await this.processRequest(operation, data)
return { success: true, result }
} catch (error: any) {
console.error(`Error in ${operation}:`, error)
// Categorize errors
if (error instanceof ValidationError) {
return {
success: false,
error: 'validation',
message: error.message,
fields: error.fields
}
}
if (error instanceof NotFoundError) {
return {
success: false,
error: 'not_found',
message: error.message
}
}
if (error instanceof PermissionError) {
return {
success: false,
error: 'permission_denied',
message: error.message
}
}
// Generic error
return {
success: false,
error: 'internal_error',
message: process.env.NODE_ENV === 'development' ? error.message : 'An error occurred'
}
}
}
private validateRequest(operation: string, data?: any): void {
if (!operation) {
throw new ValidationError('Operation is required')
}
// Operation-specific validation
switch (operation) {
case 'createUser':
if (!data?.email) {
throw new ValidationError('Email is required', ['email'])
}
if (!this.isValidEmail(data.email)) {
throw new ValidationError('Invalid email', ['email'])
}
break
}
}
}
Testing Containers
Unit Tests
import { describe, it, expect } from '@jest/globals'
describe('CalculatorContainer', () => {
let container: CalculatorContainer
beforeEach(() => {
container = new CalculatorContainer('test-uuid' as ContainerUuid)
})
afterEach(async () => {
await container.terminate()
})
it('should add numbers', async () => {
const result = await container.request('add', { a: 2, b: 3 })
expect(result).toEqual({ result: 5 })
})
it('should handle division by zero', async () => {
await expect(container.request('divide', { a: 10, b: 0 })).rejects.toThrow('Division by zero')
})
it('should reject unknown operations', async () => {
await expect(container.request('unknown', {})).rejects.toThrow('Unknown operation')
})
})
Integration Tests
describe('Container Integration', () => {
let tickManager: TickManager
let network: Network
let client: NetworkClient
beforeAll(async () => {
// Setup infrastructure
tickManager = new TickManagerImpl(1)
tickManager.start()
network = new NetworkImpl(tickManager)
// Connect client and serve agent using serveAgent
client = createNetworkClient('localhost:3737')
await client.waitConnection()
await client.serveAgent('localhost:3738', {
calculator: async (options) => ({
uuid: options.uuid ?? ('calc-1' as ContainerUuid),
container: new CalculatorContainer('calc-1' as ContainerUuid),
endpoint: 'test://calc-1' as any
})
})
})
afterAll(async () => {
await client.close()
tickManager.stop()
})
it('should perform calculations via network', async () => {
const ref = await client.get('calculator' as any, {})
const result = await ref.request('multiply', { a: 6, b: 7 })
expect(result).toEqual({ result: 42 })
await ref.close()
})
})
Best Practices
1. Validate Inputs
Always validate incoming data:
private validateCreateUser(data: any): void {
if (!data?.name || typeof data.name !== 'string') {
throw new ValidationError('Name must be a non-empty string')
}
if (!data?.email || !this.isValidEmail(data.email)) {
throw new ValidationError('Valid email is required')
}
}
2. Use Type Safety
Define proper types:
interface CreateUserRequest {
name: string
email: string
role?: UserRole
}
interface UpdateUserRequest {
userId: string
updates: Partial<User>
}
async request(operation: string, data?: any): Promise<any> {
switch (operation) {
case 'createUser':
return await this.createUser(data as CreateUserRequest)
case 'updateUser':
return await this.updateUser(data as UpdateUserRequest)
}
}
3. Log Operations
Add structured logging:
async request(operation: string, data?: any, clientId?: ClientUuid): Promise<any> {
const startTime = Date.now()
console.log('Request', {
container: this.uuid,
operation,
clientId,
timestamp: new Date().toISOString()
})
try {
const result = await this.handleRequest(operation, data, clientId)
console.log('Success', {
container: this.uuid,
operation,
duration: Date.now() - startTime
})
return result
} catch (error: any) {
console.error('Error', {
container: this.uuid,
operation,
error: error.message,
duration: Date.now() - startTime
})
throw error
}
}
4. Handle Cleanup Properly
Always clean up in terminate():
async terminate(): Promise<void> {
try {
// 1. Stop accepting new requests
this.isTerminating = true
// 2. Wait for pending operations
await this.waitForPendingOperations()
// 3. Notify clients
await this.broadcast({ type: 'containerClosing' })
// 4. Close connections
await this.closeConnections()
// 5. Clear state
this.clearState()
} catch (error) {
console.error('Error during termination:', error)
}
}
5. Document Operations
Document your container's API:
/**
* User Management Container
*
* Operations:
* - createUser(data: CreateUserRequest): Promise<CreateUserResponse>
* - getUser(data: { userId: string }): Promise<GetUserResponse>
* - updateUser(data: UpdateUserRequest): Promise<UpdateUserResponse>
* - deleteUser(data: { userId: string }): Promise<DeleteUserResponse>
* - listUsers(data: ListUsersRequest): Promise<ListUsersResponse>
*
* Events:
* - userCreated: { user: User }
* - userUpdated: { userId: string, changes: Partial<User> }
* - userDeleted: { userId: string }
*/
export class UserManagementContainer implements Container {
// ...
}
Common Patterns
Singleton Container
For containers that should have only one instance:
// Use stateless container with HA
agent.addStatelessContainer(
'singleton-service' as ContainerUuid,
'singleton' as ContainerKind,
'singleton://agent/service' as ContainerEndpointRef,
new SingletonContainer('singleton-service' as ContainerUuid)
)
Container with Dependencies
Inject dependencies:
export class ServiceContainer implements Container {
constructor(
readonly uuid: ContainerUuid,
private readonly database: Database,
private readonly cache: CacheService,
private readonly eventBus: EventBus
) {}
// Factory function
static async create(uuid: ContainerUuid): Promise<ServiceContainer> {
const db = await Database.connect()
const cache = new CacheService()
const eventBus = new EventBus()
return new ServiceContainer(uuid, db, cache, eventBus)
}
}
Container Pool
For resource-intensive containers:
// Agent maintains a pool
const containerPool = new ContainerPool(5) // Max 5 instances
const client = createNetworkClient('localhost:3737')
await client.waitConnection(5000)
await client.serveAgent('localhost:3738', {
worker: async (options) => {
const container = await containerPool.acquire()
return {
uuid: container.uuid,
container,
endpoint: `worker://agent/${container.uuid}` as any
}
}
})
Next Steps
- Agent Development Guide - Deploy your containers
- Error Handling Best Practices - Advanced error handling
- Testing Guide - Comprehensive testing strategies
- Examples - Real-world container examples
Need help? Check the Troubleshooting Guide or open an issue on GitHub.