Files
huly-platform/foundations/net/docs/CONTAINER_DEVELOPMENT.md
+1 98652c6476 Include sub projects (#10201)
* 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 commit 54631d353e.

* Revert "Move LowLevelStorage to server"

This reverts commit aafb8f6f12.

* 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>
2025-11-26 19:15:30 +05:00

945 lines
22 KiB
Markdown

# Container Development Guide
Learn how to build robust containers for Huly Virtual Network.
## Table of Contents
- [Introduction](#introduction)
- [Container Basics](#container-basics)
- [Container Lifecycle](#container-lifecycle)
- [Implementing Request Handlers](#implementing-request-handlers)
- [Event Broadcasting](#event-broadcasting)
- [State Management](#state-management)
- [Error Handling](#error-handling)
- [Testing Containers](#testing-containers)
- [Best Practices](#best-practices)
- [Common Patterns](#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:
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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()`:
```typescript
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:
```typescript
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:
```typescript
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
```typescript
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:
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
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():
```typescript
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:
```typescript
/**
* 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:
```typescript
// 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:
```typescript
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:
```typescript
// 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](AGENT_DEVELOPMENT.md) - Deploy your containers
- [Error Handling Best Practices](ERROR_HANDLING.md) - Advanced error handling
- [Testing Guide](TESTING.md) - Comprehensive testing strategies
- [Examples](../examples/) - Real-world container examples
---
Need help? Check the [Troubleshooting Guide](TROUBLESHOOTING.md) or open an issue on GitHub.