🎭 CHORUS now contains full BZZZ functionality adapted for containers Core systems ported: - P2P networking (libp2p with DHT and PubSub) - Task coordination (COOEE protocol) - HMMM collaborative reasoning - SHHH encryption and security - SLURP admin election system - UCXL content addressing - UCXI server integration - Hypercore logging system - Health monitoring and graceful shutdown - License validation with KACHING Container adaptations: - Environment variable configuration (no YAML files) - Container-optimized logging to stdout/stderr - Auto-generated agent IDs for container deployments - Docker-first architecture All proven BZZZ P2P protocols, AI integration, and collaboration features are now available in containerized form. Next: Build and test container deployment. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
236 lines
6.7 KiB
Go
236 lines
6.7 KiB
Go
package hmmm_adapter
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Joiner joins a pub/sub topic (ensure availability before publish).
|
|
type Joiner func(topic string) error
|
|
|
|
// Publisher publishes a raw JSON payload to a topic.
|
|
type Publisher func(topic string, payload []byte) error
|
|
|
|
// Adapter bridges BZZZ pub/sub to a RawPublisher-compatible interface.
|
|
// It does not impose any message envelope so HMMM can publish raw JSON frames.
|
|
// The adapter provides additional features like topic caching, metrics, and validation.
|
|
type Adapter struct {
|
|
join Joiner
|
|
publish Publisher
|
|
|
|
// Topic join cache to avoid redundant joins
|
|
joinedTopics map[string]bool
|
|
joinedTopicsMu sync.RWMutex
|
|
|
|
// Metrics tracking
|
|
publishCount int64
|
|
joinCount int64
|
|
errorCount int64
|
|
metricsLock sync.RWMutex
|
|
|
|
// Configuration
|
|
maxPayloadSize int
|
|
joinTimeout time.Duration
|
|
publishTimeout time.Duration
|
|
}
|
|
|
|
// AdapterConfig holds configuration options for the Adapter
|
|
type AdapterConfig struct {
|
|
MaxPayloadSize int `yaml:"max_payload_size"`
|
|
JoinTimeout time.Duration `yaml:"join_timeout"`
|
|
PublishTimeout time.Duration `yaml:"publish_timeout"`
|
|
}
|
|
|
|
// DefaultAdapterConfig returns sensible defaults for the adapter
|
|
func DefaultAdapterConfig() AdapterConfig {
|
|
return AdapterConfig{
|
|
MaxPayloadSize: 1024 * 1024, // 1MB max payload
|
|
JoinTimeout: 30 * time.Second,
|
|
PublishTimeout: 10 * time.Second,
|
|
}
|
|
}
|
|
|
|
// NewAdapter constructs a new adapter with explicit join/publish hooks.
|
|
// Wire these to BZZZ pubsub methods, e.g., JoinDynamicTopic and a thin PublishRaw helper.
|
|
func NewAdapter(join Joiner, publish Publisher) *Adapter {
|
|
return NewAdapterWithConfig(join, publish, DefaultAdapterConfig())
|
|
}
|
|
|
|
// NewAdapterWithConfig constructs a new adapter with custom configuration.
|
|
func NewAdapterWithConfig(join Joiner, publish Publisher, config AdapterConfig) *Adapter {
|
|
return &Adapter{
|
|
join: join,
|
|
publish: publish,
|
|
joinedTopics: make(map[string]bool),
|
|
maxPayloadSize: config.MaxPayloadSize,
|
|
joinTimeout: config.JoinTimeout,
|
|
publishTimeout: config.PublishTimeout,
|
|
}
|
|
}
|
|
|
|
// Publish ensures the topic is joined before sending a raw payload.
|
|
// Includes validation, caching, metrics, and timeout handling.
|
|
func (a *Adapter) Publish(ctx context.Context, topic string, payload []byte) error {
|
|
// Input validation
|
|
if topic == "" {
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("topic cannot be empty")
|
|
}
|
|
if len(payload) == 0 {
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("payload cannot be empty")
|
|
}
|
|
if len(payload) > a.maxPayloadSize {
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("payload size %d exceeds maximum %d bytes", len(payload), a.maxPayloadSize)
|
|
}
|
|
|
|
// Check if we need to join the topic (with caching)
|
|
if !a.isTopicJoined(topic) {
|
|
joinCtx, cancel := context.WithTimeout(ctx, a.joinTimeout)
|
|
defer cancel()
|
|
|
|
if err := a.joinTopic(joinCtx, topic); err != nil {
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("failed to join topic %s: %w", topic, err)
|
|
}
|
|
}
|
|
|
|
// Publish with timeout
|
|
publishCtx, cancel := context.WithTimeout(ctx, a.publishTimeout)
|
|
defer cancel()
|
|
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- a.publish(topic, payload)
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("failed to publish to topic %s: %w", topic, err)
|
|
}
|
|
a.incrementPublishCount()
|
|
return nil
|
|
case <-publishCtx.Done():
|
|
a.incrementErrorCount()
|
|
return fmt.Errorf("publish to topic %s timed out after %v", topic, a.publishTimeout)
|
|
}
|
|
}
|
|
|
|
// isTopicJoined checks if a topic has already been joined (with caching)
|
|
func (a *Adapter) isTopicJoined(topic string) bool {
|
|
a.joinedTopicsMu.RLock()
|
|
defer a.joinedTopicsMu.RUnlock()
|
|
return a.joinedTopics[topic]
|
|
}
|
|
|
|
// joinTopic joins a topic and updates the cache
|
|
func (a *Adapter) joinTopic(ctx context.Context, topic string) error {
|
|
// Double-check locking pattern to avoid redundant joins
|
|
if a.isTopicJoined(topic) {
|
|
return nil
|
|
}
|
|
|
|
a.joinedTopicsMu.Lock()
|
|
defer a.joinedTopicsMu.Unlock()
|
|
|
|
// Check again after acquiring write lock
|
|
if a.joinedTopics[topic] {
|
|
return nil
|
|
}
|
|
|
|
// Execute join with context
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- a.join(topic)
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err == nil {
|
|
a.joinedTopics[topic] = true
|
|
a.incrementJoinCount()
|
|
}
|
|
return err
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
}
|
|
}
|
|
|
|
// GetMetrics returns current adapter metrics
|
|
func (a *Adapter) GetMetrics() AdapterMetrics {
|
|
a.metricsLock.RLock()
|
|
defer a.metricsLock.RUnlock()
|
|
|
|
return AdapterMetrics{
|
|
PublishCount: a.publishCount,
|
|
JoinCount: a.joinCount,
|
|
ErrorCount: a.errorCount,
|
|
JoinedTopics: len(a.joinedTopics),
|
|
}
|
|
}
|
|
|
|
// AdapterMetrics holds metrics data for the adapter
|
|
type AdapterMetrics struct {
|
|
PublishCount int64 `json:"publish_count"`
|
|
JoinCount int64 `json:"join_count"`
|
|
ErrorCount int64 `json:"error_count"`
|
|
JoinedTopics int `json:"joined_topics"`
|
|
}
|
|
|
|
// ResetMetrics resets all metrics counters (useful for testing)
|
|
func (a *Adapter) ResetMetrics() {
|
|
a.metricsLock.Lock()
|
|
defer a.metricsLock.Unlock()
|
|
|
|
a.publishCount = 0
|
|
a.joinCount = 0
|
|
a.errorCount = 0
|
|
}
|
|
|
|
// ClearTopicCache clears the joined topics cache (useful for testing or reconnections)
|
|
func (a *Adapter) ClearTopicCache() {
|
|
a.joinedTopicsMu.Lock()
|
|
defer a.joinedTopicsMu.Unlock()
|
|
|
|
a.joinedTopics = make(map[string]bool)
|
|
}
|
|
|
|
// GetJoinedTopics returns a list of currently joined topics
|
|
func (a *Adapter) GetJoinedTopics() []string {
|
|
a.joinedTopicsMu.RLock()
|
|
defer a.joinedTopicsMu.RUnlock()
|
|
|
|
topics := make([]string, 0, len(a.joinedTopics))
|
|
for topic := range a.joinedTopics {
|
|
topics = append(topics, topic)
|
|
}
|
|
return topics
|
|
}
|
|
|
|
// incrementPublishCount safely increments the publish counter
|
|
func (a *Adapter) incrementPublishCount() {
|
|
a.metricsLock.Lock()
|
|
a.publishCount++
|
|
a.metricsLock.Unlock()
|
|
}
|
|
|
|
// incrementJoinCount safely increments the join counter
|
|
func (a *Adapter) incrementJoinCount() {
|
|
a.metricsLock.Lock()
|
|
a.joinCount++
|
|
a.metricsLock.Unlock()
|
|
}
|
|
|
|
// incrementErrorCount safely increments the error counter
|
|
func (a *Adapter) incrementErrorCount() {
|
|
a.metricsLock.Lock()
|
|
a.errorCount++
|
|
a.metricsLock.Unlock()
|
|
}
|
|
|