// Package netmgr manages multiple concurrent network contexts. // Each network gets its own derived identity, mesh, store, and anchor connection. // The Manager fans out events from all networks to IPC subscribers, tagging each // event with the network_id so clients can route them appropriately. package netmgr import ( "context" "crypto/sha256" "encoding/hex" "fmt" "log" "path/filepath" "sync" "github.com/waste-go/internal/anchor" "github.com/waste-go/internal/crypto" "github.com/waste-go/internal/mesh" "github.com/waste-go/internal/proto" "github.com/waste-go/internal/store" ) // Config holds the Manager's startup configuration. type Config struct { MasterIdentity *crypto.Identity StoreDir string // base directory for per-network SQLite files AnchorURL string // WebSocket anchor URL used for all networks ShareDir string // shared file directory (global for now; per-network in future) } // Network is a single joined network context. type Network struct { ID string // first 8 hex chars of networkHash — stable, short, opaque Name string Hash string // full SHA-256("yaw2-net:"+name), hex Identity *crypto.Identity Mesh *mesh.Mesh Store *store.Store cancel context.CancelFunc } // Manager owns all joined networks and fans out their events to IPC subscribers. type Manager struct { cfg Config mu sync.RWMutex networks map[string]*Network // keyed by Network.ID order []string // insertion order for "default" lookups subsMu sync.Mutex subs []chan proto.IpcMessage } // New creates a Manager from the given config. func New(cfg Config) *Manager { return &Manager{ networks: make(map[string]*Network), cfg: cfg, } } // Join creates or rejoins a named network. Returns the network ID. // If the network is already joined the existing ID is returned immediately. func (mgr *Manager) Join(name string) (string, error) { netHash := hashNetName(name) netID := netHash[:8] mgr.mu.Lock() if _, exists := mgr.networks[netID]; exists { mgr.mu.Unlock() return netID, nil } mgr.mu.Unlock() // Derive a network-specific identity. netID_full := netID derived, err := crypto.DeriveForNetwork(mgr.cfg.MasterIdentity, netHash) if err != nil { return "", fmt.Errorf("derive identity for %q: %w", name, err) } // Open per-network store. dbPath := filepath.Join(mgr.cfg.StoreDir, "messages-"+netID_full+".db") st, err := store.Open(dbPath) if err != nil { return "", fmt.Errorf("open store for %q: %w", name, err) } m := mesh.New(derived, st) if mgr.cfg.ShareDir != "" { m.ShareDir = mgr.cfg.ShareDir } // Forward all mesh events to the Manager's fan-out, tagging with network_id. meshEvents := m.Subscribe() go func() { for evt := range meshEvents { evt.NetworkID = netID mgr.emit(evt) } }() ctx, cancel := context.WithCancel(context.Background()) net := &Network{ ID: netID, Name: name, Hash: netHash, Identity: derived, Mesh: m, Store: st, cancel: cancel, } mgr.mu.Lock() mgr.networks[netID] = net mgr.order = append(mgr.order, netID) mgr.mu.Unlock() log.Printf("netmgr: joining network %q (id=%s peer=%s)", name, netID, derived.PeerID().Short()) if mgr.cfg.AnchorURL != "" { go func() { anchor.Run(ctx, mgr.cfg.AnchorURL, name, derived, m) log.Printf("netmgr: left network %q", name) }() } mgr.emit(proto.IpcMessage{ Type: proto.EvtNetworkJoined, NetworkID: netID, NetworkName: name, LocalPeer: peerPtr(derived.PeerInfo()), }) return netID, nil } // Leave cancels a network context by ID. Closes its store. func (mgr *Manager) Leave(netID string) { mgr.mu.Lock() net, ok := mgr.networks[netID] if ok { delete(mgr.networks, netID) mgr.order = removeStr(mgr.order, netID) } mgr.mu.Unlock() if !ok { return } net.cancel() net.Store.Close() net.Mesh.Unsubscribe(net.Mesh.Subscribe()) // drain + close subscription log.Printf("netmgr: left network %q (id=%s)", net.Name, netID) mgr.emit(proto.IpcMessage{Type: proto.EvtNetworkLeft, NetworkID: netID}) } // LeaveAll leaves every joined network. func (mgr *Manager) LeaveAll() { mgr.mu.RLock() ids := make([]string, len(mgr.order)) copy(ids, mgr.order) mgr.mu.RUnlock() for _, id := range ids { mgr.Leave(id) } } // Get returns a network by ID. func (mgr *Manager) Get(netID string) (*Network, bool) { mgr.mu.RLock() defer mgr.mu.RUnlock() n, ok := mgr.networks[netID] return n, ok } // Default returns the first joined network, or nil if none are joined. // Used to route commands that carry no network_id (backward compat). func (mgr *Manager) Default() *Network { mgr.mu.RLock() defer mgr.mu.RUnlock() if len(mgr.order) == 0 { return nil } return mgr.networks[mgr.order[0]] } // Resolve returns the network for netID, or the default if netID is empty. func (mgr *Manager) Resolve(netID string) *Network { if netID == "" { return mgr.Default() } n, _ := mgr.Get(netID) return n } // All returns a snapshot of all joined networks in join order. func (mgr *Manager) All() []*Network { mgr.mu.RLock() defer mgr.mu.RUnlock() out := make([]*Network, 0, len(mgr.order)) for _, id := range mgr.order { out = append(out, mgr.networks[id]) } return out } // AnchorURL returns the configured anchor URL. func (mgr *Manager) AnchorURL() string { return mgr.cfg.AnchorURL } // Subscribe returns a channel that receives tagged events from all networks. func (mgr *Manager) Subscribe() <-chan proto.IpcMessage { ch := make(chan proto.IpcMessage, 128) mgr.subsMu.Lock() mgr.subs = append(mgr.subs, ch) mgr.subsMu.Unlock() return ch } // Unsubscribe removes and closes a subscription channel. func (mgr *Manager) Unsubscribe(ch <-chan proto.IpcMessage) { mgr.subsMu.Lock() defer mgr.subsMu.Unlock() for i, s := range mgr.subs { if s == ch { mgr.subs = append(mgr.subs[:i], mgr.subs[i+1:]...) close(s) return } } } func (mgr *Manager) emit(msg proto.IpcMessage) { mgr.subsMu.Lock() defer mgr.subsMu.Unlock() for _, ch := range mgr.subs { select { case ch <- msg: default: } } } // ── helpers ─────────────────────────────────────────────────────────────────── func hashNetName(name string) string { h := sha256.Sum256([]byte("yaw2-net:" + name)) return hex.EncodeToString(h[:]) } func removeStr(ss []string, s string) []string { out := ss[:0] for _, v := range ss { if v != s { out = append(out, v) } } return out } func peerPtr(p proto.PeerInfo) *proto.PeerInfo { return &p }