Browse Source
- Create negentropy.Handler interface in pkg/interfaces/negentropy - Add EmbeddedHandler that wraps negentropy.Manager for embedded mode - Update app/handle-negentropy.go to use interface instead of gRPC client - Update gRPC client to implement the Handler interface - Update startup.go to initialize embedded negentropy when not in gRPC mode - Enables NIP-77 set reconciliation in monolithic binary without gRPC Files modified: - pkg/interfaces/negentropy/negentropy.go: New interface definition - pkg/sync/negentropy/embedded.go: New embedded handler implementation - pkg/sync/negentropy/grpc/client.go: Implement Handler interface - app/handle-negentropy.go: Use interface type - pkg/relay/startup.go: Initialize embedded negentropy - pkg/version/version: Bump to v0.57.1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>main v0.57.1
6 changed files with 345 additions and 37 deletions
@ -0,0 +1,44 @@ |
|||||||
|
// Package negentropy defines the interface for NIP-77 negentropy operations.
|
||||||
|
package negentropy |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
|
||||||
|
commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" |
||||||
|
) |
||||||
|
|
||||||
|
// ClientSession represents an active client negentropy session.
|
||||||
|
type ClientSession struct { |
||||||
|
SubscriptionID string |
||||||
|
ConnectionID string |
||||||
|
CreatedAt int64 |
||||||
|
LastActivity int64 |
||||||
|
RoundCount int32 |
||||||
|
} |
||||||
|
|
||||||
|
// Handler defines the interface for handling NIP-77 negentropy messages.
|
||||||
|
// This interface is implemented by both the gRPC client and the embedded handler.
|
||||||
|
type Handler interface { |
||||||
|
// HandleNegOpen processes a NEG-OPEN message from a client.
|
||||||
|
// Returns: message, haveIDs, needIDs, complete, errorStr, error
|
||||||
|
HandleNegOpen(ctx context.Context, connectionID, subscriptionID string, filter *commonv1.Filter, initialMessage []byte) ([]byte, [][]byte, [][]byte, bool, string, error) |
||||||
|
|
||||||
|
// HandleNegMsg processes a NEG-MSG message from a client.
|
||||||
|
// Returns: message, haveIDs, needIDs, complete, errorStr, error
|
||||||
|
HandleNegMsg(ctx context.Context, connectionID, subscriptionID string, message []byte) ([]byte, [][]byte, [][]byte, bool, string, error) |
||||||
|
|
||||||
|
// HandleNegClose processes a NEG-CLOSE message from a client.
|
||||||
|
HandleNegClose(ctx context.Context, connectionID, subscriptionID string) error |
||||||
|
|
||||||
|
// ListSessions returns active client negentropy sessions.
|
||||||
|
ListSessions(ctx context.Context) ([]*ClientSession, error) |
||||||
|
|
||||||
|
// CloseSession forcefully closes a client session.
|
||||||
|
CloseSession(ctx context.Context, connectionID, subscriptionID string) error |
||||||
|
|
||||||
|
// Ready returns a channel that closes when the handler is ready.
|
||||||
|
Ready() <-chan struct{} |
||||||
|
|
||||||
|
// Close cleans up resources.
|
||||||
|
Close() error |
||||||
|
} |
||||||
@ -0,0 +1,253 @@ |
|||||||
|
package negentropy |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"encoding/hex" |
||||||
|
"fmt" |
||||||
|
|
||||||
|
"lol.mleku.dev/log" |
||||||
|
|
||||||
|
"git.mleku.dev/mleku/nostr/encoders/filter" |
||||||
|
negentropylib "git.mleku.dev/mleku/nostr/negentropy" |
||||||
|
"next.orly.dev/pkg/database" |
||||||
|
negentropyiface "next.orly.dev/pkg/interfaces/negentropy" |
||||||
|
commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" |
||||||
|
) |
||||||
|
|
||||||
|
// EmbeddedHandler wraps the negentropy Manager to implement the Handler interface.
|
||||||
|
// This allows negentropy to run embedded in monolithic mode without gRPC.
|
||||||
|
type EmbeddedHandler struct { |
||||||
|
mgr *Manager |
||||||
|
db database.Database |
||||||
|
ready chan struct{} |
||||||
|
} |
||||||
|
|
||||||
|
// NewEmbeddedHandler creates a new embedded negentropy handler.
|
||||||
|
func NewEmbeddedHandler(db database.Database, cfg *Config) *EmbeddedHandler { |
||||||
|
h := &EmbeddedHandler{ |
||||||
|
mgr: NewManager(db, cfg), |
||||||
|
db: db, |
||||||
|
ready: make(chan struct{}), |
||||||
|
} |
||||||
|
close(h.ready) // Immediately ready in embedded mode
|
||||||
|
return h |
||||||
|
} |
||||||
|
|
||||||
|
// Start starts the background sync loop.
|
||||||
|
func (h *EmbeddedHandler) Start() { |
||||||
|
h.mgr.Start() |
||||||
|
} |
||||||
|
|
||||||
|
// Stop stops the background sync.
|
||||||
|
func (h *EmbeddedHandler) Stop() { |
||||||
|
h.mgr.Stop() |
||||||
|
} |
||||||
|
|
||||||
|
// Ready returns a channel that closes when the handler is ready.
|
||||||
|
func (h *EmbeddedHandler) Ready() <-chan struct{} { |
||||||
|
return h.ready |
||||||
|
} |
||||||
|
|
||||||
|
// Close cleans up resources.
|
||||||
|
func (h *EmbeddedHandler) Close() error { |
||||||
|
h.mgr.Stop() |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// HandleNegOpen processes a NEG-OPEN message from a client.
|
||||||
|
func (h *EmbeddedHandler) HandleNegOpen(ctx context.Context, connectionID, subscriptionID string, protoFilter *commonv1.Filter, initialMessage []byte) ([]byte, [][]byte, [][]byte, bool, string, error) { |
||||||
|
// Open a session for this client
|
||||||
|
session := h.mgr.OpenSession(connectionID, subscriptionID) |
||||||
|
|
||||||
|
// Build storage from local events matching the filter
|
||||||
|
storage, err := h.buildStorageForFilter(ctx, protoFilter) |
||||||
|
if err != nil { |
||||||
|
log.E.F("NEG-OPEN: failed to build storage: %v", err) |
||||||
|
return nil, nil, nil, false, fmt.Sprintf("failed to build storage: %v", err), nil |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("NEG-OPEN: built storage with %d events", storage.Size()) |
||||||
|
|
||||||
|
// Create negentropy instance for this session
|
||||||
|
neg := negentropylib.New(storage, negentropylib.DefaultFrameSizeLimit) |
||||||
|
|
||||||
|
// Store in session for later use
|
||||||
|
session.SetNegentropy(neg, storage) |
||||||
|
|
||||||
|
// If we have an initial message from client, process it
|
||||||
|
var respMsg []byte |
||||||
|
var complete bool |
||||||
|
if len(initialMessage) > 0 { |
||||||
|
respMsg, complete, err = neg.Reconcile(initialMessage) |
||||||
|
if err != nil { |
||||||
|
log.E.F("NEG-OPEN: reconcile failed: %v", err) |
||||||
|
return nil, nil, nil, false, fmt.Sprintf("reconcile failed: %v", err), nil |
||||||
|
} |
||||||
|
log.I.F("NEG-OPEN: reconcile complete=%v, response len=%d", complete, len(respMsg)) |
||||||
|
} else { |
||||||
|
// No initial message, start as server (initiator)
|
||||||
|
respMsg, err = neg.Start() |
||||||
|
if err != nil { |
||||||
|
log.E.F("NEG-OPEN: failed to start: %v", err) |
||||||
|
return nil, nil, nil, false, fmt.Sprintf("failed to start: %v", err), nil |
||||||
|
} |
||||||
|
log.D.F("NEG-OPEN: started negentropy, initial msg len=%d", len(respMsg)) |
||||||
|
} |
||||||
|
|
||||||
|
// Collect IDs we have that client needs (to send as events)
|
||||||
|
haveIDs := neg.CollectHaves() |
||||||
|
var haveIDBytes [][]byte |
||||||
|
for _, id := range haveIDs { |
||||||
|
if decoded, err := hex.DecodeString(id); err == nil { |
||||||
|
haveIDBytes = append(haveIDBytes, decoded) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Collect IDs we need from client
|
||||||
|
needIDs := neg.CollectHaveNots() |
||||||
|
var needIDBytes [][]byte |
||||||
|
for _, id := range needIDs { |
||||||
|
if decoded, err := hex.DecodeString(id); err == nil { |
||||||
|
needIDBytes = append(needIDBytes, decoded) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("NEG-OPEN: complete=%v, haves=%d, needs=%d, response len=%d", |
||||||
|
complete, len(haveIDs), len(needIDs), len(respMsg)) |
||||||
|
|
||||||
|
return respMsg, haveIDBytes, needIDBytes, complete, "", nil |
||||||
|
} |
||||||
|
|
||||||
|
// HandleNegMsg processes a NEG-MSG message from a client.
|
||||||
|
func (h *EmbeddedHandler) HandleNegMsg(ctx context.Context, connectionID, subscriptionID string, message []byte) ([]byte, [][]byte, [][]byte, bool, string, error) { |
||||||
|
// Update session activity
|
||||||
|
h.mgr.UpdateSessionActivity(connectionID, subscriptionID) |
||||||
|
|
||||||
|
// Look up session
|
||||||
|
session, ok := h.mgr.GetSession(connectionID, subscriptionID) |
||||||
|
if !ok { |
||||||
|
return nil, nil, nil, false, "session not found", nil |
||||||
|
} |
||||||
|
|
||||||
|
neg := session.GetNegentropy() |
||||||
|
if neg == nil { |
||||||
|
return nil, nil, nil, false, "session has no negentropy state", nil |
||||||
|
} |
||||||
|
|
||||||
|
// Process the message
|
||||||
|
respMsg, complete, err := neg.Reconcile(message) |
||||||
|
if err != nil { |
||||||
|
log.E.F("NEG-MSG: reconcile failed: %v", err) |
||||||
|
return nil, nil, nil, false, fmt.Sprintf("reconcile failed: %v", err), nil |
||||||
|
} |
||||||
|
|
||||||
|
// Collect IDs we have that client needs
|
||||||
|
haveIDs := neg.CollectHaves() |
||||||
|
var haveIDBytes [][]byte |
||||||
|
for _, id := range haveIDs { |
||||||
|
if decoded, err := hex.DecodeString(id); err == nil { |
||||||
|
haveIDBytes = append(haveIDBytes, decoded) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Collect IDs we need from client
|
||||||
|
needIDs := neg.CollectHaveNots() |
||||||
|
var needIDBytes [][]byte |
||||||
|
for _, id := range needIDs { |
||||||
|
if decoded, err := hex.DecodeString(id); err == nil { |
||||||
|
needIDBytes = append(needIDBytes, decoded) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
log.I.F("NEG-MSG: complete=%v, haves=%d, needs=%d, response len=%d", |
||||||
|
complete, len(haveIDs), len(needIDs), len(respMsg)) |
||||||
|
|
||||||
|
return respMsg, haveIDBytes, needIDBytes, complete, "", nil |
||||||
|
} |
||||||
|
|
||||||
|
// HandleNegClose processes a NEG-CLOSE message from a client.
|
||||||
|
func (h *EmbeddedHandler) HandleNegClose(ctx context.Context, connectionID, subscriptionID string) error { |
||||||
|
h.mgr.CloseSession(connectionID, subscriptionID) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// ListSessions returns active client negentropy sessions.
|
||||||
|
func (h *EmbeddedHandler) ListSessions(ctx context.Context) ([]*negentropyiface.ClientSession, error) { |
||||||
|
sessions := h.mgr.ListSessions() |
||||||
|
|
||||||
|
result := make([]*negentropyiface.ClientSession, 0, len(sessions)) |
||||||
|
for _, sess := range sessions { |
||||||
|
result = append(result, &negentropyiface.ClientSession{ |
||||||
|
SubscriptionID: sess.SubscriptionID, |
||||||
|
ConnectionID: sess.ConnectionID, |
||||||
|
CreatedAt: sess.CreatedAt.Unix(), |
||||||
|
LastActivity: sess.LastActivity.Unix(), |
||||||
|
RoundCount: sess.RoundCount, |
||||||
|
}) |
||||||
|
} |
||||||
|
return result, nil |
||||||
|
} |
||||||
|
|
||||||
|
// CloseSession forcefully closes a client session.
|
||||||
|
func (h *EmbeddedHandler) CloseSession(ctx context.Context, connectionID, subscriptionID string) error { |
||||||
|
if connectionID == "" { |
||||||
|
// Close all sessions with this subscription ID
|
||||||
|
sessions := h.mgr.ListSessions() |
||||||
|
for _, sess := range sessions { |
||||||
|
if sess.SubscriptionID == subscriptionID { |
||||||
|
h.mgr.CloseSession(sess.ConnectionID, sess.SubscriptionID) |
||||||
|
} |
||||||
|
} |
||||||
|
} else { |
||||||
|
h.mgr.CloseSession(connectionID, subscriptionID) |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// buildStorageForFilter creates a negentropy Vector from local events matching the filter.
|
||||||
|
func (h *EmbeddedHandler) buildStorageForFilter(ctx context.Context, protoFilter *commonv1.Filter) (*negentropylib.Vector, error) { |
||||||
|
storage := negentropylib.NewVector() |
||||||
|
|
||||||
|
// Convert proto filter to nostr filter
|
||||||
|
f := protoToFilter(protoFilter) |
||||||
|
|
||||||
|
// If no filter provided, use a reasonable limit
|
||||||
|
if f == nil { |
||||||
|
limit := uint(1000000) |
||||||
|
f = &filter.F{Limit: &limit} |
||||||
|
} |
||||||
|
if f.Limit == nil { |
||||||
|
limit := uint(1000000) |
||||||
|
f.Limit = &limit |
||||||
|
} |
||||||
|
|
||||||
|
// Query events from database
|
||||||
|
idPkTs, err := h.db.QueryForIds(ctx, f) |
||||||
|
if err != nil { |
||||||
|
return nil, fmt.Errorf("failed to query events: %w", err) |
||||||
|
} |
||||||
|
|
||||||
|
for _, item := range idPkTs { |
||||||
|
storage.Insert(item.Ts, item.IDHex()) |
||||||
|
} |
||||||
|
|
||||||
|
storage.Seal() |
||||||
|
return storage, nil |
||||||
|
} |
||||||
|
|
||||||
|
// protoToFilter converts a proto filter to a nostr filter.
|
||||||
|
func protoToFilter(pf *commonv1.Filter) *filter.F { |
||||||
|
if pf == nil { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
f := &filter.F{} |
||||||
|
|
||||||
|
// Convert Limit
|
||||||
|
if pf.Limit != nil { |
||||||
|
limit := uint(*pf.Limit) |
||||||
|
f.Limit = &limit |
||||||
|
} |
||||||
|
|
||||||
|
return f |
||||||
|
} |
||||||
Loading…
Reference in new issue