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 @@
@@ -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 @@
@@ -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