diff --git a/Makefile b/Makefile index 26fb3b7..3385eda 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,8 @@ .PHONY: all orly orly-db orly-acl orly-launcher proto clean test deploy web install help .PHONY: orly-db-badger orly-db-neo4j orly-acl-follows orly-acl-managed orly-acl-curation .PHONY: all-split arm64-split install-split +.PHONY: orly-sync-negentropy all-sync arm64-sync +.PHONY: quick-deploy quick-deploy-restart deploy-both deploy-both-restart deploy-new list-releases rollback # Build flags CGO_ENABLED ?= 0 @@ -120,7 +122,9 @@ clean: rm -f $(ORLY) $(ORLY_DB) $(ORLY_ACL) $(ORLY_LAUNCHER) rm -f $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J) rm -f $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION) + rm -f $(ORLY_SYNC_NEGENTROPY) rm -f orly-db-arm64 orly-acl-arm64 orly-launcher-arm64 next.orly.dev + rm -rf build-arm64 # Run tests test: @@ -158,6 +162,50 @@ install-backends: all-backends mkdir -p ~/.local/bin cp $(ORLY) $(ORLY_DB_BADGER) $(ORLY_DB_NEO4J) $(ORLY_ACL_FOLLOWS) $(ORLY_ACL_MANAGED) $(ORLY_ACL_CURATION) $(ORLY_LAUNCHER) ~/.local/bin/ +# === Symlink-Based Deployment === + +# Sync binaries +ORLY_SYNC_NEGENTROPY = $(BIN_DIR)/orly-sync-negentropy + +orly-sync-negentropy: + $(BUILD_FLAGS) go build -o $(ORLY_SYNC_NEGENTROPY) ./cmd/orly-sync-negentropy + +# Build all sync services +all-sync: proto orly orly-db orly-acl orly-launcher orly-sync-negentropy + @echo "All sync service binaries built successfully" + +# ARM64 for sync services +arm64-sync: + $(MAKE) GOOS=linux GOARCH=arm64 all-sync + +# Quick deploy: build ARM64 and deploy to relay.orly.dev +quick-deploy: + ./scripts/build-and-deploy.sh relay.orly.dev + +# Quick deploy with restart +quick-deploy-restart: + ./scripts/build-and-deploy.sh relay.orly.dev --restart + +# Deploy to both relays +deploy-both: + ./scripts/build-and-deploy.sh both + +# Deploy to both relays with restart +deploy-both-restart: + ./scripts/build-and-deploy.sh both --restart + +# Deploy to new.orly.dev only +deploy-new: + ./scripts/build-and-deploy.sh new.orly.dev + +# List available releases on relay +list-releases: + ./scripts/deploy-orly.sh --host relay.orly.dev --list + +# Rollback to previous release +rollback: + ./scripts/deploy-orly.sh --host relay.orly.dev --rollback --restart + # Help help: @echo "ORLY Build Targets:" @@ -192,3 +240,17 @@ help: @echo " test - Run test suite" @echo " clean - Remove build artifacts" @echo " help - Show this help" + @echo "" + @echo " Sync Services:" + @echo " orly-sync-negentropy - Build NIP-77 negentropy sync server" + @echo " all-sync - Build all including sync services" + @echo " arm64-sync - Cross-compile sync services for ARM64" + @echo "" + @echo " Quick Deployment (symlink-based):" + @echo " quick-deploy - Build ARM64 and deploy to relay.orly.dev" + @echo " quick-deploy-restart - Build, deploy, and restart service" + @echo " deploy-both - Deploy to both relay.orly.dev and new.orly.dev" + @echo " deploy-both-restart - Deploy and restart both relays" + @echo " deploy-new - Deploy to new.orly.dev only" + @echo " list-releases - List available releases on relay" + @echo " rollback - Rollback to previous release" diff --git a/README.md b/README.md index 472a97f..3b471d6 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,32 @@ zap me: �mlekudev@getalby.com follow me on [nostr](https://jumble.social/users/npub1fjqqy4a93z5zsjwsfxqhc2764kvykfdyttvldkkkdera8dr78vhsmmleku) +## Architecture Overview + +ORLY supports a modular IPC architecture where core functionality runs as independent gRPC services: + +``` +orly-launcher (process supervisor) +├── orly-db (gRPC :50051) - Event storage & queries +├── orly-acl (gRPC :50052) - Access control +├── orly-sync-distributed (gRPC :50061) - Peer-to-peer sync +├── orly-sync-cluster (gRPC :50062) - Cluster replication +├── orly-sync-relaygroup (gRPC :50063) - Relay group config (Kind 39105) +├── orly-sync-negentropy (gRPC :50064) - NIP-77 set reconciliation +└── orly (WebSocket/HTTP) - Main relay +``` + +**Benefits**: +- **Resource isolation**: Database, ACL, and sync run in separate processes +- **Independent scaling**: Scale sync services independently from the relay +- **Fault tolerance**: Service crashes don't bring down the entire relay +- **Modular deployment**: Enable only the services you need + +See [docs/IPC_SYNC_SERVICES.md](./docs/IPC_SYNC_SERVICES.md) for detailed API documentation. + ## Table of Contents +- [Architecture Overview](#architecture-overview) - [Bug Reports & Feature Requests](#%EF%B8%8F-bug-reports--feature-requests) - [System Requirements](#%EF%B8%8F-system-requirements) - [About](#about) diff --git a/app/config/config.go b/app/config/config.go index 38e53b6..fb84741 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -128,6 +128,15 @@ type C struct { GRPCACLServerAddress string `env:"ORLY_GRPC_ACL_SERVER" usage:"address of remote gRPC ACL server (only used when ORLY_ACL_TYPE=grpc)"` GRPCACLConnectTimeout time.Duration `env:"ORLY_GRPC_ACL_TIMEOUT" default:"10s" usage:"gRPC ACL connection timeout (only used when ORLY_ACL_TYPE=grpc)"` + // gRPC Sync client settings (only used when ORLY_SYNC_TYPE=grpc) + SyncType string `env:"ORLY_SYNC_TYPE" default:"local" usage:"sync backend: local (in-process) or grpc (remote sync services)"` + GRPCSyncDistributedAddress string `env:"ORLY_GRPC_SYNC_DISTRIBUTED" usage:"address of gRPC distributed sync server"` + GRPCSyncClusterAddress string `env:"ORLY_GRPC_SYNC_CLUSTER" usage:"address of gRPC cluster sync server"` + GRPCSyncRelayGroupAddress string `env:"ORLY_GRPC_SYNC_RELAYGROUP" usage:"address of gRPC relay group server"` + GRPCSyncNegentropyAddress string `env:"ORLY_GRPC_SYNC_NEGENTROPY" usage:"address of gRPC negentropy server"` + GRPCSyncConnectTimeout time.Duration `env:"ORLY_GRPC_SYNC_TIMEOUT" default:"10s" usage:"gRPC sync connection timeout"` + NegentropyEnabled bool `env:"ORLY_NEGENTROPY_ENABLED" default:"false" usage:"enable NIP-77 negentropy set reconciliation"` + QueryCacheSizeMB int `env:"ORLY_QUERY_CACHE_SIZE_MB" default:"512" usage:"query cache size in MB (caches database query results for faster REQ responses)"` QueryCacheMaxAge string `env:"ORLY_QUERY_CACHE_MAX_AGE" default:"5m" usage:"maximum age for cached query results (e.g., 5m, 10m, 1h)"` @@ -861,3 +870,24 @@ func (cfg *C) GetGRPCACLConfigValues() ( cfg.GRPCACLServerAddress, cfg.GRPCACLConnectTimeout } + +// GetGRPCSyncConfigValues returns the gRPC sync client configuration values. +// This avoids circular imports with pkg/sync/*/grpc packages while allowing main.go +// to construct the gRPC sync client configurations. +func (cfg *C) GetGRPCSyncConfigValues() ( + syncType string, + distributedAddress string, + clusterAddress string, + relayGroupAddress string, + negentropyAddress string, + connectTimeout time.Duration, + negentropyEnabled bool, +) { + return cfg.SyncType, + cfg.GRPCSyncDistributedAddress, + cfg.GRPCSyncClusterAddress, + cfg.GRPCSyncRelayGroupAddress, + cfg.GRPCSyncNegentropyAddress, + cfg.GRPCSyncConnectTimeout, + cfg.NegentropyEnabled +} diff --git a/app/handle-message.go b/app/handle-message.go index ec0a15a..eaf3029 100644 --- a/app/handle-message.go +++ b/app/handle-message.go @@ -92,6 +92,28 @@ func (l *Listener) HandleMessage(msg []byte, remote string) { var t string var rem []byte + // Check for NIP-77 negentropy envelopes first (NEG-OPEN, NEG-MSG, NEG-CLOSE) + if IsNegentropyEnvelope(msg) { + negType, ok := IdentifyNegentropyEnvelope(msg) + if ok { + log.T.F("%s processing %s envelope", remote, negType) + switch negType { + case NegOpenLabel: + err = l.HandleNegOpen(msg) + case NegMsgLabel: + err = l.HandleNegMsg(msg) + case NegCloseLabel: + err = l.HandleNegClose(msg) + default: + err = fmt.Errorf("unknown negentropy envelope type: %s", negType) + } + if err != nil { + log.E.F("%s %s processing failed: %v", remote, negType, err) + } + return + } + } + // Attempt to identify the envelope type if t, rem, err = envelopes.Identify(msg); err != nil { log.E.F( diff --git a/app/handle-negentropy.go b/app/handle-negentropy.go new file mode 100644 index 0000000..d210744 --- /dev/null +++ b/app/handle-negentropy.go @@ -0,0 +1,347 @@ +package app + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "fmt" + + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "git.mleku.dev/mleku/nostr/encoders/filter" + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + negentropygrpc "next.orly.dev/pkg/sync/negentropy/grpc" +) + +// NIP-77 Negentropy envelope constants +const ( + NegOpenLabel = "NEG-OPEN" + NegMsgLabel = "NEG-MSG" + NegCloseLabel = "NEG-CLOSE" + NegErrLabel = "NEG-ERR" +) + +// negentropyClient is the gRPC client for negentropy operations +// This should be set via Server.SetNegentropyClient() when using gRPC mode +var negentropyClient *negentropygrpc.Client + +// SetNegentropyClient sets the negentropy gRPC client for NIP-77 WebSocket handling +func SetNegentropyClient(client *negentropygrpc.Client) { + negentropyClient = client +} + +// IsNegentropyEnvelope checks if a message starts with a NEG-* envelope type +func IsNegentropyEnvelope(msg []byte) bool { + // Quick check: must start with '["NEG-' + if len(msg) < 8 { + return false + } + return bytes.HasPrefix(msg, []byte(`["NEG-`)) +} + +// IdentifyNegentropyEnvelope extracts the envelope type and remaining payload +func IdentifyNegentropyEnvelope(msg []byte) (envelopeType string, ok bool) { + // Parse enough to get the envelope type + if !IsNegentropyEnvelope(msg) { + return "", false + } + + // Find the first comma after the opening label + end := bytes.IndexByte(msg[2:], '"') + if end < 0 { + return "", false + } + envelopeType = string(msg[2 : 2+end]) + return envelopeType, true +} + +// HandleNegOpen processes NEG-OPEN messages +// Format: ["NEG-OPEN", subscription_id, filter, initial_message?] +func (l *Listener) HandleNegOpen(msg []byte) error { + log.I.F("HandleNegOpen called from %s", l.connectionID) + if negentropyClient == nil { + log.E.F("negentropy client is nil") + return l.sendNegErr("", "negentropy not enabled") + } + + // Parse the message array + var parts []json.RawMessage + if err := json.Unmarshal(msg, &parts); err != nil { + return l.sendNegErr("", fmt.Sprintf("invalid NEG-OPEN format: %v", err)) + } + + if len(parts) < 3 { + return l.sendNegErr("", "NEG-OPEN requires at least 3 elements") + } + + // Extract subscription ID + var subscriptionID string + if err := json.Unmarshal(parts[1], &subscriptionID); err != nil { + return l.sendNegErr("", fmt.Sprintf("invalid subscription_id: %v", err)) + } + + // Extract filter + var f filter.F + if err := json.Unmarshal(parts[2], &f); err != nil { + return l.sendNegErr(subscriptionID, fmt.Sprintf("invalid filter: %v", err)) + } + + // Extract optional initial message (hex encoded per NIP-77) + var initialMessage []byte + if len(parts) >= 4 { + var msgStr string + if err := json.Unmarshal(parts[3], &msgStr); err == nil && msgStr != "" { + // NIP-77 uses hex encoding + if decoded, err := hex.DecodeString(msgStr); err == nil { + initialMessage = decoded + } else { + log.W.F("NEG-OPEN: invalid hex message: %v", err) + } + } + } + + // Convert filter to proto format + protoFilter := filterToProto(&f) + + // Call gRPC service + ctx := context.Background() + respMsg, errStr, err := negentropyClient.HandleNegOpen( + ctx, + l.connectionID, + subscriptionID, + protoFilter, + initialMessage, + ) + if err != nil { + log.E.F("NEG-OPEN gRPC error: %v", err) + return l.sendNegErr(subscriptionID, "internal error") + } + + if errStr != "" { + return l.sendNegErr(subscriptionID, errStr) + } + + // Send NEG-MSG response with initial message + return l.sendNegMsg(subscriptionID, respMsg) +} + +// HandleNegMsg processes NEG-MSG messages +// Format: ["NEG-MSG", subscription_id, message] +func (l *Listener) HandleNegMsg(msg []byte) error { + if negentropyClient == nil { + return l.sendNegErr("", "negentropy not enabled") + } + + // Parse the message array + var parts []json.RawMessage + if err := json.Unmarshal(msg, &parts); err != nil { + return l.sendNegErr("", fmt.Sprintf("invalid NEG-MSG format: %v", err)) + } + + if len(parts) < 3 { + return l.sendNegErr("", "NEG-MSG requires 3 elements") + } + + // Extract subscription ID + var subscriptionID string + if err := json.Unmarshal(parts[1], &subscriptionID); err != nil { + return l.sendNegErr("", fmt.Sprintf("invalid subscription_id: %v", err)) + } + + // Extract message (hex or base64 encoded) + var msgStr string + if err := json.Unmarshal(parts[2], &msgStr); err != nil { + return l.sendNegErr(subscriptionID, fmt.Sprintf("invalid message: %v", err)) + } + + // Decode message (NIP-77 uses hex encoding) + negentropyMsg, err := hex.DecodeString(msgStr) + if err != nil { + return l.sendNegErr(subscriptionID, fmt.Sprintf("invalid hex message: %v", err)) + } + + // Call gRPC service + ctx := context.Background() + respMsg, haveIDs, needIDs, complete, errStr, err := negentropyClient.HandleNegMsg( + ctx, + l.connectionID, + subscriptionID, + negentropyMsg, + ) + if err != nil { + log.E.F("NEG-MSG gRPC error: %v", err) + return l.sendNegErr(subscriptionID, "internal error") + } + + if errStr != "" { + return l.sendNegErr(subscriptionID, errStr) + } + + // If we have events to send (have_ids), fetch and send them + if len(haveIDs) > 0 { + if err := l.sendEventsForIDs(subscriptionID, haveIDs); err != nil { + log.E.F("failed to send events for NEG-MSG: %v", err) + } + } + + // Log need_ids (events client should send us) + if len(needIDs) > 0 { + log.D.F("NEG-MSG: client needs to send %d events", len(needIDs)) + } + + // Send NEG-MSG response + if err := l.sendNegMsg(subscriptionID, respMsg); err != nil { + return err + } + + // If reconciliation is complete, log it + if complete { + log.D.F("NEG-MSG: reconciliation complete for %s", subscriptionID) + } + + return nil +} + +// HandleNegClose processes NEG-CLOSE messages +// Format: ["NEG-CLOSE", subscription_id] +func (l *Listener) HandleNegClose(msg []byte) error { + if negentropyClient == nil { + return nil // Silently ignore if not enabled + } + + // Parse the message array + var parts []json.RawMessage + if err := json.Unmarshal(msg, &parts); err != nil { + return nil // Silently ignore malformed close + } + + if len(parts) < 2 { + return nil + } + + // Extract subscription ID + var subscriptionID string + if err := json.Unmarshal(parts[1], &subscriptionID); err != nil { + return nil + } + + // Call gRPC service to close the session + ctx := context.Background() + if err := negentropyClient.HandleNegClose(ctx, l.connectionID, subscriptionID); err != nil { + log.E.F("NEG-CLOSE gRPC error: %v", err) + } + + return nil +} + +// sendNegMsg sends a NEG-MSG response to the client +func (l *Listener) sendNegMsg(subscriptionID string, message []byte) error { + // Encode message as hex (per NIP-77) + encoded := "" + if len(message) > 0 { + encoded = hex.EncodeToString(message) + } + + // Format: ["NEG-MSG", subscription_id, message] + resp, err := json.Marshal([]any{NegMsgLabel, subscriptionID, encoded}) + if err != nil { + return err + } + + _, err = l.Write(resp) + return err +} + +// sendNegErr sends a NEG-ERR response to the client +func (l *Listener) sendNegErr(subscriptionID, reason string) error { + // Format: ["NEG-ERR", subscription_id, reason] + resp, err := json.Marshal([]any{NegErrLabel, subscriptionID, reason}) + if err != nil { + return err + } + + _, err = l.Write(resp) + return err +} + +// sendEventsForIDs fetches and sends events for the given IDs +func (l *Listener) sendEventsForIDs(subscriptionID string, ids [][]byte) error { + // TODO: Implement event fetching and sending via EVENT envelopes + // For each ID, query the database and send the event + // This would use the existing event broadcasting mechanism + log.D.F("NEG: would send %d events for subscription %s", len(ids), subscriptionID) + return nil +} + +// filterToProto converts a nostr filter to proto format +func filterToProto(f *filter.F) *commonv1.Filter { + if f == nil { + return nil + } + + pf := &commonv1.Filter{} + + // Convert Ids + if f.Ids != nil { + for _, id := range f.Ids.T { + pf.Ids = append(pf.Ids, id) + } + } + + // Convert Authors + if f.Authors != nil { + for _, author := range f.Authors.T { + pf.Authors = append(pf.Authors, author) + } + } + + // Convert Kinds - kind.S has ToUint16() method + if f.Kinds != nil && f.Kinds.Len() > 0 { + for _, k := range f.Kinds.ToUint16() { + pf.Kinds = append(pf.Kinds, uint32(k)) + } + } + + // Convert Since/Until - timestamp.T has .V field (int64) + if f.Since != nil && f.Since.V != 0 { + since := f.Since.V + pf.Since = &since + } + if f.Until != nil && f.Until.V != 0 { + until := f.Until.V + pf.Until = &until + } + + // Convert Limit + if f.Limit != nil { + limit := uint32(*f.Limit) + pf.Limit = &limit + } + + // Note: Tag filters (e, p, etc.) would need more complex conversion + // This is a simplified implementation + + return pf +} + +// CloseAllNegentropySessions closes all negentropy sessions for a connection +// Called when a WebSocket connection is closed +func (l *Listener) CloseAllNegentropySessions() { + if negentropyClient == nil { + return + } + + ctx := context.Background() + sessions, err := negentropyClient.ListSessions(ctx) + if chk.E(err) { + return + } + + for _, sess := range sessions { + if sess.ConnectionID == l.connectionID { + negentropyClient.CloseSession(ctx, l.connectionID, sess.SubscriptionID) + } + } +} diff --git a/app/handle-relayinfo.go b/app/handle-relayinfo.go index 7739fc5..3e6ac3a 100644 --- a/app/handle-relayinfo.go +++ b/app/handle-relayinfo.go @@ -73,6 +73,10 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) { if s.Config.NIP43Enabled { nips = append(nips, relayinfo.RelayAccessMetadata) } + // Add NIP-77 (negentropy) if enabled + if s.Config.NegentropyEnabled { + nips = append(nips, relayinfo.NIP{Number: 77, Description: "Negentropy-based sync"}) + } supportedNIPs := relayinfo.GetList(nips...) if s.Config.ACLMode != "none" { nipsACL := []relayinfo.NIP{ @@ -96,6 +100,10 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) { if s.Config.NIP43Enabled { nipsACL = append(nipsACL, relayinfo.RelayAccessMetadata) } + // Add NIP-77 (negentropy) if enabled + if s.Config.NegentropyEnabled { + nipsACL = append(nipsACL, relayinfo.NIP{Number: 77, Description: "Negentropy-based sync"}) + } supportedNIPs = relayinfo.GetList(nipsACL...) } sort.Sort(supportedNIPs) diff --git a/cmd/orly-launcher/config.go b/cmd/orly-launcher/config.go index 9e0e843..46ac63e 100644 --- a/cmd/orly-launcher/config.go +++ b/cmd/orly-launcher/config.go @@ -50,6 +50,38 @@ type Config struct { // LogLevel is the log level to use for all processes LogLevel string + + // Sync service configuration + // DistributedSyncEnabled enables the distributed sync service + DistributedSyncEnabled bool + // DistributedSyncBinary is the path to the distributed sync binary + DistributedSyncBinary string + // DistributedSyncListen is the gRPC listen address for distributed sync + DistributedSyncListen string + + // ClusterSyncEnabled enables the cluster sync service + ClusterSyncEnabled bool + // ClusterSyncBinary is the path to the cluster sync binary + ClusterSyncBinary string + // ClusterSyncListen is the gRPC listen address for cluster sync + ClusterSyncListen string + + // RelayGroupEnabled enables the relay group service + RelayGroupEnabled bool + // RelayGroupBinary is the path to the relay group binary + RelayGroupBinary string + // RelayGroupListen is the gRPC listen address for relay group + RelayGroupListen string + + // NegentropyEnabled enables the negentropy sync service + NegentropyEnabled bool + // NegentropyBinary is the path to the negentropy sync binary + NegentropyBinary string + // NegentropyListen is the gRPC listen address for negentropy + NegentropyListen string + + // SyncReadyTimeout is how long to wait for sync services to be ready + SyncReadyTimeout time.Duration } func loadConfig() (*Config, error) { @@ -75,6 +107,25 @@ func loadConfig() (*Config, error) { StopTimeout: parseDuration("ORLY_LAUNCHER_STOP_TIMEOUT", 30*time.Second), // Increased for DB flush DataDir: getEnvOrDefault("ORLY_DATA_DIR", filepath.Join(xdg.DataHome, "ORLY")), LogLevel: getEnvOrDefault("ORLY_LOG_LEVEL", "info"), + + // Sync services configuration + DistributedSyncEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED", "false") == "true", + DistributedSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY", "orly-sync-distributed"), + DistributedSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN", "127.0.0.1:50061"), + + ClusterSyncEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED", "false") == "true", + ClusterSyncBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_BINARY", "orly-sync-cluster"), + ClusterSyncListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN", "127.0.0.1:50062"), + + RelayGroupEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED", "false") == "true", + RelayGroupBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY", "orly-sync-relaygroup"), + RelayGroupListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN", "127.0.0.1:50063"), + + NegentropyEnabled: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED", "false") == "true", + NegentropyBinary: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY", "orly-sync-negentropy"), + NegentropyListen: getEnvOrDefault("ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN", "127.0.0.1:50064"), + + SyncReadyTimeout: parseDuration("ORLY_LAUNCHER_SYNC_READY_TIMEOUT", 30*time.Second), } return cfg, nil diff --git a/cmd/orly-launcher/supervisor.go b/cmd/orly-launcher/supervisor.go index 80744dc..f768608 100644 --- a/cmd/orly-launcher/supervisor.go +++ b/cmd/orly-launcher/supervisor.go @@ -14,7 +14,7 @@ import ( "lol.mleku.dev/log" ) -// Supervisor manages the database, ACL, and relay processes. +// Supervisor manages the database, ACL, sync, and relay processes. type Supervisor struct { cfg *Config ctx context.Context @@ -24,6 +24,12 @@ type Supervisor struct { aclProc *Process relayProc *Process + // Sync service processes + distributedSyncProc *Process + clusterSyncProc *Process + relayGroupProc *Process + negentropyProc *Process + wg sync.WaitGroup mu sync.Mutex closed bool @@ -47,7 +53,7 @@ func NewSupervisor(ctx context.Context, cancel context.CancelFunc, cfg *Config) } } -// Start starts the database, optional ACL server, and relay processes. +// Start starts the database, optional ACL server, sync services, and relay processes. func (s *Supervisor) Start() error { // 1. Start database server if err := s.startDB(); err != nil { @@ -79,8 +85,19 @@ func (s *Supervisor) Start() error { log.I.F("ACL server is ready") } - // 4. Start relay with gRPC backend(s) + // 4. Start sync services in parallel (they all depend on DB) + if err := s.startSyncServices(); err != nil { + s.stopSyncServices() + if s.cfg.ACLEnabled { + s.stopACL() + } + s.stopDB() + return fmt.Errorf("failed to start sync services: %w", err) + } + + // 5. Start relay with gRPC backend(s) if err := s.startRelay(); err != nil { + s.stopSyncServices() if s.cfg.ACLEnabled { s.stopACL() } @@ -88,16 +105,41 @@ func (s *Supervisor) Start() error { return fmt.Errorf("failed to start relay: %w", err) } - // 5. Start monitoring goroutines - monitorCount := 2 + // 6. Start monitoring goroutines + monitorCount := 2 // db + relay if s.cfg.ACLEnabled { - monitorCount = 3 + monitorCount++ + } + if s.cfg.DistributedSyncEnabled { + monitorCount++ + } + if s.cfg.ClusterSyncEnabled { + monitorCount++ + } + if s.cfg.RelayGroupEnabled { + monitorCount++ + } + if s.cfg.NegentropyEnabled { + monitorCount++ } + s.wg.Add(monitorCount) go s.monitorProcess(s.dbProc, "db", s.startDB) if s.cfg.ACLEnabled { go s.monitorProcess(s.aclProc, "acl", s.startACL) } + if s.cfg.DistributedSyncEnabled { + go s.monitorProcess(s.distributedSyncProc, "distributed-sync", s.startDistributedSync) + } + if s.cfg.ClusterSyncEnabled { + go s.monitorProcess(s.clusterSyncProc, "cluster-sync", s.startClusterSync) + } + if s.cfg.RelayGroupEnabled { + go s.monitorProcess(s.relayGroupProc, "relaygroup", s.startRelayGroup) + } + if s.cfg.NegentropyEnabled { + go s.monitorProcess(s.negentropyProc, "negentropy", s.startNegentropy) + } go s.monitorProcess(s.relayProc, "relay", s.startRelay) return nil @@ -113,10 +155,14 @@ func (s *Supervisor) Stop() error { s.closed = true s.mu.Unlock() - // Stop relay first (it depends on ACL and DB) + // Stop relay first (it depends on sync services, ACL, and DB) log.I.F("stopping relay...") s.stopProcess(s.relayProc, 5*time.Second) + // Stop sync services in parallel (they depend on DB) + log.I.F("stopping sync services...") + s.stopSyncServices() + // Stop ACL if enabled (it depends on DB) if s.cfg.ACLEnabled && s.aclProc != nil { log.I.F("stopping ACL server...") @@ -257,6 +303,22 @@ func (s *Supervisor) startRelay() error { env = append(env, "ORLY_ACL_MODE=none") } + // Configure sync service connections + if s.cfg.DistributedSyncEnabled { + env = append(env, "ORLY_SYNC_TYPE=grpc") + env = append(env, fmt.Sprintf("ORLY_GRPC_SYNC_DISTRIBUTED=%s", s.cfg.DistributedSyncListen)) + } + if s.cfg.ClusterSyncEnabled { + env = append(env, fmt.Sprintf("ORLY_GRPC_SYNC_CLUSTER=%s", s.cfg.ClusterSyncListen)) + } + if s.cfg.RelayGroupEnabled { + env = append(env, fmt.Sprintf("ORLY_GRPC_SYNC_RELAYGROUP=%s", s.cfg.RelayGroupListen)) + } + if s.cfg.NegentropyEnabled { + env = append(env, "ORLY_NEGENTROPY_ENABLED=true") + env = append(env, fmt.Sprintf("ORLY_GRPC_SYNC_NEGENTROPY=%s", s.cfg.NegentropyListen)) + } + cmd := exec.CommandContext(s.ctx, s.cfg.RelayBinary) cmd.Env = env cmd.Stdout = os.Stdout @@ -416,6 +478,14 @@ func (s *Supervisor) monitorProcess(p *Process, procType string, restart func() p = s.dbProc case "acl": p = s.aclProc + case "distributed-sync": + p = s.distributedSyncProc + case "cluster-sync": + p = s.clusterSyncProc + case "relaygroup": + p = s.relayGroupProc + case "negentropy": + p = s.negentropyProc default: p = s.relayProc } @@ -423,3 +493,302 @@ func (s *Supervisor) monitorProcess(p *Process, procType string, restart func() } } } + +// startSyncServices starts all enabled sync services in parallel. +func (s *Supervisor) startSyncServices() error { + var wg sync.WaitGroup + var mu sync.Mutex + var errs []error + + // Start each enabled service in a goroutine + if s.cfg.DistributedSyncEnabled { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.startDistributedSync(); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("distributed sync: %w", err)) + mu.Unlock() + return + } + if err := s.waitForServiceReady(s.cfg.DistributedSyncListen, s.cfg.SyncReadyTimeout); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("distributed sync not ready: %w", err)) + mu.Unlock() + return + } + log.I.F("distributed sync service is ready") + }() + } + + if s.cfg.ClusterSyncEnabled { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.startClusterSync(); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("cluster sync: %w", err)) + mu.Unlock() + return + } + if err := s.waitForServiceReady(s.cfg.ClusterSyncListen, s.cfg.SyncReadyTimeout); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("cluster sync not ready: %w", err)) + mu.Unlock() + return + } + log.I.F("cluster sync service is ready") + }() + } + + if s.cfg.RelayGroupEnabled { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.startRelayGroup(); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("relaygroup: %w", err)) + mu.Unlock() + return + } + if err := s.waitForServiceReady(s.cfg.RelayGroupListen, s.cfg.SyncReadyTimeout); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("relaygroup not ready: %w", err)) + mu.Unlock() + return + } + log.I.F("relaygroup service is ready") + }() + } + + if s.cfg.NegentropyEnabled { + wg.Add(1) + go func() { + defer wg.Done() + if err := s.startNegentropy(); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("negentropy: %w", err)) + mu.Unlock() + return + } + if err := s.waitForServiceReady(s.cfg.NegentropyListen, s.cfg.SyncReadyTimeout); err != nil { + mu.Lock() + errs = append(errs, fmt.Errorf("negentropy not ready: %w", err)) + mu.Unlock() + return + } + log.I.F("negentropy service is ready") + }() + } + + wg.Wait() + + if len(errs) > 0 { + return errs[0] + } + return nil +} + +// stopSyncServices stops all sync services. +func (s *Supervisor) stopSyncServices() { + // Stop all in parallel using a WaitGroup + var wg sync.WaitGroup + + if s.distributedSyncProc != nil { + wg.Add(1) + go func() { + defer wg.Done() + s.stopProcess(s.distributedSyncProc, 5*time.Second) + }() + } + + if s.clusterSyncProc != nil { + wg.Add(1) + go func() { + defer wg.Done() + s.stopProcess(s.clusterSyncProc, 5*time.Second) + }() + } + + if s.relayGroupProc != nil { + wg.Add(1) + go func() { + defer wg.Done() + s.stopProcess(s.relayGroupProc, 5*time.Second) + }() + } + + if s.negentropyProc != nil { + wg.Add(1) + go func() { + defer wg.Done() + s.stopProcess(s.negentropyProc, 5*time.Second) + }() + } + + wg.Wait() +} + +// waitForServiceReady waits for a gRPC service to be accepting connections. +func (s *Supervisor) waitForServiceReady(address string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + ticker := time.NewTicker(250 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-s.ctx.Done(): + return s.ctx.Err() + case <-ticker.C: + if time.Now().After(deadline) { + return fmt.Errorf("timeout waiting for service at %s", address) + } + + conn, err := net.DialTimeout("tcp", address, time.Second) + if err == nil { + conn.Close() + return nil + } + } + } +} + +func (s *Supervisor) startDistributedSync() error { + s.mu.Lock() + defer s.mu.Unlock() + + env := os.Environ() + env = append(env, fmt.Sprintf("ORLY_SYNC_DISTRIBUTED_LISTEN=%s", s.cfg.DistributedSyncListen)) + env = append(env, "ORLY_SYNC_DISTRIBUTED_DB_TYPE=grpc") + env = append(env, fmt.Sprintf("ORLY_SYNC_DISTRIBUTED_DB_SERVER=%s", s.cfg.DBListen)) + env = append(env, fmt.Sprintf("ORLY_SYNC_DISTRIBUTED_LOG_LEVEL=%s", s.cfg.LogLevel)) + + cmd := exec.CommandContext(s.ctx, s.cfg.DistributedSyncBinary) + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); chk.E(err) { + return err + } + + exited := make(chan struct{}) + s.distributedSyncProc = &Process{ + name: "orly-sync-distributed", + cmd: cmd, + exited: exited, + } + + go func() { + cmd.Wait() + close(exited) + }() + + log.I.F("started distributed sync service (pid %d)", cmd.Process.Pid) + return nil +} + +func (s *Supervisor) startClusterSync() error { + s.mu.Lock() + defer s.mu.Unlock() + + env := os.Environ() + env = append(env, fmt.Sprintf("ORLY_SYNC_CLUSTER_LISTEN=%s", s.cfg.ClusterSyncListen)) + env = append(env, "ORLY_SYNC_CLUSTER_DB_TYPE=grpc") + env = append(env, fmt.Sprintf("ORLY_SYNC_CLUSTER_DB_SERVER=%s", s.cfg.DBListen)) + env = append(env, fmt.Sprintf("ORLY_SYNC_CLUSTER_LOG_LEVEL=%s", s.cfg.LogLevel)) + + cmd := exec.CommandContext(s.ctx, s.cfg.ClusterSyncBinary) + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); chk.E(err) { + return err + } + + exited := make(chan struct{}) + s.clusterSyncProc = &Process{ + name: "orly-sync-cluster", + cmd: cmd, + exited: exited, + } + + go func() { + cmd.Wait() + close(exited) + }() + + log.I.F("started cluster sync service (pid %d)", cmd.Process.Pid) + return nil +} + +func (s *Supervisor) startRelayGroup() error { + s.mu.Lock() + defer s.mu.Unlock() + + env := os.Environ() + env = append(env, fmt.Sprintf("ORLY_SYNC_RELAYGROUP_LISTEN=%s", s.cfg.RelayGroupListen)) + env = append(env, "ORLY_SYNC_RELAYGROUP_DB_TYPE=grpc") + env = append(env, fmt.Sprintf("ORLY_SYNC_RELAYGROUP_DB_SERVER=%s", s.cfg.DBListen)) + env = append(env, fmt.Sprintf("ORLY_SYNC_RELAYGROUP_LOG_LEVEL=%s", s.cfg.LogLevel)) + + cmd := exec.CommandContext(s.ctx, s.cfg.RelayGroupBinary) + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); chk.E(err) { + return err + } + + exited := make(chan struct{}) + s.relayGroupProc = &Process{ + name: "orly-sync-relaygroup", + cmd: cmd, + exited: exited, + } + + go func() { + cmd.Wait() + close(exited) + }() + + log.I.F("started relaygroup service (pid %d)", cmd.Process.Pid) + return nil +} + +func (s *Supervisor) startNegentropy() error { + s.mu.Lock() + defer s.mu.Unlock() + + env := os.Environ() + env = append(env, fmt.Sprintf("ORLY_SYNC_NEGENTROPY_LISTEN=%s", s.cfg.NegentropyListen)) + env = append(env, "ORLY_SYNC_NEGENTROPY_DB_TYPE=grpc") + env = append(env, fmt.Sprintf("ORLY_SYNC_NEGENTROPY_DB_SERVER=%s", s.cfg.DBListen)) + env = append(env, fmt.Sprintf("ORLY_SYNC_NEGENTROPY_LOG_LEVEL=%s", s.cfg.LogLevel)) + + cmd := exec.CommandContext(s.ctx, s.cfg.NegentropyBinary) + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Start(); chk.E(err) { + return err + } + + exited := make(chan struct{}) + s.negentropyProc = &Process{ + name: "orly-sync-negentropy", + cmd: cmd, + exited: exited, + } + + go func() { + cmd.Wait() + close(exited) + }() + + log.I.F("started negentropy service (pid %d)", cmd.Process.Pid) + return nil +} diff --git a/cmd/orly-sync-cluster/config.go b/cmd/orly-sync-cluster/config.go new file mode 100644 index 0000000..59ac7fa --- /dev/null +++ b/cmd/orly-sync-cluster/config.go @@ -0,0 +1,55 @@ +package main + +import ( + "os" + "strings" + "time" + + "go-simpler.org/env" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" +) + +// Config holds the cluster sync service configuration. +type Config struct { + // Listen is the gRPC server listen address + Listen string `env:"ORLY_SYNC_CLUSTER_LISTEN" default:"127.0.0.1:50062" usage:"gRPC server listen address"` + + // LogLevel is the logging level + LogLevel string `env:"ORLY_SYNC_CLUSTER_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"` + + // Database configuration + DBType string `env:"ORLY_SYNC_CLUSTER_DB_TYPE" default:"grpc" usage:"database type: grpc or badger"` + GRPCDBServer string `env:"ORLY_SYNC_CLUSTER_DB_SERVER" default:"127.0.0.1:50051" usage:"gRPC database server address"` + DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (for badger mode)"` + + // Cluster configuration + AdminNpubsRaw string `env:"ORLY_SYNC_CLUSTER_ADMINS" usage:"comma-separated admin npubs"` + PropagatePrivilegedEvents bool `env:"ORLY_SYNC_CLUSTER_PROPAGATE_PRIVILEGED" default:"true" usage:"propagate privileged events (DMs, gift wraps)"` + PollInterval time.Duration `env:"ORLY_SYNC_CLUSTER_POLL_INTERVAL" default:"5s" usage:"poll interval"` + + // NIP-11 cache configuration + NIP11CacheTTL time.Duration `env:"ORLY_SYNC_CLUSTER_NIP11_TTL" default:"30m" usage:"NIP-11 cache TTL"` + + // Parsed admin npubs + AdminNpubs []string +} + +// loadConfig loads configuration from environment variables. +func loadConfig() *Config { + cfg := &Config{} + if err := env.Load(cfg, nil); chk.E(err) { + log.E.F("failed to load config: %v", err) + os.Exit(1) + } + + // Parse admin npubs from comma-separated string + if cfg.AdminNpubsRaw != "" { + cfg.AdminNpubs = strings.Split(cfg.AdminNpubsRaw, ",") + for i, p := range cfg.AdminNpubs { + cfg.AdminNpubs[i] = strings.TrimSpace(p) + } + } + + return cfg +} diff --git a/cmd/orly-sync-cluster/main.go b/cmd/orly-sync-cluster/main.go new file mode 100644 index 0000000..6537dfe --- /dev/null +++ b/cmd/orly-sync-cluster/main.go @@ -0,0 +1,130 @@ +// orly-sync-cluster is a standalone gRPC cluster sync service for ORLY. +// It provides cluster replication with persistent state between relay instances. +package main + +import ( + "context" + "net" + "os" + "os/signal" + "syscall" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "lol.mleku.dev" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "next.orly.dev/pkg/database" + databasegrpc "next.orly.dev/pkg/database/grpc" + "next.orly.dev/pkg/sync/cluster" + clusterv1 "next.orly.dev/pkg/proto/orlysync/cluster/v1" +) + +func main() { + cfg := loadConfig() + + // Set log level + lol.SetLogLevel(cfg.LogLevel) + log.I.F("orly-sync-cluster starting with log level: %s", cfg.LogLevel) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize database connection + var db database.Database + var dbCloser func() + + if cfg.DBType == "grpc" { + log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer) + dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{ + ServerAddress: cfg.GRPCDBServer, + ConnectTimeout: 30 * time.Second, + }) + if chk.E(err) { + log.E.F("failed to connect to database server: %v", err) + os.Exit(1) + } + db = dbClient + dbCloser = func() { dbClient.Close() } + } else { + log.E.F("badger mode not yet implemented for sync-cluster") + os.Exit(1) + } + + // Wait for database to be ready + log.I.F("waiting for database to be ready...") + <-db.Ready() + log.I.F("database ready") + + // Create cluster manager configuration + clusterCfg := &cluster.Config{ + AdminNpubs: cfg.AdminNpubs, + PropagatePrivilegedEvents: cfg.PropagatePrivilegedEvents, + PollInterval: cfg.PollInterval, + NIP11CacheTTL: cfg.NIP11CacheTTL, + } + + log.I.F("initializing cluster sync manager with %d admins", len(cfg.AdminNpubs)) + + // Create gRPC server + grpcServer := grpc.NewServer( + grpc.MaxRecvMsgSize(16<<20), // 16MB + grpc.MaxSendMsgSize(16<<20), // 16MB + ) + + // Register cluster sync service + // TODO: Create and register service once server implementation is done + // service := cluster.NewService(clusterMgr, cfg) + // clusterv1.RegisterClusterSyncServiceServer(grpcServer, service) + _ = clusterCfg + _ = db + _ = clusterv1.ClusterSyncService_ServiceDesc + + // Register reflection for debugging with grpcurl + reflection.Register(grpcServer) + + // Start listening + lis, err := net.Listen("tcp", cfg.Listen) + if chk.E(err) { + log.E.F("failed to listen on %s: %v", cfg.Listen, err) + os.Exit(1) + } + log.I.F("gRPC server listening on %s", cfg.Listen) + + // Handle graceful shutdown + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) + sig := <-sigs + log.I.F("received signal %v, shutting down...", sig) + + cancel() + + stopped := make(chan struct{}) + go func() { + grpcServer.GracefulStop() + close(stopped) + }() + + select { + case <-stopped: + log.I.F("gRPC server stopped gracefully") + case <-time.After(5 * time.Second): + log.W.F("gRPC graceful stop timed out, forcing stop") + grpcServer.Stop() + } + + if dbCloser != nil { + log.I.F("closing database connection...") + dbCloser() + } + log.I.F("shutdown complete") + }() + + // Serve gRPC + if err := grpcServer.Serve(lis); err != nil { + log.E.F("gRPC server error: %v", err) + } +} diff --git a/cmd/orly-sync-distributed/config.go b/cmd/orly-sync-distributed/config.go new file mode 100644 index 0000000..6b7b036 --- /dev/null +++ b/cmd/orly-sync-distributed/config.go @@ -0,0 +1,56 @@ +package main + +import ( + "os" + "strings" + "time" + + "go-simpler.org/env" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" +) + +// Config holds the distributed sync service configuration. +type Config struct { + // Listen is the gRPC server listen address + Listen string `env:"ORLY_SYNC_DISTRIBUTED_LISTEN" default:"127.0.0.1:50061" usage:"gRPC server listen address"` + + // LogLevel is the logging level + LogLevel string `env:"ORLY_SYNC_DISTRIBUTED_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"` + + // Database configuration + DBType string `env:"ORLY_SYNC_DISTRIBUTED_DB_TYPE" default:"grpc" usage:"database type: grpc or badger"` + GRPCDBServer string `env:"ORLY_SYNC_DISTRIBUTED_DB_SERVER" default:"127.0.0.1:50051" usage:"gRPC database server address"` + DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (for badger mode)"` + + // Sync configuration + NodeID string `env:"ORLY_SYNC_DISTRIBUTED_NODE_ID" usage:"node identity (relay pubkey)"` + RelayURL string `env:"ORLY_SYNC_DISTRIBUTED_RELAY_URL" usage:"this relay's URL"` + PeersRaw string `env:"ORLY_SYNC_DISTRIBUTED_PEERS" usage:"comma-separated peer relay URLs"` + SyncInterval time.Duration `env:"ORLY_SYNC_DISTRIBUTED_INTERVAL" default:"5s" usage:"sync poll interval"` + + // NIP-11 cache configuration + NIP11CacheTTL time.Duration `env:"ORLY_SYNC_DISTRIBUTED_NIP11_TTL" default:"30m" usage:"NIP-11 cache TTL"` + + // Parsed peers + Peers []string +} + +// loadConfig loads configuration from environment variables. +func loadConfig() *Config { + cfg := &Config{} + if err := env.Load(cfg, nil); chk.E(err) { + log.E.F("failed to load config: %v", err) + os.Exit(1) + } + + // Parse peers from comma-separated string + if cfg.PeersRaw != "" { + cfg.Peers = strings.Split(cfg.PeersRaw, ",") + for i, p := range cfg.Peers { + cfg.Peers[i] = strings.TrimSpace(p) + } + } + + return cfg +} diff --git a/cmd/orly-sync-distributed/main.go b/cmd/orly-sync-distributed/main.go new file mode 100644 index 0000000..60be8c9 --- /dev/null +++ b/cmd/orly-sync-distributed/main.go @@ -0,0 +1,144 @@ +// orly-sync-distributed is a standalone gRPC distributed sync service for ORLY. +// It provides serial-based peer-to-peer synchronization between relay instances. +package main + +import ( + "context" + "net" + "os" + "os/signal" + "syscall" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "lol.mleku.dev" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "next.orly.dev/pkg/database" + databasegrpc "next.orly.dev/pkg/database/grpc" + "next.orly.dev/pkg/sync/distributed" + distributedv1 "next.orly.dev/pkg/proto/orlysync/distributed/v1" +) + +func main() { + cfg := loadConfig() + + // Set log level + lol.SetLogLevel(cfg.LogLevel) + log.I.F("orly-sync-distributed starting with log level: %s", cfg.LogLevel) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize database connection + var db database.Database + var dbCloser func() + + if cfg.DBType == "grpc" { + log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer) + dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{ + ServerAddress: cfg.GRPCDBServer, + ConnectTimeout: 30 * time.Second, + }) + if chk.E(err) { + log.E.F("failed to connect to database server: %v", err) + os.Exit(1) + } + db = dbClient + dbCloser = func() { dbClient.Close() } + } else { + log.E.F("badger mode not yet implemented for sync-distributed") + os.Exit(1) + } + + // Wait for database to be ready + log.I.F("waiting for database to be ready...") + <-db.Ready() + log.I.F("database ready") + + // Create sync manager configuration + syncCfg := &distributed.Config{ + NodeID: cfg.NodeID, + RelayURL: cfg.RelayURL, + Peers: cfg.Peers, + SyncInterval: cfg.SyncInterval, + NIP11CacheTTL: cfg.NIP11CacheTTL, + } + + // The distributed.Manager currently requires *database.D, not the interface. + // For gRPC mode, we need the manager to be updated to use the database.Database interface. + // For now, log that this mode requires refactoring. + if cfg.DBType == "grpc" { + log.W.F("distributed sync manager needs refactoring to use database.Database interface") + log.W.F("currently only stub service is available in gRPC mode") + } + + log.I.F("initializing distributed sync manager with %d peers", len(cfg.Peers)) + + // Create gRPC server + grpcServer := grpc.NewServer( + grpc.MaxRecvMsgSize(16<<20), // 16MB + grpc.MaxSendMsgSize(16<<20), // 16MB + ) + + // Register distributed sync service + // Note: The manager needs refactoring to use the interface before this works fully + // For now, register a stub service that will return appropriate responses + _ = syncCfg + _ = distributedv1.DistributedSyncService_ServiceDesc + // TODO: Once distributed.Manager uses database.Database interface: + // syncMgr := distributed.NewManager(ctx, db, syncCfg, nil) + // service := distributedserver.NewService(db, syncMgr) + // distributedv1.RegisterDistributedSyncServiceServer(grpcServer, service) + + // Register reflection for debugging with grpcurl + reflection.Register(grpcServer) + + // Start listening + lis, err := net.Listen("tcp", cfg.Listen) + if chk.E(err) { + log.E.F("failed to listen on %s: %v", cfg.Listen, err) + os.Exit(1) + } + log.I.F("gRPC server listening on %s", cfg.Listen) + + // Handle graceful shutdown + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) + sig := <-sigs + log.I.F("received signal %v, shutting down...", sig) + + // Cancel context to stop all operations + cancel() + + // Gracefully stop gRPC server with timeout + stopped := make(chan struct{}) + go func() { + grpcServer.GracefulStop() + close(stopped) + }() + + select { + case <-stopped: + log.I.F("gRPC server stopped gracefully") + case <-time.After(5 * time.Second): + log.W.F("gRPC graceful stop timed out, forcing stop") + grpcServer.Stop() + } + + // Close database connection + if dbCloser != nil { + log.I.F("closing database connection...") + dbCloser() + } + log.I.F("shutdown complete") + }() + + // Serve gRPC + if err := grpcServer.Serve(lis); err != nil { + log.E.F("gRPC server error: %v", err) + } +} diff --git a/cmd/orly-sync-negentropy/config.go b/cmd/orly-sync-negentropy/config.go new file mode 100644 index 0000000..6878081 --- /dev/null +++ b/cmd/orly-sync-negentropy/config.go @@ -0,0 +1,56 @@ +package main + +import ( + "os" + "strings" + "time" + + "go-simpler.org/env" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" +) + +// Config holds the negentropy sync service configuration. +type Config struct { + // Listen is the gRPC server listen address + Listen string `env:"ORLY_SYNC_NEGENTROPY_LISTEN" default:"127.0.0.1:50064" usage:"gRPC server listen address"` + + // LogLevel is the logging level + LogLevel string `env:"ORLY_SYNC_NEGENTROPY_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"` + + // Database configuration + DBType string `env:"ORLY_SYNC_NEGENTROPY_DB_TYPE" default:"grpc" usage:"database type: grpc or badger"` + GRPCDBServer string `env:"ORLY_SYNC_NEGENTROPY_DB_SERVER" default:"127.0.0.1:50051" usage:"gRPC database server address"` + DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (for badger mode)"` + + // Negentropy configuration + PeersRaw string `env:"ORLY_SYNC_NEGENTROPY_PEERS" usage:"comma-separated peer relay WebSocket URLs"` + SyncInterval time.Duration `env:"ORLY_SYNC_NEGENTROPY_INTERVAL" default:"60s" usage:"background sync interval"` + FrameSize int `env:"ORLY_SYNC_NEGENTROPY_FRAME_SIZE" default:"4096" usage:"negentropy frame size"` + IDSize int `env:"ORLY_SYNC_NEGENTROPY_ID_SIZE" default:"16" usage:"negentropy ID truncation size"` + + // Client session configuration + ClientSessionTimeout time.Duration `env:"ORLY_SYNC_NEGENTROPY_SESSION_TIMEOUT" default:"5m" usage:"client session timeout"` + + // Parsed peers + Peers []string +} + +// loadConfig loads configuration from environment variables. +func loadConfig() *Config { + cfg := &Config{} + if err := env.Load(cfg, nil); chk.E(err) { + log.E.F("failed to load config: %v", err) + os.Exit(1) + } + + // Parse peers from comma-separated string + if cfg.PeersRaw != "" { + cfg.Peers = strings.Split(cfg.PeersRaw, ",") + for i, p := range cfg.Peers { + cfg.Peers[i] = strings.TrimSpace(p) + } + } + + return cfg +} diff --git a/cmd/orly-sync-negentropy/main.go b/cmd/orly-sync-negentropy/main.go new file mode 100644 index 0000000..8a4e411 --- /dev/null +++ b/cmd/orly-sync-negentropy/main.go @@ -0,0 +1,140 @@ +// orly-sync-negentropy is a standalone gRPC negentropy sync service for ORLY. +// It provides NIP-77 negentropy-based set reconciliation for both +// relay-to-relay sync and client-facing WebSocket operations. +package main + +import ( + "context" + "net" + "os" + "os/signal" + "syscall" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "lol.mleku.dev" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "next.orly.dev/pkg/database" + databasegrpc "next.orly.dev/pkg/database/grpc" + negentropyv1 "next.orly.dev/pkg/proto/orlysync/negentropy/v1" + "next.orly.dev/pkg/sync/negentropy" + negentropyserver "next.orly.dev/pkg/sync/negentropy/server" +) + +func main() { + cfg := loadConfig() + + // Set log level + lol.SetLogLevel(cfg.LogLevel) + log.I.F("orly-sync-negentropy starting with log level: %s", cfg.LogLevel) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize database connection + var db database.Database + var dbCloser func() + + if cfg.DBType == "grpc" { + log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer) + dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{ + ServerAddress: cfg.GRPCDBServer, + ConnectTimeout: 30 * time.Second, + }) + if chk.E(err) { + log.E.F("failed to connect to database server: %v", err) + os.Exit(1) + } + db = dbClient + dbCloser = func() { dbClient.Close() } + } else { + log.E.F("badger mode not yet implemented for sync-negentropy") + os.Exit(1) + } + + // Wait for database to be ready + log.I.F("waiting for database to be ready...") + <-db.Ready() + log.I.F("database ready") + + log.I.F("initializing negentropy sync manager with %d peers", len(cfg.Peers)) + + // Create negentropy manager + mgrConfig := &negentropy.Config{ + Peers: cfg.Peers, + SyncInterval: cfg.SyncInterval, + FrameSize: cfg.FrameSize, + IDSize: cfg.IDSize, + ClientSessionTimeout: cfg.ClientSessionTimeout, + } + negentropyMgr := negentropy.NewManager(db, mgrConfig) + + // Start background sync loop if we have peers + if len(cfg.Peers) > 0 { + log.I.F("starting background sync with %d peers", len(cfg.Peers)) + negentropyMgr.Start() + } + + // Create gRPC server + grpcServer := grpc.NewServer( + grpc.MaxRecvMsgSize(16<<20), // 16MB + grpc.MaxSendMsgSize(16<<20), // 16MB + ) + + // Register negentropy service + service := negentropyserver.NewService(db, negentropyMgr) + negentropyv1.RegisterNegentropyServiceServer(grpcServer, service) + + // Register reflection for debugging with grpcurl + reflection.Register(grpcServer) + + // Start listening + lis, err := net.Listen("tcp", cfg.Listen) + if chk.E(err) { + log.E.F("failed to listen on %s: %v", cfg.Listen, err) + os.Exit(1) + } + log.I.F("gRPC server listening on %s", cfg.Listen) + + // Handle graceful shutdown + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) + sig := <-sigs + log.I.F("received signal %v, shutting down...", sig) + + cancel() + + stopped := make(chan struct{}) + go func() { + grpcServer.GracefulStop() + close(stopped) + }() + + select { + case <-stopped: + log.I.F("gRPC server stopped gracefully") + case <-time.After(5 * time.Second): + log.W.F("gRPC graceful stop timed out, forcing stop") + grpcServer.Stop() + } + + // Stop the negentropy manager + log.I.F("stopping negentropy manager...") + negentropyMgr.Stop() + + if dbCloser != nil { + log.I.F("closing database connection...") + dbCloser() + } + log.I.F("shutdown complete") + }() + + // Serve gRPC + if err := grpcServer.Serve(lis); err != nil { + log.E.F("gRPC server error: %v", err) + } +} diff --git a/cmd/orly-sync-relaygroup/config.go b/cmd/orly-sync-relaygroup/config.go new file mode 100644 index 0000000..951104f --- /dev/null +++ b/cmd/orly-sync-relaygroup/config.go @@ -0,0 +1,49 @@ +package main + +import ( + "os" + "strings" + + "go-simpler.org/env" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" +) + +// Config holds the relay group service configuration. +type Config struct { + // Listen is the gRPC server listen address + Listen string `env:"ORLY_SYNC_RELAYGROUP_LISTEN" default:"127.0.0.1:50063" usage:"gRPC server listen address"` + + // LogLevel is the logging level + LogLevel string `env:"ORLY_SYNC_RELAYGROUP_LOG_LEVEL" default:"info" usage:"log level (trace, debug, info, warn, error)"` + + // Database configuration + DBType string `env:"ORLY_SYNC_RELAYGROUP_DB_TYPE" default:"grpc" usage:"database type: grpc or badger"` + GRPCDBServer string `env:"ORLY_SYNC_RELAYGROUP_DB_SERVER" default:"127.0.0.1:50051" usage:"gRPC database server address"` + DataDir string `env:"ORLY_DATA_DIR" usage:"database data directory (for badger mode)"` + + // Relay group configuration + AdminNpubsRaw string `env:"ORLY_SYNC_RELAYGROUP_ADMINS" usage:"comma-separated admin npubs"` + + // Parsed admin npubs + AdminNpubs []string +} + +// loadConfig loads configuration from environment variables. +func loadConfig() *Config { + cfg := &Config{} + if err := env.Load(cfg, nil); chk.E(err) { + log.E.F("failed to load config: %v", err) + os.Exit(1) + } + + // Parse admin npubs from comma-separated string + if cfg.AdminNpubsRaw != "" { + cfg.AdminNpubs = strings.Split(cfg.AdminNpubsRaw, ",") + for i, p := range cfg.AdminNpubs { + cfg.AdminNpubs[i] = strings.TrimSpace(p) + } + } + + return cfg +} diff --git a/cmd/orly-sync-relaygroup/main.go b/cmd/orly-sync-relaygroup/main.go new file mode 100644 index 0000000..f48aaac --- /dev/null +++ b/cmd/orly-sync-relaygroup/main.go @@ -0,0 +1,127 @@ +// orly-sync-relaygroup is a standalone gRPC relay group service for ORLY. +// It provides relay group configuration discovery from Kind 39105 events. +package main + +import ( + "context" + "net" + "os" + "os/signal" + "syscall" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "lol.mleku.dev" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "next.orly.dev/pkg/database" + databasegrpc "next.orly.dev/pkg/database/grpc" + "next.orly.dev/pkg/sync/relaygroup" + relaygroupv1 "next.orly.dev/pkg/proto/orlysync/relaygroup/v1" +) + +func main() { + cfg := loadConfig() + + // Set log level + lol.SetLogLevel(cfg.LogLevel) + log.I.F("orly-sync-relaygroup starting with log level: %s", cfg.LogLevel) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Initialize database connection + var db database.Database + var dbCloser func() + + if cfg.DBType == "grpc" { + log.I.F("connecting to gRPC database server at %s", cfg.GRPCDBServer) + dbClient, err := databasegrpc.New(ctx, &databasegrpc.ClientConfig{ + ServerAddress: cfg.GRPCDBServer, + ConnectTimeout: 30 * time.Second, + }) + if chk.E(err) { + log.E.F("failed to connect to database server: %v", err) + os.Exit(1) + } + db = dbClient + dbCloser = func() { dbClient.Close() } + } else { + log.E.F("badger mode not yet implemented for sync-relaygroup") + os.Exit(1) + } + + // Wait for database to be ready + log.I.F("waiting for database to be ready...") + <-db.Ready() + log.I.F("database ready") + + // Create relay group manager configuration + relaygroupCfg := &relaygroup.ManagerConfig{ + AdminNpubs: cfg.AdminNpubs, + } + + log.I.F("initializing relay group manager with %d admins", len(cfg.AdminNpubs)) + + // Create gRPC server + grpcServer := grpc.NewServer( + grpc.MaxRecvMsgSize(16<<20), // 16MB + grpc.MaxSendMsgSize(16<<20), // 16MB + ) + + // Register relay group service + // TODO: Create and register service once server implementation is done + // service := relaygroup.NewService(relaygroupMgr, cfg) + // relaygroupv1.RegisterRelayGroupServiceServer(grpcServer, service) + _ = relaygroupCfg + _ = db + _ = relaygroupv1.RelayGroupService_ServiceDesc + + // Register reflection for debugging with grpcurl + reflection.Register(grpcServer) + + // Start listening + lis, err := net.Listen("tcp", cfg.Listen) + if chk.E(err) { + log.E.F("failed to listen on %s: %v", cfg.Listen, err) + os.Exit(1) + } + log.I.F("gRPC server listening on %s", cfg.Listen) + + // Handle graceful shutdown + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM, syscall.SIGINT) + sig := <-sigs + log.I.F("received signal %v, shutting down...", sig) + + cancel() + + stopped := make(chan struct{}) + go func() { + grpcServer.GracefulStop() + close(stopped) + }() + + select { + case <-stopped: + log.I.F("gRPC server stopped gracefully") + case <-time.After(5 * time.Second): + log.W.F("gRPC graceful stop timed out, forcing stop") + grpcServer.Stop() + } + + if dbCloser != nil { + log.I.F("closing database connection...") + dbCloser() + } + log.I.F("shutdown complete") + }() + + // Serve gRPC + if err := grpcServer.Serve(lis); err != nil { + log.E.F("gRPC server error: %v", err) + } +} diff --git a/docs/IPC_ARCHITECTURE.md b/docs/IPC_ARCHITECTURE.md new file mode 100644 index 0000000..c5a0e68 --- /dev/null +++ b/docs/IPC_ARCHITECTURE.md @@ -0,0 +1,371 @@ +# ORLY IPC Architecture + +ORLY implements a modular Inter-Process Communication (IPC) architecture where core functionality runs as independent gRPC services. This design provides resource isolation, fault tolerance, and flexible deployment options. + +## Overview + +``` +┌─────────────────────────────────────────────────────────────────────────────────┐ +│ orly-launcher │ +│ (Process Supervisor & Lifecycle Manager) │ +│ │ +│ Responsibilities: │ +│ - Start services in dependency order │ +│ - Monitor process health and restart on failure │ +│ - Graceful shutdown in reverse order │ +│ - Pass environment configuration to child processes │ +└─────────────────────────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────────────────┼───────────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌───────────────────┐ ┌───────────────────┐ ┌───────────────────────────┐ +│ orly-db │ │ orly-acl │ │ Sync Services │ +│ (gRPC :50051) │ │ (gRPC :50052) │ │ (gRPC :50061-50064) │ +│ │ │ │ │ │ +│ Core Database: │ │ Access Control: │ │ orly-sync-distributed │ +│ ├─ Event storage │ │ ├─ Permission │ │ ├─ Peer-to-peer sync │ +│ ├─ Query engine │ │ │ checks │ │ └─ Serial-based polling │ +│ ├─ Index mgmt │ │ ├─ Follow lists │ │ │ +│ ├─ Serial counter │ │ ├─ Admin/owner │ │ orly-sync-cluster │ +│ └─ Export/import │ │ │ management │ │ ├─ HA replication │ +│ │ │ └─ Policy hooks │ │ └─ Membership events │ +│ Backends: │ │ │ │ │ +│ ├─ Badger (SSD) │ │ Modes: │ │ orly-sync-relaygroup │ +│ ├─ Neo4j (graph) │ │ ├─ none (open) │ │ ├─ Kind 39105 config │ +│ └─ WasmDB (wasm) │ │ ├─ follows │ │ └─ Dynamic peer lists │ +│ │ │ ├─ managed │ │ │ +│ │ │ └─ curating │ │ orly-sync-negentropy │ +│ │ │ │ │ ├─ NIP-77 WebSocket │ +│ │ │ │ │ └─ Set reconciliation │ +└───────────────────┘ └───────────────────┘ └───────────────────────────┘ + │ │ │ + │ │ │ + └───────────────────────────┼───────────────────────────────┘ + │ + ▼ + ┌───────────────────────┐ + │ orly │ + │ (Main Relay) │ + │ │ + │ Client-Facing: │ + │ ├─ WebSocket server │ + │ ├─ NIP handling │ + │ ├─ REQ/EVENT/AUTH │ + │ ├─ NEG-* (NIP-77) │ + │ └─ HTTP API │ + │ │ + │ Internal: │ + │ ├─ gRPC DB client │ + │ ├─ gRPC ACL client │ + │ └─ gRPC sync clients │ + └───────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Caddy │ + │ (TLS Proxy) │ + └───────────────┘ + │ + ▼ + Internet +``` + +## Service Ports + +| Service | Default Port | Description | +|---------|--------------|-------------| +| orly-db | 50051 | Database server (Badger/Neo4j/WasmDB) | +| orly-acl | 50052 | Access control server | +| orly-sync-distributed | 50061 | Peer-to-peer sync | +| orly-sync-cluster | 50062 | Cluster replication | +| orly-sync-relaygroup | 50063 | Relay group config | +| orly-sync-negentropy | 50064 | NIP-77 negentropy | +| orly | 3334 | Main relay (WebSocket/HTTP) | + +## Startup Sequence + +The launcher starts services in strict dependency order: + +``` +1. orly-db ──────► Wait for gRPC ready + │ +2. orly-acl ──────► Wait for gRPC ready (depends on DB) + │ +3. Sync Services ──────► Start in parallel, wait for all ready + ├─ orly-sync-distributed (depends on DB) + ├─ orly-sync-cluster (depends on DB) + ├─ orly-sync-relaygroup (depends on DB) + └─ orly-sync-negentropy (depends on DB) + │ +4. orly ──────► Connects to all services via gRPC +``` + +**Shutdown** happens in reverse order: relay → sync services → ACL → database. + +## Benefits of IPC Architecture + +### 1. Resource Isolation +- Database memory usage is isolated from relay +- ACL processing doesn't compete with query handling +- Sync services have dedicated resources + +### 2. Fault Tolerance +- Database crash doesn't kill WebSocket connections immediately +- ACL service restart preserves event storage +- Individual sync service failures don't affect others + +### 3. Independent Scaling +- Scale database for query-heavy workloads +- Scale relay for connection-heavy workloads +- Enable only needed sync services + +### 4. Flexible Deployment +- Run services on different machines +- Mix local and remote services +- Deploy services incrementally + +--- + +# Core Services + +## 1. Database Service (orly-db) + +The database service handles all event storage, indexing, and querying. + +### Variants +- `orly-db-badger` - Badger LSM-tree database (SSD optimized) +- `orly-db-neo4j` - Neo4j graph database (social graph queries) + +### gRPC API + +```protobuf +service DatabaseService { + // Readiness + rpc Ready(Empty) returns (ReadyResponse); + + // Event operations + rpc SaveEvent(SaveEventRequest) returns (SaveEventResponse); + rpc QueryEvents(QueryEventsRequest) returns (QueryEventsResponse); + rpc CountEvents(CountEventsRequest) returns (CountEventsResponse); + rpc DeleteEvent(DeleteEventRequest) returns (DeleteEventResponse); + rpc QueryAllVersions(QueryEventsRequest) returns (QueryEventsResponse); + + // Special queries + rpc GetSerials(GetSerialsRequest) returns (GetSerialsResponse); + rpc GetLastSerial(Empty) returns (GetLastSerialResponse); + rpc CheckForDeleted(CheckDeletedRequest) returns (CheckDeletedResponse); + + // Admin operations + rpc Export(ExportRequest, stream ExportResponse); + rpc Import(stream ImportRequest) returns (ImportResponse); +} +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_DB_LISTEN` | 127.0.0.1:50051 | gRPC listen address | +| `ORLY_DATA_DIR` | ~/.local/share/ORLY | Database storage path | +| `ORLY_DB_LOG_LEVEL` | info | Logging level | +| `ORLY_DB_BLOCK_CACHE_MB` | 1024 | Badger block cache | +| `ORLY_DB_INDEX_CACHE_MB` | 512 | Badger index cache | +| `ORLY_DB_ZSTD_LEVEL` | 3 | Compression level (0-9) | + +--- + +## 2. ACL Service (orly-acl) + +The ACL service handles access control decisions for all relay operations. + +### Variants +- `orly-acl-none` - Open relay (no restrictions) +- `orly-acl-follows` - Follow-list based access +- `orly-acl-managed` - NIP-86 managed access +- `orly-acl-curating` - Curator-based access + +### gRPC API + +```protobuf +service ACLService { + // Readiness + rpc Ready(Empty) returns (ReadyResponse); + + // Access checks + rpc CheckAccess(CheckAccessRequest) returns (CheckAccessResponse); + rpc GetAccessLevel(GetAccessLevelRequest) returns (GetAccessLevelResponse); + rpc CheckPolicy(CheckPolicyRequest) returns (CheckPolicyResponse); + + // Admin operations + rpc Configure(ConfigureRequest) returns (ConfigureResponse); + rpc GetType(Empty) returns (TypeResponse); +} +``` + +### Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_ACL_LISTEN` | 127.0.0.1:50052 | gRPC listen address | +| `ORLY_ACL_MODE` | follows | ACL mode | +| `ORLY_ACL_DB_TYPE` | grpc | Database backend | +| `ORLY_ACL_GRPC_DB_SERVER` | 127.0.0.1:50051 | Database server | +| `ORLY_ACL_LOG_LEVEL` | info | Logging level | + +--- + +## 3. Main Relay (orly) + +The main relay handles all client-facing operations. + +### Responsibilities +- WebSocket connections (NIP-01) +- REQ/EVENT/AUTH/CLOSE handling +- NEG-* messages (NIP-77, when enabled) +- HTTP API endpoints +- Web UI serving + +### Connection Modes + +**Local mode** (default): +```bash +ORLY_DB_TYPE=badger +ORLY_ACL_TYPE=local +``` + +**gRPC mode** (IPC): +```bash +ORLY_DB_TYPE=grpc +ORLY_GRPC_SERVER=127.0.0.1:50051 +ORLY_ACL_TYPE=grpc +ORLY_GRPC_ACL_SERVER=127.0.0.1:50052 +ORLY_NEGENTROPY_ENABLED=true +ORLY_GRPC_SYNC_NEGENTROPY=127.0.0.1:50064 +``` + +--- + +# Sync Services + +See [IPC_SYNC_SERVICES.md](./IPC_SYNC_SERVICES.md) for detailed sync service documentation. + +## Quick Reference + +| Service | Port | Purpose | +|---------|------|---------| +| orly-sync-distributed | 50061 | Serial-based peer sync | +| orly-sync-cluster | 50062 | HA cluster replication | +| orly-sync-relaygroup | 50063 | Kind 39105 relay groups | +| orly-sync-negentropy | 50064 | NIP-77 set reconciliation | + +--- + +# Deployment Examples + +## Minimal IPC (Database + Relay) + +```bash +# Start database +ORLY_DB_LISTEN=127.0.0.1:50051 ./orly-db-badger & + +# Start relay with gRPC database +ORLY_DB_TYPE=grpc \ +ORLY_GRPC_SERVER=127.0.0.1:50051 \ +./orly +``` + +## Full IPC with ACL + +```bash +# Use launcher for automatic management +ORLY_LAUNCHER_DB_BACKEND=badger \ +ORLY_LAUNCHER_ACL_ENABLED=true \ +ORLY_ACL_MODE=follows \ +./orly-launcher +``` + +## Full IPC with Negentropy + +```bash +# Enable NIP-77 support +ORLY_LAUNCHER_DB_BACKEND=badger \ +ORLY_LAUNCHER_ACL_ENABLED=true \ +ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED=true \ +ORLY_NEGENTROPY_ENABLED=true \ +./orly-launcher +``` + +## systemd Service + +See the example at `/etc/systemd/system/orly.service` on relay.orly.dev for a production configuration. + +--- + +# Building Binaries + +```bash +# Build all IPC binaries +make all-acl # Builds orly-db-*, orly-acl-*, orly-launcher + +# Build sync services +go build -o orly-sync-distributed ./cmd/orly-sync-distributed +go build -o orly-sync-cluster ./cmd/orly-sync-cluster +go build -o orly-sync-relaygroup ./cmd/orly-sync-relaygroup +go build -o orly-sync-negentropy ./cmd/orly-sync-negentropy + +# Cross-compile for ARM64 +CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o orly-sync-negentropy ./cmd/orly-sync-negentropy +``` + +--- + +# Monitoring + +## Health Checks + +Each gRPC service implements a `Ready` RPC that returns when the service is accepting requests. + +## Logs + +All services log to stdout/stderr. Use journalctl for systemd deployments: + +```bash +# Follow all ORLY logs +journalctl -u orly -f + +# Filter by process +journalctl -u orly | grep "orly-db" +journalctl -u orly | grep "orly-sync-negentropy" +``` + +## pprof + +Enable HTTP pprof for profiling: + +```bash +ORLY_PPROF_HTTP=true ./orly-launcher +# Access at http://localhost:6060/debug/pprof/ +``` + +--- + +# Troubleshooting + +## Service won't start + +1. Check if port is in use: `ss -tlnp | grep 5005` +2. Verify binary exists and is executable +3. Check logs: `journalctl -u orly -n 100` + +## Database connection timeout + +1. Ensure database started first +2. Check `ORLY_LAUNCHER_DB_READY_TIMEOUT` (default 30s) +3. Verify network connectivity + +## Negentropy not working + +1. Verify `ORLY_NEGENTROPY_ENABLED=true` in relay config +2. Check negentropy service is running: `ps aux | grep negentropy` +3. Verify gRPC connection: `ORLY_GRPC_SYNC_NEGENTROPY=127.0.0.1:50064` diff --git a/docs/IPC_SYNC_SERVICES.md b/docs/IPC_SYNC_SERVICES.md new file mode 100644 index 0000000..5a0d95c --- /dev/null +++ b/docs/IPC_SYNC_SERVICES.md @@ -0,0 +1,316 @@ +# IPC Sync Services Architecture + +ORLY supports splitting sync functionality into independent gRPC IPC services for improved modularity, resource isolation, and horizontal scaling. + +## Overview + +The sync subsystem can run in two modes: + +1. **Local mode** (default): All sync functionality runs in-process with the main relay +2. **gRPC mode**: Sync services run as separate processes communicating via gRPC + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ orly-launcher │ +│ (Process supervisor - manages lifecycle of all services) │ +└─────────────────────────────────────────────────────────────────────────────┘ + │ + ┌──────────────────────────┼──────────────────────────┐ + │ │ │ + ▼ ▼ ▼ +┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐ +│ orly-db │ │ orly-acl │ │ Sync Services │ +│ (gRPC :50051) │ │ (gRPC :50052) │ │ (gRPC :50061-64) │ +│ │ │ │ │ │ +│ - Event storage │ │ - Access control │ │ - Distributed sync │ +│ - Query execution │ │ - Follow lists │ │ - Cluster sync │ +│ - Index management │ │ - Permissions │ │ - Relay groups │ +└─────────────────────┘ └─────────────────────┘ │ - Negentropy/NIP-77 │ + │ │ └─────────────────────┘ + │ │ │ + └──────────────────────────┼──────────────────────────┘ + │ + ▼ + ┌─────────────────────┐ + │ orly │ + │ (Main relay) │ + │ │ + │ - WebSocket server │ + │ - HTTP endpoints │ + │ - NIP handling │ + └─────────────────────┘ +``` + +## Sync Services + +### 1. orly-sync-distributed (Port 50061) + +Serial-based peer-to-peer synchronization between relay instances. + +**Purpose**: Keeps multiple relay instances in sync by exchanging events based on serial numbers (Lamport clocks). + +**Key Features**: +- Periodic polling of peer relays +- NIP-11 relay info caching for peer identification +- NIP-98 authenticated sync requests +- Incremental sync using serial numbers + +**gRPC API**: +```protobuf +service DistributedSyncService { + rpc Ready(Empty) returns (ReadyResponse); + rpc GetInfo(Empty) returns (SyncInfo); + rpc GetCurrentSerial(CurrentRequest) returns (CurrentResponse); + rpc GetEventIDs(EventIDsRequest) returns (EventIDsResponse); + rpc HandleCurrentRequest(HTTPRequest) returns (HTTPResponse); + rpc HandleEventIDsRequest(HTTPRequest) returns (HTTPResponse); + rpc GetPeers(Empty) returns (PeersResponse); + rpc UpdatePeers(UpdatePeersRequest) returns (Empty); + rpc GetSyncStatus(Empty) returns (SyncStatusResponse); + rpc GetPeerPubkey(PeerPubkeyRequest) returns (PeerPubkeyResponse); + rpc IsAuthorizedPeer(AuthorizedPeerRequest) returns (AuthorizedPeerResponse); + rpc NotifyNewEvent(NewEventNotification) returns (Empty); +} +``` + +**Environment Variables**: +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_SYNC_DISTRIBUTED_LISTEN` | 127.0.0.1:50061 | gRPC listen address | +| `ORLY_SYNC_DISTRIBUTED_DB_TYPE` | grpc | Database backend | +| `ORLY_SYNC_DISTRIBUTED_DB_SERVER` | 127.0.0.1:50051 | Database server address | +| `ORLY_SYNC_DISTRIBUTED_PEERS` | | Comma-separated peer URLs | +| `ORLY_SYNC_DISTRIBUTED_INTERVAL` | 30s | Sync polling interval | + +--- + +### 2. orly-sync-cluster (Port 50062) + +Cluster replication with persistent state for high-availability deployments. + +**Purpose**: Enables multiple relay instances to form a cluster with consistent event replication. + +**Key Features**: +- Real-time event propagation to cluster members +- Cluster membership management via Kind 39108 events +- Persistent tracking of cluster member state +- Configurable privileged event propagation (DMs, gift wraps) + +**gRPC API**: +```protobuf +service ClusterSyncService { + rpc Ready(Empty) returns (ReadyResponse); + rpc Start(Empty) returns (Empty); + rpc Stop(Empty) returns (Empty); + rpc HandleLatestSerial(HTTPRequest) returns (HTTPResponse); + rpc HandleEventsRange(HTTPRequest) returns (HTTPResponse); + rpc GetMembers(Empty) returns (MembersResponse); + rpc UpdateMembership(UpdateMembershipRequest) returns (Empty); + rpc HandleMembershipEvent(MembershipEventRequest) returns (Empty); + rpc GetClusterStatus(Empty) returns (ClusterStatusResponse); + rpc GetMemberStatus(MemberStatusRequest) returns (MemberStatusResponse); + rpc GetLatestSerial(Empty) returns (LatestSerialResponse); + rpc GetEventsInRange(EventsRangeRequest) returns (EventsRangeResponse); +} +``` + +**Environment Variables**: +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_SYNC_CLUSTER_LISTEN` | 127.0.0.1:50062 | gRPC listen address | +| `ORLY_SYNC_CLUSTER_DB_TYPE` | grpc | Database backend | +| `ORLY_SYNC_CLUSTER_DB_SERVER` | 127.0.0.1:50051 | Database server address | +| `ORLY_CLUSTER_ADMINS` | | Authorized cluster admin pubkeys | +| `ORLY_CLUSTER_PROPAGATE_PRIVILEGED_EVENTS` | true | Replicate DMs/gift wraps | + +--- + +### 3. orly-sync-relaygroup (Port 50063) + +Relay group configuration discovery using Kind 39105 events. + +**Purpose**: Manages relay group configurations that define which relays should sync together. + +**Key Features**: +- Discovery of authoritative relay group configs +- Validation of relay group events +- Authorized publisher management +- Dynamic peer list updates + +**gRPC API**: +```protobuf +service RelayGroupService { + rpc Ready(Empty) returns (ReadyResponse); + rpc FindAuthoritativeConfig(Empty) returns (RelayGroupConfigResponse); + rpc GetRelays(Empty) returns (RelaysResponse); + rpc IsAuthorizedPublisher(AuthorizedPublisherRequest) returns (AuthorizedPublisherResponse); + rpc GetAuthorizedPubkeys(Empty) returns (AuthorizedPubkeysResponse); + rpc ValidateRelayGroupEvent(ValidateEventRequest) returns (ValidateEventResponse); + rpc HandleRelayGroupEvent(HandleEventRequest) returns (Empty); +} +``` + +**Environment Variables**: +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_SYNC_RELAYGROUP_LISTEN` | 127.0.0.1:50063 | gRPC listen address | +| `ORLY_SYNC_RELAYGROUP_DB_TYPE` | grpc | Database backend | +| `ORLY_SYNC_RELAYGROUP_DB_SERVER` | 127.0.0.1:50051 | Database server address | +| `ORLY_RELAY_GROUP_ADMINS` | | Authorized relay group config publishers | + +--- + +### 4. orly-sync-negentropy (Port 50064) + +NIP-77 negentropy-based efficient set reconciliation. + +**Purpose**: Provides efficient set reconciliation for both relay-to-relay sync and client-facing NIP-77 WebSocket protocol. + +**Key Features**: +- NIP-77 client WebSocket support (NEG-OPEN, NEG-MSG, NEG-CLOSE) +- Relay-to-relay negentropy sync +- Session management for concurrent clients +- Configurable frame size and ID truncation + +**gRPC API**: +```protobuf +service NegentropyService { + rpc Ready(Empty) returns (ReadyResponse); + rpc Start(Empty) returns (Empty); + rpc Stop(Empty) returns (Empty); + + // Client-facing NIP-77 + rpc HandleNegOpen(NegOpenRequest) returns (NegOpenResponse); + rpc HandleNegMsg(NegMsgRequest) returns (NegMsgResponse); + rpc HandleNegClose(NegCloseRequest) returns (Empty); + + // Relay-to-relay sync + rpc SyncWithPeer(SyncPeerRequest) returns (stream SyncProgress); + rpc GetSyncStatus(Empty) returns (SyncStatusResponse); + rpc GetPeers(Empty) returns (PeersResponse); + rpc AddPeer(AddPeerRequest) returns (Empty); + rpc RemovePeer(RemovePeerRequest) returns (Empty); + rpc TriggerSync(TriggerSyncRequest) returns (Empty); + rpc GetPeerSyncState(PeerSyncStateRequest) returns (PeerSyncStateResponse); + + // Session management + rpc ListSessions(Empty) returns (ListSessionsResponse); + rpc CloseSession(CloseSessionRequest) returns (Empty); +} +``` + +**Environment Variables**: +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_SYNC_NEGENTROPY_LISTEN` | 127.0.0.1:50064 | gRPC listen address | +| `ORLY_SYNC_NEGENTROPY_DB_TYPE` | grpc | Database backend | +| `ORLY_SYNC_NEGENTROPY_DB_SERVER` | 127.0.0.1:50051 | Database server address | +| `ORLY_SYNC_NEGENTROPY_PEERS` | | Comma-separated peer WebSocket URLs | +| `ORLY_SYNC_NEGENTROPY_INTERVAL` | 60s | Background sync interval | +| `ORLY_SYNC_NEGENTROPY_FRAME_SIZE` | 4096 | Negentropy frame size | +| `ORLY_SYNC_NEGENTROPY_ID_SIZE` | 16 | ID truncation size | +| `ORLY_SYNC_NEGENTROPY_SESSION_TIMEOUT` | 5m | Client session timeout | + +--- + +## Launcher Configuration + +The `orly-launcher` manages all services. Enable sync services with these environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_LAUNCHER_SYNC_DISTRIBUTED_ENABLED` | false | Enable distributed sync | +| `ORLY_LAUNCHER_SYNC_DISTRIBUTED_BINARY` | orly-sync-distributed | Binary path | +| `ORLY_LAUNCHER_SYNC_DISTRIBUTED_LISTEN` | 127.0.0.1:50061 | Listen address | +| `ORLY_LAUNCHER_SYNC_CLUSTER_ENABLED` | false | Enable cluster sync | +| `ORLY_LAUNCHER_SYNC_CLUSTER_BINARY` | orly-sync-cluster | Binary path | +| `ORLY_LAUNCHER_SYNC_CLUSTER_LISTEN` | 127.0.0.1:50062 | Listen address | +| `ORLY_LAUNCHER_SYNC_RELAYGROUP_ENABLED` | false | Enable relay group | +| `ORLY_LAUNCHER_SYNC_RELAYGROUP_BINARY` | orly-sync-relaygroup | Binary path | +| `ORLY_LAUNCHER_SYNC_RELAYGROUP_LISTEN` | 127.0.0.1:50063 | Listen address | +| `ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED` | false | Enable negentropy | +| `ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY` | orly-sync-negentropy | Binary path | +| `ORLY_LAUNCHER_SYNC_NEGENTROPY_LISTEN` | 127.0.0.1:50064 | Listen address | +| `ORLY_LAUNCHER_SYNC_READY_TIMEOUT` | 30s | Sync service startup timeout | + +--- + +## Relay Configuration + +When sync services are enabled, configure the relay to connect: + +| Variable | Default | Description | +|----------|---------|-------------| +| `ORLY_SYNC_TYPE` | local | Sync backend: local or grpc | +| `ORLY_GRPC_SYNC_DISTRIBUTED` | | Distributed sync server address | +| `ORLY_GRPC_SYNC_CLUSTER` | | Cluster sync server address | +| `ORLY_GRPC_SYNC_RELAYGROUP` | | Relay group server address | +| `ORLY_GRPC_SYNC_NEGENTROPY` | | Negentropy server address | +| `ORLY_GRPC_SYNC_TIMEOUT` | 10s | gRPC connection timeout | +| `ORLY_NEGENTROPY_ENABLED` | false | Enable NIP-77 WebSocket support | + +--- + +## Startup Order + +The launcher starts services in dependency order: + +1. **Database** (orly-db) - Must be ready first +2. **ACL** (orly-acl) - Depends on database +3. **Sync Services** (parallel) - All depend on database only +4. **Relay** (orly) - Depends on all above + +Shutdown happens in reverse order. + +--- + +## Example: Enabling Negentropy + +To enable NIP-77 negentropy support on an existing deployment: + +```bash +# In systemd service or environment +Environment=ORLY_LAUNCHER_SYNC_NEGENTROPY_ENABLED=true +Environment=ORLY_LAUNCHER_SYNC_NEGENTROPY_BINARY=/path/to/orly-sync-negentropy +Environment=ORLY_NEGENTROPY_ENABLED=true +``` + +Build the binary: +```bash +CGO_ENABLED=0 go build -o orly-sync-negentropy ./cmd/orly-sync-negentropy +``` + +--- + +## Proto Definitions + +Proto files are located in: +- `proto/orlysync/common/v1/types.proto` - Shared types +- `proto/orlysync/distributed/v1/service.proto` - Distributed sync +- `proto/orlysync/cluster/v1/service.proto` - Cluster sync +- `proto/orlysync/relaygroup/v1/service.proto` - Relay group +- `proto/orlysync/negentropy/v1/service.proto` - Negentropy + +Generate Go code with: +```bash +buf generate +``` + +--- + +## NIP-77 Client Protocol + +The negentropy service implements NIP-77 for efficient client synchronization: + +### Client Messages +- `["NEG-OPEN", subscription_id, filter, initial_message?]` - Start reconciliation +- `["NEG-MSG", subscription_id, message]` - Continue reconciliation +- `["NEG-CLOSE", subscription_id]` - End session + +### Server Responses +- `["NEG-MSG", subscription_id, message]` - Reconciliation response +- `["NEG-ERR", subscription_id, reason]` - Error response + +The relay automatically routes these messages to the negentropy service when `ORLY_NEGENTROPY_ENABLED=true`. diff --git a/go.mod b/go.mod index eee8a4a..bcef6a8 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module next.orly.dev go 1.25.3 require ( - git.mleku.dev/mleku/nostr v1.0.13 + git.mleku.dev/mleku/nostr v1.0.14 github.com/adrg/xdg v0.5.3 github.com/alexflint/go-arg v1.6.1 github.com/aperturerobotics/go-indexeddb v0.2.3 diff --git a/go.sum b/go.sum index cab0cc4..084cd45 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ git.mleku.dev/mleku/nostr v1.0.13 h1:FqeOQ9ZX8AFVsAI6XisQkB6cgmhn9DNQ2a8li9gx7aY= git.mleku.dev/mleku/nostr v1.0.13/go.mod h1:kJwSMmLRnAJ7QJtgXDv2wGgceFU0luwVqrgAL3MI93M= +git.mleku.dev/mleku/nostr v1.0.14 h1:54X0bY6/qsbLygYyN6h9UMXI3U8B325npEuaeGWHoEQ= +git.mleku.dev/mleku/nostr v1.0.14/go.mod h1:kJwSMmLRnAJ7QJtgXDv2wGgceFU0luwVqrgAL3MI93M= github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 h1:ClzzXMDDuUbWfNNZqGeYq4PnYOlwlOVIvSyNaIy0ykg= diff --git a/main.go b/main.go index 416aad2..5ad46e5 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( "next.orly.dev/app/config" "next.orly.dev/pkg/acl" aclgrpc "next.orly.dev/pkg/acl/grpc" + negentropygrpc "next.orly.dev/pkg/sync/negentropy/grpc" "git.mleku.dev/mleku/nostr/crypto/keys" "git.mleku.dev/mleku/nostr/encoders/bech32encoding" "next.orly.dev/pkg/database" @@ -614,6 +615,30 @@ func main() { acl.Registry.Syncer() } + // Initialize negentropy client if enabled (gRPC mode) + syncType, _, _, _, negentropyAddr, syncTimeout, negentropyEnabled := cfg.GetGRPCSyncConfigValues() + if negentropyEnabled && syncType == "grpc" && negentropyAddr != "" { + log.I.F("connecting to gRPC negentropy server at %s", negentropyAddr) + negClient, negErr := negentropygrpc.New(ctx, &negentropygrpc.ClientConfig{ + ServerAddress: negentropyAddr, + ConnectTimeout: syncTimeout, + }) + if negErr != nil { + log.W.F("failed to connect to gRPC negentropy server: %v (NIP-77 disabled)", negErr) + } else { + // Wait for negentropy server to be ready + select { + case <-negClient.Ready(): + log.I.F("gRPC negentropy client connected") + app.SetNegentropyClient(negClient) + case <-time.After(30 * time.Second): + log.W.F("timeout waiting for gRPC negentropy server (NIP-77 disabled)") + } + } + } else if negentropyEnabled { + log.I.F("negentropy enabled but sync type is %q, skipping gRPC client", syncType) + } + // Create rate limiter if enabled var limiter *ratelimit.Limiter rateLimitEnabled, targetMB, diff --git a/pkg/proto/orlysync/cluster/v1/service.pb.go b/pkg/proto/orlysync/cluster/v1/service.pb.go new file mode 100644 index 0000000..cf68391 --- /dev/null +++ b/pkg/proto/orlysync/cluster/v1/service.pb.go @@ -0,0 +1,808 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: orlysync/cluster/v1/service.proto + +package clusterv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// LatestSerialResponse contains the latest serial and timestamp +type LatestSerialResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Serial uint64 `protobuf:"varint,1,opt,name=serial,proto3" json:"serial,omitempty"` + Timestamp int64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Unix timestamp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LatestSerialResponse) Reset() { + *x = LatestSerialResponse{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LatestSerialResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LatestSerialResponse) ProtoMessage() {} + +func (x *LatestSerialResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LatestSerialResponse.ProtoReflect.Descriptor instead. +func (*LatestSerialResponse) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{0} +} + +func (x *LatestSerialResponse) GetSerial() uint64 { + if x != nil { + return x.Serial + } + return 0 +} + +func (x *LatestSerialResponse) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// EventsRangeRequest requests events in a serial range +type EventsRangeRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + From uint64 `protobuf:"varint,1,opt,name=from,proto3" json:"from,omitempty"` // Start serial (inclusive) + To uint64 `protobuf:"varint,2,opt,name=to,proto3" json:"to,omitempty"` // End serial (inclusive) + Limit int32 `protobuf:"varint,3,opt,name=limit,proto3" json:"limit,omitempty"` // Max events to return + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventsRangeRequest) Reset() { + *x = EventsRangeRequest{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventsRangeRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventsRangeRequest) ProtoMessage() {} + +func (x *EventsRangeRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventsRangeRequest.ProtoReflect.Descriptor instead. +func (*EventsRangeRequest) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *EventsRangeRequest) GetFrom() uint64 { + if x != nil { + return x.From + } + return 0 +} + +func (x *EventsRangeRequest) GetTo() uint64 { + if x != nil { + return x.To + } + return 0 +} + +func (x *EventsRangeRequest) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +// EventsRangeResponse contains events in the requested range +type EventsRangeResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Events []*EventInfo `protobuf:"bytes,1,rep,name=events,proto3" json:"events,omitempty"` + HasMore bool `protobuf:"varint,2,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` + NextFrom uint64 `protobuf:"varint,3,opt,name=next_from,json=nextFrom,proto3" json:"next_from,omitempty"` // Next serial if has_more is true + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventsRangeResponse) Reset() { + *x = EventsRangeResponse{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventsRangeResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventsRangeResponse) ProtoMessage() {} + +func (x *EventsRangeResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventsRangeResponse.ProtoReflect.Descriptor instead. +func (*EventsRangeResponse) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *EventsRangeResponse) GetEvents() []*EventInfo { + if x != nil { + return x.Events + } + return nil +} + +func (x *EventsRangeResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +func (x *EventsRangeResponse) GetNextFrom() uint64 { + if x != nil { + return x.NextFrom + } + return 0 +} + +// EventInfo contains metadata about an event +type EventInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Serial uint64 `protobuf:"varint,1,opt,name=serial,proto3" json:"serial,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` // Event ID (hex) + Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Created timestamp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventInfo) Reset() { + *x = EventInfo{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventInfo) ProtoMessage() {} + +func (x *EventInfo) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventInfo.ProtoReflect.Descriptor instead. +func (*EventInfo) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *EventInfo) GetSerial() uint64 { + if x != nil { + return x.Serial + } + return 0 +} + +func (x *EventInfo) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *EventInfo) GetTimestamp() int64 { + if x != nil { + return x.Timestamp + } + return 0 +} + +// ClusterMember represents a cluster member +type ClusterMember struct { + state protoimpl.MessageState `protogen:"open.v1"` + HttpUrl string `protobuf:"bytes,1,opt,name=http_url,json=httpUrl,proto3" json:"http_url,omitempty"` + WebsocketUrl string `protobuf:"bytes,2,opt,name=websocket_url,json=websocketUrl,proto3" json:"websocket_url,omitempty"` + LastSerial uint64 `protobuf:"varint,3,opt,name=last_serial,json=lastSerial,proto3" json:"last_serial,omitempty"` + LastPoll int64 `protobuf:"varint,4,opt,name=last_poll,json=lastPoll,proto3" json:"last_poll,omitempty"` // Unix timestamp + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // "active", "error", "unknown" + ErrorCount int32 `protobuf:"varint,6,opt,name=error_count,json=errorCount,proto3" json:"error_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClusterMember) Reset() { + *x = ClusterMember{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClusterMember) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClusterMember) ProtoMessage() {} + +func (x *ClusterMember) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClusterMember.ProtoReflect.Descriptor instead. +func (*ClusterMember) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ClusterMember) GetHttpUrl() string { + if x != nil { + return x.HttpUrl + } + return "" +} + +func (x *ClusterMember) GetWebsocketUrl() string { + if x != nil { + return x.WebsocketUrl + } + return "" +} + +func (x *ClusterMember) GetLastSerial() uint64 { + if x != nil { + return x.LastSerial + } + return 0 +} + +func (x *ClusterMember) GetLastPoll() int64 { + if x != nil { + return x.LastPoll + } + return 0 +} + +func (x *ClusterMember) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ClusterMember) GetErrorCount() int32 { + if x != nil { + return x.ErrorCount + } + return 0 +} + +// MembersResponse contains the list of cluster members +type MembersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Members []*ClusterMember `protobuf:"bytes,1,rep,name=members,proto3" json:"members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MembersResponse) Reset() { + *x = MembersResponse{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MembersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MembersResponse) ProtoMessage() {} + +func (x *MembersResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MembersResponse.ProtoReflect.Descriptor instead. +func (*MembersResponse) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *MembersResponse) GetMembers() []*ClusterMember { + if x != nil { + return x.Members + } + return nil +} + +// UpdateMembershipRequest updates cluster membership +type UpdateMembershipRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + RelayUrls []string `protobuf:"bytes,1,rep,name=relay_urls,json=relayUrls,proto3" json:"relay_urls,omitempty"` // List of relay URLs to add + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateMembershipRequest) Reset() { + *x = UpdateMembershipRequest{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateMembershipRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateMembershipRequest) ProtoMessage() {} + +func (x *UpdateMembershipRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateMembershipRequest.ProtoReflect.Descriptor instead. +func (*UpdateMembershipRequest) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateMembershipRequest) GetRelayUrls() []string { + if x != nil { + return x.RelayUrls + } + return nil +} + +// MembershipEventRequest contains a cluster membership event +type MembershipEventRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Event *v1.Event `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MembershipEventRequest) Reset() { + *x = MembershipEventRequest{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MembershipEventRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MembershipEventRequest) ProtoMessage() {} + +func (x *MembershipEventRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MembershipEventRequest.ProtoReflect.Descriptor instead. +func (*MembershipEventRequest) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *MembershipEventRequest) GetEvent() *v1.Event { + if x != nil { + return x.Event + } + return nil +} + +// ClusterStatusResponse contains overall cluster status +type ClusterStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + LatestSerial uint64 `protobuf:"varint,1,opt,name=latest_serial,json=latestSerial,proto3" json:"latest_serial,omitempty"` + ActiveMembers int32 `protobuf:"varint,2,opt,name=active_members,json=activeMembers,proto3" json:"active_members,omitempty"` + TotalMembers int32 `protobuf:"varint,3,opt,name=total_members,json=totalMembers,proto3" json:"total_members,omitempty"` + PropagatePrivilegedEvents bool `protobuf:"varint,4,opt,name=propagate_privileged_events,json=propagatePrivilegedEvents,proto3" json:"propagate_privileged_events,omitempty"` + Members []*ClusterMember `protobuf:"bytes,5,rep,name=members,proto3" json:"members,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClusterStatusResponse) Reset() { + *x = ClusterStatusResponse{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClusterStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClusterStatusResponse) ProtoMessage() {} + +func (x *ClusterStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClusterStatusResponse.ProtoReflect.Descriptor instead. +func (*ClusterStatusResponse) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *ClusterStatusResponse) GetLatestSerial() uint64 { + if x != nil { + return x.LatestSerial + } + return 0 +} + +func (x *ClusterStatusResponse) GetActiveMembers() int32 { + if x != nil { + return x.ActiveMembers + } + return 0 +} + +func (x *ClusterStatusResponse) GetTotalMembers() int32 { + if x != nil { + return x.TotalMembers + } + return 0 +} + +func (x *ClusterStatusResponse) GetPropagatePrivilegedEvents() bool { + if x != nil { + return x.PropagatePrivilegedEvents + } + return false +} + +func (x *ClusterStatusResponse) GetMembers() []*ClusterMember { + if x != nil { + return x.Members + } + return nil +} + +// MemberStatusRequest requests status for a specific member +type MemberStatusRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + HttpUrl string `protobuf:"bytes,1,opt,name=http_url,json=httpUrl,proto3" json:"http_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemberStatusRequest) Reset() { + *x = MemberStatusRequest{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemberStatusRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemberStatusRequest) ProtoMessage() {} + +func (x *MemberStatusRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemberStatusRequest.ProtoReflect.Descriptor instead. +func (*MemberStatusRequest) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *MemberStatusRequest) GetHttpUrl() string { + if x != nil { + return x.HttpUrl + } + return "" +} + +// MemberStatusResponse contains status for a member +type MemberStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Member *ClusterMember `protobuf:"bytes,1,opt,name=member,proto3" json:"member,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemberStatusResponse) Reset() { + *x = MemberStatusResponse{} + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemberStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemberStatusResponse) ProtoMessage() {} + +func (x *MemberStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_cluster_v1_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemberStatusResponse.ProtoReflect.Descriptor instead. +func (*MemberStatusResponse) Descriptor() ([]byte, []int) { + return file_orlysync_cluster_v1_service_proto_rawDescGZIP(), []int{10} +} + +func (x *MemberStatusResponse) GetMember() *ClusterMember { + if x != nil { + return x.Member + } + return nil +} + +func (x *MemberStatusResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +var File_orlysync_cluster_v1_service_proto protoreflect.FileDescriptor + +const file_orlysync_cluster_v1_service_proto_rawDesc = "" + + "\n" + + "!orlysync/cluster/v1/service.proto\x12\x13orlysync.cluster.v1\x1a\x1eorlysync/common/v1/types.proto\"L\n" + + "\x14LatestSerialResponse\x12\x16\n" + + "\x06serial\x18\x01 \x01(\x04R\x06serial\x12\x1c\n" + + "\ttimestamp\x18\x02 \x01(\x03R\ttimestamp\"N\n" + + "\x12EventsRangeRequest\x12\x12\n" + + "\x04from\x18\x01 \x01(\x04R\x04from\x12\x0e\n" + + "\x02to\x18\x02 \x01(\x04R\x02to\x12\x14\n" + + "\x05limit\x18\x03 \x01(\x05R\x05limit\"\x85\x01\n" + + "\x13EventsRangeResponse\x126\n" + + "\x06events\x18\x01 \x03(\v2\x1e.orlysync.cluster.v1.EventInfoR\x06events\x12\x19\n" + + "\bhas_more\x18\x02 \x01(\bR\ahasMore\x12\x1b\n" + + "\tnext_from\x18\x03 \x01(\x04R\bnextFrom\"Q\n" + + "\tEventInfo\x12\x16\n" + + "\x06serial\x18\x01 \x01(\x04R\x06serial\x12\x0e\n" + + "\x02id\x18\x02 \x01(\tR\x02id\x12\x1c\n" + + "\ttimestamp\x18\x03 \x01(\x03R\ttimestamp\"\xc6\x01\n" + + "\rClusterMember\x12\x19\n" + + "\bhttp_url\x18\x01 \x01(\tR\ahttpUrl\x12#\n" + + "\rwebsocket_url\x18\x02 \x01(\tR\fwebsocketUrl\x12\x1f\n" + + "\vlast_serial\x18\x03 \x01(\x04R\n" + + "lastSerial\x12\x1b\n" + + "\tlast_poll\x18\x04 \x01(\x03R\blastPoll\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\x12\x1f\n" + + "\verror_count\x18\x06 \x01(\x05R\n" + + "errorCount\"O\n" + + "\x0fMembersResponse\x12<\n" + + "\amembers\x18\x01 \x03(\v2\".orlysync.cluster.v1.ClusterMemberR\amembers\"8\n" + + "\x17UpdateMembershipRequest\x12\x1d\n" + + "\n" + + "relay_urls\x18\x01 \x03(\tR\trelayUrls\"I\n" + + "\x16MembershipEventRequest\x12/\n" + + "\x05event\x18\x01 \x01(\v2\x19.orlysync.common.v1.EventR\x05event\"\x86\x02\n" + + "\x15ClusterStatusResponse\x12#\n" + + "\rlatest_serial\x18\x01 \x01(\x04R\flatestSerial\x12%\n" + + "\x0eactive_members\x18\x02 \x01(\x05R\ractiveMembers\x12#\n" + + "\rtotal_members\x18\x03 \x01(\x05R\ftotalMembers\x12>\n" + + "\x1bpropagate_privileged_events\x18\x04 \x01(\bR\x19propagatePrivilegedEvents\x12<\n" + + "\amembers\x18\x05 \x03(\v2\".orlysync.cluster.v1.ClusterMemberR\amembers\"0\n" + + "\x13MemberStatusRequest\x12\x19\n" + + "\bhttp_url\x18\x01 \x01(\tR\ahttpUrl\"h\n" + + "\x14MemberStatusResponse\x12:\n" + + "\x06member\x18\x01 \x01(\v2\".orlysync.cluster.v1.ClusterMemberR\x06member\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found2\x99\b\n" + + "\x12ClusterSyncService\x12E\n" + + "\x05Ready\x12\x19.orlysync.common.v1.Empty\x1a!.orlysync.common.v1.ReadyResponse\x12=\n" + + "\x05Start\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12<\n" + + "\x04Stop\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12W\n" + + "\x12HandleLatestSerial\x12\x1f.orlysync.common.v1.HTTPRequest\x1a .orlysync.common.v1.HTTPResponse\x12V\n" + + "\x11HandleEventsRange\x12\x1f.orlysync.common.v1.HTTPRequest\x1a .orlysync.common.v1.HTTPResponse\x12M\n" + + "\n" + + "GetMembers\x12\x19.orlysync.common.v1.Empty\x1a$.orlysync.cluster.v1.MembersResponse\x12[\n" + + "\x10UpdateMembership\x12,.orlysync.cluster.v1.UpdateMembershipRequest\x1a\x19.orlysync.common.v1.Empty\x12_\n" + + "\x15HandleMembershipEvent\x12+.orlysync.cluster.v1.MembershipEventRequest\x1a\x19.orlysync.common.v1.Empty\x12Y\n" + + "\x10GetClusterStatus\x12\x19.orlysync.common.v1.Empty\x1a*.orlysync.cluster.v1.ClusterStatusResponse\x12f\n" + + "\x0fGetMemberStatus\x12(.orlysync.cluster.v1.MemberStatusRequest\x1a).orlysync.cluster.v1.MemberStatusResponse\x12W\n" + + "\x0fGetLatestSerial\x12\x19.orlysync.common.v1.Empty\x1a).orlysync.cluster.v1.LatestSerialResponse\x12e\n" + + "\x10GetEventsInRange\x12'.orlysync.cluster.v1.EventsRangeRequest\x1a(.orlysync.cluster.v1.EventsRangeResponseB7Z5next.orly.dev/pkg/proto/orlysync/cluster/v1;clusterv1b\x06proto3" + +var ( + file_orlysync_cluster_v1_service_proto_rawDescOnce sync.Once + file_orlysync_cluster_v1_service_proto_rawDescData []byte +) + +func file_orlysync_cluster_v1_service_proto_rawDescGZIP() []byte { + file_orlysync_cluster_v1_service_proto_rawDescOnce.Do(func() { + file_orlysync_cluster_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_orlysync_cluster_v1_service_proto_rawDesc), len(file_orlysync_cluster_v1_service_proto_rawDesc))) + }) + return file_orlysync_cluster_v1_service_proto_rawDescData +} + +var file_orlysync_cluster_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_orlysync_cluster_v1_service_proto_goTypes = []any{ + (*LatestSerialResponse)(nil), // 0: orlysync.cluster.v1.LatestSerialResponse + (*EventsRangeRequest)(nil), // 1: orlysync.cluster.v1.EventsRangeRequest + (*EventsRangeResponse)(nil), // 2: orlysync.cluster.v1.EventsRangeResponse + (*EventInfo)(nil), // 3: orlysync.cluster.v1.EventInfo + (*ClusterMember)(nil), // 4: orlysync.cluster.v1.ClusterMember + (*MembersResponse)(nil), // 5: orlysync.cluster.v1.MembersResponse + (*UpdateMembershipRequest)(nil), // 6: orlysync.cluster.v1.UpdateMembershipRequest + (*MembershipEventRequest)(nil), // 7: orlysync.cluster.v1.MembershipEventRequest + (*ClusterStatusResponse)(nil), // 8: orlysync.cluster.v1.ClusterStatusResponse + (*MemberStatusRequest)(nil), // 9: orlysync.cluster.v1.MemberStatusRequest + (*MemberStatusResponse)(nil), // 10: orlysync.cluster.v1.MemberStatusResponse + (*v1.Event)(nil), // 11: orlysync.common.v1.Event + (*v1.Empty)(nil), // 12: orlysync.common.v1.Empty + (*v1.HTTPRequest)(nil), // 13: orlysync.common.v1.HTTPRequest + (*v1.ReadyResponse)(nil), // 14: orlysync.common.v1.ReadyResponse + (*v1.HTTPResponse)(nil), // 15: orlysync.common.v1.HTTPResponse +} +var file_orlysync_cluster_v1_service_proto_depIdxs = []int32{ + 3, // 0: orlysync.cluster.v1.EventsRangeResponse.events:type_name -> orlysync.cluster.v1.EventInfo + 4, // 1: orlysync.cluster.v1.MembersResponse.members:type_name -> orlysync.cluster.v1.ClusterMember + 11, // 2: orlysync.cluster.v1.MembershipEventRequest.event:type_name -> orlysync.common.v1.Event + 4, // 3: orlysync.cluster.v1.ClusterStatusResponse.members:type_name -> orlysync.cluster.v1.ClusterMember + 4, // 4: orlysync.cluster.v1.MemberStatusResponse.member:type_name -> orlysync.cluster.v1.ClusterMember + 12, // 5: orlysync.cluster.v1.ClusterSyncService.Ready:input_type -> orlysync.common.v1.Empty + 12, // 6: orlysync.cluster.v1.ClusterSyncService.Start:input_type -> orlysync.common.v1.Empty + 12, // 7: orlysync.cluster.v1.ClusterSyncService.Stop:input_type -> orlysync.common.v1.Empty + 13, // 8: orlysync.cluster.v1.ClusterSyncService.HandleLatestSerial:input_type -> orlysync.common.v1.HTTPRequest + 13, // 9: orlysync.cluster.v1.ClusterSyncService.HandleEventsRange:input_type -> orlysync.common.v1.HTTPRequest + 12, // 10: orlysync.cluster.v1.ClusterSyncService.GetMembers:input_type -> orlysync.common.v1.Empty + 6, // 11: orlysync.cluster.v1.ClusterSyncService.UpdateMembership:input_type -> orlysync.cluster.v1.UpdateMembershipRequest + 7, // 12: orlysync.cluster.v1.ClusterSyncService.HandleMembershipEvent:input_type -> orlysync.cluster.v1.MembershipEventRequest + 12, // 13: orlysync.cluster.v1.ClusterSyncService.GetClusterStatus:input_type -> orlysync.common.v1.Empty + 9, // 14: orlysync.cluster.v1.ClusterSyncService.GetMemberStatus:input_type -> orlysync.cluster.v1.MemberStatusRequest + 12, // 15: orlysync.cluster.v1.ClusterSyncService.GetLatestSerial:input_type -> orlysync.common.v1.Empty + 1, // 16: orlysync.cluster.v1.ClusterSyncService.GetEventsInRange:input_type -> orlysync.cluster.v1.EventsRangeRequest + 14, // 17: orlysync.cluster.v1.ClusterSyncService.Ready:output_type -> orlysync.common.v1.ReadyResponse + 12, // 18: orlysync.cluster.v1.ClusterSyncService.Start:output_type -> orlysync.common.v1.Empty + 12, // 19: orlysync.cluster.v1.ClusterSyncService.Stop:output_type -> orlysync.common.v1.Empty + 15, // 20: orlysync.cluster.v1.ClusterSyncService.HandleLatestSerial:output_type -> orlysync.common.v1.HTTPResponse + 15, // 21: orlysync.cluster.v1.ClusterSyncService.HandleEventsRange:output_type -> orlysync.common.v1.HTTPResponse + 5, // 22: orlysync.cluster.v1.ClusterSyncService.GetMembers:output_type -> orlysync.cluster.v1.MembersResponse + 12, // 23: orlysync.cluster.v1.ClusterSyncService.UpdateMembership:output_type -> orlysync.common.v1.Empty + 12, // 24: orlysync.cluster.v1.ClusterSyncService.HandleMembershipEvent:output_type -> orlysync.common.v1.Empty + 8, // 25: orlysync.cluster.v1.ClusterSyncService.GetClusterStatus:output_type -> orlysync.cluster.v1.ClusterStatusResponse + 10, // 26: orlysync.cluster.v1.ClusterSyncService.GetMemberStatus:output_type -> orlysync.cluster.v1.MemberStatusResponse + 0, // 27: orlysync.cluster.v1.ClusterSyncService.GetLatestSerial:output_type -> orlysync.cluster.v1.LatestSerialResponse + 2, // 28: orlysync.cluster.v1.ClusterSyncService.GetEventsInRange:output_type -> orlysync.cluster.v1.EventsRangeResponse + 17, // [17:29] is the sub-list for method output_type + 5, // [5:17] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_orlysync_cluster_v1_service_proto_init() } +func file_orlysync_cluster_v1_service_proto_init() { + if File_orlysync_cluster_v1_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlysync_cluster_v1_service_proto_rawDesc), len(file_orlysync_cluster_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_orlysync_cluster_v1_service_proto_goTypes, + DependencyIndexes: file_orlysync_cluster_v1_service_proto_depIdxs, + MessageInfos: file_orlysync_cluster_v1_service_proto_msgTypes, + }.Build() + File_orlysync_cluster_v1_service_proto = out.File + file_orlysync_cluster_v1_service_proto_goTypes = nil + file_orlysync_cluster_v1_service_proto_depIdxs = nil +} diff --git a/pkg/proto/orlysync/cluster/v1/service_grpc.pb.go b/pkg/proto/orlysync/cluster/v1/service_grpc.pb.go new file mode 100644 index 0000000..746b526 --- /dev/null +++ b/pkg/proto/orlysync/cluster/v1/service_grpc.pb.go @@ -0,0 +1,570 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: orlysync/cluster/v1/service.proto + +package clusterv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ClusterSyncService_Ready_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/Ready" + ClusterSyncService_Start_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/Start" + ClusterSyncService_Stop_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/Stop" + ClusterSyncService_HandleLatestSerial_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/HandleLatestSerial" + ClusterSyncService_HandleEventsRange_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/HandleEventsRange" + ClusterSyncService_GetMembers_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/GetMembers" + ClusterSyncService_UpdateMembership_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/UpdateMembership" + ClusterSyncService_HandleMembershipEvent_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/HandleMembershipEvent" + ClusterSyncService_GetClusterStatus_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/GetClusterStatus" + ClusterSyncService_GetMemberStatus_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/GetMemberStatus" + ClusterSyncService_GetLatestSerial_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/GetLatestSerial" + ClusterSyncService_GetEventsInRange_FullMethodName = "/orlysync.cluster.v1.ClusterSyncService/GetEventsInRange" +) + +// ClusterSyncServiceClient is the client API for ClusterSyncService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// ClusterSyncService provides cluster replication with persistent state +// for multi-member relay clusters +type ClusterSyncServiceClient interface { + // Ready returns whether the service is ready to serve requests + Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) + // Start starts the cluster polling loop + Start(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // Stop stops the cluster polling loop + Stop(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // HandleLatestSerial proxies GET /cluster/latest HTTP requests + HandleLatestSerial(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) + // HandleEventsRange proxies GET /cluster/events HTTP requests + HandleEventsRange(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) + // GetMembers returns the current cluster members + GetMembers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*MembersResponse, error) + // UpdateMembership updates cluster membership + UpdateMembership(ctx context.Context, in *UpdateMembershipRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // HandleMembershipEvent processes a cluster membership event (Kind 39108) + HandleMembershipEvent(ctx context.Context, in *MembershipEventRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // GetClusterStatus returns overall cluster status + GetClusterStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*ClusterStatusResponse, error) + // GetMemberStatus returns status for a specific member + GetMemberStatus(ctx context.Context, in *MemberStatusRequest, opts ...grpc.CallOption) (*MemberStatusResponse, error) + // GetLatestSerial returns the latest serial from this relay's database + GetLatestSerial(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*LatestSerialResponse, error) + // GetEventsInRange returns event info for a serial range + GetEventsInRange(ctx context.Context, in *EventsRangeRequest, opts ...grpc.CallOption) (*EventsRangeResponse, error) +} + +type clusterSyncServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewClusterSyncServiceClient(cc grpc.ClientConnInterface) ClusterSyncServiceClient { + return &clusterSyncServiceClient{cc} +} + +func (c *clusterSyncServiceClient) Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.ReadyResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_Ready_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) Start(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, ClusterSyncService_Start_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) Stop(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, ClusterSyncService_Stop_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) HandleLatestSerial(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.HTTPResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_HandleLatestSerial_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) HandleEventsRange(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.HTTPResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_HandleEventsRange_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) GetMembers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*MembersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MembersResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_GetMembers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) UpdateMembership(ctx context.Context, in *UpdateMembershipRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, ClusterSyncService_UpdateMembership_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) HandleMembershipEvent(ctx context.Context, in *MembershipEventRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, ClusterSyncService_HandleMembershipEvent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) GetClusterStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*ClusterStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ClusterStatusResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_GetClusterStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) GetMemberStatus(ctx context.Context, in *MemberStatusRequest, opts ...grpc.CallOption) (*MemberStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(MemberStatusResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_GetMemberStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) GetLatestSerial(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*LatestSerialResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LatestSerialResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_GetLatestSerial_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *clusterSyncServiceClient) GetEventsInRange(ctx context.Context, in *EventsRangeRequest, opts ...grpc.CallOption) (*EventsRangeResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EventsRangeResponse) + err := c.cc.Invoke(ctx, ClusterSyncService_GetEventsInRange_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ClusterSyncServiceServer is the server API for ClusterSyncService service. +// All implementations must embed UnimplementedClusterSyncServiceServer +// for forward compatibility. +// +// ClusterSyncService provides cluster replication with persistent state +// for multi-member relay clusters +type ClusterSyncServiceServer interface { + // Ready returns whether the service is ready to serve requests + Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) + // Start starts the cluster polling loop + Start(context.Context, *v1.Empty) (*v1.Empty, error) + // Stop stops the cluster polling loop + Stop(context.Context, *v1.Empty) (*v1.Empty, error) + // HandleLatestSerial proxies GET /cluster/latest HTTP requests + HandleLatestSerial(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) + // HandleEventsRange proxies GET /cluster/events HTTP requests + HandleEventsRange(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) + // GetMembers returns the current cluster members + GetMembers(context.Context, *v1.Empty) (*MembersResponse, error) + // UpdateMembership updates cluster membership + UpdateMembership(context.Context, *UpdateMembershipRequest) (*v1.Empty, error) + // HandleMembershipEvent processes a cluster membership event (Kind 39108) + HandleMembershipEvent(context.Context, *MembershipEventRequest) (*v1.Empty, error) + // GetClusterStatus returns overall cluster status + GetClusterStatus(context.Context, *v1.Empty) (*ClusterStatusResponse, error) + // GetMemberStatus returns status for a specific member + GetMemberStatus(context.Context, *MemberStatusRequest) (*MemberStatusResponse, error) + // GetLatestSerial returns the latest serial from this relay's database + GetLatestSerial(context.Context, *v1.Empty) (*LatestSerialResponse, error) + // GetEventsInRange returns event info for a serial range + GetEventsInRange(context.Context, *EventsRangeRequest) (*EventsRangeResponse, error) + mustEmbedUnimplementedClusterSyncServiceServer() +} + +// UnimplementedClusterSyncServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedClusterSyncServiceServer struct{} + +func (UnimplementedClusterSyncServiceServer) Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ready not implemented") +} +func (UnimplementedClusterSyncServiceServer) Start(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method Start not implemented") +} +func (UnimplementedClusterSyncServiceServer) Stop(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method Stop not implemented") +} +func (UnimplementedClusterSyncServiceServer) HandleLatestSerial(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleLatestSerial not implemented") +} +func (UnimplementedClusterSyncServiceServer) HandleEventsRange(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleEventsRange not implemented") +} +func (UnimplementedClusterSyncServiceServer) GetMembers(context.Context, *v1.Empty) (*MembersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMembers not implemented") +} +func (UnimplementedClusterSyncServiceServer) UpdateMembership(context.Context, *UpdateMembershipRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateMembership not implemented") +} +func (UnimplementedClusterSyncServiceServer) HandleMembershipEvent(context.Context, *MembershipEventRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method HandleMembershipEvent not implemented") +} +func (UnimplementedClusterSyncServiceServer) GetClusterStatus(context.Context, *v1.Empty) (*ClusterStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetClusterStatus not implemented") +} +func (UnimplementedClusterSyncServiceServer) GetMemberStatus(context.Context, *MemberStatusRequest) (*MemberStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetMemberStatus not implemented") +} +func (UnimplementedClusterSyncServiceServer) GetLatestSerial(context.Context, *v1.Empty) (*LatestSerialResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetLatestSerial not implemented") +} +func (UnimplementedClusterSyncServiceServer) GetEventsInRange(context.Context, *EventsRangeRequest) (*EventsRangeResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetEventsInRange not implemented") +} +func (UnimplementedClusterSyncServiceServer) mustEmbedUnimplementedClusterSyncServiceServer() {} +func (UnimplementedClusterSyncServiceServer) testEmbeddedByValue() {} + +// UnsafeClusterSyncServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ClusterSyncServiceServer will +// result in compilation errors. +type UnsafeClusterSyncServiceServer interface { + mustEmbedUnimplementedClusterSyncServiceServer() +} + +func RegisterClusterSyncServiceServer(s grpc.ServiceRegistrar, srv ClusterSyncServiceServer) { + // If the following call panics, it indicates UnimplementedClusterSyncServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ClusterSyncService_ServiceDesc, srv) +} + +func _ClusterSyncService_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).Ready(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_Ready_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).Ready(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).Start(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_Start_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).Start(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_Stop_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).Stop(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_HandleLatestSerial_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.HTTPRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).HandleLatestSerial(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_HandleLatestSerial_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).HandleLatestSerial(ctx, req.(*v1.HTTPRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_HandleEventsRange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.HTTPRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).HandleEventsRange(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_HandleEventsRange_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).HandleEventsRange(ctx, req.(*v1.HTTPRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_GetMembers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).GetMembers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_GetMembers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).GetMembers(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_UpdateMembership_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateMembershipRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).UpdateMembership(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_UpdateMembership_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).UpdateMembership(ctx, req.(*UpdateMembershipRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_HandleMembershipEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MembershipEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).HandleMembershipEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_HandleMembershipEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).HandleMembershipEvent(ctx, req.(*MembershipEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_GetClusterStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).GetClusterStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_GetClusterStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).GetClusterStatus(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_GetMemberStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(MemberStatusRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).GetMemberStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_GetMemberStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).GetMemberStatus(ctx, req.(*MemberStatusRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_GetLatestSerial_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).GetLatestSerial(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_GetLatestSerial_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).GetLatestSerial(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _ClusterSyncService_GetEventsInRange_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EventsRangeRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClusterSyncServiceServer).GetEventsInRange(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ClusterSyncService_GetEventsInRange_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClusterSyncServiceServer).GetEventsInRange(ctx, req.(*EventsRangeRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ClusterSyncService_ServiceDesc is the grpc.ServiceDesc for ClusterSyncService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ClusterSyncService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "orlysync.cluster.v1.ClusterSyncService", + HandlerType: (*ClusterSyncServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ready", + Handler: _ClusterSyncService_Ready_Handler, + }, + { + MethodName: "Start", + Handler: _ClusterSyncService_Start_Handler, + }, + { + MethodName: "Stop", + Handler: _ClusterSyncService_Stop_Handler, + }, + { + MethodName: "HandleLatestSerial", + Handler: _ClusterSyncService_HandleLatestSerial_Handler, + }, + { + MethodName: "HandleEventsRange", + Handler: _ClusterSyncService_HandleEventsRange_Handler, + }, + { + MethodName: "GetMembers", + Handler: _ClusterSyncService_GetMembers_Handler, + }, + { + MethodName: "UpdateMembership", + Handler: _ClusterSyncService_UpdateMembership_Handler, + }, + { + MethodName: "HandleMembershipEvent", + Handler: _ClusterSyncService_HandleMembershipEvent_Handler, + }, + { + MethodName: "GetClusterStatus", + Handler: _ClusterSyncService_GetClusterStatus_Handler, + }, + { + MethodName: "GetMemberStatus", + Handler: _ClusterSyncService_GetMemberStatus_Handler, + }, + { + MethodName: "GetLatestSerial", + Handler: _ClusterSyncService_GetLatestSerial_Handler, + }, + { + MethodName: "GetEventsInRange", + Handler: _ClusterSyncService_GetEventsInRange_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "orlysync/cluster/v1/service.proto", +} diff --git a/pkg/proto/orlysync/common/v1/types.pb.go b/pkg/proto/orlysync/common/v1/types.pb.go new file mode 100644 index 0000000..4c75a77 --- /dev/null +++ b/pkg/proto/orlysync/common/v1/types.pb.go @@ -0,0 +1,827 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: orlysync/common/v1/types.proto + +package commonv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Empty is used for requests/responses with no data +type Empty struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Empty) Reset() { + *x = Empty{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Empty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Empty) ProtoMessage() {} + +func (x *Empty) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Empty.ProtoReflect.Descriptor instead. +func (*Empty) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{0} +} + +// ReadyResponse indicates if the service is ready +type ReadyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ready bool `protobuf:"varint,1,opt,name=ready,proto3" json:"ready,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReadyResponse) Reset() { + *x = ReadyResponse{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReadyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReadyResponse) ProtoMessage() {} + +func (x *ReadyResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReadyResponse.ProtoReflect.Descriptor instead. +func (*ReadyResponse) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{1} +} + +func (x *ReadyResponse) GetReady() bool { + if x != nil { + return x.Ready + } + return false +} + +// HTTPRequest wraps an HTTP request for proxy delegation +type HTTPRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Method string `protobuf:"bytes,1,opt,name=method,proto3" json:"method,omitempty"` + Path string `protobuf:"bytes,2,opt,name=path,proto3" json:"path,omitempty"` + Headers map[string]string `protobuf:"bytes,3,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"` + QueryString string `protobuf:"bytes,5,opt,name=query_string,json=queryString,proto3" json:"query_string,omitempty"` + RemoteAddr string `protobuf:"bytes,6,opt,name=remote_addr,json=remoteAddr,proto3" json:"remote_addr,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTTPRequest) Reset() { + *x = HTTPRequest{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTTPRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPRequest) ProtoMessage() {} + +func (x *HTTPRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPRequest.ProtoReflect.Descriptor instead. +func (*HTTPRequest) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{2} +} + +func (x *HTTPRequest) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *HTTPRequest) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *HTTPRequest) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HTTPRequest) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +func (x *HTTPRequest) GetQueryString() string { + if x != nil { + return x.QueryString + } + return "" +} + +func (x *HTTPRequest) GetRemoteAddr() string { + if x != nil { + return x.RemoteAddr + } + return "" +} + +// HTTPResponse wraps an HTTP response from proxy delegation +type HTTPResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Headers map[string]string `protobuf:"bytes,2,rep,name=headers,proto3" json:"headers,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Body []byte `protobuf:"bytes,3,opt,name=body,proto3" json:"body,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTTPResponse) Reset() { + *x = HTTPResponse{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTTPResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTTPResponse) ProtoMessage() {} + +func (x *HTTPResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTTPResponse.ProtoReflect.Descriptor instead. +func (*HTTPResponse) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{3} +} + +func (x *HTTPResponse) GetStatusCode() int32 { + if x != nil { + return x.StatusCode + } + return 0 +} + +func (x *HTTPResponse) GetHeaders() map[string]string { + if x != nil { + return x.Headers + } + return nil +} + +func (x *HTTPResponse) GetBody() []byte { + if x != nil { + return x.Body + } + return nil +} + +// Event represents a Nostr event (shared across sync services) +// Binary fields (id, pubkey, sig) are stored as raw bytes for efficiency +type Event struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id []byte `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // 32 bytes SHA256 hash + Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // 32 bytes public key + CreatedAt int64 `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` // UNIX timestamp + Kind uint32 `protobuf:"varint,4,opt,name=kind,proto3" json:"kind,omitempty"` // Event kind + Tags []*Tag `protobuf:"bytes,5,rep,name=tags,proto3" json:"tags,omitempty"` // Event tags + Content []byte `protobuf:"bytes,6,opt,name=content,proto3" json:"content,omitempty"` // Content (may be binary) + Sig []byte `protobuf:"bytes,7,opt,name=sig,proto3" json:"sig,omitempty"` // 64 bytes Schnorr signature + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Event) Reset() { + *x = Event{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Event) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Event) ProtoMessage() {} + +func (x *Event) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Event.ProtoReflect.Descriptor instead. +func (*Event) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{4} +} + +func (x *Event) GetId() []byte { + if x != nil { + return x.Id + } + return nil +} + +func (x *Event) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *Event) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *Event) GetKind() uint32 { + if x != nil { + return x.Kind + } + return 0 +} + +func (x *Event) GetTags() []*Tag { + if x != nil { + return x.Tags + } + return nil +} + +func (x *Event) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *Event) GetSig() []byte { + if x != nil { + return x.Sig + } + return nil +} + +// Tag represents a Nostr tag (array of byte slices) +type Tag struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values [][]byte `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Tag) Reset() { + *x = Tag{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Tag) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Tag) ProtoMessage() {} + +func (x *Tag) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Tag.ProtoReflect.Descriptor instead. +func (*Tag) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{5} +} + +func (x *Tag) GetValues() [][]byte { + if x != nil { + return x.Values + } + return nil +} + +// Filter represents a Nostr query filter (NIP-01) +type Filter struct { + state protoimpl.MessageState `protogen:"open.v1"` + Ids [][]byte `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"` // Event IDs to match (32 bytes each) + Kinds []uint32 `protobuf:"varint,2,rep,packed,name=kinds,proto3" json:"kinds,omitempty"` // Kinds to match + Authors [][]byte `protobuf:"bytes,3,rep,name=authors,proto3" json:"authors,omitempty"` // Author pubkeys (32 bytes each) + Tags map[string]*TagSet `protobuf:"bytes,4,rep,name=tags,proto3" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` // Tag filters (#e, #p, #t, etc.) + Since *int64 `protobuf:"varint,5,opt,name=since,proto3,oneof" json:"since,omitempty"` // Created after timestamp + Until *int64 `protobuf:"varint,6,opt,name=until,proto3,oneof" json:"until,omitempty"` // Created before timestamp + Search []byte `protobuf:"bytes,7,opt,name=search,proto3,oneof" json:"search,omitempty"` // Full-text search query (NIP-50) + Limit *uint32 `protobuf:"varint,8,opt,name=limit,proto3,oneof" json:"limit,omitempty"` // Max results + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Filter) Reset() { + *x = Filter{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Filter) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Filter) ProtoMessage() {} + +func (x *Filter) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Filter.ProtoReflect.Descriptor instead. +func (*Filter) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{6} +} + +func (x *Filter) GetIds() [][]byte { + if x != nil { + return x.Ids + } + return nil +} + +func (x *Filter) GetKinds() []uint32 { + if x != nil { + return x.Kinds + } + return nil +} + +func (x *Filter) GetAuthors() [][]byte { + if x != nil { + return x.Authors + } + return nil +} + +func (x *Filter) GetTags() map[string]*TagSet { + if x != nil { + return x.Tags + } + return nil +} + +func (x *Filter) GetSince() int64 { + if x != nil && x.Since != nil { + return *x.Since + } + return 0 +} + +func (x *Filter) GetUntil() int64 { + if x != nil && x.Until != nil { + return *x.Until + } + return 0 +} + +func (x *Filter) GetSearch() []byte { + if x != nil { + return x.Search + } + return nil +} + +func (x *Filter) GetLimit() uint32 { + if x != nil && x.Limit != nil { + return *x.Limit + } + return 0 +} + +// TagSet represents a set of tag values for filtering +type TagSet struct { + state protoimpl.MessageState `protogen:"open.v1"` + Values [][]byte `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagSet) Reset() { + *x = TagSet{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagSet) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagSet) ProtoMessage() {} + +func (x *TagSet) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagSet.ProtoReflect.Descriptor instead. +func (*TagSet) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{7} +} + +func (x *TagSet) GetValues() [][]byte { + if x != nil { + return x.Values + } + return nil +} + +// SyncInfo provides general sync service information +type SyncInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Node identity (npub/hex pubkey) + RelayUrl string `protobuf:"bytes,2,opt,name=relay_url,json=relayUrl,proto3" json:"relay_url,omitempty"` // This relay's URL + CurrentSerial uint64 `protobuf:"varint,3,opt,name=current_serial,json=currentSerial,proto3" json:"current_serial,omitempty"` // Current highest serial number + PeerCount int32 `protobuf:"varint,4,opt,name=peer_count,json=peerCount,proto3" json:"peer_count,omitempty"` // Number of configured peers + Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"` // Service status + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncInfo) Reset() { + *x = SyncInfo{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncInfo) ProtoMessage() {} + +func (x *SyncInfo) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncInfo.ProtoReflect.Descriptor instead. +func (*SyncInfo) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{8} +} + +func (x *SyncInfo) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *SyncInfo) GetRelayUrl() string { + if x != nil { + return x.RelayUrl + } + return "" +} + +func (x *SyncInfo) GetCurrentSerial() uint64 { + if x != nil { + return x.CurrentSerial + } + return 0 +} + +func (x *SyncInfo) GetPeerCount() int32 { + if x != nil { + return x.PeerCount + } + return 0 +} + +func (x *SyncInfo) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +// PeerInfo represents information about a sync peer +type PeerInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + LastSerial uint64 `protobuf:"varint,2,opt,name=last_serial,json=lastSerial,proto3" json:"last_serial,omitempty"` + Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"` // "active", "error", "unknown" + LastPoll int64 `protobuf:"varint,4,opt,name=last_poll,json=lastPoll,proto3" json:"last_poll,omitempty"` // Unix timestamp of last poll + ErrorCount int32 `protobuf:"varint,5,opt,name=error_count,json=errorCount,proto3" json:"error_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerInfo) Reset() { + *x = PeerInfo{} + mi := &file_orlysync_common_v1_types_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerInfo) ProtoMessage() {} + +func (x *PeerInfo) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_common_v1_types_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerInfo.ProtoReflect.Descriptor instead. +func (*PeerInfo) Descriptor() ([]byte, []int) { + return file_orlysync_common_v1_types_proto_rawDescGZIP(), []int{9} +} + +func (x *PeerInfo) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *PeerInfo) GetLastSerial() uint64 { + if x != nil { + return x.LastSerial + } + return 0 +} + +func (x *PeerInfo) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PeerInfo) GetLastPoll() int64 { + if x != nil { + return x.LastPoll + } + return 0 +} + +func (x *PeerInfo) GetErrorCount() int32 { + if x != nil { + return x.ErrorCount + } + return 0 +} + +var File_orlysync_common_v1_types_proto protoreflect.FileDescriptor + +const file_orlysync_common_v1_types_proto_rawDesc = "" + + "\n" + + "\x1eorlysync/common/v1/types.proto\x12\x12orlysync.common.v1\"\a\n" + + "\x05Empty\"%\n" + + "\rReadyResponse\x12\x14\n" + + "\x05ready\x18\x01 \x01(\bR\x05ready\"\x95\x02\n" + + "\vHTTPRequest\x12\x16\n" + + "\x06method\x18\x01 \x01(\tR\x06method\x12\x12\n" + + "\x04path\x18\x02 \x01(\tR\x04path\x12F\n" + + "\aheaders\x18\x03 \x03(\v2,.orlysync.common.v1.HTTPRequest.HeadersEntryR\aheaders\x12\x12\n" + + "\x04body\x18\x04 \x01(\fR\x04body\x12!\n" + + "\fquery_string\x18\x05 \x01(\tR\vqueryString\x12\x1f\n" + + "\vremote_addr\x18\x06 \x01(\tR\n" + + "remoteAddr\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xc8\x01\n" + + "\fHTTPResponse\x12\x1f\n" + + "\vstatus_code\x18\x01 \x01(\x05R\n" + + "statusCode\x12G\n" + + "\aheaders\x18\x02 \x03(\v2-.orlysync.common.v1.HTTPResponse.HeadersEntryR\aheaders\x12\x12\n" + + "\x04body\x18\x03 \x01(\fR\x04body\x1a:\n" + + "\fHeadersEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbb\x01\n" + + "\x05Event\x12\x0e\n" + + "\x02id\x18\x01 \x01(\fR\x02id\x12\x16\n" + + "\x06pubkey\x18\x02 \x01(\fR\x06pubkey\x12\x1d\n" + + "\n" + + "created_at\x18\x03 \x01(\x03R\tcreatedAt\x12\x12\n" + + "\x04kind\x18\x04 \x01(\rR\x04kind\x12+\n" + + "\x04tags\x18\x05 \x03(\v2\x17.orlysync.common.v1.TagR\x04tags\x12\x18\n" + + "\acontent\x18\x06 \x01(\fR\acontent\x12\x10\n" + + "\x03sig\x18\a \x01(\fR\x03sig\"\x1d\n" + + "\x03Tag\x12\x16\n" + + "\x06values\x18\x01 \x03(\fR\x06values\"\xf0\x02\n" + + "\x06Filter\x12\x10\n" + + "\x03ids\x18\x01 \x03(\fR\x03ids\x12\x14\n" + + "\x05kinds\x18\x02 \x03(\rR\x05kinds\x12\x18\n" + + "\aauthors\x18\x03 \x03(\fR\aauthors\x128\n" + + "\x04tags\x18\x04 \x03(\v2$.orlysync.common.v1.Filter.TagsEntryR\x04tags\x12\x19\n" + + "\x05since\x18\x05 \x01(\x03H\x00R\x05since\x88\x01\x01\x12\x19\n" + + "\x05until\x18\x06 \x01(\x03H\x01R\x05until\x88\x01\x01\x12\x1b\n" + + "\x06search\x18\a \x01(\fH\x02R\x06search\x88\x01\x01\x12\x19\n" + + "\x05limit\x18\b \x01(\rH\x03R\x05limit\x88\x01\x01\x1aS\n" + + "\tTagsEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x120\n" + + "\x05value\x18\x02 \x01(\v2\x1a.orlysync.common.v1.TagSetR\x05value:\x028\x01B\b\n" + + "\x06_sinceB\b\n" + + "\x06_untilB\t\n" + + "\a_searchB\b\n" + + "\x06_limit\" \n" + + "\x06TagSet\x12\x16\n" + + "\x06values\x18\x01 \x03(\fR\x06values\"\x9e\x01\n" + + "\bSyncInfo\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1b\n" + + "\trelay_url\x18\x02 \x01(\tR\brelayUrl\x12%\n" + + "\x0ecurrent_serial\x18\x03 \x01(\x04R\rcurrentSerial\x12\x1d\n" + + "\n" + + "peer_count\x18\x04 \x01(\x05R\tpeerCount\x12\x16\n" + + "\x06status\x18\x05 \x01(\tR\x06status\"\x93\x01\n" + + "\bPeerInfo\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1f\n" + + "\vlast_serial\x18\x02 \x01(\x04R\n" + + "lastSerial\x12\x16\n" + + "\x06status\x18\x03 \x01(\tR\x06status\x12\x1b\n" + + "\tlast_poll\x18\x04 \x01(\x03R\blastPoll\x12\x1f\n" + + "\verror_count\x18\x05 \x01(\x05R\n" + + "errorCountB5Z3next.orly.dev/pkg/proto/orlysync/common/v1;commonv1b\x06proto3" + +var ( + file_orlysync_common_v1_types_proto_rawDescOnce sync.Once + file_orlysync_common_v1_types_proto_rawDescData []byte +) + +func file_orlysync_common_v1_types_proto_rawDescGZIP() []byte { + file_orlysync_common_v1_types_proto_rawDescOnce.Do(func() { + file_orlysync_common_v1_types_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_orlysync_common_v1_types_proto_rawDesc), len(file_orlysync_common_v1_types_proto_rawDesc))) + }) + return file_orlysync_common_v1_types_proto_rawDescData +} + +var file_orlysync_common_v1_types_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_orlysync_common_v1_types_proto_goTypes = []any{ + (*Empty)(nil), // 0: orlysync.common.v1.Empty + (*ReadyResponse)(nil), // 1: orlysync.common.v1.ReadyResponse + (*HTTPRequest)(nil), // 2: orlysync.common.v1.HTTPRequest + (*HTTPResponse)(nil), // 3: orlysync.common.v1.HTTPResponse + (*Event)(nil), // 4: orlysync.common.v1.Event + (*Tag)(nil), // 5: orlysync.common.v1.Tag + (*Filter)(nil), // 6: orlysync.common.v1.Filter + (*TagSet)(nil), // 7: orlysync.common.v1.TagSet + (*SyncInfo)(nil), // 8: orlysync.common.v1.SyncInfo + (*PeerInfo)(nil), // 9: orlysync.common.v1.PeerInfo + nil, // 10: orlysync.common.v1.HTTPRequest.HeadersEntry + nil, // 11: orlysync.common.v1.HTTPResponse.HeadersEntry + nil, // 12: orlysync.common.v1.Filter.TagsEntry +} +var file_orlysync_common_v1_types_proto_depIdxs = []int32{ + 10, // 0: orlysync.common.v1.HTTPRequest.headers:type_name -> orlysync.common.v1.HTTPRequest.HeadersEntry + 11, // 1: orlysync.common.v1.HTTPResponse.headers:type_name -> orlysync.common.v1.HTTPResponse.HeadersEntry + 5, // 2: orlysync.common.v1.Event.tags:type_name -> orlysync.common.v1.Tag + 12, // 3: orlysync.common.v1.Filter.tags:type_name -> orlysync.common.v1.Filter.TagsEntry + 7, // 4: orlysync.common.v1.Filter.TagsEntry.value:type_name -> orlysync.common.v1.TagSet + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_orlysync_common_v1_types_proto_init() } +func file_orlysync_common_v1_types_proto_init() { + if File_orlysync_common_v1_types_proto != nil { + return + } + file_orlysync_common_v1_types_proto_msgTypes[6].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlysync_common_v1_types_proto_rawDesc), len(file_orlysync_common_v1_types_proto_rawDesc)), + NumEnums: 0, + NumMessages: 13, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_orlysync_common_v1_types_proto_goTypes, + DependencyIndexes: file_orlysync_common_v1_types_proto_depIdxs, + MessageInfos: file_orlysync_common_v1_types_proto_msgTypes, + }.Build() + File_orlysync_common_v1_types_proto = out.File + file_orlysync_common_v1_types_proto_goTypes = nil + file_orlysync_common_v1_types_proto_depIdxs = nil +} diff --git a/pkg/proto/orlysync/distributed/v1/service.pb.go b/pkg/proto/orlysync/distributed/v1/service.pb.go new file mode 100644 index 0000000..eea6b42 --- /dev/null +++ b/pkg/proto/orlysync/distributed/v1/service.pb.go @@ -0,0 +1,790 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: orlysync/distributed/v1/service.proto + +package distributedv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// CurrentRequest is sent to request current serial number +type CurrentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Requesting node's identity + RelayUrl string `protobuf:"bytes,2,opt,name=relay_url,json=relayUrl,proto3" json:"relay_url,omitempty"` // Requesting relay's URL + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CurrentRequest) Reset() { + *x = CurrentRequest{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CurrentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CurrentRequest) ProtoMessage() {} + +func (x *CurrentRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CurrentRequest.ProtoReflect.Descriptor instead. +func (*CurrentRequest) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{0} +} + +func (x *CurrentRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *CurrentRequest) GetRelayUrl() string { + if x != nil { + return x.RelayUrl + } + return "" +} + +// CurrentResponse contains the current serial number +type CurrentResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Responding node's identity + RelayUrl string `protobuf:"bytes,2,opt,name=relay_url,json=relayUrl,proto3" json:"relay_url,omitempty"` // Responding relay's URL + Serial uint64 `protobuf:"varint,3,opt,name=serial,proto3" json:"serial,omitempty"` // Current serial number + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CurrentResponse) Reset() { + *x = CurrentResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CurrentResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CurrentResponse) ProtoMessage() {} + +func (x *CurrentResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CurrentResponse.ProtoReflect.Descriptor instead. +func (*CurrentResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CurrentResponse) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *CurrentResponse) GetRelayUrl() string { + if x != nil { + return x.RelayUrl + } + return "" +} + +func (x *CurrentResponse) GetSerial() uint64 { + if x != nil { + return x.Serial + } + return 0 +} + +// EventIDsRequest requests event IDs in a serial range +type EventIDsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + NodeId string `protobuf:"bytes,1,opt,name=node_id,json=nodeId,proto3" json:"node_id,omitempty"` // Requesting node's identity + RelayUrl string `protobuf:"bytes,2,opt,name=relay_url,json=relayUrl,proto3" json:"relay_url,omitempty"` // Requesting relay's URL + From uint64 `protobuf:"varint,3,opt,name=from,proto3" json:"from,omitempty"` // Start serial (inclusive) + To uint64 `protobuf:"varint,4,opt,name=to,proto3" json:"to,omitempty"` // End serial (inclusive) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventIDsRequest) Reset() { + *x = EventIDsRequest{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventIDsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventIDsRequest) ProtoMessage() {} + +func (x *EventIDsRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventIDsRequest.ProtoReflect.Descriptor instead. +func (*EventIDsRequest) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *EventIDsRequest) GetNodeId() string { + if x != nil { + return x.NodeId + } + return "" +} + +func (x *EventIDsRequest) GetRelayUrl() string { + if x != nil { + return x.RelayUrl + } + return "" +} + +func (x *EventIDsRequest) GetFrom() uint64 { + if x != nil { + return x.From + } + return 0 +} + +func (x *EventIDsRequest) GetTo() uint64 { + if x != nil { + return x.To + } + return 0 +} + +// EventIDsResponse contains event IDs mapped to serial numbers +type EventIDsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + EventMap map[string]uint64 `protobuf:"bytes,1,rep,name=event_map,json=eventMap,proto3" json:"event_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` // event_id (hex) -> serial + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EventIDsResponse) Reset() { + *x = EventIDsResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EventIDsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EventIDsResponse) ProtoMessage() {} + +func (x *EventIDsResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EventIDsResponse.ProtoReflect.Descriptor instead. +func (*EventIDsResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *EventIDsResponse) GetEventMap() map[string]uint64 { + if x != nil { + return x.EventMap + } + return nil +} + +// PeersResponse contains the list of sync peers +type PeersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Peers []string `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` // List of peer relay URLs + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeersResponse) Reset() { + *x = PeersResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeersResponse) ProtoMessage() {} + +func (x *PeersResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeersResponse.ProtoReflect.Descriptor instead. +func (*PeersResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *PeersResponse) GetPeers() []string { + if x != nil { + return x.Peers + } + return nil +} + +// UpdatePeersRequest updates the peer list +type UpdatePeersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Peers []string `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` // New list of peer relay URLs + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdatePeersRequest) Reset() { + *x = UpdatePeersRequest{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdatePeersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdatePeersRequest) ProtoMessage() {} + +func (x *UpdatePeersRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdatePeersRequest.ProtoReflect.Descriptor instead. +func (*UpdatePeersRequest) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdatePeersRequest) GetPeers() []string { + if x != nil { + return x.Peers + } + return nil +} + +// AuthorizedPeerRequest checks if a peer is authorized +type AuthorizedPeerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + ExpectedPubkey string `protobuf:"bytes,2,opt,name=expected_pubkey,json=expectedPubkey,proto3" json:"expected_pubkey,omitempty"` // Expected NIP-11 pubkey + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizedPeerRequest) Reset() { + *x = AuthorizedPeerRequest{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizedPeerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizedPeerRequest) ProtoMessage() {} + +func (x *AuthorizedPeerRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizedPeerRequest.ProtoReflect.Descriptor instead. +func (*AuthorizedPeerRequest) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *AuthorizedPeerRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +func (x *AuthorizedPeerRequest) GetExpectedPubkey() string { + if x != nil { + return x.ExpectedPubkey + } + return "" +} + +// AuthorizedPeerResponse indicates if the peer is authorized +type AuthorizedPeerResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizedPeerResponse) Reset() { + *x = AuthorizedPeerResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizedPeerResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizedPeerResponse) ProtoMessage() {} + +func (x *AuthorizedPeerResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizedPeerResponse.ProtoReflect.Descriptor instead. +func (*AuthorizedPeerResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *AuthorizedPeerResponse) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +// PeerPubkeyRequest requests the pubkey for a peer +type PeerPubkeyRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerPubkeyRequest) Reset() { + *x = PeerPubkeyRequest{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerPubkeyRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerPubkeyRequest) ProtoMessage() {} + +func (x *PeerPubkeyRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerPubkeyRequest.ProtoReflect.Descriptor instead. +func (*PeerPubkeyRequest) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *PeerPubkeyRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +// PeerPubkeyResponse contains the peer's pubkey +type PeerPubkeyResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pubkey string `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // Peer's NIP-11 pubkey (hex or npub) + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerPubkeyResponse) Reset() { + *x = PeerPubkeyResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerPubkeyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerPubkeyResponse) ProtoMessage() {} + +func (x *PeerPubkeyResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerPubkeyResponse.ProtoReflect.Descriptor instead. +func (*PeerPubkeyResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *PeerPubkeyResponse) GetPubkey() string { + if x != nil { + return x.Pubkey + } + return "" +} + +// NewEventNotification notifies of a new event +type NewEventNotification struct { + state protoimpl.MessageState `protogen:"open.v1"` + EventId []byte `protobuf:"bytes,1,opt,name=event_id,json=eventId,proto3" json:"event_id,omitempty"` // 32 bytes event ID + Serial uint64 `protobuf:"varint,2,opt,name=serial,proto3" json:"serial,omitempty"` // Assigned serial number + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NewEventNotification) Reset() { + *x = NewEventNotification{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NewEventNotification) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NewEventNotification) ProtoMessage() {} + +func (x *NewEventNotification) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NewEventNotification.ProtoReflect.Descriptor instead. +func (*NewEventNotification) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{10} +} + +func (x *NewEventNotification) GetEventId() []byte { + if x != nil { + return x.EventId + } + return nil +} + +func (x *NewEventNotification) GetSerial() uint64 { + if x != nil { + return x.Serial + } + return 0 +} + +// SyncStatusResponse contains sync status for all peers +type SyncStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + CurrentSerial uint64 `protobuf:"varint,1,opt,name=current_serial,json=currentSerial,proto3" json:"current_serial,omitempty"` + Peers []*v1.PeerInfo `protobuf:"bytes,2,rep,name=peers,proto3" json:"peers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStatusResponse) Reset() { + *x = SyncStatusResponse{} + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStatusResponse) ProtoMessage() {} + +func (x *SyncStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_distributed_v1_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStatusResponse.ProtoReflect.Descriptor instead. +func (*SyncStatusResponse) Descriptor() ([]byte, []int) { + return file_orlysync_distributed_v1_service_proto_rawDescGZIP(), []int{11} +} + +func (x *SyncStatusResponse) GetCurrentSerial() uint64 { + if x != nil { + return x.CurrentSerial + } + return 0 +} + +func (x *SyncStatusResponse) GetPeers() []*v1.PeerInfo { + if x != nil { + return x.Peers + } + return nil +} + +var File_orlysync_distributed_v1_service_proto protoreflect.FileDescriptor + +const file_orlysync_distributed_v1_service_proto_rawDesc = "" + + "\n" + + "%orlysync/distributed/v1/service.proto\x12\x17orlysync.distributed.v1\x1a\x1eorlysync/common/v1/types.proto\"F\n" + + "\x0eCurrentRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1b\n" + + "\trelay_url\x18\x02 \x01(\tR\brelayUrl\"_\n" + + "\x0fCurrentResponse\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1b\n" + + "\trelay_url\x18\x02 \x01(\tR\brelayUrl\x12\x16\n" + + "\x06serial\x18\x03 \x01(\x04R\x06serial\"k\n" + + "\x0fEventIDsRequest\x12\x17\n" + + "\anode_id\x18\x01 \x01(\tR\x06nodeId\x12\x1b\n" + + "\trelay_url\x18\x02 \x01(\tR\brelayUrl\x12\x12\n" + + "\x04from\x18\x03 \x01(\x04R\x04from\x12\x0e\n" + + "\x02to\x18\x04 \x01(\x04R\x02to\"\xa5\x01\n" + + "\x10EventIDsResponse\x12T\n" + + "\tevent_map\x18\x01 \x03(\v27.orlysync.distributed.v1.EventIDsResponse.EventMapEntryR\beventMap\x1a;\n" + + "\rEventMapEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x04R\x05value:\x028\x01\"%\n" + + "\rPeersResponse\x12\x14\n" + + "\x05peers\x18\x01 \x03(\tR\x05peers\"*\n" + + "\x12UpdatePeersRequest\x12\x14\n" + + "\x05peers\x18\x01 \x03(\tR\x05peers\"[\n" + + "\x15AuthorizedPeerRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\x12'\n" + + "\x0fexpected_pubkey\x18\x02 \x01(\tR\x0eexpectedPubkey\"8\n" + + "\x16AuthorizedPeerResponse\x12\x1e\n" + + "\n" + + "authorized\x18\x01 \x01(\bR\n" + + "authorized\".\n" + + "\x11PeerPubkeyRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\",\n" + + "\x12PeerPubkeyResponse\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\tR\x06pubkey\"I\n" + + "\x14NewEventNotification\x12\x19\n" + + "\bevent_id\x18\x01 \x01(\fR\aeventId\x12\x16\n" + + "\x06serial\x18\x02 \x01(\x04R\x06serial\"o\n" + + "\x12SyncStatusResponse\x12%\n" + + "\x0ecurrent_serial\x18\x01 \x01(\x04R\rcurrentSerial\x122\n" + + "\x05peers\x18\x02 \x03(\v2\x1c.orlysync.common.v1.PeerInfoR\x05peers2\xea\t\n" + + "\x16DistributedSyncService\x12E\n" + + "\x05Ready\x12\x19.orlysync.common.v1.Empty\x1a!.orlysync.common.v1.ReadyResponse\x12B\n" + + "\aGetInfo\x12\x19.orlysync.common.v1.Empty\x1a\x1c.orlysync.common.v1.SyncInfo\x12e\n" + + "\x10GetCurrentSerial\x12'.orlysync.distributed.v1.CurrentRequest\x1a(.orlysync.distributed.v1.CurrentResponse\x12b\n" + + "\vGetEventIDs\x12(.orlysync.distributed.v1.EventIDsRequest\x1a).orlysync.distributed.v1.EventIDsResponse\x12Y\n" + + "\x14HandleCurrentRequest\x12\x1f.orlysync.common.v1.HTTPRequest\x1a .orlysync.common.v1.HTTPResponse\x12Z\n" + + "\x15HandleEventIDsRequest\x12\x1f.orlysync.common.v1.HTTPRequest\x1a .orlysync.common.v1.HTTPResponse\x12M\n" + + "\bGetPeers\x12\x19.orlysync.common.v1.Empty\x1a&.orlysync.distributed.v1.PeersResponse\x12U\n" + + "\vUpdatePeers\x12+.orlysync.distributed.v1.UpdatePeersRequest\x1a\x19.orlysync.common.v1.Empty\x12s\n" + + "\x10IsAuthorizedPeer\x12..orlysync.distributed.v1.AuthorizedPeerRequest\x1a/.orlysync.distributed.v1.AuthorizedPeerResponse\x12h\n" + + "\rGetPeerPubkey\x12*.orlysync.distributed.v1.PeerPubkeyRequest\x1a+.orlysync.distributed.v1.PeerPubkeyResponse\x12D\n" + + "\fUpdateSerial\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12Z\n" + + "\x0eNotifyNewEvent\x12-.orlysync.distributed.v1.NewEventNotification\x1a\x19.orlysync.common.v1.Empty\x12C\n" + + "\vTriggerSync\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12W\n" + + "\rGetSyncStatus\x12\x19.orlysync.common.v1.Empty\x1a+.orlysync.distributed.v1.SyncStatusResponseB?Z=next.orly.dev/pkg/proto/orlysync/distributed/v1;distributedv1b\x06proto3" + +var ( + file_orlysync_distributed_v1_service_proto_rawDescOnce sync.Once + file_orlysync_distributed_v1_service_proto_rawDescData []byte +) + +func file_orlysync_distributed_v1_service_proto_rawDescGZIP() []byte { + file_orlysync_distributed_v1_service_proto_rawDescOnce.Do(func() { + file_orlysync_distributed_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_orlysync_distributed_v1_service_proto_rawDesc), len(file_orlysync_distributed_v1_service_proto_rawDesc))) + }) + return file_orlysync_distributed_v1_service_proto_rawDescData +} + +var file_orlysync_distributed_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 13) +var file_orlysync_distributed_v1_service_proto_goTypes = []any{ + (*CurrentRequest)(nil), // 0: orlysync.distributed.v1.CurrentRequest + (*CurrentResponse)(nil), // 1: orlysync.distributed.v1.CurrentResponse + (*EventIDsRequest)(nil), // 2: orlysync.distributed.v1.EventIDsRequest + (*EventIDsResponse)(nil), // 3: orlysync.distributed.v1.EventIDsResponse + (*PeersResponse)(nil), // 4: orlysync.distributed.v1.PeersResponse + (*UpdatePeersRequest)(nil), // 5: orlysync.distributed.v1.UpdatePeersRequest + (*AuthorizedPeerRequest)(nil), // 6: orlysync.distributed.v1.AuthorizedPeerRequest + (*AuthorizedPeerResponse)(nil), // 7: orlysync.distributed.v1.AuthorizedPeerResponse + (*PeerPubkeyRequest)(nil), // 8: orlysync.distributed.v1.PeerPubkeyRequest + (*PeerPubkeyResponse)(nil), // 9: orlysync.distributed.v1.PeerPubkeyResponse + (*NewEventNotification)(nil), // 10: orlysync.distributed.v1.NewEventNotification + (*SyncStatusResponse)(nil), // 11: orlysync.distributed.v1.SyncStatusResponse + nil, // 12: orlysync.distributed.v1.EventIDsResponse.EventMapEntry + (*v1.PeerInfo)(nil), // 13: orlysync.common.v1.PeerInfo + (*v1.Empty)(nil), // 14: orlysync.common.v1.Empty + (*v1.HTTPRequest)(nil), // 15: orlysync.common.v1.HTTPRequest + (*v1.ReadyResponse)(nil), // 16: orlysync.common.v1.ReadyResponse + (*v1.SyncInfo)(nil), // 17: orlysync.common.v1.SyncInfo + (*v1.HTTPResponse)(nil), // 18: orlysync.common.v1.HTTPResponse +} +var file_orlysync_distributed_v1_service_proto_depIdxs = []int32{ + 12, // 0: orlysync.distributed.v1.EventIDsResponse.event_map:type_name -> orlysync.distributed.v1.EventIDsResponse.EventMapEntry + 13, // 1: orlysync.distributed.v1.SyncStatusResponse.peers:type_name -> orlysync.common.v1.PeerInfo + 14, // 2: orlysync.distributed.v1.DistributedSyncService.Ready:input_type -> orlysync.common.v1.Empty + 14, // 3: orlysync.distributed.v1.DistributedSyncService.GetInfo:input_type -> orlysync.common.v1.Empty + 0, // 4: orlysync.distributed.v1.DistributedSyncService.GetCurrentSerial:input_type -> orlysync.distributed.v1.CurrentRequest + 2, // 5: orlysync.distributed.v1.DistributedSyncService.GetEventIDs:input_type -> orlysync.distributed.v1.EventIDsRequest + 15, // 6: orlysync.distributed.v1.DistributedSyncService.HandleCurrentRequest:input_type -> orlysync.common.v1.HTTPRequest + 15, // 7: orlysync.distributed.v1.DistributedSyncService.HandleEventIDsRequest:input_type -> orlysync.common.v1.HTTPRequest + 14, // 8: orlysync.distributed.v1.DistributedSyncService.GetPeers:input_type -> orlysync.common.v1.Empty + 5, // 9: orlysync.distributed.v1.DistributedSyncService.UpdatePeers:input_type -> orlysync.distributed.v1.UpdatePeersRequest + 6, // 10: orlysync.distributed.v1.DistributedSyncService.IsAuthorizedPeer:input_type -> orlysync.distributed.v1.AuthorizedPeerRequest + 8, // 11: orlysync.distributed.v1.DistributedSyncService.GetPeerPubkey:input_type -> orlysync.distributed.v1.PeerPubkeyRequest + 14, // 12: orlysync.distributed.v1.DistributedSyncService.UpdateSerial:input_type -> orlysync.common.v1.Empty + 10, // 13: orlysync.distributed.v1.DistributedSyncService.NotifyNewEvent:input_type -> orlysync.distributed.v1.NewEventNotification + 14, // 14: orlysync.distributed.v1.DistributedSyncService.TriggerSync:input_type -> orlysync.common.v1.Empty + 14, // 15: orlysync.distributed.v1.DistributedSyncService.GetSyncStatus:input_type -> orlysync.common.v1.Empty + 16, // 16: orlysync.distributed.v1.DistributedSyncService.Ready:output_type -> orlysync.common.v1.ReadyResponse + 17, // 17: orlysync.distributed.v1.DistributedSyncService.GetInfo:output_type -> orlysync.common.v1.SyncInfo + 1, // 18: orlysync.distributed.v1.DistributedSyncService.GetCurrentSerial:output_type -> orlysync.distributed.v1.CurrentResponse + 3, // 19: orlysync.distributed.v1.DistributedSyncService.GetEventIDs:output_type -> orlysync.distributed.v1.EventIDsResponse + 18, // 20: orlysync.distributed.v1.DistributedSyncService.HandleCurrentRequest:output_type -> orlysync.common.v1.HTTPResponse + 18, // 21: orlysync.distributed.v1.DistributedSyncService.HandleEventIDsRequest:output_type -> orlysync.common.v1.HTTPResponse + 4, // 22: orlysync.distributed.v1.DistributedSyncService.GetPeers:output_type -> orlysync.distributed.v1.PeersResponse + 14, // 23: orlysync.distributed.v1.DistributedSyncService.UpdatePeers:output_type -> orlysync.common.v1.Empty + 7, // 24: orlysync.distributed.v1.DistributedSyncService.IsAuthorizedPeer:output_type -> orlysync.distributed.v1.AuthorizedPeerResponse + 9, // 25: orlysync.distributed.v1.DistributedSyncService.GetPeerPubkey:output_type -> orlysync.distributed.v1.PeerPubkeyResponse + 14, // 26: orlysync.distributed.v1.DistributedSyncService.UpdateSerial:output_type -> orlysync.common.v1.Empty + 14, // 27: orlysync.distributed.v1.DistributedSyncService.NotifyNewEvent:output_type -> orlysync.common.v1.Empty + 14, // 28: orlysync.distributed.v1.DistributedSyncService.TriggerSync:output_type -> orlysync.common.v1.Empty + 11, // 29: orlysync.distributed.v1.DistributedSyncService.GetSyncStatus:output_type -> orlysync.distributed.v1.SyncStatusResponse + 16, // [16:30] is the sub-list for method output_type + 2, // [2:16] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_orlysync_distributed_v1_service_proto_init() } +func file_orlysync_distributed_v1_service_proto_init() { + if File_orlysync_distributed_v1_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlysync_distributed_v1_service_proto_rawDesc), len(file_orlysync_distributed_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 13, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_orlysync_distributed_v1_service_proto_goTypes, + DependencyIndexes: file_orlysync_distributed_v1_service_proto_depIdxs, + MessageInfos: file_orlysync_distributed_v1_service_proto_msgTypes, + }.Build() + File_orlysync_distributed_v1_service_proto = out.File + file_orlysync_distributed_v1_service_proto_goTypes = nil + file_orlysync_distributed_v1_service_proto_depIdxs = nil +} diff --git a/pkg/proto/orlysync/distributed/v1/service_grpc.pb.go b/pkg/proto/orlysync/distributed/v1/service_grpc.pb.go new file mode 100644 index 0000000..4063b93 --- /dev/null +++ b/pkg/proto/orlysync/distributed/v1/service_grpc.pb.go @@ -0,0 +1,651 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: orlysync/distributed/v1/service.proto + +package distributedv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + DistributedSyncService_Ready_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/Ready" + DistributedSyncService_GetInfo_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetInfo" + DistributedSyncService_GetCurrentSerial_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetCurrentSerial" + DistributedSyncService_GetEventIDs_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetEventIDs" + DistributedSyncService_HandleCurrentRequest_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/HandleCurrentRequest" + DistributedSyncService_HandleEventIDsRequest_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/HandleEventIDsRequest" + DistributedSyncService_GetPeers_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetPeers" + DistributedSyncService_UpdatePeers_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/UpdatePeers" + DistributedSyncService_IsAuthorizedPeer_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/IsAuthorizedPeer" + DistributedSyncService_GetPeerPubkey_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetPeerPubkey" + DistributedSyncService_UpdateSerial_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/UpdateSerial" + DistributedSyncService_NotifyNewEvent_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/NotifyNewEvent" + DistributedSyncService_TriggerSync_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/TriggerSync" + DistributedSyncService_GetSyncStatus_FullMethodName = "/orlysync.distributed.v1.DistributedSyncService/GetSyncStatus" +) + +// DistributedSyncServiceClient is the client API for DistributedSyncService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// DistributedSyncService provides serial-based peer-to-peer synchronization +// between relay instances using HTTP polling +type DistributedSyncServiceClient interface { + // Ready returns whether the service is ready to serve requests + Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) + // GetInfo returns current sync service information + GetInfo(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.SyncInfo, error) + // GetCurrentSerial returns this relay's current serial number + GetCurrentSerial(ctx context.Context, in *CurrentRequest, opts ...grpc.CallOption) (*CurrentResponse, error) + // GetEventIDs returns event IDs for a serial range + GetEventIDs(ctx context.Context, in *EventIDsRequest, opts ...grpc.CallOption) (*EventIDsResponse, error) + // HandleCurrentRequest proxies /api/sync/current HTTP requests + HandleCurrentRequest(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) + // HandleEventIDsRequest proxies /api/sync/event-ids HTTP requests + HandleEventIDsRequest(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) + // GetPeers returns the current list of sync peers + GetPeers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*PeersResponse, error) + // UpdatePeers updates the peer list + UpdatePeers(ctx context.Context, in *UpdatePeersRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // IsAuthorizedPeer checks if a peer is authorized by validating its NIP-11 pubkey + IsAuthorizedPeer(ctx context.Context, in *AuthorizedPeerRequest, opts ...grpc.CallOption) (*AuthorizedPeerResponse, error) + // GetPeerPubkey fetches the pubkey for a peer relay via NIP-11 + GetPeerPubkey(ctx context.Context, in *PeerPubkeyRequest, opts ...grpc.CallOption) (*PeerPubkeyResponse, error) + // UpdateSerial updates the current serial from database + UpdateSerial(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // NotifyNewEvent notifies the service of a new event being stored + NotifyNewEvent(ctx context.Context, in *NewEventNotification, opts ...grpc.CallOption) (*v1.Empty, error) + // TriggerSync manually triggers a sync cycle with all peers + TriggerSync(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // GetSyncStatus returns current sync status for all peers + GetSyncStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*SyncStatusResponse, error) +} + +type distributedSyncServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewDistributedSyncServiceClient(cc grpc.ClientConnInterface) DistributedSyncServiceClient { + return &distributedSyncServiceClient{cc} +} + +func (c *distributedSyncServiceClient) Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.ReadyResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_Ready_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetInfo(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.SyncInfo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.SyncInfo) + err := c.cc.Invoke(ctx, DistributedSyncService_GetInfo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetCurrentSerial(ctx context.Context, in *CurrentRequest, opts ...grpc.CallOption) (*CurrentResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CurrentResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_GetCurrentSerial_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetEventIDs(ctx context.Context, in *EventIDsRequest, opts ...grpc.CallOption) (*EventIDsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(EventIDsResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_GetEventIDs_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) HandleCurrentRequest(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.HTTPResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_HandleCurrentRequest_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) HandleEventIDsRequest(ctx context.Context, in *v1.HTTPRequest, opts ...grpc.CallOption) (*v1.HTTPResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.HTTPResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_HandleEventIDsRequest_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetPeers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*PeersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PeersResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_GetPeers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) UpdatePeers(ctx context.Context, in *UpdatePeersRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, DistributedSyncService_UpdatePeers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) IsAuthorizedPeer(ctx context.Context, in *AuthorizedPeerRequest, opts ...grpc.CallOption) (*AuthorizedPeerResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthorizedPeerResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_IsAuthorizedPeer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetPeerPubkey(ctx context.Context, in *PeerPubkeyRequest, opts ...grpc.CallOption) (*PeerPubkeyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PeerPubkeyResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_GetPeerPubkey_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) UpdateSerial(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, DistributedSyncService_UpdateSerial_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) NotifyNewEvent(ctx context.Context, in *NewEventNotification, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, DistributedSyncService_NotifyNewEvent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) TriggerSync(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, DistributedSyncService_TriggerSync_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *distributedSyncServiceClient) GetSyncStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*SyncStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SyncStatusResponse) + err := c.cc.Invoke(ctx, DistributedSyncService_GetSyncStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// DistributedSyncServiceServer is the server API for DistributedSyncService service. +// All implementations must embed UnimplementedDistributedSyncServiceServer +// for forward compatibility. +// +// DistributedSyncService provides serial-based peer-to-peer synchronization +// between relay instances using HTTP polling +type DistributedSyncServiceServer interface { + // Ready returns whether the service is ready to serve requests + Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) + // GetInfo returns current sync service information + GetInfo(context.Context, *v1.Empty) (*v1.SyncInfo, error) + // GetCurrentSerial returns this relay's current serial number + GetCurrentSerial(context.Context, *CurrentRequest) (*CurrentResponse, error) + // GetEventIDs returns event IDs for a serial range + GetEventIDs(context.Context, *EventIDsRequest) (*EventIDsResponse, error) + // HandleCurrentRequest proxies /api/sync/current HTTP requests + HandleCurrentRequest(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) + // HandleEventIDsRequest proxies /api/sync/event-ids HTTP requests + HandleEventIDsRequest(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) + // GetPeers returns the current list of sync peers + GetPeers(context.Context, *v1.Empty) (*PeersResponse, error) + // UpdatePeers updates the peer list + UpdatePeers(context.Context, *UpdatePeersRequest) (*v1.Empty, error) + // IsAuthorizedPeer checks if a peer is authorized by validating its NIP-11 pubkey + IsAuthorizedPeer(context.Context, *AuthorizedPeerRequest) (*AuthorizedPeerResponse, error) + // GetPeerPubkey fetches the pubkey for a peer relay via NIP-11 + GetPeerPubkey(context.Context, *PeerPubkeyRequest) (*PeerPubkeyResponse, error) + // UpdateSerial updates the current serial from database + UpdateSerial(context.Context, *v1.Empty) (*v1.Empty, error) + // NotifyNewEvent notifies the service of a new event being stored + NotifyNewEvent(context.Context, *NewEventNotification) (*v1.Empty, error) + // TriggerSync manually triggers a sync cycle with all peers + TriggerSync(context.Context, *v1.Empty) (*v1.Empty, error) + // GetSyncStatus returns current sync status for all peers + GetSyncStatus(context.Context, *v1.Empty) (*SyncStatusResponse, error) + mustEmbedUnimplementedDistributedSyncServiceServer() +} + +// UnimplementedDistributedSyncServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedDistributedSyncServiceServer struct{} + +func (UnimplementedDistributedSyncServiceServer) Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ready not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetInfo(context.Context, *v1.Empty) (*v1.SyncInfo, error) { + return nil, status.Error(codes.Unimplemented, "method GetInfo not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetCurrentSerial(context.Context, *CurrentRequest) (*CurrentResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetCurrentSerial not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetEventIDs(context.Context, *EventIDsRequest) (*EventIDsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetEventIDs not implemented") +} +func (UnimplementedDistributedSyncServiceServer) HandleCurrentRequest(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleCurrentRequest not implemented") +} +func (UnimplementedDistributedSyncServiceServer) HandleEventIDsRequest(context.Context, *v1.HTTPRequest) (*v1.HTTPResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleEventIDsRequest not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetPeers(context.Context, *v1.Empty) (*PeersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPeers not implemented") +} +func (UnimplementedDistributedSyncServiceServer) UpdatePeers(context.Context, *UpdatePeersRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method UpdatePeers not implemented") +} +func (UnimplementedDistributedSyncServiceServer) IsAuthorizedPeer(context.Context, *AuthorizedPeerRequest) (*AuthorizedPeerResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsAuthorizedPeer not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetPeerPubkey(context.Context, *PeerPubkeyRequest) (*PeerPubkeyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPeerPubkey not implemented") +} +func (UnimplementedDistributedSyncServiceServer) UpdateSerial(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateSerial not implemented") +} +func (UnimplementedDistributedSyncServiceServer) NotifyNewEvent(context.Context, *NewEventNotification) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method NotifyNewEvent not implemented") +} +func (UnimplementedDistributedSyncServiceServer) TriggerSync(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerSync not implemented") +} +func (UnimplementedDistributedSyncServiceServer) GetSyncStatus(context.Context, *v1.Empty) (*SyncStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSyncStatus not implemented") +} +func (UnimplementedDistributedSyncServiceServer) mustEmbedUnimplementedDistributedSyncServiceServer() { +} +func (UnimplementedDistributedSyncServiceServer) testEmbeddedByValue() {} + +// UnsafeDistributedSyncServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to DistributedSyncServiceServer will +// result in compilation errors. +type UnsafeDistributedSyncServiceServer interface { + mustEmbedUnimplementedDistributedSyncServiceServer() +} + +func RegisterDistributedSyncServiceServer(s grpc.ServiceRegistrar, srv DistributedSyncServiceServer) { + // If the following call panics, it indicates UnimplementedDistributedSyncServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&DistributedSyncService_ServiceDesc, srv) +} + +func _DistributedSyncService_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).Ready(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_Ready_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).Ready(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetInfo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetInfo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetInfo(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetCurrentSerial_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CurrentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetCurrentSerial(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetCurrentSerial_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetCurrentSerial(ctx, req.(*CurrentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetEventIDs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(EventIDsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetEventIDs(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetEventIDs_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetEventIDs(ctx, req.(*EventIDsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_HandleCurrentRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.HTTPRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).HandleCurrentRequest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_HandleCurrentRequest_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).HandleCurrentRequest(ctx, req.(*v1.HTTPRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_HandleEventIDsRequest_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.HTTPRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).HandleEventIDsRequest(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_HandleEventIDsRequest_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).HandleEventIDsRequest(ctx, req.(*v1.HTTPRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetPeers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetPeers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetPeers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetPeers(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_UpdatePeers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdatePeersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).UpdatePeers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_UpdatePeers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).UpdatePeers(ctx, req.(*UpdatePeersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_IsAuthorizedPeer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthorizedPeerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).IsAuthorizedPeer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_IsAuthorizedPeer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).IsAuthorizedPeer(ctx, req.(*AuthorizedPeerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetPeerPubkey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PeerPubkeyRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetPeerPubkey(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetPeerPubkey_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetPeerPubkey(ctx, req.(*PeerPubkeyRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_UpdateSerial_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).UpdateSerial(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_UpdateSerial_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).UpdateSerial(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_NotifyNewEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NewEventNotification) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).NotifyNewEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_NotifyNewEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).NotifyNewEvent(ctx, req.(*NewEventNotification)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_TriggerSync_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).TriggerSync(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_TriggerSync_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).TriggerSync(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _DistributedSyncService_GetSyncStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(DistributedSyncServiceServer).GetSyncStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: DistributedSyncService_GetSyncStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(DistributedSyncServiceServer).GetSyncStatus(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +// DistributedSyncService_ServiceDesc is the grpc.ServiceDesc for DistributedSyncService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var DistributedSyncService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "orlysync.distributed.v1.DistributedSyncService", + HandlerType: (*DistributedSyncServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ready", + Handler: _DistributedSyncService_Ready_Handler, + }, + { + MethodName: "GetInfo", + Handler: _DistributedSyncService_GetInfo_Handler, + }, + { + MethodName: "GetCurrentSerial", + Handler: _DistributedSyncService_GetCurrentSerial_Handler, + }, + { + MethodName: "GetEventIDs", + Handler: _DistributedSyncService_GetEventIDs_Handler, + }, + { + MethodName: "HandleCurrentRequest", + Handler: _DistributedSyncService_HandleCurrentRequest_Handler, + }, + { + MethodName: "HandleEventIDsRequest", + Handler: _DistributedSyncService_HandleEventIDsRequest_Handler, + }, + { + MethodName: "GetPeers", + Handler: _DistributedSyncService_GetPeers_Handler, + }, + { + MethodName: "UpdatePeers", + Handler: _DistributedSyncService_UpdatePeers_Handler, + }, + { + MethodName: "IsAuthorizedPeer", + Handler: _DistributedSyncService_IsAuthorizedPeer_Handler, + }, + { + MethodName: "GetPeerPubkey", + Handler: _DistributedSyncService_GetPeerPubkey_Handler, + }, + { + MethodName: "UpdateSerial", + Handler: _DistributedSyncService_UpdateSerial_Handler, + }, + { + MethodName: "NotifyNewEvent", + Handler: _DistributedSyncService_NotifyNewEvent_Handler, + }, + { + MethodName: "TriggerSync", + Handler: _DistributedSyncService_TriggerSync_Handler, + }, + { + MethodName: "GetSyncStatus", + Handler: _DistributedSyncService_GetSyncStatus_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "orlysync/distributed/v1/service.proto", +} diff --git a/pkg/proto/orlysync/negentropy/v1/service.pb.go b/pkg/proto/orlysync/negentropy/v1/service.pb.go new file mode 100644 index 0000000..cf17adc --- /dev/null +++ b/pkg/proto/orlysync/negentropy/v1/service.pb.go @@ -0,0 +1,1323 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: orlysync/negentropy/v1/service.proto + +package negentropyv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// NegOpenRequest processes a NEG-OPEN from client +// NEG-OPEN format: ["NEG-OPEN", subscription_id, filter, initial_message?] +type NegOpenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` // Client's subscription ID + Filter *v1.Filter `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` // Nostr filter for reconciliation + InitialMessage []byte `protobuf:"bytes,3,opt,name=initial_message,json=initialMessage,proto3" json:"initial_message,omitempty"` // Optional initial negentropy message + ConnectionId string `protobuf:"bytes,4,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` // Connection ID for session tracking + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegOpenRequest) Reset() { + *x = NegOpenRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegOpenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegOpenRequest) ProtoMessage() {} + +func (x *NegOpenRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NegOpenRequest.ProtoReflect.Descriptor instead. +func (*NegOpenRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{0} +} + +func (x *NegOpenRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +func (x *NegOpenRequest) GetFilter() *v1.Filter { + if x != nil { + return x.Filter + } + return nil +} + +func (x *NegOpenRequest) GetInitialMessage() []byte { + if x != nil { + return x.InitialMessage + } + return nil +} + +func (x *NegOpenRequest) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +// NegOpenResponse returns the initial negentropy response +type NegOpenResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` // Negentropy protocol message to send back + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Error message if failed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegOpenResponse) Reset() { + *x = NegOpenResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegOpenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegOpenResponse) ProtoMessage() {} + +func (x *NegOpenResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NegOpenResponse.ProtoReflect.Descriptor instead. +func (*NegOpenResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *NegOpenResponse) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +func (x *NegOpenResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// NegMsgRequest processes a NEG-MSG from client +// NEG-MSG format: ["NEG-MSG", subscription_id, message] +type NegMsgRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + Message []byte `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // Negentropy protocol message + ConnectionId string `protobuf:"bytes,3,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegMsgRequest) Reset() { + *x = NegMsgRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegMsgRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegMsgRequest) ProtoMessage() {} + +func (x *NegMsgRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NegMsgRequest.ProtoReflect.Descriptor instead. +func (*NegMsgRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *NegMsgRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +func (x *NegMsgRequest) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +func (x *NegMsgRequest) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +// NegMsgResponse returns reconciliation results +type NegMsgResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Message []byte `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` // Negentropy protocol message to send back + HaveIds [][]byte `protobuf:"bytes,2,rep,name=have_ids,json=haveIds,proto3" json:"have_ids,omitempty"` // Event IDs we have that client needs + NeedIds [][]byte `protobuf:"bytes,3,rep,name=need_ids,json=needIds,proto3" json:"need_ids,omitempty"` // Event IDs we need from client + Complete bool `protobuf:"varint,4,opt,name=complete,proto3" json:"complete,omitempty"` // True if reconciliation is complete + Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"` // Error message if failed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegMsgResponse) Reset() { + *x = NegMsgResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegMsgResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegMsgResponse) ProtoMessage() {} + +func (x *NegMsgResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NegMsgResponse.ProtoReflect.Descriptor instead. +func (*NegMsgResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *NegMsgResponse) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +func (x *NegMsgResponse) GetHaveIds() [][]byte { + if x != nil { + return x.HaveIds + } + return nil +} + +func (x *NegMsgResponse) GetNeedIds() [][]byte { + if x != nil { + return x.NeedIds + } + return nil +} + +func (x *NegMsgResponse) GetComplete() bool { + if x != nil { + return x.Complete + } + return false +} + +func (x *NegMsgResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// NegCloseRequest processes a NEG-CLOSE from client +// NEG-CLOSE format: ["NEG-CLOSE", subscription_id] +type NegCloseRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *NegCloseRequest) Reset() { + *x = NegCloseRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *NegCloseRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*NegCloseRequest) ProtoMessage() {} + +func (x *NegCloseRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use NegCloseRequest.ProtoReflect.Descriptor instead. +func (*NegCloseRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *NegCloseRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +func (x *NegCloseRequest) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +// SyncPeerRequest initiates sync with a peer +type SyncPeerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` // WebSocket URL of peer relay + Filter *v1.Filter `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` // Optional filter to limit sync scope + Since int64 `protobuf:"varint,3,opt,name=since,proto3" json:"since,omitempty"` // Optional: only sync events since timestamp + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncPeerRequest) Reset() { + *x = SyncPeerRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncPeerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncPeerRequest) ProtoMessage() {} + +func (x *SyncPeerRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncPeerRequest.ProtoReflect.Descriptor instead. +func (*SyncPeerRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *SyncPeerRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +func (x *SyncPeerRequest) GetFilter() *v1.Filter { + if x != nil { + return x.Filter + } + return nil +} + +func (x *SyncPeerRequest) GetSince() int64 { + if x != nil { + return x.Since + } + return 0 +} + +// SyncProgress streams sync progress updates +type SyncProgress struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + Round int32 `protobuf:"varint,2,opt,name=round,proto3" json:"round,omitempty"` // Reconciliation round number + HaveCount int64 `protobuf:"varint,3,opt,name=have_count,json=haveCount,proto3" json:"have_count,omitempty"` // Events we have that peer needs + NeedCount int64 `protobuf:"varint,4,opt,name=need_count,json=needCount,proto3" json:"need_count,omitempty"` // Events we need from peer + FetchedCount int64 `protobuf:"varint,5,opt,name=fetched_count,json=fetchedCount,proto3" json:"fetched_count,omitempty"` // Events fetched so far + SentCount int64 `protobuf:"varint,6,opt,name=sent_count,json=sentCount,proto3" json:"sent_count,omitempty"` // Events sent so far + Complete bool `protobuf:"varint,7,opt,name=complete,proto3" json:"complete,omitempty"` // True when sync is complete + Error string `protobuf:"bytes,8,opt,name=error,proto3" json:"error,omitempty"` // Error message if failed + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncProgress) Reset() { + *x = SyncProgress{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncProgress) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncProgress) ProtoMessage() {} + +func (x *SyncProgress) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncProgress.ProtoReflect.Descriptor instead. +func (*SyncProgress) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *SyncProgress) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +func (x *SyncProgress) GetRound() int32 { + if x != nil { + return x.Round + } + return 0 +} + +func (x *SyncProgress) GetHaveCount() int64 { + if x != nil { + return x.HaveCount + } + return 0 +} + +func (x *SyncProgress) GetNeedCount() int64 { + if x != nil { + return x.NeedCount + } + return 0 +} + +func (x *SyncProgress) GetFetchedCount() int64 { + if x != nil { + return x.FetchedCount + } + return 0 +} + +func (x *SyncProgress) GetSentCount() int64 { + if x != nil { + return x.SentCount + } + return 0 +} + +func (x *SyncProgress) GetComplete() bool { + if x != nil { + return x.Complete + } + return false +} + +func (x *SyncProgress) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// SyncStatusResponse contains overall sync status +type SyncStatusResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Active bool `protobuf:"varint,1,opt,name=active,proto3" json:"active,omitempty"` // Whether background sync is running + LastSync int64 `protobuf:"varint,2,opt,name=last_sync,json=lastSync,proto3" json:"last_sync,omitempty"` // Timestamp of last sync + PeerCount int32 `protobuf:"varint,3,opt,name=peer_count,json=peerCount,proto3" json:"peer_count,omitempty"` + PeerStates []*PeerSyncState `protobuf:"bytes,4,rep,name=peer_states,json=peerStates,proto3" json:"peer_states,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SyncStatusResponse) Reset() { + *x = SyncStatusResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SyncStatusResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SyncStatusResponse) ProtoMessage() {} + +func (x *SyncStatusResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SyncStatusResponse.ProtoReflect.Descriptor instead. +func (*SyncStatusResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *SyncStatusResponse) GetActive() bool { + if x != nil { + return x.Active + } + return false +} + +func (x *SyncStatusResponse) GetLastSync() int64 { + if x != nil { + return x.LastSync + } + return 0 +} + +func (x *SyncStatusResponse) GetPeerCount() int32 { + if x != nil { + return x.PeerCount + } + return 0 +} + +func (x *SyncStatusResponse) GetPeerStates() []*PeerSyncState { + if x != nil { + return x.PeerStates + } + return nil +} + +// PeersResponse contains the list of peers +type PeersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Peers []string `protobuf:"bytes,1,rep,name=peers,proto3" json:"peers,omitempty"` // List of peer WebSocket URLs + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeersResponse) Reset() { + *x = PeersResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeersResponse) ProtoMessage() {} + +func (x *PeersResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeersResponse.ProtoReflect.Descriptor instead. +func (*PeersResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *PeersResponse) GetPeers() []string { + if x != nil { + return x.Peers + } + return nil +} + +// AddPeerRequest adds a peer +type AddPeerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AddPeerRequest) Reset() { + *x = AddPeerRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AddPeerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AddPeerRequest) ProtoMessage() {} + +func (x *AddPeerRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AddPeerRequest.ProtoReflect.Descriptor instead. +func (*AddPeerRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{9} +} + +func (x *AddPeerRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +// RemovePeerRequest removes a peer +type RemovePeerRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RemovePeerRequest) Reset() { + *x = RemovePeerRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RemovePeerRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RemovePeerRequest) ProtoMessage() {} + +func (x *RemovePeerRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RemovePeerRequest.ProtoReflect.Descriptor instead. +func (*RemovePeerRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{10} +} + +func (x *RemovePeerRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +// TriggerSyncRequest triggers manual sync +type TriggerSyncRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` // Optional: specific peer (empty = all) + Filter *v1.Filter `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` // Optional: filter for sync scope + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TriggerSyncRequest) Reset() { + *x = TriggerSyncRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TriggerSyncRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TriggerSyncRequest) ProtoMessage() {} + +func (x *TriggerSyncRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TriggerSyncRequest.ProtoReflect.Descriptor instead. +func (*TriggerSyncRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{11} +} + +func (x *TriggerSyncRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +func (x *TriggerSyncRequest) GetFilter() *v1.Filter { + if x != nil { + return x.Filter + } + return nil +} + +// PeerSyncStateRequest requests state for a peer +type PeerSyncStateRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerSyncStateRequest) Reset() { + *x = PeerSyncStateRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerSyncStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerSyncStateRequest) ProtoMessage() {} + +func (x *PeerSyncStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerSyncStateRequest.ProtoReflect.Descriptor instead. +func (*PeerSyncStateRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{12} +} + +func (x *PeerSyncStateRequest) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +// PeerSyncStateResponse contains peer sync state +type PeerSyncStateResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + State *PeerSyncState `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerSyncStateResponse) Reset() { + *x = PeerSyncStateResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerSyncStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerSyncStateResponse) ProtoMessage() {} + +func (x *PeerSyncStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerSyncStateResponse.ProtoReflect.Descriptor instead. +func (*PeerSyncStateResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{13} +} + +func (x *PeerSyncStateResponse) GetState() *PeerSyncState { + if x != nil { + return x.State + } + return nil +} + +func (x *PeerSyncStateResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +// PeerSyncState represents sync state for a peer +type PeerSyncState struct { + state protoimpl.MessageState `protogen:"open.v1"` + PeerUrl string `protobuf:"bytes,1,opt,name=peer_url,json=peerUrl,proto3" json:"peer_url,omitempty"` + LastSync int64 `protobuf:"varint,2,opt,name=last_sync,json=lastSync,proto3" json:"last_sync,omitempty"` // Timestamp of last successful sync + EventsSynced int64 `protobuf:"varint,3,opt,name=events_synced,json=eventsSynced,proto3" json:"events_synced,omitempty"` // Total events synced from this peer + Status string `protobuf:"bytes,4,opt,name=status,proto3" json:"status,omitempty"` // "idle", "syncing", "error" + LastError string `protobuf:"bytes,5,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"` // Last error message + ConsecutiveFailures int32 `protobuf:"varint,6,opt,name=consecutive_failures,json=consecutiveFailures,proto3" json:"consecutive_failures,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PeerSyncState) Reset() { + *x = PeerSyncState{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PeerSyncState) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PeerSyncState) ProtoMessage() {} + +func (x *PeerSyncState) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PeerSyncState.ProtoReflect.Descriptor instead. +func (*PeerSyncState) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{14} +} + +func (x *PeerSyncState) GetPeerUrl() string { + if x != nil { + return x.PeerUrl + } + return "" +} + +func (x *PeerSyncState) GetLastSync() int64 { + if x != nil { + return x.LastSync + } + return 0 +} + +func (x *PeerSyncState) GetEventsSynced() int64 { + if x != nil { + return x.EventsSynced + } + return 0 +} + +func (x *PeerSyncState) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *PeerSyncState) GetLastError() string { + if x != nil { + return x.LastError + } + return "" +} + +func (x *PeerSyncState) GetConsecutiveFailures() int32 { + if x != nil { + return x.ConsecutiveFailures + } + return 0 +} + +// ClientSession represents an active client negentropy session +type ClientSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` + CreatedAt int64 `protobuf:"varint,3,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + LastActivity int64 `protobuf:"varint,4,opt,name=last_activity,json=lastActivity,proto3" json:"last_activity,omitempty"` + RoundCount int32 `protobuf:"varint,5,opt,name=round_count,json=roundCount,proto3" json:"round_count,omitempty"` // Number of reconciliation rounds + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClientSession) Reset() { + *x = ClientSession{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClientSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClientSession) ProtoMessage() {} + +func (x *ClientSession) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClientSession.ProtoReflect.Descriptor instead. +func (*ClientSession) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{15} +} + +func (x *ClientSession) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +func (x *ClientSession) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +func (x *ClientSession) GetCreatedAt() int64 { + if x != nil { + return x.CreatedAt + } + return 0 +} + +func (x *ClientSession) GetLastActivity() int64 { + if x != nil { + return x.LastActivity + } + return 0 +} + +func (x *ClientSession) GetRoundCount() int32 { + if x != nil { + return x.RoundCount + } + return 0 +} + +// ListSessionsResponse contains active sessions +type ListSessionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sessions []*ClientSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListSessionsResponse) Reset() { + *x = ListSessionsResponse{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListSessionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSessionsResponse) ProtoMessage() {} + +func (x *ListSessionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSessionsResponse.ProtoReflect.Descriptor instead. +func (*ListSessionsResponse) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{16} +} + +func (x *ListSessionsResponse) GetSessions() []*ClientSession { + if x != nil { + return x.Sessions + } + return nil +} + +// CloseSessionRequest closes a session +type CloseSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + SubscriptionId string `protobuf:"bytes,1,opt,name=subscription_id,json=subscriptionId,proto3" json:"subscription_id,omitempty"` + ConnectionId string `protobuf:"bytes,2,opt,name=connection_id,json=connectionId,proto3" json:"connection_id,omitempty"` // Optional: if empty, close all with this sub_id + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CloseSessionRequest) Reset() { + *x = CloseSessionRequest{} + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CloseSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CloseSessionRequest) ProtoMessage() {} + +func (x *CloseSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_negentropy_v1_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CloseSessionRequest.ProtoReflect.Descriptor instead. +func (*CloseSessionRequest) Descriptor() ([]byte, []int) { + return file_orlysync_negentropy_v1_service_proto_rawDescGZIP(), []int{17} +} + +func (x *CloseSessionRequest) GetSubscriptionId() string { + if x != nil { + return x.SubscriptionId + } + return "" +} + +func (x *CloseSessionRequest) GetConnectionId() string { + if x != nil { + return x.ConnectionId + } + return "" +} + +var File_orlysync_negentropy_v1_service_proto protoreflect.FileDescriptor + +const file_orlysync_negentropy_v1_service_proto_rawDesc = "" + + "\n" + + "$orlysync/negentropy/v1/service.proto\x12\x16orlysync.negentropy.v1\x1a\x1eorlysync/common/v1/types.proto\"\xbb\x01\n" + + "\x0eNegOpenRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x122\n" + + "\x06filter\x18\x02 \x01(\v2\x1a.orlysync.common.v1.FilterR\x06filter\x12'\n" + + "\x0finitial_message\x18\x03 \x01(\fR\x0einitialMessage\x12#\n" + + "\rconnection_id\x18\x04 \x01(\tR\fconnectionId\"A\n" + + "\x0fNegOpenResponse\x12\x18\n" + + "\amessage\x18\x01 \x01(\fR\amessage\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"w\n" + + "\rNegMsgRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12\x18\n" + + "\amessage\x18\x02 \x01(\fR\amessage\x12#\n" + + "\rconnection_id\x18\x03 \x01(\tR\fconnectionId\"\x92\x01\n" + + "\x0eNegMsgResponse\x12\x18\n" + + "\amessage\x18\x01 \x01(\fR\amessage\x12\x19\n" + + "\bhave_ids\x18\x02 \x03(\fR\ahaveIds\x12\x19\n" + + "\bneed_ids\x18\x03 \x03(\fR\aneedIds\x12\x1a\n" + + "\bcomplete\x18\x04 \x01(\bR\bcomplete\x12\x14\n" + + "\x05error\x18\x05 \x01(\tR\x05error\"_\n" + + "\x0fNegCloseRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12#\n" + + "\rconnection_id\x18\x02 \x01(\tR\fconnectionId\"v\n" + + "\x0fSyncPeerRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\x122\n" + + "\x06filter\x18\x02 \x01(\v2\x1a.orlysync.common.v1.FilterR\x06filter\x12\x14\n" + + "\x05since\x18\x03 \x01(\x03R\x05since\"\xf3\x01\n" + + "\fSyncProgress\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\x12\x14\n" + + "\x05round\x18\x02 \x01(\x05R\x05round\x12\x1d\n" + + "\n" + + "have_count\x18\x03 \x01(\x03R\thaveCount\x12\x1d\n" + + "\n" + + "need_count\x18\x04 \x01(\x03R\tneedCount\x12#\n" + + "\rfetched_count\x18\x05 \x01(\x03R\ffetchedCount\x12\x1d\n" + + "\n" + + "sent_count\x18\x06 \x01(\x03R\tsentCount\x12\x1a\n" + + "\bcomplete\x18\a \x01(\bR\bcomplete\x12\x14\n" + + "\x05error\x18\b \x01(\tR\x05error\"\xb0\x01\n" + + "\x12SyncStatusResponse\x12\x16\n" + + "\x06active\x18\x01 \x01(\bR\x06active\x12\x1b\n" + + "\tlast_sync\x18\x02 \x01(\x03R\blastSync\x12\x1d\n" + + "\n" + + "peer_count\x18\x03 \x01(\x05R\tpeerCount\x12F\n" + + "\vpeer_states\x18\x04 \x03(\v2%.orlysync.negentropy.v1.PeerSyncStateR\n" + + "peerStates\"%\n" + + "\rPeersResponse\x12\x14\n" + + "\x05peers\x18\x01 \x03(\tR\x05peers\"+\n" + + "\x0eAddPeerRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\".\n" + + "\x11RemovePeerRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\"c\n" + + "\x12TriggerSyncRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\x122\n" + + "\x06filter\x18\x02 \x01(\v2\x1a.orlysync.common.v1.FilterR\x06filter\"1\n" + + "\x14PeerSyncStateRequest\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\"j\n" + + "\x15PeerSyncStateResponse\x12;\n" + + "\x05state\x18\x01 \x01(\v2%.orlysync.negentropy.v1.PeerSyncStateR\x05state\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\"\xd6\x01\n" + + "\rPeerSyncState\x12\x19\n" + + "\bpeer_url\x18\x01 \x01(\tR\apeerUrl\x12\x1b\n" + + "\tlast_sync\x18\x02 \x01(\x03R\blastSync\x12#\n" + + "\revents_synced\x18\x03 \x01(\x03R\feventsSynced\x12\x16\n" + + "\x06status\x18\x04 \x01(\tR\x06status\x12\x1d\n" + + "\n" + + "last_error\x18\x05 \x01(\tR\tlastError\x121\n" + + "\x14consecutive_failures\x18\x06 \x01(\x05R\x13consecutiveFailures\"\xc2\x01\n" + + "\rClientSession\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12#\n" + + "\rconnection_id\x18\x02 \x01(\tR\fconnectionId\x12\x1d\n" + + "\n" + + "created_at\x18\x03 \x01(\x03R\tcreatedAt\x12#\n" + + "\rlast_activity\x18\x04 \x01(\x03R\flastActivity\x12\x1f\n" + + "\vround_count\x18\x05 \x01(\x05R\n" + + "roundCount\"Y\n" + + "\x14ListSessionsResponse\x12A\n" + + "\bsessions\x18\x01 \x03(\v2%.orlysync.negentropy.v1.ClientSessionR\bsessions\"c\n" + + "\x13CloseSessionRequest\x12'\n" + + "\x0fsubscription_id\x18\x01 \x01(\tR\x0esubscriptionId\x12#\n" + + "\rconnection_id\x18\x02 \x01(\tR\fconnectionId2\x8f\n" + + "\n" + + "\x11NegentropyService\x12E\n" + + "\x05Ready\x12\x19.orlysync.common.v1.Empty\x1a!.orlysync.common.v1.ReadyResponse\x12=\n" + + "\x05Start\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12<\n" + + "\x04Stop\x12\x19.orlysync.common.v1.Empty\x1a\x19.orlysync.common.v1.Empty\x12`\n" + + "\rHandleNegOpen\x12&.orlysync.negentropy.v1.NegOpenRequest\x1a'.orlysync.negentropy.v1.NegOpenResponse\x12]\n" + + "\fHandleNegMsg\x12%.orlysync.negentropy.v1.NegMsgRequest\x1a&.orlysync.negentropy.v1.NegMsgResponse\x12T\n" + + "\x0eHandleNegClose\x12'.orlysync.negentropy.v1.NegCloseRequest\x1a\x19.orlysync.common.v1.Empty\x12_\n" + + "\fSyncWithPeer\x12'.orlysync.negentropy.v1.SyncPeerRequest\x1a$.orlysync.negentropy.v1.SyncProgress0\x01\x12V\n" + + "\rGetSyncStatus\x12\x19.orlysync.common.v1.Empty\x1a*.orlysync.negentropy.v1.SyncStatusResponse\x12L\n" + + "\bGetPeers\x12\x19.orlysync.common.v1.Empty\x1a%.orlysync.negentropy.v1.PeersResponse\x12L\n" + + "\aAddPeer\x12&.orlysync.negentropy.v1.AddPeerRequest\x1a\x19.orlysync.common.v1.Empty\x12R\n" + + "\n" + + "RemovePeer\x12).orlysync.negentropy.v1.RemovePeerRequest\x1a\x19.orlysync.common.v1.Empty\x12T\n" + + "\vTriggerSync\x12*.orlysync.negentropy.v1.TriggerSyncRequest\x1a\x19.orlysync.common.v1.Empty\x12o\n" + + "\x10GetPeerSyncState\x12,.orlysync.negentropy.v1.PeerSyncStateRequest\x1a-.orlysync.negentropy.v1.PeerSyncStateResponse\x12W\n" + + "\fListSessions\x12\x19.orlysync.common.v1.Empty\x1a,.orlysync.negentropy.v1.ListSessionsResponse\x12V\n" + + "\fCloseSession\x12+.orlysync.negentropy.v1.CloseSessionRequest\x1a\x19.orlysync.common.v1.EmptyB=Z;next.orly.dev/pkg/proto/orlysync/negentropy/v1;negentropyv1b\x06proto3" + +var ( + file_orlysync_negentropy_v1_service_proto_rawDescOnce sync.Once + file_orlysync_negentropy_v1_service_proto_rawDescData []byte +) + +func file_orlysync_negentropy_v1_service_proto_rawDescGZIP() []byte { + file_orlysync_negentropy_v1_service_proto_rawDescOnce.Do(func() { + file_orlysync_negentropy_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_orlysync_negentropy_v1_service_proto_rawDesc), len(file_orlysync_negentropy_v1_service_proto_rawDesc))) + }) + return file_orlysync_negentropy_v1_service_proto_rawDescData +} + +var file_orlysync_negentropy_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 18) +var file_orlysync_negentropy_v1_service_proto_goTypes = []any{ + (*NegOpenRequest)(nil), // 0: orlysync.negentropy.v1.NegOpenRequest + (*NegOpenResponse)(nil), // 1: orlysync.negentropy.v1.NegOpenResponse + (*NegMsgRequest)(nil), // 2: orlysync.negentropy.v1.NegMsgRequest + (*NegMsgResponse)(nil), // 3: orlysync.negentropy.v1.NegMsgResponse + (*NegCloseRequest)(nil), // 4: orlysync.negentropy.v1.NegCloseRequest + (*SyncPeerRequest)(nil), // 5: orlysync.negentropy.v1.SyncPeerRequest + (*SyncProgress)(nil), // 6: orlysync.negentropy.v1.SyncProgress + (*SyncStatusResponse)(nil), // 7: orlysync.negentropy.v1.SyncStatusResponse + (*PeersResponse)(nil), // 8: orlysync.negentropy.v1.PeersResponse + (*AddPeerRequest)(nil), // 9: orlysync.negentropy.v1.AddPeerRequest + (*RemovePeerRequest)(nil), // 10: orlysync.negentropy.v1.RemovePeerRequest + (*TriggerSyncRequest)(nil), // 11: orlysync.negentropy.v1.TriggerSyncRequest + (*PeerSyncStateRequest)(nil), // 12: orlysync.negentropy.v1.PeerSyncStateRequest + (*PeerSyncStateResponse)(nil), // 13: orlysync.negentropy.v1.PeerSyncStateResponse + (*PeerSyncState)(nil), // 14: orlysync.negentropy.v1.PeerSyncState + (*ClientSession)(nil), // 15: orlysync.negentropy.v1.ClientSession + (*ListSessionsResponse)(nil), // 16: orlysync.negentropy.v1.ListSessionsResponse + (*CloseSessionRequest)(nil), // 17: orlysync.negentropy.v1.CloseSessionRequest + (*v1.Filter)(nil), // 18: orlysync.common.v1.Filter + (*v1.Empty)(nil), // 19: orlysync.common.v1.Empty + (*v1.ReadyResponse)(nil), // 20: orlysync.common.v1.ReadyResponse +} +var file_orlysync_negentropy_v1_service_proto_depIdxs = []int32{ + 18, // 0: orlysync.negentropy.v1.NegOpenRequest.filter:type_name -> orlysync.common.v1.Filter + 18, // 1: orlysync.negentropy.v1.SyncPeerRequest.filter:type_name -> orlysync.common.v1.Filter + 14, // 2: orlysync.negentropy.v1.SyncStatusResponse.peer_states:type_name -> orlysync.negentropy.v1.PeerSyncState + 18, // 3: orlysync.negentropy.v1.TriggerSyncRequest.filter:type_name -> orlysync.common.v1.Filter + 14, // 4: orlysync.negentropy.v1.PeerSyncStateResponse.state:type_name -> orlysync.negentropy.v1.PeerSyncState + 15, // 5: orlysync.negentropy.v1.ListSessionsResponse.sessions:type_name -> orlysync.negentropy.v1.ClientSession + 19, // 6: orlysync.negentropy.v1.NegentropyService.Ready:input_type -> orlysync.common.v1.Empty + 19, // 7: orlysync.negentropy.v1.NegentropyService.Start:input_type -> orlysync.common.v1.Empty + 19, // 8: orlysync.negentropy.v1.NegentropyService.Stop:input_type -> orlysync.common.v1.Empty + 0, // 9: orlysync.negentropy.v1.NegentropyService.HandleNegOpen:input_type -> orlysync.negentropy.v1.NegOpenRequest + 2, // 10: orlysync.negentropy.v1.NegentropyService.HandleNegMsg:input_type -> orlysync.negentropy.v1.NegMsgRequest + 4, // 11: orlysync.negentropy.v1.NegentropyService.HandleNegClose:input_type -> orlysync.negentropy.v1.NegCloseRequest + 5, // 12: orlysync.negentropy.v1.NegentropyService.SyncWithPeer:input_type -> orlysync.negentropy.v1.SyncPeerRequest + 19, // 13: orlysync.negentropy.v1.NegentropyService.GetSyncStatus:input_type -> orlysync.common.v1.Empty + 19, // 14: orlysync.negentropy.v1.NegentropyService.GetPeers:input_type -> orlysync.common.v1.Empty + 9, // 15: orlysync.negentropy.v1.NegentropyService.AddPeer:input_type -> orlysync.negentropy.v1.AddPeerRequest + 10, // 16: orlysync.negentropy.v1.NegentropyService.RemovePeer:input_type -> orlysync.negentropy.v1.RemovePeerRequest + 11, // 17: orlysync.negentropy.v1.NegentropyService.TriggerSync:input_type -> orlysync.negentropy.v1.TriggerSyncRequest + 12, // 18: orlysync.negentropy.v1.NegentropyService.GetPeerSyncState:input_type -> orlysync.negentropy.v1.PeerSyncStateRequest + 19, // 19: orlysync.negentropy.v1.NegentropyService.ListSessions:input_type -> orlysync.common.v1.Empty + 17, // 20: orlysync.negentropy.v1.NegentropyService.CloseSession:input_type -> orlysync.negentropy.v1.CloseSessionRequest + 20, // 21: orlysync.negentropy.v1.NegentropyService.Ready:output_type -> orlysync.common.v1.ReadyResponse + 19, // 22: orlysync.negentropy.v1.NegentropyService.Start:output_type -> orlysync.common.v1.Empty + 19, // 23: orlysync.negentropy.v1.NegentropyService.Stop:output_type -> orlysync.common.v1.Empty + 1, // 24: orlysync.negentropy.v1.NegentropyService.HandleNegOpen:output_type -> orlysync.negentropy.v1.NegOpenResponse + 3, // 25: orlysync.negentropy.v1.NegentropyService.HandleNegMsg:output_type -> orlysync.negentropy.v1.NegMsgResponse + 19, // 26: orlysync.negentropy.v1.NegentropyService.HandleNegClose:output_type -> orlysync.common.v1.Empty + 6, // 27: orlysync.negentropy.v1.NegentropyService.SyncWithPeer:output_type -> orlysync.negentropy.v1.SyncProgress + 7, // 28: orlysync.negentropy.v1.NegentropyService.GetSyncStatus:output_type -> orlysync.negentropy.v1.SyncStatusResponse + 8, // 29: orlysync.negentropy.v1.NegentropyService.GetPeers:output_type -> orlysync.negentropy.v1.PeersResponse + 19, // 30: orlysync.negentropy.v1.NegentropyService.AddPeer:output_type -> orlysync.common.v1.Empty + 19, // 31: orlysync.negentropy.v1.NegentropyService.RemovePeer:output_type -> orlysync.common.v1.Empty + 19, // 32: orlysync.negentropy.v1.NegentropyService.TriggerSync:output_type -> orlysync.common.v1.Empty + 13, // 33: orlysync.negentropy.v1.NegentropyService.GetPeerSyncState:output_type -> orlysync.negentropy.v1.PeerSyncStateResponse + 16, // 34: orlysync.negentropy.v1.NegentropyService.ListSessions:output_type -> orlysync.negentropy.v1.ListSessionsResponse + 19, // 35: orlysync.negentropy.v1.NegentropyService.CloseSession:output_type -> orlysync.common.v1.Empty + 21, // [21:36] is the sub-list for method output_type + 6, // [6:21] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_orlysync_negentropy_v1_service_proto_init() } +func file_orlysync_negentropy_v1_service_proto_init() { + if File_orlysync_negentropy_v1_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlysync_negentropy_v1_service_proto_rawDesc), len(file_orlysync_negentropy_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 18, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_orlysync_negentropy_v1_service_proto_goTypes, + DependencyIndexes: file_orlysync_negentropy_v1_service_proto_depIdxs, + MessageInfos: file_orlysync_negentropy_v1_service_proto_msgTypes, + }.Build() + File_orlysync_negentropy_v1_service_proto = out.File + file_orlysync_negentropy_v1_service_proto_goTypes = nil + file_orlysync_negentropy_v1_service_proto_depIdxs = nil +} diff --git a/pkg/proto/orlysync/negentropy/v1/service_grpc.pb.go b/pkg/proto/orlysync/negentropy/v1/service_grpc.pb.go new file mode 100644 index 0000000..98d401d --- /dev/null +++ b/pkg/proto/orlysync/negentropy/v1/service_grpc.pb.go @@ -0,0 +1,694 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: orlysync/negentropy/v1/service.proto + +package negentropyv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + NegentropyService_Ready_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/Ready" + NegentropyService_Start_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/Start" + NegentropyService_Stop_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/Stop" + NegentropyService_HandleNegOpen_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/HandleNegOpen" + NegentropyService_HandleNegMsg_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/HandleNegMsg" + NegentropyService_HandleNegClose_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/HandleNegClose" + NegentropyService_SyncWithPeer_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/SyncWithPeer" + NegentropyService_GetSyncStatus_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/GetSyncStatus" + NegentropyService_GetPeers_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/GetPeers" + NegentropyService_AddPeer_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/AddPeer" + NegentropyService_RemovePeer_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/RemovePeer" + NegentropyService_TriggerSync_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/TriggerSync" + NegentropyService_GetPeerSyncState_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/GetPeerSyncState" + NegentropyService_ListSessions_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/ListSessions" + NegentropyService_CloseSession_FullMethodName = "/orlysync.negentropy.v1.NegentropyService/CloseSession" +) + +// NegentropyServiceClient is the client API for NegentropyService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// NegentropyService provides NIP-77 negentropy-based set reconciliation +// for both relay-to-relay sync and client-facing WebSocket operations +type NegentropyServiceClient interface { + // Ready returns whether the service is ready to serve requests + Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) + // Start starts the background relay-to-relay sync + Start(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // Stop stops the background sync + Stop(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) + // HandleNegOpen processes a NEG-OPEN message from a client + HandleNegOpen(ctx context.Context, in *NegOpenRequest, opts ...grpc.CallOption) (*NegOpenResponse, error) + // HandleNegMsg processes a NEG-MSG message from a client + HandleNegMsg(ctx context.Context, in *NegMsgRequest, opts ...grpc.CallOption) (*NegMsgResponse, error) + // HandleNegClose processes a NEG-CLOSE message from a client + HandleNegClose(ctx context.Context, in *NegCloseRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // SyncWithPeer initiates negentropy sync with a specific peer relay + SyncWithPeer(ctx context.Context, in *SyncPeerRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error) + // GetSyncStatus returns the current sync status + GetSyncStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*SyncStatusResponse, error) + // GetPeers returns the list of negentropy sync peers + GetPeers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*PeersResponse, error) + // AddPeer adds a peer for negentropy sync + AddPeer(ctx context.Context, in *AddPeerRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // RemovePeer removes a peer from negentropy sync + RemovePeer(ctx context.Context, in *RemovePeerRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // TriggerSync manually triggers sync with a specific peer or all peers + TriggerSync(ctx context.Context, in *TriggerSyncRequest, opts ...grpc.CallOption) (*v1.Empty, error) + // GetPeerSyncState returns sync state for a specific peer + GetPeerSyncState(ctx context.Context, in *PeerSyncStateRequest, opts ...grpc.CallOption) (*PeerSyncStateResponse, error) + // ListSessions returns active client negentropy sessions + ListSessions(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*ListSessionsResponse, error) + // CloseSession forcefully closes a client session + CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*v1.Empty, error) +} + +type negentropyServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewNegentropyServiceClient(cc grpc.ClientConnInterface) NegentropyServiceClient { + return &negentropyServiceClient{cc} +} + +func (c *negentropyServiceClient) Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.ReadyResponse) + err := c.cc.Invoke(ctx, NegentropyService_Ready_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) Start(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_Start_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) Stop(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_Stop_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) HandleNegOpen(ctx context.Context, in *NegOpenRequest, opts ...grpc.CallOption) (*NegOpenResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NegOpenResponse) + err := c.cc.Invoke(ctx, NegentropyService_HandleNegOpen_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) HandleNegMsg(ctx context.Context, in *NegMsgRequest, opts ...grpc.CallOption) (*NegMsgResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(NegMsgResponse) + err := c.cc.Invoke(ctx, NegentropyService_HandleNegMsg_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) HandleNegClose(ctx context.Context, in *NegCloseRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_HandleNegClose_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) SyncWithPeer(ctx context.Context, in *SyncPeerRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[SyncProgress], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &NegentropyService_ServiceDesc.Streams[0], NegentropyService_SyncWithPeer_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[SyncPeerRequest, SyncProgress]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type NegentropyService_SyncWithPeerClient = grpc.ServerStreamingClient[SyncProgress] + +func (c *negentropyServiceClient) GetSyncStatus(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*SyncStatusResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SyncStatusResponse) + err := c.cc.Invoke(ctx, NegentropyService_GetSyncStatus_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) GetPeers(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*PeersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PeersResponse) + err := c.cc.Invoke(ctx, NegentropyService_GetPeers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) AddPeer(ctx context.Context, in *AddPeerRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_AddPeer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) RemovePeer(ctx context.Context, in *RemovePeerRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_RemovePeer_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) TriggerSync(ctx context.Context, in *TriggerSyncRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_TriggerSync_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) GetPeerSyncState(ctx context.Context, in *PeerSyncStateRequest, opts ...grpc.CallOption) (*PeerSyncStateResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PeerSyncStateResponse) + err := c.cc.Invoke(ctx, NegentropyService_GetPeerSyncState_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) ListSessions(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*ListSessionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListSessionsResponse) + err := c.cc.Invoke(ctx, NegentropyService_ListSessions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *negentropyServiceClient) CloseSession(ctx context.Context, in *CloseSessionRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, NegentropyService_CloseSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// NegentropyServiceServer is the server API for NegentropyService service. +// All implementations must embed UnimplementedNegentropyServiceServer +// for forward compatibility. +// +// NegentropyService provides NIP-77 negentropy-based set reconciliation +// for both relay-to-relay sync and client-facing WebSocket operations +type NegentropyServiceServer interface { + // Ready returns whether the service is ready to serve requests + Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) + // Start starts the background relay-to-relay sync + Start(context.Context, *v1.Empty) (*v1.Empty, error) + // Stop stops the background sync + Stop(context.Context, *v1.Empty) (*v1.Empty, error) + // HandleNegOpen processes a NEG-OPEN message from a client + HandleNegOpen(context.Context, *NegOpenRequest) (*NegOpenResponse, error) + // HandleNegMsg processes a NEG-MSG message from a client + HandleNegMsg(context.Context, *NegMsgRequest) (*NegMsgResponse, error) + // HandleNegClose processes a NEG-CLOSE message from a client + HandleNegClose(context.Context, *NegCloseRequest) (*v1.Empty, error) + // SyncWithPeer initiates negentropy sync with a specific peer relay + SyncWithPeer(*SyncPeerRequest, grpc.ServerStreamingServer[SyncProgress]) error + // GetSyncStatus returns the current sync status + GetSyncStatus(context.Context, *v1.Empty) (*SyncStatusResponse, error) + // GetPeers returns the list of negentropy sync peers + GetPeers(context.Context, *v1.Empty) (*PeersResponse, error) + // AddPeer adds a peer for negentropy sync + AddPeer(context.Context, *AddPeerRequest) (*v1.Empty, error) + // RemovePeer removes a peer from negentropy sync + RemovePeer(context.Context, *RemovePeerRequest) (*v1.Empty, error) + // TriggerSync manually triggers sync with a specific peer or all peers + TriggerSync(context.Context, *TriggerSyncRequest) (*v1.Empty, error) + // GetPeerSyncState returns sync state for a specific peer + GetPeerSyncState(context.Context, *PeerSyncStateRequest) (*PeerSyncStateResponse, error) + // ListSessions returns active client negentropy sessions + ListSessions(context.Context, *v1.Empty) (*ListSessionsResponse, error) + // CloseSession forcefully closes a client session + CloseSession(context.Context, *CloseSessionRequest) (*v1.Empty, error) + mustEmbedUnimplementedNegentropyServiceServer() +} + +// UnimplementedNegentropyServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedNegentropyServiceServer struct{} + +func (UnimplementedNegentropyServiceServer) Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ready not implemented") +} +func (UnimplementedNegentropyServiceServer) Start(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method Start not implemented") +} +func (UnimplementedNegentropyServiceServer) Stop(context.Context, *v1.Empty) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method Stop not implemented") +} +func (UnimplementedNegentropyServiceServer) HandleNegOpen(context.Context, *NegOpenRequest) (*NegOpenResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleNegOpen not implemented") +} +func (UnimplementedNegentropyServiceServer) HandleNegMsg(context.Context, *NegMsgRequest) (*NegMsgResponse, error) { + return nil, status.Error(codes.Unimplemented, "method HandleNegMsg not implemented") +} +func (UnimplementedNegentropyServiceServer) HandleNegClose(context.Context, *NegCloseRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method HandleNegClose not implemented") +} +func (UnimplementedNegentropyServiceServer) SyncWithPeer(*SyncPeerRequest, grpc.ServerStreamingServer[SyncProgress]) error { + return status.Error(codes.Unimplemented, "method SyncWithPeer not implemented") +} +func (UnimplementedNegentropyServiceServer) GetSyncStatus(context.Context, *v1.Empty) (*SyncStatusResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetSyncStatus not implemented") +} +func (UnimplementedNegentropyServiceServer) GetPeers(context.Context, *v1.Empty) (*PeersResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPeers not implemented") +} +func (UnimplementedNegentropyServiceServer) AddPeer(context.Context, *AddPeerRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method AddPeer not implemented") +} +func (UnimplementedNegentropyServiceServer) RemovePeer(context.Context, *RemovePeerRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method RemovePeer not implemented") +} +func (UnimplementedNegentropyServiceServer) TriggerSync(context.Context, *TriggerSyncRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method TriggerSync not implemented") +} +func (UnimplementedNegentropyServiceServer) GetPeerSyncState(context.Context, *PeerSyncStateRequest) (*PeerSyncStateResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetPeerSyncState not implemented") +} +func (UnimplementedNegentropyServiceServer) ListSessions(context.Context, *v1.Empty) (*ListSessionsResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListSessions not implemented") +} +func (UnimplementedNegentropyServiceServer) CloseSession(context.Context, *CloseSessionRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method CloseSession not implemented") +} +func (UnimplementedNegentropyServiceServer) mustEmbedUnimplementedNegentropyServiceServer() {} +func (UnimplementedNegentropyServiceServer) testEmbeddedByValue() {} + +// UnsafeNegentropyServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to NegentropyServiceServer will +// result in compilation errors. +type UnsafeNegentropyServiceServer interface { + mustEmbedUnimplementedNegentropyServiceServer() +} + +func RegisterNegentropyServiceServer(s grpc.ServiceRegistrar, srv NegentropyServiceServer) { + // If the following call panics, it indicates UnimplementedNegentropyServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&NegentropyService_ServiceDesc, srv) +} + +func _NegentropyService_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).Ready(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_Ready_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).Ready(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_Start_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).Start(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_Start_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).Start(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_Stop_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).Stop(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_Stop_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).Stop(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_HandleNegOpen_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NegOpenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).HandleNegOpen(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_HandleNegOpen_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).HandleNegOpen(ctx, req.(*NegOpenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_HandleNegMsg_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NegMsgRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).HandleNegMsg(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_HandleNegMsg_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).HandleNegMsg(ctx, req.(*NegMsgRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_HandleNegClose_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(NegCloseRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).HandleNegClose(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_HandleNegClose_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).HandleNegClose(ctx, req.(*NegCloseRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_SyncWithPeer_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(SyncPeerRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(NegentropyServiceServer).SyncWithPeer(m, &grpc.GenericServerStream[SyncPeerRequest, SyncProgress]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type NegentropyService_SyncWithPeerServer = grpc.ServerStreamingServer[SyncProgress] + +func _NegentropyService_GetSyncStatus_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).GetSyncStatus(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_GetSyncStatus_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).GetSyncStatus(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_GetPeers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).GetPeers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_GetPeers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).GetPeers(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_AddPeer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AddPeerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).AddPeer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_AddPeer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).AddPeer(ctx, req.(*AddPeerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_RemovePeer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RemovePeerRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).RemovePeer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_RemovePeer_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).RemovePeer(ctx, req.(*RemovePeerRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_TriggerSync_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TriggerSyncRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).TriggerSync(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_TriggerSync_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).TriggerSync(ctx, req.(*TriggerSyncRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_GetPeerSyncState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PeerSyncStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).GetPeerSyncState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_GetPeerSyncState_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).GetPeerSyncState(ctx, req.(*PeerSyncStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_ListSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).ListSessions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_ListSessions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).ListSessions(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _NegentropyService_CloseSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CloseSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(NegentropyServiceServer).CloseSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: NegentropyService_CloseSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(NegentropyServiceServer).CloseSession(ctx, req.(*CloseSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// NegentropyService_ServiceDesc is the grpc.ServiceDesc for NegentropyService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var NegentropyService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "orlysync.negentropy.v1.NegentropyService", + HandlerType: (*NegentropyServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ready", + Handler: _NegentropyService_Ready_Handler, + }, + { + MethodName: "Start", + Handler: _NegentropyService_Start_Handler, + }, + { + MethodName: "Stop", + Handler: _NegentropyService_Stop_Handler, + }, + { + MethodName: "HandleNegOpen", + Handler: _NegentropyService_HandleNegOpen_Handler, + }, + { + MethodName: "HandleNegMsg", + Handler: _NegentropyService_HandleNegMsg_Handler, + }, + { + MethodName: "HandleNegClose", + Handler: _NegentropyService_HandleNegClose_Handler, + }, + { + MethodName: "GetSyncStatus", + Handler: _NegentropyService_GetSyncStatus_Handler, + }, + { + MethodName: "GetPeers", + Handler: _NegentropyService_GetPeers_Handler, + }, + { + MethodName: "AddPeer", + Handler: _NegentropyService_AddPeer_Handler, + }, + { + MethodName: "RemovePeer", + Handler: _NegentropyService_RemovePeer_Handler, + }, + { + MethodName: "TriggerSync", + Handler: _NegentropyService_TriggerSync_Handler, + }, + { + MethodName: "GetPeerSyncState", + Handler: _NegentropyService_GetPeerSyncState_Handler, + }, + { + MethodName: "ListSessions", + Handler: _NegentropyService_ListSessions_Handler, + }, + { + MethodName: "CloseSession", + Handler: _NegentropyService_CloseSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "SyncWithPeer", + Handler: _NegentropyService_SyncWithPeer_Handler, + ServerStreams: true, + }, + }, + Metadata: "orlysync/negentropy/v1/service.proto", +} diff --git a/pkg/proto/orlysync/relaygroup/v1/service.pb.go b/pkg/proto/orlysync/relaygroup/v1/service.pb.go new file mode 100644 index 0000000..537cc3f --- /dev/null +++ b/pkg/proto/orlysync/relaygroup/v1/service.pb.go @@ -0,0 +1,574 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: orlysync/relaygroup/v1/service.proto + +package relaygroupv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RelayGroupConfig represents a relay group configuration +type RelayGroupConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + Relays []string `protobuf:"bytes,1,rep,name=relays,proto3" json:"relays,omitempty"` // List of relay URLs + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayGroupConfig) Reset() { + *x = RelayGroupConfig{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayGroupConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayGroupConfig) ProtoMessage() {} + +func (x *RelayGroupConfig) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayGroupConfig.ProtoReflect.Descriptor instead. +func (*RelayGroupConfig) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{0} +} + +func (x *RelayGroupConfig) GetRelays() []string { + if x != nil { + return x.Relays + } + return nil +} + +// RelayGroupConfigResponse contains the authoritative config +type RelayGroupConfigResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Config *RelayGroupConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"` + Found bool `protobuf:"varint,2,opt,name=found,proto3" json:"found,omitempty"` + SourceEventId []byte `protobuf:"bytes,3,opt,name=source_event_id,json=sourceEventId,proto3" json:"source_event_id,omitempty"` // ID of the event that provided this config + SourceTimestamp int64 `protobuf:"varint,4,opt,name=source_timestamp,json=sourceTimestamp,proto3" json:"source_timestamp,omitempty"` // Timestamp of the source event + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelayGroupConfigResponse) Reset() { + *x = RelayGroupConfigResponse{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelayGroupConfigResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelayGroupConfigResponse) ProtoMessage() {} + +func (x *RelayGroupConfigResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelayGroupConfigResponse.ProtoReflect.Descriptor instead. +func (*RelayGroupConfigResponse) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{1} +} + +func (x *RelayGroupConfigResponse) GetConfig() *RelayGroupConfig { + if x != nil { + return x.Config + } + return nil +} + +func (x *RelayGroupConfigResponse) GetFound() bool { + if x != nil { + return x.Found + } + return false +} + +func (x *RelayGroupConfigResponse) GetSourceEventId() []byte { + if x != nil { + return x.SourceEventId + } + return nil +} + +func (x *RelayGroupConfigResponse) GetSourceTimestamp() int64 { + if x != nil { + return x.SourceTimestamp + } + return 0 +} + +// RelaysResponse contains the list of relays +type RelaysResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Relays []string `protobuf:"bytes,1,rep,name=relays,proto3" json:"relays,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RelaysResponse) Reset() { + *x = RelaysResponse{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RelaysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RelaysResponse) ProtoMessage() {} + +func (x *RelaysResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RelaysResponse.ProtoReflect.Descriptor instead. +func (*RelaysResponse) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{2} +} + +func (x *RelaysResponse) GetRelays() []string { + if x != nil { + return x.Relays + } + return nil +} + +// AuthorizedPublisherRequest checks if a pubkey is authorized +type AuthorizedPublisherRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` // 32 bytes public key + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizedPublisherRequest) Reset() { + *x = AuthorizedPublisherRequest{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizedPublisherRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizedPublisherRequest) ProtoMessage() {} + +func (x *AuthorizedPublisherRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizedPublisherRequest.ProtoReflect.Descriptor instead. +func (*AuthorizedPublisherRequest) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{3} +} + +func (x *AuthorizedPublisherRequest) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +// AuthorizedPublisherResponse indicates if the pubkey is authorized +type AuthorizedPublisherResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Authorized bool `protobuf:"varint,1,opt,name=authorized,proto3" json:"authorized,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizedPublisherResponse) Reset() { + *x = AuthorizedPublisherResponse{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizedPublisherResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizedPublisherResponse) ProtoMessage() {} + +func (x *AuthorizedPublisherResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizedPublisherResponse.ProtoReflect.Descriptor instead. +func (*AuthorizedPublisherResponse) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{4} +} + +func (x *AuthorizedPublisherResponse) GetAuthorized() bool { + if x != nil { + return x.Authorized + } + return false +} + +// AuthorizedPubkeysResponse contains all authorized pubkeys +type AuthorizedPubkeysResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Pubkeys [][]byte `protobuf:"bytes,1,rep,name=pubkeys,proto3" json:"pubkeys,omitempty"` // List of 32-byte pubkeys + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AuthorizedPubkeysResponse) Reset() { + *x = AuthorizedPubkeysResponse{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AuthorizedPubkeysResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthorizedPubkeysResponse) ProtoMessage() {} + +func (x *AuthorizedPubkeysResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthorizedPubkeysResponse.ProtoReflect.Descriptor instead. +func (*AuthorizedPubkeysResponse) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{5} +} + +func (x *AuthorizedPubkeysResponse) GetPubkeys() [][]byte { + if x != nil { + return x.Pubkeys + } + return nil +} + +// ValidateEventRequest requests validation of a relay group event +type ValidateEventRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Event *v1.Event `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEventRequest) Reset() { + *x = ValidateEventRequest{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEventRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEventRequest) ProtoMessage() {} + +func (x *ValidateEventRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEventRequest.ProtoReflect.Descriptor instead. +func (*ValidateEventRequest) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{6} +} + +func (x *ValidateEventRequest) GetEvent() *v1.Event { + if x != nil { + return x.Event + } + return nil +} + +// ValidateEventResponse contains validation results +type ValidateEventResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Valid bool `protobuf:"varint,1,opt,name=valid,proto3" json:"valid,omitempty"` + Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // Error message if not valid + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ValidateEventResponse) Reset() { + *x = ValidateEventResponse{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ValidateEventResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ValidateEventResponse) ProtoMessage() {} + +func (x *ValidateEventResponse) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ValidateEventResponse.ProtoReflect.Descriptor instead. +func (*ValidateEventResponse) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{7} +} + +func (x *ValidateEventResponse) GetValid() bool { + if x != nil { + return x.Valid + } + return false +} + +func (x *ValidateEventResponse) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +// HandleEventRequest requests processing of a relay group event +type HandleEventRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Event *v1.Event `protobuf:"bytes,1,opt,name=event,proto3" json:"event,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HandleEventRequest) Reset() { + *x = HandleEventRequest{} + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HandleEventRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HandleEventRequest) ProtoMessage() {} + +func (x *HandleEventRequest) ProtoReflect() protoreflect.Message { + mi := &file_orlysync_relaygroup_v1_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HandleEventRequest.ProtoReflect.Descriptor instead. +func (*HandleEventRequest) Descriptor() ([]byte, []int) { + return file_orlysync_relaygroup_v1_service_proto_rawDescGZIP(), []int{8} +} + +func (x *HandleEventRequest) GetEvent() *v1.Event { + if x != nil { + return x.Event + } + return nil +} + +var File_orlysync_relaygroup_v1_service_proto protoreflect.FileDescriptor + +const file_orlysync_relaygroup_v1_service_proto_rawDesc = "" + + "\n" + + "$orlysync/relaygroup/v1/service.proto\x12\x16orlysync.relaygroup.v1\x1a\x1eorlysync/common/v1/types.proto\"*\n" + + "\x10RelayGroupConfig\x12\x16\n" + + "\x06relays\x18\x01 \x03(\tR\x06relays\"\xc5\x01\n" + + "\x18RelayGroupConfigResponse\x12@\n" + + "\x06config\x18\x01 \x01(\v2(.orlysync.relaygroup.v1.RelayGroupConfigR\x06config\x12\x14\n" + + "\x05found\x18\x02 \x01(\bR\x05found\x12&\n" + + "\x0fsource_event_id\x18\x03 \x01(\fR\rsourceEventId\x12)\n" + + "\x10source_timestamp\x18\x04 \x01(\x03R\x0fsourceTimestamp\"(\n" + + "\x0eRelaysResponse\x12\x16\n" + + "\x06relays\x18\x01 \x03(\tR\x06relays\"4\n" + + "\x1aAuthorizedPublisherRequest\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\"=\n" + + "\x1bAuthorizedPublisherResponse\x12\x1e\n" + + "\n" + + "authorized\x18\x01 \x01(\bR\n" + + "authorized\"5\n" + + "\x19AuthorizedPubkeysResponse\x12\x18\n" + + "\apubkeys\x18\x01 \x03(\fR\apubkeys\"G\n" + + "\x14ValidateEventRequest\x12/\n" + + "\x05event\x18\x01 \x01(\v2\x19.orlysync.common.v1.EventR\x05event\"C\n" + + "\x15ValidateEventResponse\x12\x14\n" + + "\x05valid\x18\x01 \x01(\bR\x05valid\x12\x14\n" + + "\x05error\x18\x02 \x01(\tR\x05error\"E\n" + + "\x12HandleEventRequest\x12/\n" + + "\x05event\x18\x01 \x01(\v2\x19.orlysync.common.v1.EventR\x05event2\xd3\x05\n" + + "\x11RelayGroupService\x12E\n" + + "\x05Ready\x12\x19.orlysync.common.v1.Empty\x1a!.orlysync.common.v1.ReadyResponse\x12f\n" + + "\x17FindAuthoritativeConfig\x12\x19.orlysync.common.v1.Empty\x1a0.orlysync.relaygroup.v1.RelayGroupConfigResponse\x12N\n" + + "\tGetRelays\x12\x19.orlysync.common.v1.Empty\x1a&.orlysync.relaygroup.v1.RelaysResponse\x12\x80\x01\n" + + "\x15IsAuthorizedPublisher\x122.orlysync.relaygroup.v1.AuthorizedPublisherRequest\x1a3.orlysync.relaygroup.v1.AuthorizedPublisherResponse\x12d\n" + + "\x14GetAuthorizedPubkeys\x12\x19.orlysync.common.v1.Empty\x1a1.orlysync.relaygroup.v1.AuthorizedPubkeysResponse\x12v\n" + + "\x17ValidateRelayGroupEvent\x12,.orlysync.relaygroup.v1.ValidateEventRequest\x1a-.orlysync.relaygroup.v1.ValidateEventResponse\x12^\n" + + "\x15HandleRelayGroupEvent\x12*.orlysync.relaygroup.v1.HandleEventRequest\x1a\x19.orlysync.common.v1.EmptyB=Z;next.orly.dev/pkg/proto/orlysync/relaygroup/v1;relaygroupv1b\x06proto3" + +var ( + file_orlysync_relaygroup_v1_service_proto_rawDescOnce sync.Once + file_orlysync_relaygroup_v1_service_proto_rawDescData []byte +) + +func file_orlysync_relaygroup_v1_service_proto_rawDescGZIP() []byte { + file_orlysync_relaygroup_v1_service_proto_rawDescOnce.Do(func() { + file_orlysync_relaygroup_v1_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_orlysync_relaygroup_v1_service_proto_rawDesc), len(file_orlysync_relaygroup_v1_service_proto_rawDesc))) + }) + return file_orlysync_relaygroup_v1_service_proto_rawDescData +} + +var file_orlysync_relaygroup_v1_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_orlysync_relaygroup_v1_service_proto_goTypes = []any{ + (*RelayGroupConfig)(nil), // 0: orlysync.relaygroup.v1.RelayGroupConfig + (*RelayGroupConfigResponse)(nil), // 1: orlysync.relaygroup.v1.RelayGroupConfigResponse + (*RelaysResponse)(nil), // 2: orlysync.relaygroup.v1.RelaysResponse + (*AuthorizedPublisherRequest)(nil), // 3: orlysync.relaygroup.v1.AuthorizedPublisherRequest + (*AuthorizedPublisherResponse)(nil), // 4: orlysync.relaygroup.v1.AuthorizedPublisherResponse + (*AuthorizedPubkeysResponse)(nil), // 5: orlysync.relaygroup.v1.AuthorizedPubkeysResponse + (*ValidateEventRequest)(nil), // 6: orlysync.relaygroup.v1.ValidateEventRequest + (*ValidateEventResponse)(nil), // 7: orlysync.relaygroup.v1.ValidateEventResponse + (*HandleEventRequest)(nil), // 8: orlysync.relaygroup.v1.HandleEventRequest + (*v1.Event)(nil), // 9: orlysync.common.v1.Event + (*v1.Empty)(nil), // 10: orlysync.common.v1.Empty + (*v1.ReadyResponse)(nil), // 11: orlysync.common.v1.ReadyResponse +} +var file_orlysync_relaygroup_v1_service_proto_depIdxs = []int32{ + 0, // 0: orlysync.relaygroup.v1.RelayGroupConfigResponse.config:type_name -> orlysync.relaygroup.v1.RelayGroupConfig + 9, // 1: orlysync.relaygroup.v1.ValidateEventRequest.event:type_name -> orlysync.common.v1.Event + 9, // 2: orlysync.relaygroup.v1.HandleEventRequest.event:type_name -> orlysync.common.v1.Event + 10, // 3: orlysync.relaygroup.v1.RelayGroupService.Ready:input_type -> orlysync.common.v1.Empty + 10, // 4: orlysync.relaygroup.v1.RelayGroupService.FindAuthoritativeConfig:input_type -> orlysync.common.v1.Empty + 10, // 5: orlysync.relaygroup.v1.RelayGroupService.GetRelays:input_type -> orlysync.common.v1.Empty + 3, // 6: orlysync.relaygroup.v1.RelayGroupService.IsAuthorizedPublisher:input_type -> orlysync.relaygroup.v1.AuthorizedPublisherRequest + 10, // 7: orlysync.relaygroup.v1.RelayGroupService.GetAuthorizedPubkeys:input_type -> orlysync.common.v1.Empty + 6, // 8: orlysync.relaygroup.v1.RelayGroupService.ValidateRelayGroupEvent:input_type -> orlysync.relaygroup.v1.ValidateEventRequest + 8, // 9: orlysync.relaygroup.v1.RelayGroupService.HandleRelayGroupEvent:input_type -> orlysync.relaygroup.v1.HandleEventRequest + 11, // 10: orlysync.relaygroup.v1.RelayGroupService.Ready:output_type -> orlysync.common.v1.ReadyResponse + 1, // 11: orlysync.relaygroup.v1.RelayGroupService.FindAuthoritativeConfig:output_type -> orlysync.relaygroup.v1.RelayGroupConfigResponse + 2, // 12: orlysync.relaygroup.v1.RelayGroupService.GetRelays:output_type -> orlysync.relaygroup.v1.RelaysResponse + 4, // 13: orlysync.relaygroup.v1.RelayGroupService.IsAuthorizedPublisher:output_type -> orlysync.relaygroup.v1.AuthorizedPublisherResponse + 5, // 14: orlysync.relaygroup.v1.RelayGroupService.GetAuthorizedPubkeys:output_type -> orlysync.relaygroup.v1.AuthorizedPubkeysResponse + 7, // 15: orlysync.relaygroup.v1.RelayGroupService.ValidateRelayGroupEvent:output_type -> orlysync.relaygroup.v1.ValidateEventResponse + 10, // 16: orlysync.relaygroup.v1.RelayGroupService.HandleRelayGroupEvent:output_type -> orlysync.common.v1.Empty + 10, // [10:17] is the sub-list for method output_type + 3, // [3:10] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_orlysync_relaygroup_v1_service_proto_init() } +func file_orlysync_relaygroup_v1_service_proto_init() { + if File_orlysync_relaygroup_v1_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_orlysync_relaygroup_v1_service_proto_rawDesc), len(file_orlysync_relaygroup_v1_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 9, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_orlysync_relaygroup_v1_service_proto_goTypes, + DependencyIndexes: file_orlysync_relaygroup_v1_service_proto_depIdxs, + MessageInfos: file_orlysync_relaygroup_v1_service_proto_msgTypes, + }.Build() + File_orlysync_relaygroup_v1_service_proto = out.File + file_orlysync_relaygroup_v1_service_proto_goTypes = nil + file_orlysync_relaygroup_v1_service_proto_depIdxs = nil +} diff --git a/pkg/proto/orlysync/relaygroup/v1/service_grpc.pb.go b/pkg/proto/orlysync/relaygroup/v1/service_grpc.pb.go new file mode 100644 index 0000000..be6758c --- /dev/null +++ b/pkg/proto/orlysync/relaygroup/v1/service_grpc.pb.go @@ -0,0 +1,372 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.0 +// - protoc (unknown) +// source: orlysync/relaygroup/v1/service.proto + +package relaygroupv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + v1 "next.orly.dev/pkg/proto/orlysync/common/v1" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + RelayGroupService_Ready_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/Ready" + RelayGroupService_FindAuthoritativeConfig_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/FindAuthoritativeConfig" + RelayGroupService_GetRelays_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/GetRelays" + RelayGroupService_IsAuthorizedPublisher_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/IsAuthorizedPublisher" + RelayGroupService_GetAuthorizedPubkeys_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/GetAuthorizedPubkeys" + RelayGroupService_ValidateRelayGroupEvent_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/ValidateRelayGroupEvent" + RelayGroupService_HandleRelayGroupEvent_FullMethodName = "/orlysync.relaygroup.v1.RelayGroupService/HandleRelayGroupEvent" +) + +// RelayGroupServiceClient is the client API for RelayGroupService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// RelayGroupService provides relay group configuration discovery +// by selecting authoritative config from Kind 39105 events +type RelayGroupServiceClient interface { + // Ready returns whether the service is ready to serve requests + Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) + // FindAuthoritativeConfig finds the authoritative relay group configuration + // using timestamp ordering with hash tie-breaking + FindAuthoritativeConfig(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*RelayGroupConfigResponse, error) + // GetRelays returns the list of relays from the authoritative config + GetRelays(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*RelaysResponse, error) + // IsAuthorizedPublisher checks if a pubkey can publish relay group configs + IsAuthorizedPublisher(ctx context.Context, in *AuthorizedPublisherRequest, opts ...grpc.CallOption) (*AuthorizedPublisherResponse, error) + // GetAuthorizedPubkeys returns all authorized publisher pubkeys + GetAuthorizedPubkeys(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*AuthorizedPubkeysResponse, error) + // ValidateRelayGroupEvent validates a relay group configuration event + ValidateRelayGroupEvent(ctx context.Context, in *ValidateEventRequest, opts ...grpc.CallOption) (*ValidateEventResponse, error) + // HandleRelayGroupEvent processes a relay group event and triggers peer updates + HandleRelayGroupEvent(ctx context.Context, in *HandleEventRequest, opts ...grpc.CallOption) (*v1.Empty, error) +} + +type relayGroupServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewRelayGroupServiceClient(cc grpc.ClientConnInterface) RelayGroupServiceClient { + return &relayGroupServiceClient{cc} +} + +func (c *relayGroupServiceClient) Ready(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*v1.ReadyResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.ReadyResponse) + err := c.cc.Invoke(ctx, RelayGroupService_Ready_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) FindAuthoritativeConfig(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*RelayGroupConfigResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RelayGroupConfigResponse) + err := c.cc.Invoke(ctx, RelayGroupService_FindAuthoritativeConfig_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) GetRelays(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*RelaysResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RelaysResponse) + err := c.cc.Invoke(ctx, RelayGroupService_GetRelays_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) IsAuthorizedPublisher(ctx context.Context, in *AuthorizedPublisherRequest, opts ...grpc.CallOption) (*AuthorizedPublisherResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthorizedPublisherResponse) + err := c.cc.Invoke(ctx, RelayGroupService_IsAuthorizedPublisher_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) GetAuthorizedPubkeys(ctx context.Context, in *v1.Empty, opts ...grpc.CallOption) (*AuthorizedPubkeysResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(AuthorizedPubkeysResponse) + err := c.cc.Invoke(ctx, RelayGroupService_GetAuthorizedPubkeys_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) ValidateRelayGroupEvent(ctx context.Context, in *ValidateEventRequest, opts ...grpc.CallOption) (*ValidateEventResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ValidateEventResponse) + err := c.cc.Invoke(ctx, RelayGroupService_ValidateRelayGroupEvent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *relayGroupServiceClient) HandleRelayGroupEvent(ctx context.Context, in *HandleEventRequest, opts ...grpc.CallOption) (*v1.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(v1.Empty) + err := c.cc.Invoke(ctx, RelayGroupService_HandleRelayGroupEvent_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// RelayGroupServiceServer is the server API for RelayGroupService service. +// All implementations must embed UnimplementedRelayGroupServiceServer +// for forward compatibility. +// +// RelayGroupService provides relay group configuration discovery +// by selecting authoritative config from Kind 39105 events +type RelayGroupServiceServer interface { + // Ready returns whether the service is ready to serve requests + Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) + // FindAuthoritativeConfig finds the authoritative relay group configuration + // using timestamp ordering with hash tie-breaking + FindAuthoritativeConfig(context.Context, *v1.Empty) (*RelayGroupConfigResponse, error) + // GetRelays returns the list of relays from the authoritative config + GetRelays(context.Context, *v1.Empty) (*RelaysResponse, error) + // IsAuthorizedPublisher checks if a pubkey can publish relay group configs + IsAuthorizedPublisher(context.Context, *AuthorizedPublisherRequest) (*AuthorizedPublisherResponse, error) + // GetAuthorizedPubkeys returns all authorized publisher pubkeys + GetAuthorizedPubkeys(context.Context, *v1.Empty) (*AuthorizedPubkeysResponse, error) + // ValidateRelayGroupEvent validates a relay group configuration event + ValidateRelayGroupEvent(context.Context, *ValidateEventRequest) (*ValidateEventResponse, error) + // HandleRelayGroupEvent processes a relay group event and triggers peer updates + HandleRelayGroupEvent(context.Context, *HandleEventRequest) (*v1.Empty, error) + mustEmbedUnimplementedRelayGroupServiceServer() +} + +// UnimplementedRelayGroupServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedRelayGroupServiceServer struct{} + +func (UnimplementedRelayGroupServiceServer) Ready(context.Context, *v1.Empty) (*v1.ReadyResponse, error) { + return nil, status.Error(codes.Unimplemented, "method Ready not implemented") +} +func (UnimplementedRelayGroupServiceServer) FindAuthoritativeConfig(context.Context, *v1.Empty) (*RelayGroupConfigResponse, error) { + return nil, status.Error(codes.Unimplemented, "method FindAuthoritativeConfig not implemented") +} +func (UnimplementedRelayGroupServiceServer) GetRelays(context.Context, *v1.Empty) (*RelaysResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetRelays not implemented") +} +func (UnimplementedRelayGroupServiceServer) IsAuthorizedPublisher(context.Context, *AuthorizedPublisherRequest) (*AuthorizedPublisherResponse, error) { + return nil, status.Error(codes.Unimplemented, "method IsAuthorizedPublisher not implemented") +} +func (UnimplementedRelayGroupServiceServer) GetAuthorizedPubkeys(context.Context, *v1.Empty) (*AuthorizedPubkeysResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetAuthorizedPubkeys not implemented") +} +func (UnimplementedRelayGroupServiceServer) ValidateRelayGroupEvent(context.Context, *ValidateEventRequest) (*ValidateEventResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ValidateRelayGroupEvent not implemented") +} +func (UnimplementedRelayGroupServiceServer) HandleRelayGroupEvent(context.Context, *HandleEventRequest) (*v1.Empty, error) { + return nil, status.Error(codes.Unimplemented, "method HandleRelayGroupEvent not implemented") +} +func (UnimplementedRelayGroupServiceServer) mustEmbedUnimplementedRelayGroupServiceServer() {} +func (UnimplementedRelayGroupServiceServer) testEmbeddedByValue() {} + +// UnsafeRelayGroupServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to RelayGroupServiceServer will +// result in compilation errors. +type UnsafeRelayGroupServiceServer interface { + mustEmbedUnimplementedRelayGroupServiceServer() +} + +func RegisterRelayGroupServiceServer(s grpc.ServiceRegistrar, srv RelayGroupServiceServer) { + // If the following call panics, it indicates UnimplementedRelayGroupServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&RelayGroupService_ServiceDesc, srv) +} + +func _RelayGroupService_Ready_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).Ready(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_Ready_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).Ready(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_FindAuthoritativeConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).FindAuthoritativeConfig(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_FindAuthoritativeConfig_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).FindAuthoritativeConfig(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_GetRelays_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).GetRelays(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_GetRelays_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).GetRelays(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_IsAuthorizedPublisher_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(AuthorizedPublisherRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).IsAuthorizedPublisher(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_IsAuthorizedPublisher_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).IsAuthorizedPublisher(ctx, req.(*AuthorizedPublisherRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_GetAuthorizedPubkeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(v1.Empty) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).GetAuthorizedPubkeys(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_GetAuthorizedPubkeys_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).GetAuthorizedPubkeys(ctx, req.(*v1.Empty)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_ValidateRelayGroupEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ValidateEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).ValidateRelayGroupEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_ValidateRelayGroupEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).ValidateRelayGroupEvent(ctx, req.(*ValidateEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _RelayGroupService_HandleRelayGroupEvent_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(HandleEventRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(RelayGroupServiceServer).HandleRelayGroupEvent(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: RelayGroupService_HandleRelayGroupEvent_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(RelayGroupServiceServer).HandleRelayGroupEvent(ctx, req.(*HandleEventRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// RelayGroupService_ServiceDesc is the grpc.ServiceDesc for RelayGroupService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var RelayGroupService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "orlysync.relaygroup.v1.RelayGroupService", + HandlerType: (*RelayGroupServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "Ready", + Handler: _RelayGroupService_Ready_Handler, + }, + { + MethodName: "FindAuthoritativeConfig", + Handler: _RelayGroupService_FindAuthoritativeConfig_Handler, + }, + { + MethodName: "GetRelays", + Handler: _RelayGroupService_GetRelays_Handler, + }, + { + MethodName: "IsAuthorizedPublisher", + Handler: _RelayGroupService_IsAuthorizedPublisher_Handler, + }, + { + MethodName: "GetAuthorizedPubkeys", + Handler: _RelayGroupService_GetAuthorizedPubkeys_Handler, + }, + { + MethodName: "ValidateRelayGroupEvent", + Handler: _RelayGroupService_ValidateRelayGroupEvent_Handler, + }, + { + MethodName: "HandleRelayGroupEvent", + Handler: _RelayGroupService_HandleRelayGroupEvent_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "orlysync/relaygroup/v1/service.proto", +} diff --git a/pkg/sync/cluster/grpc/client.go b/pkg/sync/cluster/grpc/client.go new file mode 100644 index 0000000..360fe12 --- /dev/null +++ b/pkg/sync/cluster/grpc/client.go @@ -0,0 +1,187 @@ +// Package grpc provides a gRPC client for the cluster sync service. +package grpc + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "lol.mleku.dev/log" + + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + clusterv1 "next.orly.dev/pkg/proto/orlysync/cluster/v1" +) + +// Client is a gRPC client for the cluster sync service. +type Client struct { + conn *grpc.ClientConn + client clusterv1.ClusterSyncServiceClient + ready chan struct{} +} + +// ClientConfig holds configuration for the gRPC client. +type ClientConfig struct { + ServerAddress string + ConnectTimeout time.Duration +} + +// New creates a new gRPC cluster sync client. +func New(ctx context.Context, cfg *ClientConfig) (*Client, error) { + timeout := cfg.ConnectTimeout + if timeout == 0 { + timeout = 10 * time.Second + } + + dialCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(16<<20), // 16MB + grpc.MaxCallSendMsgSize(16<<20), // 16MB + ), + ) + if err != nil { + return nil, err + } + + c := &Client{ + conn: conn, + client: clusterv1.NewClusterSyncServiceClient(conn), + ready: make(chan struct{}), + } + + go c.waitForReady(ctx) + + return c, nil +} + +func (c *Client) waitForReady(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + resp, err := c.client.Ready(ctx, &commonv1.Empty{}) + if err == nil && resp.Ready { + close(c.ready) + log.I.F("gRPC cluster sync client connected and ready") + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// Close closes the gRPC connection. +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +// Ready returns a channel that closes when the client is ready. +func (c *Client) Ready() <-chan struct{} { + return c.ready +} + +// Start starts the cluster polling loop. +func (c *Client) Start(ctx context.Context) error { + _, err := c.client.Start(ctx, &commonv1.Empty{}) + return err +} + +// Stop stops the cluster polling loop. +func (c *Client) Stop(ctx context.Context) error { + _, err := c.client.Stop(ctx, &commonv1.Empty{}) + return err +} + +// HandleLatestSerial proxies an HTTP latest serial request. +func (c *Client) HandleLatestSerial(ctx context.Context, method string, body []byte, headers map[string]string) (int, []byte, map[string]string, error) { + resp, err := c.client.HandleLatestSerial(ctx, &commonv1.HTTPRequest{ + Method: method, + Body: body, + Headers: headers, + }) + if err != nil { + return 0, nil, nil, err + } + return int(resp.StatusCode), resp.Body, resp.Headers, nil +} + +// HandleEventsRange proxies an HTTP events range request. +func (c *Client) HandleEventsRange(ctx context.Context, method string, body []byte, headers map[string]string, queryString string) (int, []byte, map[string]string, error) { + resp, err := c.client.HandleEventsRange(ctx, &commonv1.HTTPRequest{ + Method: method, + Body: body, + Headers: headers, + QueryString: queryString, + }) + if err != nil { + return 0, nil, nil, err + } + return int(resp.StatusCode), resp.Body, resp.Headers, nil +} + +// GetMembers returns the current cluster members. +func (c *Client) GetMembers(ctx context.Context) ([]*clusterv1.ClusterMember, error) { + resp, err := c.client.GetMembers(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + return resp.Members, nil +} + +// UpdateMembership updates cluster membership. +func (c *Client) UpdateMembership(ctx context.Context, relayURLs []string) error { + _, err := c.client.UpdateMembership(ctx, &clusterv1.UpdateMembershipRequest{ + RelayUrls: relayURLs, + }) + return err +} + +// HandleMembershipEvent processes a cluster membership event. +func (c *Client) HandleMembershipEvent(ctx context.Context, event *commonv1.Event) error { + _, err := c.client.HandleMembershipEvent(ctx, &clusterv1.MembershipEventRequest{ + Event: event, + }) + return err +} + +// GetClusterStatus returns overall cluster status. +func (c *Client) GetClusterStatus(ctx context.Context) (*clusterv1.ClusterStatusResponse, error) { + return c.client.GetClusterStatus(ctx, &commonv1.Empty{}) +} + +// GetMemberStatus returns status for a specific member. +func (c *Client) GetMemberStatus(ctx context.Context, httpURL string) (*clusterv1.MemberStatusResponse, error) { + return c.client.GetMemberStatus(ctx, &clusterv1.MemberStatusRequest{ + HttpUrl: httpURL, + }) +} + +// GetLatestSerial returns the latest serial from this relay's database. +func (c *Client) GetLatestSerial(ctx context.Context) (uint64, int64, error) { + resp, err := c.client.GetLatestSerial(ctx, &commonv1.Empty{}) + if err != nil { + return 0, 0, err + } + return resp.Serial, resp.Timestamp, nil +} + +// GetEventsInRange returns event info for a serial range. +func (c *Client) GetEventsInRange(ctx context.Context, from, to uint64, limit int32) ([]*clusterv1.EventInfo, bool, uint64, error) { + resp, err := c.client.GetEventsInRange(ctx, &clusterv1.EventsRangeRequest{ + From: from, + To: to, + Limit: limit, + }) + if err != nil { + return nil, false, 0, err + } + return resp.Events, resp.HasMore, resp.NextFrom, nil +} diff --git a/pkg/sync/cluster.go b/pkg/sync/cluster/manager.go similarity index 68% rename from pkg/sync/cluster.go rename to pkg/sync/cluster/manager.go index 15ddc00..36049cb 100644 --- a/pkg/sync/cluster.go +++ b/pkg/sync/cluster/manager.go @@ -1,4 +1,5 @@ -package sync +// Package cluster provides cluster replication with persistent state +package cluster import ( "context" @@ -6,37 +7,45 @@ import ( "encoding/json" "fmt" "net/http" - "sync" + gosync "sync" "time" "github.com/dgraph-io/badger/v4" - "lol.mleku.dev/log" - "next.orly.dev/pkg/database" - "next.orly.dev/pkg/database/indexes/types" "git.mleku.dev/mleku/nostr/crypto/keys" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/encoders/kind" + "lol.mleku.dev/log" + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/database/indexes/types" + "next.orly.dev/pkg/sync/common" ) -type ClusterManager struct { - ctx context.Context - cancel context.CancelFunc - db *database.D - adminNpubs []string - relayIdentityPubkey string // Our relay's identity pubkey (hex) - selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups) - members map[string]*ClusterMember // keyed by relay URL - membersMux sync.RWMutex - pollTicker *time.Ticker - pollDone chan struct{} - httpClient *http.Client - propagatePrivilegedEvents bool - publisher interface{ Deliver(*event.E) } - nip11Cache *NIP11Cache -} - -type ClusterMember struct { +// EventPublisher is an interface for publishing events +type EventPublisher interface { + Deliver(*event.E) +} + +// Manager handles cluster replication between relay instances +type Manager struct { + ctx context.Context + cancel context.CancelFunc + db *database.D + adminNpubs []string + relayIdentityPubkey string // Our relay's identity pubkey (hex) + selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups) + members map[string]*Member // keyed by relay URL + membersMux gosync.RWMutex + pollTicker *time.Ticker + pollDone chan struct{} + httpClient *http.Client + propagatePrivilegedEvents bool + publisher EventPublisher + nip11Cache *common.NIP11Cache +} + +// Member represents a cluster member +type Member struct { HTTPURL string WebSocketURL string LastSerial uint64 @@ -45,26 +54,51 @@ type ClusterMember struct { ErrorCount int } +// LatestSerialResponse returns the latest serial type LatestSerialResponse struct { Serial uint64 `json:"serial"` Timestamp int64 `json:"timestamp"` } +// EventsRangeResponse contains events in a range type EventsRangeResponse struct { Events []EventInfo `json:"events"` HasMore bool `json:"has_more"` NextFrom uint64 `json:"next_from,omitempty"` } +// EventInfo contains metadata about an event type EventInfo struct { Serial uint64 `json:"serial"` ID string `json:"id"` Timestamp int64 `json:"timestamp"` } -func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string, propagatePrivilegedEvents bool, publisher interface{ Deliver(*event.E) }) *ClusterManager { +// Config holds configuration for the cluster manager +type Config struct { + AdminNpubs []string + PropagatePrivilegedEvents bool + PollInterval time.Duration + NIP11CacheTTL time.Duration +} + +// DefaultConfig returns default configuration +func DefaultConfig() *Config { + return &Config{ + PropagatePrivilegedEvents: true, + PollInterval: 5 * time.Second, + NIP11CacheTTL: 30 * time.Minute, + } +} + +// NewManager creates a new cluster manager +func NewManager(ctx context.Context, db *database.D, cfg *Config, publisher EventPublisher) *Manager { ctx, cancel := context.WithCancel(ctx) + if cfg == nil { + cfg = DefaultConfig() + } + // Get our relay identity pubkey var relayPubkey string if skb, err := db.GetRelayIdentitySecret(); err == nil && len(skb) == 32 { @@ -73,27 +107,28 @@ func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string, } } - cm := &ClusterManager{ + cm := &Manager{ ctx: ctx, cancel: cancel, db: db, - adminNpubs: adminNpubs, + adminNpubs: cfg.AdminNpubs, relayIdentityPubkey: relayPubkey, selfURLs: make(map[string]bool), - members: make(map[string]*ClusterMember), + members: make(map[string]*Member), pollDone: make(chan struct{}), - propagatePrivilegedEvents: propagatePrivilegedEvents, + propagatePrivilegedEvents: cfg.PropagatePrivilegedEvents, publisher: publisher, httpClient: &http.Client{ Timeout: 30 * time.Second, }, - nip11Cache: NewNIP11Cache(30 * time.Minute), + nip11Cache: common.NewNIP11Cache(cfg.NIP11CacheTTL), } return cm } -func (cm *ClusterManager) Start() { +// Start starts the cluster polling loop +func (cm *Manager) Start() { log.I.Ln("starting cluster replication manager") // Load persisted peer state from database @@ -105,7 +140,8 @@ func (cm *ClusterManager) Start() { go cm.pollingLoop() } -func (cm *ClusterManager) Stop() { +// Stop stops the cluster polling loop +func (cm *Manager) Stop() { log.I.Ln("stopping cluster replication manager") cm.cancel() if cm.pollTicker != nil { @@ -114,7 +150,73 @@ func (cm *ClusterManager) Stop() { <-cm.pollDone } -func (cm *ClusterManager) pollingLoop() { +// GetRelayIdentityPubkey returns the relay's identity pubkey +func (cm *Manager) GetRelayIdentityPubkey() string { + return cm.relayIdentityPubkey +} + +// GetMembers returns a copy of the current members +func (cm *Manager) GetMembers() []*Member { + cm.membersMux.RLock() + defer cm.membersMux.RUnlock() + members := make([]*Member, 0, len(cm.members)) + for _, m := range cm.members { + memberCopy := *m + members = append(members, &memberCopy) + } + return members +} + +// GetMember returns a specific member by URL or nil if not found +func (cm *Manager) GetMember(httpURL string) *Member { + cm.membersMux.RLock() + defer cm.membersMux.RUnlock() + if m, ok := cm.members[httpURL]; ok { + memberCopy := *m + return &memberCopy + } + return nil +} + +// GetLatestSerial returns the latest serial and timestamp from the database +func (cm *Manager) GetLatestSerial() (uint64, int64) { + serial, err := cm.getLatestSerialFromDB() + if err != nil { + return 0, time.Now().Unix() + } + return serial, time.Now().Unix() +} + +// PropagatePrivilegedEvents returns whether privileged events should be propagated +func (cm *Manager) PropagatePrivilegedEvents() bool { + return cm.propagatePrivilegedEvents +} + +// GetEventsInRange returns events in a serial range with pagination +func (cm *Manager) GetEventsInRange(from, to uint64, limit int) ([]EventInfo, bool, uint64) { + events, hasMore, nextFrom, err := cm.getEventsInRangeFromDB(from, to, limit) + if err != nil { + return nil, false, 0 + } + return events, hasMore, nextFrom +} + +// IsSelfURL checks if a URL is our own relay +func (cm *Manager) IsSelfURL(url string) bool { + cm.membersMux.RLock() + result := cm.selfURLs[url] + cm.membersMux.RUnlock() + return result +} + +// MarkSelfURL marks a URL as belonging to us +func (cm *Manager) MarkSelfURL(url string) { + cm.membersMux.Lock() + cm.selfURLs[url] = true + cm.membersMux.Unlock() +} + +func (cm *Manager) pollingLoop() { defer close(cm.pollDone) for { @@ -127,9 +229,9 @@ func (cm *ClusterManager) pollingLoop() { } } -func (cm *ClusterManager) pollAllMembers() { +func (cm *Manager) pollAllMembers() { cm.membersMux.RLock() - members := make([]*ClusterMember, 0, len(cm.members)) + members := make([]*Member, 0, len(cm.members)) for _, member := range cm.members { members = append(members, member) } @@ -140,7 +242,7 @@ func (cm *ClusterManager) pollAllMembers() { } } -func (cm *ClusterManager) pollMember(member *ClusterMember) { +func (cm *Manager) pollMember(member *Member) { // Get latest serial from peer latestResp, err := cm.getLatestSerial(member.HTTPURL) if err != nil { @@ -167,17 +269,17 @@ func (cm *ClusterManager) pollMember(member *ClusterMember) { return } - // Process fetched events - for _, eventInfo := range eventsResp.Events { - if cm.shouldFetchEvent(eventInfo) { - // Fetch full event via WebSocket and store it - if err := cm.fetchAndStoreEvent(member.WebSocketURL, eventInfo.ID, cm.publisher); err != nil { - log.W.F("failed to fetch/store event %s from %s: %v", eventInfo.ID, member.HTTPURL, err) - } else { - log.D.F("successfully replicated event %s from %s", eventInfo.ID, member.HTTPURL) - } + // Process fetched events + for _, eventInfo := range eventsResp.Events { + if cm.shouldFetchEvent(eventInfo) { + // Fetch full event via WebSocket and store it + if err := cm.fetchAndStoreEvent(member.WebSocketURL, eventInfo.ID, cm.publisher); err != nil { + log.W.F("failed to fetch/store event %s from %s: %v", eventInfo.ID, member.HTTPURL, err) + } else { + log.D.F("successfully replicated event %s from %s", eventInfo.ID, member.HTTPURL) } } + } // Update last serial if we processed all events if !eventsResp.HasMore && member.LastSerial != to { @@ -189,7 +291,7 @@ func (cm *ClusterManager) pollMember(member *ClusterMember) { } } -func (cm *ClusterManager) getLatestSerial(peerURL string) (*LatestSerialResponse, error) { +func (cm *Manager) getLatestSerial(peerURL string) (*LatestSerialResponse, error) { url := fmt.Sprintf("%s/cluster/latest", peerURL) resp, err := cm.httpClient.Get(url) if err != nil { @@ -209,7 +311,7 @@ func (cm *ClusterManager) getLatestSerial(peerURL string) (*LatestSerialResponse return &result, nil } -func (cm *ClusterManager) getEventsInRange(peerURL string, from, to uint64, limit int) (*EventsRangeResponse, error) { +func (cm *Manager) getEventsInRange(peerURL string, from, to uint64, limit int) (*EventsRangeResponse, error) { url := fmt.Sprintf("%s/cluster/events?from=%d&to=%d&limit=%d", peerURL, from, to, limit) resp, err := cm.httpClient.Get(url) if err != nil { @@ -229,13 +331,13 @@ func (cm *ClusterManager) getEventsInRange(peerURL string, from, to uint64, limi return &result, nil } -func (cm *ClusterManager) shouldFetchEvent(eventInfo EventInfo) bool { +func (cm *Manager) shouldFetchEvent(eventInfo EventInfo) bool { // Relays MAY choose not to store every event they receive // For now, accept all events return true } -func (cm *ClusterManager) updateMemberStatus(member *ClusterMember, status string) { +func (cm *Manager) updateMemberStatus(member *Member, status string) { member.Status = status if status == "error" { member.ErrorCount++ @@ -244,7 +346,8 @@ func (cm *ClusterManager) updateMemberStatus(member *ClusterMember, status strin } } -func (cm *ClusterManager) UpdateMembership(relayURLs []string) { +// UpdateMembership updates the cluster membership +func (cm *Manager) UpdateMembership(relayURLs []string) { cm.membersMux.Lock() defer cm.membersMux.Unlock() @@ -297,7 +400,7 @@ func (cm *ClusterManager) UpdateMembership(relayURLs []string) { } // Add member - member := &ClusterMember{ + member := &Member{ HTTPURL: url, WebSocketURL: url, // TODO: Convert to WebSocket URL LastSerial: 0, @@ -309,7 +412,7 @@ func (cm *ClusterManager) UpdateMembership(relayURLs []string) { } // HandleMembershipEvent processes a cluster membership event (Kind 39108) -func (cm *ClusterManager) HandleMembershipEvent(event *event.E) error { +func (cm *Manager) HandleMembershipEvent(ev *event.E) error { // Verify the event is signed by a cluster admin adminFound := false for _, adminNpub := range cm.adminNpubs { @@ -326,7 +429,7 @@ func (cm *ClusterManager) HandleMembershipEvent(event *event.E) error { // Parse the relay URLs from the tags var relayURLs []string - for _, tag := range *event.Tags { + for _, tag := range *ev.Tags { if len(tag.T) >= 2 && string(tag.T[0]) == "relay" { relayURLs = append(relayURLs, string(tag.T[1])) } @@ -339,21 +442,21 @@ func (cm *ClusterManager) HandleMembershipEvent(event *event.E) error { // Update cluster membership cm.UpdateMembership(relayURLs) - log.I.F("updated cluster membership with %d relays from event %x", len(relayURLs), event.ID) + log.I.F("updated cluster membership with %d relays from event %x", len(relayURLs), ev.ID) return nil } // HTTP Handlers -func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Request) { +// HandleLatestSerial handles GET /cluster/latest +func (cm *Manager) HandleLatestSerial(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Check if request is from ourselves by examining the Referer or Origin header - // Note: Self-members are already filtered out, but this catches edge cases origin := r.Header.Get("Origin") referer := r.Header.Get("Referer") @@ -386,7 +489,7 @@ func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Requ } } - // Get the latest serial from database by querying for the highest serial + // Get the latest serial from database latestSerial, err := cm.getLatestSerialFromDB() if err != nil { log.W.F("failed to get latest serial: %v", err) @@ -403,14 +506,14 @@ func (cm *ClusterManager) HandleLatestSerial(w http.ResponseWriter, r *http.Requ json.NewEncoder(w).Encode(response) } -func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Request) { +// HandleEventsRange handles GET /cluster/events +func (cm *Manager) HandleEventsRange(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } - // Check if request is from ourselves by examining the Referer or Origin header - // Note: Self-members are already filtered out, but this catches edge cases + // Check if request is from ourselves origin := r.Header.Get("Origin") referer := r.Header.Get("Referer") @@ -434,7 +537,6 @@ func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Reque if err == nil && peerPubkey == cm.relayIdentityPubkey { log.D.F("rejecting cluster events request from self (discovered): %s", checkURL) - // Cache for future fast lookups cm.membersMux.Lock() cm.selfURLs[checkURL] = true cm.membersMux.Unlock() @@ -466,7 +568,7 @@ func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Reque } // Get events in range - events, hasMore, nextFrom, err := cm.getEventsInRangeFromDB(from, to, int(limit)) + events, hasMore, nextFrom, err := cm.getEventsInRangeFromDB(from, to, limit) if err != nil { log.W.F("failed to get events in range: %v", err) http.Error(w, "Internal server error", http.StatusInternalServerError) @@ -483,24 +585,21 @@ func (cm *ClusterManager) HandleEventsRange(w http.ResponseWriter, r *http.Reque json.NewEncoder(w).Encode(response) } -func (cm *ClusterManager) getLatestSerialFromDB() (uint64, error) { - // Query the database to find the highest serial number - // We'll iterate through the event keys to find the maximum serial +func (cm *Manager) getLatestSerialFromDB() (uint64, error) { var maxSerial uint64 = 0 err := cm.db.View(func(txn *badger.Txn) error { it := txn.NewIterator(badger.IteratorOptions{ - Reverse: true, // Start from highest - Prefix: []byte{0}, // Event keys start with 0 + Reverse: true, + Prefix: []byte{0}, }) defer it.Close() - // Look for the first event key (which should have the highest serial in reverse iteration) it.Seek([]byte{0}) if it.Valid() { key := it.Item().Key() - if len(key) >= 5 { // Serial is in the last 5 bytes - serial := binary.BigEndian.Uint64(key[len(key)-8:]) >> 24 // Convert from Uint40 + if len(key) >= 5 { + serial := binary.BigEndian.Uint64(key[len(key)-8:]) >> 24 if serial > maxSerial { maxSerial = serial } @@ -513,12 +612,11 @@ func (cm *ClusterManager) getLatestSerialFromDB() (uint64, error) { return maxSerial, err } -func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([]EventInfo, bool, uint64, error) { +func (cm *Manager) getEventsInRangeFromDB(from, to uint64, limit int) ([]EventInfo, bool, uint64, error) { var events []EventInfo var hasMore bool var nextFrom uint64 - // Convert serials to Uint40 format for querying fromSerial := &types.Uint40{} toSerial := &types.Uint40{} @@ -529,11 +627,9 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] return nil, false, 0, err } - // Query events by serial range err := cm.db.View(func(txn *badger.Txn) error { - // Iterate through event keys in the database it := txn.NewIterator(badger.IteratorOptions{ - Prefix: []byte{0}, // Event keys start with 0 + Prefix: []byte{0}, }) defer it.Close() @@ -543,15 +639,11 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] for it.Valid() && count < limit { key := it.Item().Key() - // Check if this is an event key (starts with event prefix) if len(key) >= 8 && key[0] == 0 && key[1] == 0 && key[2] == 0 { - // Extract serial from the last 5 bytes (Uint40) if len(key) >= 8 { - serial := binary.BigEndian.Uint64(key[len(key)-8:]) >> 24 // Convert from Uint40 + serial := binary.BigEndian.Uint64(key[len(key)-8:]) >> 24 - // Check if serial is in range if serial >= from && serial <= to { - // Fetch the full event to check if it's privileged serial40 := &types.Uint40{} if err := serial40.Set(serial); err != nil { continue @@ -562,7 +654,6 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] continue } - // Check if we should propagate this event shouldPropagate := true if !cm.propagatePrivilegedEvents && kind.IsPrivileged(ev.Kind) { shouldPropagate = false @@ -577,7 +668,6 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] count++ } - // Free the event ev.Free() } } @@ -586,10 +676,8 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] it.Next() } - // Check if there are more events if it.Valid() { hasMore = true - // Try to get the next serial nextKey := it.Item().Key() if len(nextKey) >= 8 && nextKey[0] == 0 && nextKey[1] == 0 && nextKey[2] == 0 { nextSerial := binary.BigEndian.Uint64(nextKey[len(nextKey)-8:]) >> 24 @@ -603,27 +691,10 @@ func (cm *ClusterManager) getEventsInRangeFromDB(from, to uint64, limit int) ([] return events, hasMore, nextFrom, err } -func (cm *ClusterManager) fetchAndStoreEvent(wsURL, eventID string, publisher interface{ Deliver(*event.E) }) error { +func (cm *Manager) fetchAndStoreEvent(wsURL, eventID string, publisher EventPublisher) error { // TODO: Implement WebSocket connection and event fetching - // For now, this is a placeholder that assumes the event can be fetched - // In a full implementation, this would: - // 1. Connect to the WebSocket endpoint - // 2. Send a REQ message for the specific event ID - // 3. Receive the EVENT message - // 4. Validate and store the event in the local database - // 5. Propagate the event to subscribers via the publisher - - // Placeholder - mark as not implemented for now log.D.F("fetchAndStoreEvent called for %s from %s (placeholder implementation)", eventID, wsURL) - - // Note: When implementing the full WebSocket fetching logic, after storing the event, - // the publisher should be called like this: - // if publisher != nil { - // clonedEvent := fetchedEvent.Clone() - // go publisher.Deliver(clonedEvent) - // } - - return nil // Return success for now + return nil } // Database key prefixes for cluster state persistence @@ -631,8 +702,7 @@ const ( clusterPeerStatePrefix = "cluster:peer:" ) -// loadPeerState loads persisted peer state from the database -func (cm *ClusterManager) loadPeerState() error { +func (cm *Manager) loadPeerState() error { cm.membersMux.Lock() defer cm.membersMux.Unlock() @@ -647,10 +717,8 @@ func (cm *ClusterManager) loadPeerState() error { item := it.Item() key := item.Key() - // Extract peer URL from key (remove prefix) peerURL := string(key[len(prefix):]) - // Read the serial value var serial uint64 err := item.Value(func(val []byte) error { if len(val) == 8 { @@ -663,15 +731,13 @@ func (cm *ClusterManager) loadPeerState() error { continue } - // Update existing member or create new one if member, exists := cm.members[peerURL]; exists { member.LastSerial = serial log.D.F("loaded persisted serial %d for existing peer %s", serial, peerURL) } else { - // Create member with persisted state - member := &ClusterMember{ + member := &Member{ HTTPURL: peerURL, - WebSocketURL: peerURL, // TODO: Convert to WebSocket URL + WebSocketURL: peerURL, LastSerial: serial, Status: "unknown", } @@ -683,8 +749,7 @@ func (cm *ClusterManager) loadPeerState() error { }) } -// savePeerState saves the current serial for a peer to the database -func (cm *ClusterManager) savePeerState(peerURL string, serial uint64) error { +func (cm *Manager) savePeerState(peerURL string, serial uint64) error { key := []byte(clusterPeerStatePrefix + peerURL) value := make([]byte, 8) binary.BigEndian.PutUint64(value, serial) @@ -694,8 +759,7 @@ func (cm *ClusterManager) savePeerState(peerURL string, serial uint64) error { }) } -// removePeerState removes persisted state for a peer from the database -func (cm *ClusterManager) removePeerState(peerURL string) error { +func (cm *Manager) removePeerState(peerURL string) error { key := []byte(clusterPeerStatePrefix + peerURL) return cm.db.Update(func(txn *badger.Txn) error { diff --git a/pkg/sync/cluster/server/service.go b/pkg/sync/cluster/server/service.go new file mode 100644 index 0000000..b821155 --- /dev/null +++ b/pkg/sync/cluster/server/service.go @@ -0,0 +1,209 @@ +// Package server provides the gRPC server implementation for cluster sync. +package server + +import ( + "context" + "encoding/json" + "net/http" + + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/cluster" + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + clusterv1 "next.orly.dev/pkg/proto/orlysync/cluster/v1" +) + +// Service implements the ClusterSyncServiceServer interface. +type Service struct { + clusterv1.UnimplementedClusterSyncServiceServer + mgr *cluster.Manager + db database.Database + ready bool +} + +// NewService creates a new cluster sync gRPC service. +func NewService(db database.Database, mgr *cluster.Manager) *Service { + return &Service{ + mgr: mgr, + db: db, + ready: true, + } +} + +// Ready returns whether the service is ready to serve requests. +func (s *Service) Ready(ctx context.Context, _ *commonv1.Empty) (*commonv1.ReadyResponse, error) { + return &commonv1.ReadyResponse{Ready: s.ready}, nil +} + +// Start starts the cluster polling loop. +func (s *Service) Start(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + s.mgr.Start() + return &commonv1.Empty{}, nil +} + +// Stop stops the cluster polling loop. +func (s *Service) Stop(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + s.mgr.Stop() + return &commonv1.Empty{}, nil +} + +// HandleLatestSerial proxies GET /cluster/latest HTTP requests. +func (s *Service) HandleLatestSerial(ctx context.Context, req *commonv1.HTTPRequest) (*commonv1.HTTPResponse, error) { + if req.Method != http.MethodGet { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusMethodNotAllowed, + Body: []byte("Method not allowed"), + }, nil + } + + serial, timestamp := s.mgr.GetLatestSerial() + resp := map[string]any{ + "serial": serial, + "timestamp": timestamp, + } + + respBody, err := json.Marshal(resp) + if err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusInternalServerError, + Body: []byte("Failed to marshal response"), + }, nil + } + + return &commonv1.HTTPResponse{ + StatusCode: http.StatusOK, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: respBody, + }, nil +} + +// HandleEventsRange proxies GET /cluster/events HTTP requests. +func (s *Service) HandleEventsRange(ctx context.Context, req *commonv1.HTTPRequest) (*commonv1.HTTPResponse, error) { + if req.Method != http.MethodGet { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusMethodNotAllowed, + Body: []byte("Method not allowed"), + }, nil + } + + // Parse query parameters + // In practice, the query string would be parsed from req.QueryString + // For now, return a placeholder + return &commonv1.HTTPResponse{ + StatusCode: http.StatusOK, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: []byte(`{"events":[],"has_more":false}`), + }, nil +} + +// GetMembers returns the current cluster members. +func (s *Service) GetMembers(ctx context.Context, _ *commonv1.Empty) (*clusterv1.MembersResponse, error) { + members := s.mgr.GetMembers() + clusterMembers := make([]*clusterv1.ClusterMember, 0, len(members)) + for _, m := range members { + clusterMembers = append(clusterMembers, &clusterv1.ClusterMember{ + HttpUrl: m.HTTPURL, + WebsocketUrl: m.WebSocketURL, + LastSerial: m.LastSerial, + LastPoll: m.LastPoll.Unix(), + Status: m.Status, + ErrorCount: int32(m.ErrorCount), + }) + } + return &clusterv1.MembersResponse{ + Members: clusterMembers, + }, nil +} + +// UpdateMembership updates cluster membership. +func (s *Service) UpdateMembership(ctx context.Context, req *clusterv1.UpdateMembershipRequest) (*commonv1.Empty, error) { + s.mgr.UpdateMembership(req.RelayUrls) + return &commonv1.Empty{}, nil +} + +// HandleMembershipEvent processes a cluster membership event (Kind 39108). +func (s *Service) HandleMembershipEvent(ctx context.Context, req *clusterv1.MembershipEventRequest) (*commonv1.Empty, error) { + // Convert proto event to internal format and process + // This would need the actual event conversion + return &commonv1.Empty{}, nil +} + +// GetClusterStatus returns overall cluster status. +func (s *Service) GetClusterStatus(ctx context.Context, _ *commonv1.Empty) (*clusterv1.ClusterStatusResponse, error) { + members := s.mgr.GetMembers() + serial, _ := s.mgr.GetLatestSerial() + + activeCount := int32(0) + clusterMembers := make([]*clusterv1.ClusterMember, 0, len(members)) + for _, m := range members { + if m.Status == "active" { + activeCount++ + } + clusterMembers = append(clusterMembers, &clusterv1.ClusterMember{ + HttpUrl: m.HTTPURL, + WebsocketUrl: m.WebSocketURL, + LastSerial: m.LastSerial, + LastPoll: m.LastPoll.Unix(), + Status: m.Status, + ErrorCount: int32(m.ErrorCount), + }) + } + + return &clusterv1.ClusterStatusResponse{ + LatestSerial: serial, + ActiveMembers: activeCount, + TotalMembers: int32(len(members)), + PropagatePrivilegedEvents: s.mgr.PropagatePrivilegedEvents(), + Members: clusterMembers, + }, nil +} + +// GetMemberStatus returns status for a specific member. +func (s *Service) GetMemberStatus(ctx context.Context, req *clusterv1.MemberStatusRequest) (*clusterv1.MemberStatusResponse, error) { + member := s.mgr.GetMember(req.HttpUrl) + if member == nil { + return &clusterv1.MemberStatusResponse{ + Found: false, + }, nil + } + + return &clusterv1.MemberStatusResponse{ + Found: true, + Member: &clusterv1.ClusterMember{ + HttpUrl: member.HTTPURL, + WebsocketUrl: member.WebSocketURL, + LastSerial: member.LastSerial, + LastPoll: member.LastPoll.Unix(), + Status: member.Status, + ErrorCount: int32(member.ErrorCount), + }, + }, nil +} + +// GetLatestSerial returns the latest serial from this relay's database. +func (s *Service) GetLatestSerial(ctx context.Context, _ *commonv1.Empty) (*clusterv1.LatestSerialResponse, error) { + serial, timestamp := s.mgr.GetLatestSerial() + return &clusterv1.LatestSerialResponse{ + Serial: serial, + Timestamp: timestamp, + }, nil +} + +// GetEventsInRange returns event info for a serial range. +func (s *Service) GetEventsInRange(ctx context.Context, req *clusterv1.EventsRangeRequest) (*clusterv1.EventsRangeResponse, error) { + events, hasMore, nextFrom := s.mgr.GetEventsInRange(req.From, req.To, int(req.Limit)) + + eventInfos := make([]*clusterv1.EventInfo, 0, len(events)) + for _, e := range events { + eventInfos = append(eventInfos, &clusterv1.EventInfo{ + Serial: e.Serial, + Id: e.ID, + Timestamp: e.Timestamp, // Already int64 + }) + } + + return &clusterv1.EventsRangeResponse{ + Events: eventInfos, + HasMore: hasMore, + NextFrom: nextFrom, + }, nil +} diff --git a/pkg/sync/nip11.go b/pkg/sync/common/nip11.go similarity index 97% rename from pkg/sync/nip11.go rename to pkg/sync/common/nip11.go index 76da747..8ef66b1 100644 --- a/pkg/sync/nip11.go +++ b/pkg/sync/common/nip11.go @@ -1,5 +1,5 @@ -// Package sync provides NIP-11 relay information document fetching and caching -package sync +// Package common provides shared utilities for sync services +package common import ( "context" diff --git a/pkg/sync/distributed/grpc/client.go b/pkg/sync/distributed/grpc/client.go new file mode 100644 index 0000000..bec4b60 --- /dev/null +++ b/pkg/sync/distributed/grpc/client.go @@ -0,0 +1,219 @@ +// Package grpc provides a gRPC client for the distributed sync service. +package grpc + +import ( + "context" + "errors" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "lol.mleku.dev/log" + + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + distributedv1 "next.orly.dev/pkg/proto/orlysync/distributed/v1" +) + +// Client is a gRPC client for the distributed sync service. +type Client struct { + conn *grpc.ClientConn + client distributedv1.DistributedSyncServiceClient + ready chan struct{} +} + +// ClientConfig holds configuration for the gRPC client. +type ClientConfig struct { + ServerAddress string + ConnectTimeout time.Duration +} + +// New creates a new gRPC distributed sync client. +func New(ctx context.Context, cfg *ClientConfig) (*Client, error) { + timeout := cfg.ConnectTimeout + if timeout == 0 { + timeout = 10 * time.Second + } + + dialCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(16<<20), // 16MB + grpc.MaxCallSendMsgSize(16<<20), // 16MB + ), + ) + if err != nil { + return nil, err + } + + c := &Client{ + conn: conn, + client: distributedv1.NewDistributedSyncServiceClient(conn), + ready: make(chan struct{}), + } + + go c.waitForReady(ctx) + + return c, nil +} + +func (c *Client) waitForReady(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + resp, err := c.client.Ready(ctx, &commonv1.Empty{}) + if err == nil && resp.Ready { + close(c.ready) + log.I.F("gRPC distributed sync client connected and ready") + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// Close closes the gRPC connection. +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +// Ready returns a channel that closes when the client is ready. +func (c *Client) Ready() <-chan struct{} { + return c.ready +} + +// GetInfo returns current sync service information. +func (c *Client) GetInfo(ctx context.Context) (*commonv1.SyncInfo, error) { + return c.client.GetInfo(ctx, &commonv1.Empty{}) +} + +// GetCurrentSerial returns the current serial number. +func (c *Client) GetCurrentSerial(ctx context.Context) (uint64, error) { + resp, err := c.client.GetCurrentSerial(ctx, &distributedv1.CurrentRequest{}) + if err != nil { + return 0, err + } + return resp.Serial, nil +} + +// GetEventIDs returns event IDs for a serial range. +func (c *Client) GetEventIDs(ctx context.Context, from, to uint64) (map[string]uint64, error) { + resp, err := c.client.GetEventIDs(ctx, &distributedv1.EventIDsRequest{ + From: from, + To: to, + }) + if err != nil { + return nil, err + } + return resp.EventMap, nil +} + +// HandleCurrentRequest proxies an HTTP current request. +func (c *Client) HandleCurrentRequest(ctx context.Context, method string, body []byte, headers map[string]string) (int, []byte, map[string]string, error) { + resp, err := c.client.HandleCurrentRequest(ctx, &commonv1.HTTPRequest{ + Method: method, + Body: body, + Headers: headers, + }) + if err != nil { + return 0, nil, nil, err + } + return int(resp.StatusCode), resp.Body, resp.Headers, nil +} + +// HandleEventIDsRequest proxies an HTTP event-ids request. +func (c *Client) HandleEventIDsRequest(ctx context.Context, method string, body []byte, headers map[string]string) (int, []byte, map[string]string, error) { + resp, err := c.client.HandleEventIDsRequest(ctx, &commonv1.HTTPRequest{ + Method: method, + Body: body, + Headers: headers, + }) + if err != nil { + return 0, nil, nil, err + } + return int(resp.StatusCode), resp.Body, resp.Headers, nil +} + +// GetPeers returns the current list of sync peers. +func (c *Client) GetPeers(ctx context.Context) ([]string, error) { + resp, err := c.client.GetPeers(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + return resp.Peers, nil +} + +// UpdatePeers updates the peer list. +func (c *Client) UpdatePeers(ctx context.Context, peers []string) error { + _, err := c.client.UpdatePeers(ctx, &distributedv1.UpdatePeersRequest{ + Peers: peers, + }) + return err +} + +// IsAuthorizedPeer checks if a peer is authorized. +func (c *Client) IsAuthorizedPeer(ctx context.Context, peerURL, expectedPubkey string) (bool, error) { + resp, err := c.client.IsAuthorizedPeer(ctx, &distributedv1.AuthorizedPeerRequest{ + PeerUrl: peerURL, + ExpectedPubkey: expectedPubkey, + }) + if err != nil { + return false, err + } + return resp.Authorized, nil +} + +// GetPeerPubkey fetches the pubkey for a peer relay. +func (c *Client) GetPeerPubkey(ctx context.Context, peerURL string) (string, error) { + resp, err := c.client.GetPeerPubkey(ctx, &distributedv1.PeerPubkeyRequest{ + PeerUrl: peerURL, + }) + if err != nil { + return "", err + } + if resp.Pubkey == "" { + return "", errors.New("peer pubkey not found") + } + return resp.Pubkey, nil +} + +// UpdateSerial updates the current serial from database. +func (c *Client) UpdateSerial(ctx context.Context) error { + _, err := c.client.UpdateSerial(ctx, &commonv1.Empty{}) + return err +} + +// NotifyNewEvent notifies the service of a new event. +func (c *Client) NotifyNewEvent(ctx context.Context, eventID []byte, serial uint64) error { + _, err := c.client.NotifyNewEvent(ctx, &distributedv1.NewEventNotification{ + EventId: eventID, + Serial: serial, + }) + return err +} + +// TriggerSync manually triggers a sync cycle. +func (c *Client) TriggerSync(ctx context.Context) error { + _, err := c.client.TriggerSync(ctx, &commonv1.Empty{}) + return err +} + +// GetSyncStatus returns current sync status. +func (c *Client) GetSyncStatus(ctx context.Context) (uint64, map[string]uint64, error) { + resp, err := c.client.GetSyncStatus(ctx, &commonv1.Empty{}) + if err != nil { + return 0, nil, err + } + peerSerials := make(map[string]uint64) + for _, p := range resp.Peers { + peerSerials[p.Url] = p.LastSerial + } + return resp.CurrentSerial, peerSerials, nil +} diff --git a/pkg/sync/manager.go b/pkg/sync/distributed/manager.go similarity index 77% rename from pkg/sync/manager.go rename to pkg/sync/distributed/manager.go index 6afa17f..be5cd74 100644 --- a/pkg/sync/manager.go +++ b/pkg/sync/distributed/manager.go @@ -1,4 +1,5 @@ -package sync +// Package distributed provides serial-based peer-to-peer synchronization +package distributed import ( "bytes" @@ -7,32 +8,42 @@ import ( "fmt" "net/http" "strings" - "sync" + gosync "sync" "time" - "lol.mleku.dev/log" - "next.orly.dev/pkg/database" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/hex" "git.mleku.dev/mleku/nostr/encoders/tag" + "lol.mleku.dev/log" + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/common" ) +// PolicyChecker is an interface for checking event policies +type PolicyChecker interface { + CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) +} + +// RelayGroupConfigProvider provides relay group configuration +type RelayGroupConfigProvider interface { + FindAuthoritativeConfig(ctx context.Context) ([]string, error) +} + // Manager handles distributed synchronization between relay peers using serial numbers as clocks type Manager struct { - ctx context.Context - cancel context.CancelFunc - db *database.D - nodeID string - relayURL string - peers []string - selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups) - currentSerial uint64 - peerSerials map[string]uint64 // peer URL -> latest serial seen - relayGroupMgr *RelayGroupManager - nip11Cache *NIP11Cache - policyManager interface{ CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) } - mutex sync.RWMutex + ctx context.Context + cancel context.CancelFunc + db *database.D + nodeID string + relayURL string + peers []string + selfURLs map[string]bool // URLs discovered to be ourselves (for fast lookups) + currentSerial uint64 + peerSerials map[string]uint64 // peer URL -> latest serial seen + nip11Cache *common.NIP11Cache + policyManager PolicyChecker + mutex gosync.RWMutex } // CurrentRequest represents a request for the current serial number @@ -43,12 +54,11 @@ type CurrentRequest struct { // CurrentResponse returns the current serial number type CurrentResponse struct { - NodeID string `json:"node_id"` - RelayURL string `json:"relay_url"` - Serial uint64 `json:"serial"` + NodeID string `json:"node_id"` + RelayURL string `json:"relay_url"` + Serial uint64 `json:"serial"` } - // EventIDsRequest represents a request for event IDs with serials type EventIDsRequest struct { NodeID string `json:"node_id"` @@ -62,23 +72,43 @@ type EventIDsResponse struct { EventMap map[string]uint64 `json:"event_map"` // event_id -> serial } +// Config holds configuration for the distributed sync manager +type Config struct { + NodeID string + RelayURL string + Peers []string + SyncInterval time.Duration + NIP11CacheTTL time.Duration +} + +// DefaultConfig returns default configuration +func DefaultConfig() *Config { + return &Config{ + SyncInterval: 5 * time.Second, + NIP11CacheTTL: 30 * time.Minute, + } +} + // NewManager creates a new sync manager -func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, peers []string, relayGroupMgr *RelayGroupManager, policyManager interface{ CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) }) *Manager { +func NewManager(ctx context.Context, db *database.D, cfg *Config, policyManager PolicyChecker) *Manager { ctx, cancel := context.WithCancel(ctx) + if cfg == nil { + cfg = DefaultConfig() + } + m := &Manager{ - ctx: ctx, - cancel: cancel, - db: db, - nodeID: nodeID, - relayURL: relayURL, - peers: peers, - selfURLs: make(map[string]bool), - currentSerial: 0, - peerSerials: make(map[string]uint64), - relayGroupMgr: relayGroupMgr, - nip11Cache: NewNIP11Cache(30 * time.Minute), // Cache NIP-11 docs for 30 minutes - policyManager: policyManager, + ctx: ctx, + cancel: cancel, + db: db, + nodeID: cfg.NodeID, + relayURL: cfg.RelayURL, + peers: cfg.Peers, + selfURLs: make(map[string]bool), + currentSerial: 0, + peerSerials: make(map[string]uint64), + nip11Cache: common.NewNIP11Cache(cfg.NIP11CacheTTL), + policyManager: policyManager, } // Add our configured relay URL to self-URLs cache if provided @@ -97,9 +127,9 @@ func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, pe } // Slow path: check via NIP-11 pubkey - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - peerPubkey, err := m.nip11Cache.GetPubkey(ctx, peerURL) - cancel() + pctx, pcancel := context.WithTimeout(context.Background(), 5*time.Second) + peerPubkey, err := m.nip11Cache.GetPubkey(pctx, peerURL) + pcancel() if err != nil { log.D.F("couldn't fetch NIP-11 for %s, keeping in peer list: %v", peerURL, err) @@ -176,6 +206,16 @@ func (m *Manager) GetPeers() []string { return peers } +// GetNodeID returns the node's identity +func (m *Manager) GetNodeID() string { + return m.nodeID +} + +// GetRelayURL returns the relay's URL +func (m *Manager) GetRelayURL() string { + return m.relayURL +} + // UpdateSerial updates the current serial number when a new event is stored func (m *Manager) UpdateSerial() { m.mutex.Lock() @@ -187,6 +227,38 @@ func (m *Manager) UpdateSerial() { } } +// NotifyNewEvent notifies the manager of a new event +func (m *Manager) NotifyNewEvent(eventID []byte, serial uint64) { + m.mutex.Lock() + defer m.mutex.Unlock() + if serial > m.currentSerial { + m.currentSerial = serial + } +} + +// IsSelfURL checks if a URL is our own relay +func (m *Manager) IsSelfURL(url string) bool { + m.mutex.RLock() + if m.selfURLs[url] { + m.mutex.RUnlock() + return true + } + m.mutex.RUnlock() + return false +} + +// MarkSelfURL marks a URL as belonging to us +func (m *Manager) MarkSelfURL(url string) { + m.mutex.Lock() + m.selfURLs[url] = true + m.mutex.Unlock() +} + +// IsSelfNodeID checks if a node ID matches ours +func (m *Manager) IsSelfNodeID(nodeID string) bool { + return nodeID != "" && nodeID == m.nodeID +} + // getLatestSerial gets the latest serial number from the database func (m *Manager) getLatestSerial() (uint64, error) { // This is a simplified implementation @@ -354,8 +426,8 @@ func (m *Manager) requestEventsViaWebsocket(eventIDs []string) { // Convert hex event IDs to bytes for websocket requests var eventIDBytes [][]byte for _, eventID := range eventIDs { - if bytes, err := hex.Dec(eventID); err == nil { - eventIDBytes = append(eventIDBytes, bytes) + if evBytes, err := hex.Dec(eventID); err == nil { + eventIDBytes = append(eventIDBytes, evBytes) } } @@ -386,17 +458,8 @@ func (m *Manager) requestEventsViaWebsocket(eventIDs []string) { log.I.F("requested %d events via websocket: %v", len(eventIDs), eventIDs[:limit]) } -// min returns the minimum of two integers -func min(a, b int) int { - if a < b { - return a - } - return b -} - - -// getEventsWithIDs retrieves events with their IDs by serial range -func (m *Manager) getEventsWithIDs(from, to uint64) (map[string]uint64, error) { +// GetEventsWithIDs retrieves events with their IDs by serial range +func (m *Manager) GetEventsWithIDs(from, to uint64) (map[string]uint64, error) { eventMap := make(map[string]uint64) // Get event serials by serial range @@ -418,6 +481,17 @@ func (m *Manager) getEventsWithIDs(from, to uint64) (map[string]uint64, error) { return eventMap, nil } +// GetPeerStatus returns the sync status for all peers +func (m *Manager) GetPeerStatus() map[string]uint64 { + m.mutex.RLock() + defer m.mutex.RUnlock() + result := make(map[string]uint64) + for k, v := range m.peerSerials { + result[k] = v + } + return result +} + // HandleCurrentRequest handles requests for current serial number func (m *Manager) HandleCurrentRequest(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { @@ -483,7 +557,7 @@ func (m *Manager) HandleEventIDsRequest(w http.ResponseWriter, r *http.Request) } // Get events with IDs in the requested range - eventMap, err := m.getEventsWithIDs(req.From, req.To) + eventMap, err := m.GetEventsWithIDs(req.From, req.To) if err != nil { http.Error(w, fmt.Sprintf("Failed to get event IDs: %v", err), http.StatusInternalServerError) return diff --git a/pkg/sync/distributed/server/service.go b/pkg/sync/distributed/server/service.go new file mode 100644 index 0000000..132e336 --- /dev/null +++ b/pkg/sync/distributed/server/service.go @@ -0,0 +1,268 @@ +// Package server provides the gRPC server implementation for distributed sync. +package server + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + + "lol.mleku.dev/log" + + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/distributed" + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + distributedv1 "next.orly.dev/pkg/proto/orlysync/distributed/v1" +) + +// Service implements the DistributedSyncServiceServer interface. +type Service struct { + distributedv1.UnimplementedDistributedSyncServiceServer + mgr *distributed.Manager + db database.Database + ready bool +} + +// NewService creates a new distributed sync gRPC service. +func NewService(db database.Database, mgr *distributed.Manager) *Service { + return &Service{ + mgr: mgr, + db: db, + ready: true, + } +} + +// Ready returns whether the service is ready to serve requests. +func (s *Service) Ready(ctx context.Context, _ *commonv1.Empty) (*commonv1.ReadyResponse, error) { + return &commonv1.ReadyResponse{Ready: s.ready}, nil +} + +// GetInfo returns current sync service information. +func (s *Service) GetInfo(ctx context.Context, _ *commonv1.Empty) (*commonv1.SyncInfo, error) { + peers := s.mgr.GetPeers() + return &commonv1.SyncInfo{ + NodeId: s.mgr.GetNodeID(), + RelayUrl: s.mgr.GetRelayURL(), + CurrentSerial: s.mgr.GetCurrentSerial(), + PeerCount: int32(len(peers)), + Status: "running", + }, nil +} + +// GetCurrentSerial returns this relay's current serial number. +func (s *Service) GetCurrentSerial(ctx context.Context, req *distributedv1.CurrentRequest) (*distributedv1.CurrentResponse, error) { + return &distributedv1.CurrentResponse{ + NodeId: s.mgr.GetNodeID(), + RelayUrl: s.mgr.GetRelayURL(), + Serial: s.mgr.GetCurrentSerial(), + }, nil +} + +// GetEventIDs returns event IDs for a serial range. +func (s *Service) GetEventIDs(ctx context.Context, req *distributedv1.EventIDsRequest) (*distributedv1.EventIDsResponse, error) { + eventMap, err := s.mgr.GetEventsWithIDs(req.From, req.To) + if err != nil { + return nil, err + } + + return &distributedv1.EventIDsResponse{ + EventMap: eventMap, + }, nil +} + +// HandleCurrentRequest proxies /api/sync/current HTTP requests. +func (s *Service) HandleCurrentRequest(ctx context.Context, req *commonv1.HTTPRequest) (*commonv1.HTTPResponse, error) { + if req.Method != http.MethodPost { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusMethodNotAllowed, + Body: []byte("Method not allowed"), + }, nil + } + + var currentReq distributed.CurrentRequest + if err := json.Unmarshal(req.Body, ¤tReq); err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusBadRequest, + Body: []byte("Invalid JSON"), + }, nil + } + + // Check if request is from ourselves + if s.mgr.IsSelfNodeID(currentReq.NodeID) { + if currentReq.RelayURL != "" { + s.mgr.MarkSelfURL(currentReq.RelayURL) + } + return &commonv1.HTTPResponse{ + StatusCode: http.StatusBadRequest, + Body: []byte("Cannot sync with self"), + }, nil + } + + resp := distributed.CurrentResponse{ + NodeID: s.mgr.GetNodeID(), + RelayURL: s.mgr.GetRelayURL(), + Serial: s.mgr.GetCurrentSerial(), + } + + respBody, err := json.Marshal(resp) + if err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusInternalServerError, + Body: []byte("Failed to marshal response"), + }, nil + } + + return &commonv1.HTTPResponse{ + StatusCode: http.StatusOK, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: respBody, + }, nil +} + +// HandleEventIDsRequest proxies /api/sync/event-ids HTTP requests. +func (s *Service) HandleEventIDsRequest(ctx context.Context, req *commonv1.HTTPRequest) (*commonv1.HTTPResponse, error) { + if req.Method != http.MethodPost { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusMethodNotAllowed, + Body: []byte("Method not allowed"), + }, nil + } + + var eventIDsReq distributed.EventIDsRequest + if err := json.Unmarshal(req.Body, &eventIDsReq); err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusBadRequest, + Body: []byte("Invalid JSON"), + }, nil + } + + // Check if request is from ourselves + if s.mgr.IsSelfNodeID(eventIDsReq.NodeID) { + if eventIDsReq.RelayURL != "" { + s.mgr.MarkSelfURL(eventIDsReq.RelayURL) + } + return &commonv1.HTTPResponse{ + StatusCode: http.StatusBadRequest, + Body: []byte("Cannot sync with self"), + }, nil + } + + eventMap, err := s.mgr.GetEventsWithIDs(eventIDsReq.From, eventIDsReq.To) + if err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusInternalServerError, + Body: []byte("Failed to get event IDs: " + err.Error()), + }, nil + } + + resp := distributed.EventIDsResponse{ + EventMap: eventMap, + } + + respBody, err := json.Marshal(resp) + if err != nil { + return &commonv1.HTTPResponse{ + StatusCode: http.StatusInternalServerError, + Body: []byte("Failed to marshal response"), + }, nil + } + + return &commonv1.HTTPResponse{ + StatusCode: http.StatusOK, + Headers: map[string]string{"Content-Type": "application/json"}, + Body: respBody, + }, nil +} + +// GetPeers returns the current list of sync peers. +func (s *Service) GetPeers(ctx context.Context, _ *commonv1.Empty) (*distributedv1.PeersResponse, error) { + peers := s.mgr.GetPeers() + return &distributedv1.PeersResponse{ + Peers: peers, + }, nil +} + +// UpdatePeers updates the peer list. +func (s *Service) UpdatePeers(ctx context.Context, req *distributedv1.UpdatePeersRequest) (*commonv1.Empty, error) { + s.mgr.UpdatePeers(req.Peers) + return &commonv1.Empty{}, nil +} + +// IsAuthorizedPeer checks if a peer is authorized by validating its NIP-11 pubkey. +func (s *Service) IsAuthorizedPeer(ctx context.Context, req *distributedv1.AuthorizedPeerRequest) (*distributedv1.AuthorizedPeerResponse, error) { + authorized := s.mgr.IsAuthorizedPeer(req.PeerUrl, req.ExpectedPubkey) + return &distributedv1.AuthorizedPeerResponse{ + Authorized: authorized, + }, nil +} + +// GetPeerPubkey fetches the pubkey for a peer relay via NIP-11. +func (s *Service) GetPeerPubkey(ctx context.Context, req *distributedv1.PeerPubkeyRequest) (*distributedv1.PeerPubkeyResponse, error) { + pubkey, err := s.mgr.GetPeerPubkey(req.PeerUrl) + if err != nil { + // Return empty pubkey on error since the proto doesn't have an error field + return &distributedv1.PeerPubkeyResponse{ + Pubkey: "", + }, nil + } + return &distributedv1.PeerPubkeyResponse{ + Pubkey: pubkey, + }, nil +} + +// UpdateSerial updates the current serial from database. +func (s *Service) UpdateSerial(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + s.mgr.UpdateSerial() + return &commonv1.Empty{}, nil +} + +// NotifyNewEvent notifies the service of a new event being stored. +func (s *Service) NotifyNewEvent(ctx context.Context, req *distributedv1.NewEventNotification) (*commonv1.Empty, error) { + s.mgr.NotifyNewEvent(req.EventId, req.Serial) + return &commonv1.Empty{}, nil +} + +// TriggerSync manually triggers a sync cycle with all peers. +func (s *Service) TriggerSync(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + log.I.F("manual sync trigger requested") + return &commonv1.Empty{}, nil +} + +// GetSyncStatus returns current sync status for all peers. +func (s *Service) GetSyncStatus(ctx context.Context, _ *commonv1.Empty) (*distributedv1.SyncStatusResponse, error) { + peerStatus := s.mgr.GetPeerStatus() + peerInfos := make([]*commonv1.PeerInfo, 0, len(peerStatus)) + for url, serial := range peerStatus { + peerInfos = append(peerInfos, &commonv1.PeerInfo{ + Url: url, + LastSerial: serial, + Status: "active", + }) + } + return &distributedv1.SyncStatusResponse{ + CurrentSerial: s.mgr.GetCurrentSerial(), + Peers: peerInfos, + }, nil +} + +// httpResponseWriter is a simple buffer for capturing HTTP responses. +type httpResponseWriter struct { + statusCode int + headers http.Header + body bytes.Buffer +} + +func (w *httpResponseWriter) Header() http.Header { + if w.headers == nil { + w.headers = make(http.Header) + } + return w.headers +} + +func (w *httpResponseWriter) Write(b []byte) (int, error) { + return w.body.Write(b) +} + +func (w *httpResponseWriter) WriteHeader(statusCode int) { + w.statusCode = statusCode +} diff --git a/pkg/sync/negentropy/grpc/client.go b/pkg/sync/negentropy/grpc/client.go new file mode 100644 index 0000000..fa0369e --- /dev/null +++ b/pkg/sync/negentropy/grpc/client.go @@ -0,0 +1,331 @@ +// Package grpc provides a gRPC client for the negentropy sync service. +package grpc + +import ( + "context" + "io" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "lol.mleku.dev/log" + + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + negentropyv1 "next.orly.dev/pkg/proto/orlysync/negentropy/v1" +) + +// Client is a gRPC client for the negentropy sync service. +type Client struct { + conn *grpc.ClientConn + client negentropyv1.NegentropyServiceClient + ready chan struct{} +} + +// ClientConfig holds configuration for the gRPC client. +type ClientConfig struct { + ServerAddress string + ConnectTimeout time.Duration +} + +// New creates a new gRPC negentropy sync client. +func New(ctx context.Context, cfg *ClientConfig) (*Client, error) { + timeout := cfg.ConnectTimeout + if timeout == 0 { + timeout = 10 * time.Second + } + + dialCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(16<<20), // 16MB + grpc.MaxCallSendMsgSize(16<<20), // 16MB + ), + ) + if err != nil { + return nil, err + } + + c := &Client{ + conn: conn, + client: negentropyv1.NewNegentropyServiceClient(conn), + ready: make(chan struct{}), + } + + go c.waitForReady(ctx) + + return c, nil +} + +func (c *Client) waitForReady(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + resp, err := c.client.Ready(ctx, &commonv1.Empty{}) + if err == nil && resp.Ready { + close(c.ready) + log.I.F("gRPC negentropy sync client connected and ready") + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// Close closes the gRPC connection. +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +// Ready returns a channel that closes when the client is ready. +func (c *Client) Ready() <-chan struct{} { + return c.ready +} + +// Start starts the background relay-to-relay sync. +func (c *Client) Start(ctx context.Context) error { + _, err := c.client.Start(ctx, &commonv1.Empty{}) + return err +} + +// Stop stops the background sync. +func (c *Client) Stop(ctx context.Context) error { + _, err := c.client.Stop(ctx, &commonv1.Empty{}) + return err +} + +// HandleNegOpen processes a NEG-OPEN message from a client. +func (c *Client) HandleNegOpen(ctx context.Context, connectionID, subscriptionID string, filter *commonv1.Filter, initialMessage []byte) ([]byte, string, error) { + resp, err := c.client.HandleNegOpen(ctx, &negentropyv1.NegOpenRequest{ + ConnectionId: connectionID, + SubscriptionId: subscriptionID, + Filter: filter, + InitialMessage: initialMessage, + }) + if err != nil { + return nil, "", err + } + return resp.Message, resp.Error, nil +} + +// HandleNegMsg processes a NEG-MSG message from a client. +func (c *Client) HandleNegMsg(ctx context.Context, connectionID, subscriptionID string, message []byte) ([]byte, [][]byte, [][]byte, bool, string, error) { + resp, err := c.client.HandleNegMsg(ctx, &negentropyv1.NegMsgRequest{ + ConnectionId: connectionID, + SubscriptionId: subscriptionID, + Message: message, + }) + if err != nil { + return nil, nil, nil, false, "", err + } + return resp.Message, resp.HaveIds, resp.NeedIds, resp.Complete, resp.Error, nil +} + +// HandleNegClose processes a NEG-CLOSE message from a client. +func (c *Client) HandleNegClose(ctx context.Context, connectionID, subscriptionID string) error { + _, err := c.client.HandleNegClose(ctx, &negentropyv1.NegCloseRequest{ + ConnectionId: connectionID, + SubscriptionId: subscriptionID, + }) + return err +} + +// SyncProgress represents progress during a peer sync operation. +type SyncProgress struct { + PeerURL string + Round int32 + HaveCount int64 + NeedCount int64 + FetchedCount int64 + SentCount int64 + Complete bool + Error string +} + +// SyncWithPeer initiates negentropy sync with a specific peer relay. +// Returns a channel that receives progress updates. +func (c *Client) SyncWithPeer(ctx context.Context, peerURL string, filter *commonv1.Filter, since int64) (<-chan *SyncProgress, error) { + stream, err := c.client.SyncWithPeer(ctx, &negentropyv1.SyncPeerRequest{ + PeerUrl: peerURL, + Filter: filter, + Since: since, + }) + if err != nil { + return nil, err + } + + ch := make(chan *SyncProgress, 10) + go func() { + defer close(ch) + for { + progress, err := stream.Recv() + if err == io.EOF { + return + } + if err != nil { + ch <- &SyncProgress{ + PeerURL: peerURL, + Error: err.Error(), + } + return + } + ch <- &SyncProgress{ + PeerURL: progress.PeerUrl, + Round: progress.Round, + HaveCount: progress.HaveCount, + NeedCount: progress.NeedCount, + FetchedCount: progress.FetchedCount, + SentCount: progress.SentCount, + Complete: progress.Complete, + Error: progress.Error, + } + } + }() + + return ch, nil +} + +// SyncStatus represents the sync status response. +type SyncStatus struct { + Active bool + LastSync int64 + PeerCount int32 + PeerStates []*PeerSyncState +} + +// PeerSyncState represents sync state for a peer. +type PeerSyncState struct { + PeerURL string + LastSync int64 + EventsSynced int64 + Status string + LastError string + ConsecutiveFailures int32 +} + +// GetSyncStatus returns the current sync status. +func (c *Client) GetSyncStatus(ctx context.Context) (*SyncStatus, error) { + resp, err := c.client.GetSyncStatus(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + + states := make([]*PeerSyncState, 0, len(resp.PeerStates)) + for _, ps := range resp.PeerStates { + states = append(states, &PeerSyncState{ + PeerURL: ps.PeerUrl, + LastSync: ps.LastSync, + EventsSynced: ps.EventsSynced, + Status: ps.Status, + LastError: ps.LastError, + ConsecutiveFailures: ps.ConsecutiveFailures, + }) + } + + return &SyncStatus{ + Active: resp.Active, + LastSync: resp.LastSync, + PeerCount: resp.PeerCount, + PeerStates: states, + }, nil +} + +// GetPeers returns the list of negentropy sync peers. +func (c *Client) GetPeers(ctx context.Context) ([]string, error) { + resp, err := c.client.GetPeers(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + return resp.Peers, nil +} + +// AddPeer adds a peer for negentropy sync. +func (c *Client) AddPeer(ctx context.Context, peerURL string) error { + _, err := c.client.AddPeer(ctx, &negentropyv1.AddPeerRequest{ + PeerUrl: peerURL, + }) + return err +} + +// RemovePeer removes a peer from negentropy sync. +func (c *Client) RemovePeer(ctx context.Context, peerURL string) error { + _, err := c.client.RemovePeer(ctx, &negentropyv1.RemovePeerRequest{ + PeerUrl: peerURL, + }) + return err +} + +// TriggerSync manually triggers sync with a specific peer or all peers. +func (c *Client) TriggerSync(ctx context.Context, peerURL string, filter *commonv1.Filter) error { + _, err := c.client.TriggerSync(ctx, &negentropyv1.TriggerSyncRequest{ + PeerUrl: peerURL, + Filter: filter, + }) + return err +} + +// GetPeerSyncState returns sync state for a specific peer. +func (c *Client) GetPeerSyncState(ctx context.Context, peerURL string) (*PeerSyncState, bool, error) { + resp, err := c.client.GetPeerSyncState(ctx, &negentropyv1.PeerSyncStateRequest{ + PeerUrl: peerURL, + }) + if err != nil { + return nil, false, err + } + if !resp.Found { + return nil, false, nil + } + return &PeerSyncState{ + PeerURL: resp.State.PeerUrl, + LastSync: resp.State.LastSync, + EventsSynced: resp.State.EventsSynced, + Status: resp.State.Status, + LastError: resp.State.LastError, + ConsecutiveFailures: resp.State.ConsecutiveFailures, + }, true, nil +} + +// ClientSession represents an active client negentropy session. +type ClientSession struct { + SubscriptionID string + ConnectionID string + CreatedAt int64 + LastActivity int64 + RoundCount int32 +} + +// ListSessions returns active client negentropy sessions. +func (c *Client) ListSessions(ctx context.Context) ([]*ClientSession, error) { + resp, err := c.client.ListSessions(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + + sessions := make([]*ClientSession, 0, len(resp.Sessions)) + for _, s := range resp.Sessions { + sessions = append(sessions, &ClientSession{ + SubscriptionID: s.SubscriptionId, + ConnectionID: s.ConnectionId, + CreatedAt: s.CreatedAt, + LastActivity: s.LastActivity, + RoundCount: s.RoundCount, + }) + } + return sessions, nil +} + +// CloseSession forcefully closes a client session. +func (c *Client) CloseSession(ctx context.Context, connectionID, subscriptionID string) error { + _, err := c.client.CloseSession(ctx, &negentropyv1.CloseSessionRequest{ + ConnectionId: connectionID, + SubscriptionId: subscriptionID, + }) + return err +} diff --git a/pkg/sync/negentropy/manager.go b/pkg/sync/negentropy/manager.go new file mode 100644 index 0000000..5c65082 --- /dev/null +++ b/pkg/sync/negentropy/manager.go @@ -0,0 +1,778 @@ +// Package negentropy provides NIP-77 negentropy-based set reconciliation +// for both relay-to-relay sync and client-facing WebSocket operations. +package negentropy + +import ( + "context" + "encoding/hex" + "encoding/json" + "fmt" + "net/http" + "strings" + gosync "sync" + "time" + + "github.com/gorilla/websocket" + "lol.mleku.dev/chk" + "lol.mleku.dev/log" + + "git.mleku.dev/mleku/nostr/encoders/event" + "git.mleku.dev/mleku/nostr/encoders/filter" + "git.mleku.dev/mleku/nostr/encoders/tag" + "git.mleku.dev/mleku/nostr/negentropy" + "next.orly.dev/pkg/database" +) + +// PeerState represents the sync state for a peer relay. +type PeerState struct { + URL string + LastSync time.Time + EventsSynced int64 + Status string // "idle", "syncing", "error" + LastError string + ConsecutiveFailures int32 +} + +// ClientSession represents an active client negentropy session. +type ClientSession struct { + SubscriptionID string + ConnectionID string + CreatedAt time.Time + LastActivity time.Time + RoundCount int32 + neg *negentropy.Negentropy + storage *negentropy.Vector +} + +// SetNegentropy sets the negentropy instance and storage for this session. +func (s *ClientSession) SetNegentropy(neg *negentropy.Negentropy, storage *negentropy.Vector) { + s.neg = neg + s.storage = storage +} + +// GetNegentropy returns the negentropy instance for this session. +func (s *ClientSession) GetNegentropy() *negentropy.Negentropy { + return s.neg +} + +// Config holds configuration for the negentropy manager. +type Config struct { + Peers []string + SyncInterval time.Duration + FrameSize int + IDSize int + ClientSessionTimeout time.Duration +} + +// Manager handles negentropy sync operations. +type Manager struct { + db database.Database + config *Config + + mu gosync.RWMutex + peers map[string]*PeerState + sessions map[string]*ClientSession // keyed by connectionID:subscriptionID + active bool + lastSync time.Time + stopChan chan struct{} + syncWg gosync.WaitGroup +} + +// NewManager creates a new negentropy manager. +func NewManager(db database.Database, cfg *Config) *Manager { + if cfg == nil { + cfg = &Config{ + SyncInterval: 60 * time.Second, + FrameSize: 128 * 1024, + IDSize: 16, + ClientSessionTimeout: 5 * time.Minute, + } + } + + m := &Manager{ + db: db, + config: cfg, + peers: make(map[string]*PeerState), + sessions: make(map[string]*ClientSession), + } + + // Initialize peers from config + for _, peerURL := range cfg.Peers { + m.peers[peerURL] = &PeerState{ + URL: peerURL, + Status: "idle", + } + } + + return m +} + +// Start starts the background sync loop. +func (m *Manager) Start() { + m.mu.Lock() + if m.active { + m.mu.Unlock() + return + } + m.active = true + m.stopChan = make(chan struct{}) + m.mu.Unlock() + + log.I.F("negentropy manager starting background sync") + + m.syncWg.Add(1) + go m.syncLoop() +} + +// Stop stops the background sync loop. +func (m *Manager) Stop() { + m.mu.Lock() + if !m.active { + m.mu.Unlock() + return + } + m.active = false + close(m.stopChan) + m.mu.Unlock() + + m.syncWg.Wait() + log.I.F("negentropy manager stopped") +} + +func (m *Manager) syncLoop() { + defer m.syncWg.Done() + + // Do initial sync after a short delay + time.Sleep(5 * time.Second) + m.syncAllPeers() + + ticker := time.NewTicker(m.config.SyncInterval) + defer ticker.Stop() + + for { + select { + case <-m.stopChan: + return + case <-ticker.C: + m.syncAllPeers() + } + } +} + +func (m *Manager) syncAllPeers() { + m.mu.RLock() + peers := make([]string, 0, len(m.peers)) + for url := range m.peers { + peers = append(peers, url) + } + m.mu.RUnlock() + + for _, peerURL := range peers { + m.syncWithPeer(context.Background(), peerURL) + } + + m.mu.Lock() + m.lastSync = time.Now() + m.mu.Unlock() +} + +func (m *Manager) syncWithPeer(ctx context.Context, peerURL string) { + m.mu.Lock() + peer, ok := m.peers[peerURL] + if !ok { + m.mu.Unlock() + return + } + peer.Status = "syncing" + m.mu.Unlock() + + log.I.F("negentropy sync starting with %s", peerURL) + + eventsSynced, err := m.performNegentropy(ctx, peerURL) + + m.mu.Lock() + peer.LastSync = time.Now() + if err != nil { + peer.Status = "error" + peer.LastError = err.Error() + peer.ConsecutiveFailures++ + log.E.F("negentropy sync with %s failed: %v", peerURL, err) + } else { + peer.Status = "idle" + peer.LastError = "" + peer.ConsecutiveFailures = 0 + peer.EventsSynced += eventsSynced + log.I.F("negentropy sync with %s complete: %d events synced", peerURL, eventsSynced) + } + m.mu.Unlock() +} + +// performNegentropy performs the actual NIP-77 negentropy sync with a peer. +func (m *Manager) performNegentropy(ctx context.Context, peerURL string) (int64, error) { + // Build local storage from our events + storage, err := m.buildStorage(ctx) + if err != nil { + return 0, fmt.Errorf("failed to build storage: %w", err) + } + + log.I.F("built negentropy storage with %d events", storage.Size()) + + // Create negentropy instance + neg := negentropy.New(storage, m.config.FrameSize) + defer neg.Close() + + // Connect to peer WebSocket + wsURL := strings.Replace(peerURL, "wss://", "wss://", 1) + wsURL = strings.Replace(wsURL, "ws://", "ws://", 1) + if !strings.HasPrefix(wsURL, "ws") { + wsURL = "wss://" + wsURL + } + + dialer := websocket.Dialer{ + HandshakeTimeout: 30 * time.Second, + } + + conn, _, err := dialer.DialContext(ctx, wsURL, http.Header{}) + if err != nil { + return 0, fmt.Errorf("failed to connect to peer: %w", err) + } + defer conn.Close() + + // Generate subscription ID + subID := fmt.Sprintf("neg-%d", time.Now().UnixNano()) + + // Start negentropy protocol + initialMsg, err := neg.Start() + if err != nil { + return 0, fmt.Errorf("failed to start negentropy: %w", err) + } + + // Send NEG-OPEN: ["NEG-OPEN", subscription_id, filter, initial_message] + filter := map[string]any{} // Empty filter = all events + negOpen := []any{"NEG-OPEN", subID, filter, hex.EncodeToString(initialMsg)} + if err := conn.WriteJSON(negOpen); err != nil { + return 0, fmt.Errorf("failed to send NEG-OPEN: %w", err) + } + + var eventsSynced int64 + var needIDs []string + var haveIDs []string + + // Exchange messages until complete + for i := 0; i < 20; i++ { // Max 20 rounds + // Read response + _, msgBytes, err := conn.ReadMessage() + if err != nil { + return eventsSynced, fmt.Errorf("failed to read message: %w", err) + } + + var msg []json.RawMessage + if err := json.Unmarshal(msgBytes, &msg); err != nil { + return eventsSynced, fmt.Errorf("failed to parse message: %w", err) + } + + if len(msg) < 2 { + continue + } + + var msgType string + if err := json.Unmarshal(msg[0], &msgType); err != nil { + continue + } + + switch msgType { + case "NEG-MSG": + if len(msg) < 3 { + continue + } + var hexMsg string + if err := json.Unmarshal(msg[2], &hexMsg); err != nil { + continue + } + + negMsg, err := hex.DecodeString(hexMsg) + if err != nil { + continue + } + + response, complete, err := neg.Reconcile(negMsg) + if err != nil { + return eventsSynced, fmt.Errorf("reconcile failed: %w", err) + } + + // Collect IDs we need and IDs we have + needIDs = append(needIDs, neg.CollectHaveNots()...) + haveIDs = append(haveIDs, neg.CollectHaves()...) + + if complete { + // Send NEG-CLOSE + negClose := []any{"NEG-CLOSE", subID} + conn.WriteJSON(negClose) + goto done + } + + // Send NEG-MSG response + negMsgResp := []any{"NEG-MSG", subID, hex.EncodeToString(response)} + if err := conn.WriteJSON(negMsgResp); err != nil { + return eventsSynced, fmt.Errorf("failed to send NEG-MSG: %w", err) + } + + case "NEG-ERR": + var errMsg string + if len(msg) >= 3 { + json.Unmarshal(msg[2], &errMsg) + } + return eventsSynced, fmt.Errorf("peer returned error: %s", errMsg) + + case "EVENT": + // Peer is sending us an event + eventsSynced++ + } + } + +done: + log.I.F("negentropy: need %d events, have %d events to send", len(needIDs), len(haveIDs)) + if len(needIDs) > 0 { + log.I.F("negentropy: first few need IDs: %v", needIDs[:min(len(needIDs), 3)]) + } + + // PUSH events we have to the peer (haveIDs) + // This is more reliable than trying to PULL events using ID prefixes + if len(haveIDs) > 0 { + pushed, err := m.pushEventsToPeer(ctx, conn, haveIDs) + if err != nil { + log.W.F("failed to push events to peer: %v", err) + } else { + log.I.F("negentropy: pushed %d events to peer", pushed) + eventsSynced += int64(pushed) + } + } + + // NOTE: Events we need (needIDs) will be pushed to us by the peer's sync process + // The peer runs the same negentropy sync and will identify these events as "haves" + // to push to us. We don't need to explicitly fetch them. + + return eventsSynced, nil +} + +// buildStorage creates a negentropy Vector from local events. +func (m *Manager) buildStorage(ctx context.Context) (*negentropy.Vector, error) { + storage := negentropy.NewVector() + + // Query all events from database using an empty filter + // This returns IdPkTs structs with Id, Pub, Ts, Ser fields + limit := uint(100000) + f := &filter.F{ + Limit: &limit, // Limit to 100k events for now + } + + idPkTs, err := m.db.QueryForIds(ctx, f) + if err != nil { + return nil, fmt.Errorf("failed to query events: %w", err) + } + + for _, item := range idPkTs { + // IDHex() returns lowercase hex string of the event ID + storage.Insert(item.Ts, item.IDHex()) + } + + storage.Seal() + return storage, nil +} + +// pushEventsToPeer sends events we have to the peer. +// The truncated IDs are 32-char hex prefixes, so we query our local DB and push matching events. +func (m *Manager) pushEventsToPeer(ctx context.Context, conn *websocket.Conn, truncatedIDs []string) (int, error) { + if len(truncatedIDs) == 0 { + return 0, nil + } + log.I.F("pushEventsToPeer: looking up %d events to push", len(truncatedIDs)) + + pushed := 0 + for _, truncID := range truncatedIDs { + // Query local database for events matching this ID prefix + // Use QueryByIDPrefix if available, otherwise fall back to broader query + events, err := m.queryEventsByIDPrefix(ctx, truncID) + if err != nil { + log.D.F("failed to query event with prefix %s: %v", truncID, err) + continue + } + + for _, ev := range events { + // Send event to peer + eventMsg := []any{"EVENT", ev} + if err := conn.WriteJSON(eventMsg); err != nil { + log.W.F("failed to push event %s: %v", truncID, err) + continue + } + pushed++ + } + } + + return pushed, nil +} + +// queryEventsByIDPrefix queries local database for events matching an ID prefix. +func (m *Manager) queryEventsByIDPrefix(ctx context.Context, idPrefix string) ([]*event.E, error) { + // For now, query by the prefix - Badger supports prefix iteration + // The ID prefix is 32 hex chars = 16 bytes + limit := uint(10000) // Get enough events to find our prefix matches + + // Query IDs and filter by prefix + f := &filter.F{ + Limit: &limit, + } + + idPkTs, err := m.db.QueryForIds(ctx, f) + if err != nil { + return nil, err + } + + var results []*event.E + for _, item := range idPkTs { + fullID := item.IDHex() + if len(fullID) >= len(idPrefix) && fullID[:len(idPrefix)] == idPrefix { + // Found a match - decode the full ID and fetch the event + idBytes, err := hex.DecodeString(fullID) + if err != nil { + log.D.F("failed to decode ID %s: %v", fullID, err) + continue + } + + // Create filter with the full ID + idTag := tag.NewFromBytesSlice(idBytes) + evs, err := m.db.QueryEvents(ctx, &filter.F{ + Ids: idTag, + }) + if err != nil { + log.D.F("failed to fetch event %s: %v", fullID, err) + continue + } + if len(evs) > 0 { + results = append(results, evs[0]) + } + } + } + + return results, nil +} + +// fetchEventsFromPeer fetches specific events from a peer by ID (can be prefixes). +// NOTE: This is deprecated in favor of push-based sync, but kept for reference. +func (m *Manager) fetchEventsFromPeer(ctx context.Context, conn *websocket.Conn, baseSubID string, ids []string) (int, error) { + if len(ids) == 0 { + return 0, nil + } + log.I.F("fetchEventsFromPeer: fetching %d events with IDs (first 3): %v", len(ids), ids[:min(3, len(ids))]) + + // Batch IDs into chunks of 100 + const batchSize = 100 + fetched := 0 + + for i := 0; i < len(ids); i += batchSize { + end := i + batchSize + if end > len(ids) { + end = len(ids) + } + batch := ids[i:end] + + subID := fmt.Sprintf("%s-fetch-%d", baseSubID, i/batchSize) + log.I.F("fetchEventsFromPeer: sending REQ %s for batch of %d IDs", subID, len(batch)) + + // Send REQ for these IDs + filter := map[string]any{ + "ids": batch, + } + req := []any{"REQ", subID, filter} + reqJSON, _ := json.Marshal(req) + log.D.F("fetchEventsFromPeer: REQ message: %s", string(reqJSON)[:min(500, len(reqJSON))]) + + if err := conn.WriteJSON(req); err != nil { + log.E.F("fetchEventsFromPeer: failed to send REQ: %v", err) + return fetched, fmt.Errorf("failed to send REQ: %w", err) + } + + // Read events until EOSE + messageCount := 0 + for { + _, msgBytes, err := conn.ReadMessage() + if err != nil { + log.E.F("fetchEventsFromPeer: failed to read after %d messages: %v", messageCount, err) + return fetched, fmt.Errorf("failed to read: %w", err) + } + messageCount++ + + var msg []json.RawMessage + if err := json.Unmarshal(msgBytes, &msg); err != nil { + log.D.F("fetchEventsFromPeer: failed to unmarshal message: %v", err) + continue + } + + if len(msg) < 2 { + log.D.F("fetchEventsFromPeer: message too short: %d elements", len(msg)) + continue + } + + var msgType string + if err := json.Unmarshal(msg[0], &msgType); err != nil { + log.D.F("fetchEventsFromPeer: failed to unmarshal message type: %v", err) + continue + } + + switch msgType { + case "EVENT": + if len(msg) >= 3 { + // Store the event + if err := m.storeEventFromJSON(ctx, msg[2]); err != nil { + log.W.F("fetchEventsFromPeer: failed to store event: %v", err) + } else { + fetched++ + if fetched%10 == 0 { + log.I.F("fetchEventsFromPeer: stored %d events so far", fetched) + } + } + } + case "EOSE": + log.I.F("fetchEventsFromPeer: received EOSE for %s after %d messages, fetched %d events in batch", subID, messageCount, fetched) + goto nextBatch + case "CLOSED": + var reason string + if len(msg) >= 3 { + json.Unmarshal(msg[2], &reason) + } + log.W.F("fetchEventsFromPeer: subscription %s closed: %s", subID, reason) + goto nextBatch + case "NOTICE": + var notice string + if len(msg) >= 2 { + json.Unmarshal(msg[1], ¬ice) + } + log.W.F("fetchEventsFromPeer: NOTICE from peer: %s", notice) + default: + log.D.F("fetchEventsFromPeer: unknown message type: %s", msgType) + } + } + nextBatch: + // Send CLOSE for this subscription + closeMsg := []any{"CLOSE", subID} + conn.WriteJSON(closeMsg) + } + + log.I.F("fetchEventsFromPeer: completed, total fetched: %d", fetched) + return fetched, nil +} + +// storeEventFromJSON stores an event from raw JSON. +func (m *Manager) storeEventFromJSON(ctx context.Context, eventJSON json.RawMessage) error { + // Parse the event using the nostr event encoder + ev := &event.E{} + if err := ev.UnmarshalJSON(eventJSON); err != nil { + return fmt.Errorf("failed to unmarshal event: %w", err) + } + + // Verify the event signature + if ok, err := ev.Verify(); err != nil || !ok { + return fmt.Errorf("event verification failed") + } + + // Store via database using the standard SaveEvent method + _, err := m.db.SaveEvent(ctx, ev) + return err +} + +// IsActive returns whether background sync is running. +func (m *Manager) IsActive() bool { + m.mu.RLock() + defer m.mu.RUnlock() + return m.active +} + +// LastSync returns the timestamp of the last sync cycle. +func (m *Manager) LastSync() time.Time { + m.mu.RLock() + defer m.mu.RUnlock() + return m.lastSync +} + +// GetPeers returns the list of peer URLs. +func (m *Manager) GetPeers() []string { + m.mu.RLock() + defer m.mu.RUnlock() + peers := make([]string, 0, len(m.peers)) + for url := range m.peers { + peers = append(peers, url) + } + return peers +} + +// GetPeerStates returns the sync state for all peers. +func (m *Manager) GetPeerStates() []*PeerState { + m.mu.RLock() + defer m.mu.RUnlock() + states := make([]*PeerState, 0, len(m.peers)) + for _, peer := range m.peers { + states = append(states, &PeerState{ + URL: peer.URL, + LastSync: peer.LastSync, + EventsSynced: peer.EventsSynced, + Status: peer.Status, + LastError: peer.LastError, + ConsecutiveFailures: peer.ConsecutiveFailures, + }) + } + return states +} + +// GetPeerState returns the sync state for a specific peer. +func (m *Manager) GetPeerState(peerURL string) (*PeerState, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + peer, ok := m.peers[peerURL] + if !ok { + return nil, false + } + return &PeerState{ + URL: peer.URL, + LastSync: peer.LastSync, + EventsSynced: peer.EventsSynced, + Status: peer.Status, + LastError: peer.LastError, + ConsecutiveFailures: peer.ConsecutiveFailures, + }, true +} + +// AddPeer adds a peer for negentropy sync. +func (m *Manager) AddPeer(peerURL string) { + m.mu.Lock() + defer m.mu.Unlock() + if _, ok := m.peers[peerURL]; !ok { + m.peers[peerURL] = &PeerState{ + URL: peerURL, + Status: "idle", + } + } +} + +// RemovePeer removes a peer from negentropy sync. +func (m *Manager) RemovePeer(peerURL string) { + m.mu.Lock() + defer m.mu.Unlock() + delete(m.peers, peerURL) +} + +// TriggerSync manually triggers sync with a specific peer or all peers. +func (m *Manager) TriggerSync(ctx context.Context, peerURL string) { + if peerURL == "" { + m.syncAllPeers() + } else { + m.syncWithPeer(ctx, peerURL) + } +} + +// sessionKey creates a unique key for a session. +func sessionKey(connectionID, subscriptionID string) string { + return connectionID + ":" + subscriptionID +} + +// OpenSession opens a new client negentropy session. +func (m *Manager) OpenSession(connectionID, subscriptionID string) *ClientSession { + m.mu.Lock() + defer m.mu.Unlock() + + key := sessionKey(connectionID, subscriptionID) + session := &ClientSession{ + SubscriptionID: subscriptionID, + ConnectionID: connectionID, + CreatedAt: time.Now(), + LastActivity: time.Now(), + RoundCount: 0, + } + m.sessions[key] = session + return session +} + +// GetSession retrieves an existing session. +func (m *Manager) GetSession(connectionID, subscriptionID string) (*ClientSession, bool) { + m.mu.RLock() + defer m.mu.RUnlock() + key := sessionKey(connectionID, subscriptionID) + session, ok := m.sessions[key] + return session, ok +} + +// UpdateSessionActivity updates the last activity time for a session. +func (m *Manager) UpdateSessionActivity(connectionID, subscriptionID string) { + m.mu.Lock() + defer m.mu.Unlock() + key := sessionKey(connectionID, subscriptionID) + if session, ok := m.sessions[key]; ok { + session.LastActivity = time.Now() + session.RoundCount++ + } +} + +// CloseSession closes a client session. +func (m *Manager) CloseSession(connectionID, subscriptionID string) { + m.mu.Lock() + defer m.mu.Unlock() + key := sessionKey(connectionID, subscriptionID) + if session, ok := m.sessions[key]; ok { + if session.neg != nil { + session.neg.Close() + } + } + delete(m.sessions, key) +} + +// CloseSessionsByConnection closes all sessions for a connection. +func (m *Manager) CloseSessionsByConnection(connectionID string) { + m.mu.Lock() + defer m.mu.Unlock() + for key, session := range m.sessions { + if session.ConnectionID == connectionID { + if session.neg != nil { + session.neg.Close() + } + delete(m.sessions, key) + } + } +} + +// ListSessions returns all active sessions. +func (m *Manager) ListSessions() []*ClientSession { + m.mu.RLock() + defer m.mu.RUnlock() + sessions := make([]*ClientSession, 0, len(m.sessions)) + for _, session := range m.sessions { + sessions = append(sessions, &ClientSession{ + SubscriptionID: session.SubscriptionID, + ConnectionID: session.ConnectionID, + CreatedAt: session.CreatedAt, + LastActivity: session.LastActivity, + RoundCount: session.RoundCount, + }) + } + return sessions +} + +// CleanupExpiredSessions removes sessions that have been inactive beyond timeout. +func (m *Manager) CleanupExpiredSessions() int { + m.mu.Lock() + defer m.mu.Unlock() + + cutoff := time.Now().Add(-m.config.ClientSessionTimeout) + removed := 0 + for key, session := range m.sessions { + if session.LastActivity.Before(cutoff) { + if session.neg != nil { + session.neg.Close() + } + delete(m.sessions, key) + removed++ + } + } + return removed +} + +// Ensure chk is used +var _ = chk.E diff --git a/pkg/sync/negentropy/server/service.go b/pkg/sync/negentropy/server/service.go new file mode 100644 index 0000000..08f8318 --- /dev/null +++ b/pkg/sync/negentropy/server/service.go @@ -0,0 +1,360 @@ +// Package server provides the gRPC server implementation for negentropy sync. +package server + +import ( + "context" + "fmt" + + "google.golang.org/grpc" + "lol.mleku.dev/log" + + "git.mleku.dev/mleku/nostr/encoders/filter" + negentropylib "git.mleku.dev/mleku/nostr/negentropy" + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/negentropy" + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + negentropyv1 "next.orly.dev/pkg/proto/orlysync/negentropy/v1" +) + +// Service implements the NegentropyServiceServer interface. +type Service struct { + negentropyv1.UnimplementedNegentropyServiceServer + mgr *negentropy.Manager + db database.Database + ready bool +} + +// NewService creates a new negentropy gRPC service. +func NewService(db database.Database, mgr *negentropy.Manager) *Service { + return &Service{ + mgr: mgr, + db: db, + ready: true, + } +} + +// Ready returns whether the service is ready to serve requests. +func (s *Service) Ready(ctx context.Context, _ *commonv1.Empty) (*commonv1.ReadyResponse, error) { + return &commonv1.ReadyResponse{Ready: s.ready}, nil +} + +// Start starts the background relay-to-relay sync. +func (s *Service) Start(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + s.mgr.Start() + return &commonv1.Empty{}, nil +} + +// Stop stops the background sync. +func (s *Service) Stop(ctx context.Context, _ *commonv1.Empty) (*commonv1.Empty, error) { + s.mgr.Stop() + return &commonv1.Empty{}, nil +} + +// HandleNegOpen processes a NEG-OPEN message from a client. +func (s *Service) HandleNegOpen(ctx context.Context, req *negentropyv1.NegOpenRequest) (*negentropyv1.NegOpenResponse, error) { + // Open a session for this client + session := s.mgr.OpenSession(req.ConnectionId, req.SubscriptionId) + + // Build storage from local events matching the filter + storage, err := s.buildStorageForFilter(ctx, req.Filter) + if err != nil { + log.E.F("NEG-OPEN: failed to build storage: %v", err) + return &negentropyv1.NegOpenResponse{ + Message: nil, + Error: 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 + if len(req.InitialMessage) > 0 { + var complete bool + respMsg, complete, err = neg.Reconcile(req.InitialMessage) + if err != nil { + log.E.F("NEG-OPEN: reconcile failed: %v", err) + return &negentropyv1.NegOpenResponse{ + Message: nil, + Error: 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 &negentropyv1.NegOpenResponse{ + Message: nil, + Error: fmt.Sprintf("failed to start: %v", err), + }, nil + } + log.D.F("NEG-OPEN: started negentropy, initial msg len=%d", len(respMsg)) + } + + return &negentropyv1.NegOpenResponse{ + Message: respMsg, + Error: "", + }, nil +} + +// HandleNegMsg processes a NEG-MSG message from a client. +func (s *Service) HandleNegMsg(ctx context.Context, req *negentropyv1.NegMsgRequest) (*negentropyv1.NegMsgResponse, error) { + // Update session activity + s.mgr.UpdateSessionActivity(req.ConnectionId, req.SubscriptionId) + + // Look up session + session, ok := s.mgr.GetSession(req.ConnectionId, req.SubscriptionId) + if !ok { + return &negentropyv1.NegMsgResponse{ + Error: "session not found", + }, nil + } + + neg := session.GetNegentropy() + if neg == nil { + return &negentropyv1.NegMsgResponse{ + Error: "session has no negentropy state", + }, nil + } + + // Process the message + respMsg, complete, err := neg.Reconcile(req.Message) + if err != nil { + log.E.F("NEG-MSG: reconcile failed: %v", err) + return &negentropyv1.NegMsgResponse{ + Error: fmt.Sprintf("reconcile failed: %v", err), + }, nil + } + + // Collect IDs we have that client needs (to send as events) + haveIDs := neg.CollectHaves() + var haveIDBytes [][]byte + for _, id := range haveIDs { + haveIDBytes = append(haveIDBytes, []byte(id)) + } + + // Collect IDs we need from client + needIDs := neg.CollectHaveNots() + var needIDBytes [][]byte + for _, id := range needIDs { + needIDBytes = append(needIDBytes, []byte(id)) + } + + log.I.F("NEG-MSG: complete=%v, haves=%d, needs=%d, response len=%d", + complete, len(haveIDs), len(needIDs), len(respMsg)) + + return &negentropyv1.NegMsgResponse{ + Message: respMsg, + HaveIds: haveIDBytes, + NeedIds: needIDBytes, + Complete: complete, + Error: "", + }, nil +} + +// HandleNegClose processes a NEG-CLOSE message from a client. +func (s *Service) HandleNegClose(ctx context.Context, req *negentropyv1.NegCloseRequest) (*commonv1.Empty, error) { + s.mgr.CloseSession(req.ConnectionId, req.SubscriptionId) + return &commonv1.Empty{}, nil +} + +// SyncWithPeer initiates negentropy sync with a specific peer relay. +func (s *Service) SyncWithPeer(req *negentropyv1.SyncPeerRequest, stream grpc.ServerStreamingServer[negentropyv1.SyncProgress]) error { + // Send initial progress + if err := stream.Send(&negentropyv1.SyncProgress{ + PeerUrl: req.PeerUrl, + Round: 0, + Complete: false, + }); err != nil { + return err + } + + // TODO: Implement actual NIP-77 sync with peer + // For now, just mark as complete + + s.mgr.TriggerSync(stream.Context(), req.PeerUrl) + + // Send completion + return stream.Send(&negentropyv1.SyncProgress{ + PeerUrl: req.PeerUrl, + Round: 1, + Complete: true, + }) +} + +// GetSyncStatus returns the current sync status. +func (s *Service) GetSyncStatus(ctx context.Context, _ *commonv1.Empty) (*negentropyv1.SyncStatusResponse, error) { + peerStates := s.mgr.GetPeerStates() + + states := make([]*negentropyv1.PeerSyncState, 0, len(peerStates)) + for _, ps := range peerStates { + states = append(states, &negentropyv1.PeerSyncState{ + PeerUrl: ps.URL, + LastSync: ps.LastSync.Unix(), + EventsSynced: ps.EventsSynced, + Status: ps.Status, + LastError: ps.LastError, + ConsecutiveFailures: ps.ConsecutiveFailures, + }) + } + + return &negentropyv1.SyncStatusResponse{ + Active: s.mgr.IsActive(), + LastSync: s.mgr.LastSync().Unix(), + PeerCount: int32(len(peerStates)), + PeerStates: states, + }, nil +} + +// GetPeers returns the list of negentropy sync peers. +func (s *Service) GetPeers(ctx context.Context, _ *commonv1.Empty) (*negentropyv1.PeersResponse, error) { + return &negentropyv1.PeersResponse{ + Peers: s.mgr.GetPeers(), + }, nil +} + +// AddPeer adds a peer for negentropy sync. +func (s *Service) AddPeer(ctx context.Context, req *negentropyv1.AddPeerRequest) (*commonv1.Empty, error) { + s.mgr.AddPeer(req.PeerUrl) + return &commonv1.Empty{}, nil +} + +// RemovePeer removes a peer from negentropy sync. +func (s *Service) RemovePeer(ctx context.Context, req *negentropyv1.RemovePeerRequest) (*commonv1.Empty, error) { + s.mgr.RemovePeer(req.PeerUrl) + return &commonv1.Empty{}, nil +} + +// TriggerSync manually triggers sync with a specific peer or all peers. +func (s *Service) TriggerSync(ctx context.Context, req *negentropyv1.TriggerSyncRequest) (*commonv1.Empty, error) { + s.mgr.TriggerSync(ctx, req.PeerUrl) + return &commonv1.Empty{}, nil +} + +// GetPeerSyncState returns sync state for a specific peer. +func (s *Service) GetPeerSyncState(ctx context.Context, req *negentropyv1.PeerSyncStateRequest) (*negentropyv1.PeerSyncStateResponse, error) { + state, found := s.mgr.GetPeerState(req.PeerUrl) + if !found { + return &negentropyv1.PeerSyncStateResponse{ + Found: false, + }, nil + } + + return &negentropyv1.PeerSyncStateResponse{ + Found: true, + State: &negentropyv1.PeerSyncState{ + PeerUrl: state.URL, + LastSync: state.LastSync.Unix(), + EventsSynced: state.EventsSynced, + Status: state.Status, + LastError: state.LastError, + ConsecutiveFailures: state.ConsecutiveFailures, + }, + }, nil +} + +// ListSessions returns active client negentropy sessions. +func (s *Service) ListSessions(ctx context.Context, _ *commonv1.Empty) (*negentropyv1.ListSessionsResponse, error) { + sessions := s.mgr.ListSessions() + + protoSessions := make([]*negentropyv1.ClientSession, 0, len(sessions)) + for _, sess := range sessions { + protoSessions = append(protoSessions, &negentropyv1.ClientSession{ + SubscriptionId: sess.SubscriptionID, + ConnectionId: sess.ConnectionID, + CreatedAt: sess.CreatedAt.Unix(), + LastActivity: sess.LastActivity.Unix(), + RoundCount: sess.RoundCount, + }) + } + + return &negentropyv1.ListSessionsResponse{ + Sessions: protoSessions, + }, nil +} + +// CloseSession forcefully closes a client session. +func (s *Service) CloseSession(ctx context.Context, req *negentropyv1.CloseSessionRequest) (*commonv1.Empty, error) { + if req.ConnectionId == "" { + // Close all sessions with this subscription ID + sessions := s.mgr.ListSessions() + for _, sess := range sessions { + if sess.SubscriptionID == req.SubscriptionId { + s.mgr.CloseSession(sess.ConnectionID, sess.SubscriptionID) + } + } + } else { + s.mgr.CloseSession(req.ConnectionId, req.SubscriptionId) + } + return &commonv1.Empty{}, nil +} + +// buildStorageForFilter creates a negentropy Vector from local events matching the filter. +func (s *Service) 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(100000) + f = &filter.F{Limit: &limit} + } + if f.Limit == nil { + limit := uint(100000) + f.Limit = &limit + } + + // Query events from database + idPkTs, err := s.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 Kinds + if len(pf.Kinds) > 0 { + // Create kinds from proto + // Note: We'd need proper kinds conversion here + } + + // Convert Since/Until + if pf.Since != nil { + // Set Since timestamp + } + if pf.Until != nil { + // Set Until timestamp + } + + // Convert Limit + if pf.Limit != nil { + limit := uint(*pf.Limit) + f.Limit = &limit + } + + return f +} diff --git a/pkg/sync/relaygroup/grpc/client.go b/pkg/sync/relaygroup/grpc/client.go new file mode 100644 index 0000000..5bed766 --- /dev/null +++ b/pkg/sync/relaygroup/grpc/client.go @@ -0,0 +1,142 @@ +// Package grpc provides a gRPC client for the relay group service. +package grpc + +import ( + "context" + "time" + + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "lol.mleku.dev/log" + + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + relaygroupv1 "next.orly.dev/pkg/proto/orlysync/relaygroup/v1" +) + +// Client is a gRPC client for the relay group service. +type Client struct { + conn *grpc.ClientConn + client relaygroupv1.RelayGroupServiceClient + ready chan struct{} +} + +// ClientConfig holds configuration for the gRPC client. +type ClientConfig struct { + ServerAddress string + ConnectTimeout time.Duration +} + +// New creates a new gRPC relay group client. +func New(ctx context.Context, cfg *ClientConfig) (*Client, error) { + timeout := cfg.ConnectTimeout + if timeout == 0 { + timeout = 10 * time.Second + } + + dialCtx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + conn, err := grpc.DialContext(dialCtx, cfg.ServerAddress, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions( + grpc.MaxCallRecvMsgSize(16<<20), // 16MB + grpc.MaxCallSendMsgSize(16<<20), // 16MB + ), + ) + if err != nil { + return nil, err + } + + c := &Client{ + conn: conn, + client: relaygroupv1.NewRelayGroupServiceClient(conn), + ready: make(chan struct{}), + } + + go c.waitForReady(ctx) + + return c, nil +} + +func (c *Client) waitForReady(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + default: + resp, err := c.client.Ready(ctx, &commonv1.Empty{}) + if err == nil && resp.Ready { + close(c.ready) + log.I.F("gRPC relay group client connected and ready") + return + } + time.Sleep(100 * time.Millisecond) + } + } +} + +// Close closes the gRPC connection. +func (c *Client) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +// Ready returns a channel that closes when the client is ready. +func (c *Client) Ready() <-chan struct{} { + return c.ready +} + +// FindAuthoritativeConfig finds the authoritative relay group configuration. +func (c *Client) FindAuthoritativeConfig(ctx context.Context) (*relaygroupv1.RelayGroupConfigResponse, error) { + return c.client.FindAuthoritativeConfig(ctx, &commonv1.Empty{}) +} + +// GetRelays returns the list of relays from the authoritative config. +func (c *Client) GetRelays(ctx context.Context) ([]string, error) { + resp, err := c.client.GetRelays(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + return resp.Relays, nil +} + +// IsAuthorizedPublisher checks if a pubkey can publish relay group configs. +func (c *Client) IsAuthorizedPublisher(ctx context.Context, pubkey []byte) (bool, error) { + resp, err := c.client.IsAuthorizedPublisher(ctx, &relaygroupv1.AuthorizedPublisherRequest{ + Pubkey: pubkey, + }) + if err != nil { + return false, err + } + return resp.Authorized, nil +} + +// GetAuthorizedPubkeys returns all authorized publisher pubkeys. +func (c *Client) GetAuthorizedPubkeys(ctx context.Context) ([][]byte, error) { + resp, err := c.client.GetAuthorizedPubkeys(ctx, &commonv1.Empty{}) + if err != nil { + return nil, err + } + return resp.Pubkeys, nil +} + +// ValidateRelayGroupEvent validates a relay group configuration event. +func (c *Client) ValidateRelayGroupEvent(ctx context.Context, event *commonv1.Event) (bool, string, error) { + resp, err := c.client.ValidateRelayGroupEvent(ctx, &relaygroupv1.ValidateEventRequest{ + Event: event, + }) + if err != nil { + return false, "", err + } + return resp.Valid, resp.Error, nil +} + +// HandleRelayGroupEvent processes a relay group event and triggers peer updates. +func (c *Client) HandleRelayGroupEvent(ctx context.Context, event *commonv1.Event) error { + _, err := c.client.HandleRelayGroupEvent(ctx, &relaygroupv1.HandleEventRequest{ + Event: event, + }) + return err +} diff --git a/pkg/sync/relaygroup.go b/pkg/sync/relaygroup/manager.go similarity index 63% rename from pkg/sync/relaygroup.go rename to pkg/sync/relaygroup/manager.go index 58417ac..d038023 100644 --- a/pkg/sync/relaygroup.go +++ b/pkg/sync/relaygroup/manager.go @@ -1,44 +1,56 @@ -// Package sync provides relay group configuration management -package sync +// Package relaygroup provides relay group configuration management +package relaygroup import ( "context" - "github.com/minio/sha256-simd" "encoding/hex" "encoding/json" "sort" "strings" - "lol.mleku.dev/log" - "next.orly.dev/pkg/database" + "github.com/minio/sha256-simd" "git.mleku.dev/mleku/nostr/encoders/bech32encoding" "git.mleku.dev/mleku/nostr/encoders/event" "git.mleku.dev/mleku/nostr/encoders/filter" "git.mleku.dev/mleku/nostr/encoders/kind" "git.mleku.dev/mleku/nostr/encoders/tag" + "lol.mleku.dev/log" + "next.orly.dev/pkg/database" ) -// RelayGroupConfig represents a relay group configuration event -type RelayGroupConfig struct { +// PeerUpdater is an interface for updating peer lists +type PeerUpdater interface { + UpdatePeers(peers []string) +} + +// Config represents a relay group configuration +type Config struct { Relays []string `json:"relays"` } -// RelayGroupManager handles relay group configuration -type RelayGroupManager struct { - db *database.D +// Manager handles relay group configuration +type Manager struct { + db *database.D authorizedPubkeys [][]byte } -// NewRelayGroupManager creates a new relay group manager -func NewRelayGroupManager(db *database.D, adminNpubs []string) *RelayGroupManager { +// ManagerConfig holds configuration for the relay group manager +type ManagerConfig struct { + AdminNpubs []string +} + +// NewManager creates a new relay group manager +func NewManager(db *database.D, cfg *ManagerConfig) *Manager { var pubkeys [][]byte - for _, npub := range adminNpubs { - if pk, err := bech32encoding.NpubOrHexToPublicKeyBinary(npub); err == nil { - pubkeys = append(pubkeys, pk) + if cfg != nil { + for _, npub := range cfg.AdminNpubs { + if pk, err := bech32encoding.NpubOrHexToPublicKeyBinary(npub); err == nil { + pubkeys = append(pubkeys, pk) + } } } - return &RelayGroupManager{ + return &Manager{ db: db, authorizedPubkeys: pubkeys, } @@ -46,7 +58,7 @@ func NewRelayGroupManager(db *database.D, adminNpubs []string) *RelayGroupManage // FindAuthoritativeConfig finds the authoritative relay group configuration // by selecting the latest event by timestamp, with hash tie-breaking -func (rgm *RelayGroupManager) FindAuthoritativeConfig(ctx context.Context) (*RelayGroupConfig, error) { +func (rgm *Manager) FindAuthoritativeConfig(ctx context.Context) (*Config, error) { if len(rgm.authorizedPubkeys) == 0 { return nil, nil } @@ -73,7 +85,7 @@ func (rgm *RelayGroupManager) FindAuthoritativeConfig(ctx context.Context) (*Rel } // Parse the configuration from the event content - var config RelayGroupConfig + var config Config if err := json.Unmarshal([]byte(authEvent.Content), &config); err != nil { return nil, err } @@ -81,8 +93,20 @@ func (rgm *RelayGroupManager) FindAuthoritativeConfig(ctx context.Context) (*Rel return &config, nil } +// FindAuthoritativeRelays returns just the relay URLs from the authoritative config +func (rgm *Manager) FindAuthoritativeRelays(ctx context.Context) ([]string, error) { + config, err := rgm.FindAuthoritativeConfig(ctx) + if err != nil { + return nil, err + } + if config == nil { + return nil, nil + } + return config.Relays, nil +} + // selectAuthoritativeEvent selects the authoritative event using the specified criteria -func (rgm *RelayGroupManager) selectAuthoritativeEvent(events []*event.E) *event.E { +func (rgm *Manager) selectAuthoritativeEvent(events []*event.E) *event.E { if len(events) == 0 { return nil } @@ -104,7 +128,7 @@ func (rgm *RelayGroupManager) selectAuthoritativeEvent(events []*event.E) *event } // IsAuthorizedPublisher checks if a pubkey is authorized to publish relay group configs -func (rgm *RelayGroupManager) IsAuthorizedPublisher(pubkey []byte) bool { +func (rgm *Manager) IsAuthorizedPublisher(pubkey []byte) bool { for _, authPK := range rgm.authorizedPubkeys { if string(authPK) == string(pubkey) { return true @@ -113,8 +137,19 @@ func (rgm *RelayGroupManager) IsAuthorizedPublisher(pubkey []byte) bool { return false } +// GetAuthorizedPubkeys returns all authorized pubkeys +func (rgm *Manager) GetAuthorizedPubkeys() [][]byte { + result := make([][]byte, len(rgm.authorizedPubkeys)) + for i, pk := range rgm.authorizedPubkeys { + pkCopy := make([]byte, len(pk)) + copy(pkCopy, pk) + result[i] = pkCopy + } + return result +} + // ValidateRelayGroupEvent validates a relay group configuration event -func (rgm *RelayGroupManager) ValidateRelayGroupEvent(ev *event.E) error { +func (rgm *Manager) ValidateRelayGroupEvent(ev *event.E) error { // Check if it's the right kind if ev.Kind != kind.RelayGroupConfig.K { return nil // Not our concern @@ -126,7 +161,7 @@ func (rgm *RelayGroupManager) ValidateRelayGroupEvent(ev *event.E) error { } // Try to parse the content - var config RelayGroupConfig + var config Config if err := json.Unmarshal([]byte(ev.Content), &config); err != nil { return err } @@ -140,7 +175,7 @@ func (rgm *RelayGroupManager) ValidateRelayGroupEvent(ev *event.E) error { } // HandleRelayGroupEvent processes a relay group configuration event and updates peer lists -func (rgm *RelayGroupManager) HandleRelayGroupEvent(ev *event.E, syncManager *Manager) { +func (rgm *Manager) HandleRelayGroupEvent(ev *event.E, peerUpdater PeerUpdater) { if ev.Kind != kind.RelayGroupConfig.K { return } @@ -152,8 +187,8 @@ func (rgm *RelayGroupManager) HandleRelayGroupEvent(ev *event.E, syncManager *Ma return } - if authConfig != nil { + if authConfig != nil && peerUpdater != nil { // Update the sync manager's peer list - syncManager.UpdatePeers(authConfig.Relays) + peerUpdater.UpdatePeers(authConfig.Relays) } } diff --git a/pkg/sync/relaygroup/server/service.go b/pkg/sync/relaygroup/server/service.go new file mode 100644 index 0000000..fe94d95 --- /dev/null +++ b/pkg/sync/relaygroup/server/service.go @@ -0,0 +1,99 @@ +// Package server provides the gRPC server implementation for relay group. +package server + +import ( + "context" + + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/relaygroup" + commonv1 "next.orly.dev/pkg/proto/orlysync/common/v1" + relaygroupv1 "next.orly.dev/pkg/proto/orlysync/relaygroup/v1" +) + +// Service implements the RelayGroupServiceServer interface. +type Service struct { + relaygroupv1.UnimplementedRelayGroupServiceServer + mgr *relaygroup.Manager + db database.Database + ready bool +} + +// NewService creates a new relay group gRPC service. +func NewService(db database.Database, mgr *relaygroup.Manager) *Service { + return &Service{ + mgr: mgr, + db: db, + ready: true, + } +} + +// Ready returns whether the service is ready to serve requests. +func (s *Service) Ready(ctx context.Context, _ *commonv1.Empty) (*commonv1.ReadyResponse, error) { + return &commonv1.ReadyResponse{Ready: s.ready}, nil +} + +// FindAuthoritativeConfig finds the authoritative relay group configuration. +func (s *Service) FindAuthoritativeConfig(ctx context.Context, _ *commonv1.Empty) (*relaygroupv1.RelayGroupConfigResponse, error) { + config, err := s.mgr.FindAuthoritativeConfig(ctx) + if err != nil { + return &relaygroupv1.RelayGroupConfigResponse{ + Found: false, + }, nil + } + + if config == nil { + return &relaygroupv1.RelayGroupConfigResponse{ + Found: false, + }, nil + } + + return &relaygroupv1.RelayGroupConfigResponse{ + Found: true, + Config: &relaygroupv1.RelayGroupConfig{ + Relays: config.Relays, + }, + }, nil +} + +// GetRelays returns the list of relays from the authoritative config. +func (s *Service) GetRelays(ctx context.Context, _ *commonv1.Empty) (*relaygroupv1.RelaysResponse, error) { + relays, err := s.mgr.FindAuthoritativeRelays(ctx) + if err != nil { + return &relaygroupv1.RelaysResponse{}, nil + } + + return &relaygroupv1.RelaysResponse{ + Relays: relays, + }, nil +} + +// IsAuthorizedPublisher checks if a pubkey can publish relay group configs. +func (s *Service) IsAuthorizedPublisher(ctx context.Context, req *relaygroupv1.AuthorizedPublisherRequest) (*relaygroupv1.AuthorizedPublisherResponse, error) { + authorized := s.mgr.IsAuthorizedPublisher(req.Pubkey) + return &relaygroupv1.AuthorizedPublisherResponse{ + Authorized: authorized, + }, nil +} + +// GetAuthorizedPubkeys returns all authorized publisher pubkeys. +func (s *Service) GetAuthorizedPubkeys(ctx context.Context, _ *commonv1.Empty) (*relaygroupv1.AuthorizedPubkeysResponse, error) { + pubkeys := s.mgr.GetAuthorizedPubkeys() + return &relaygroupv1.AuthorizedPubkeysResponse{ + Pubkeys: pubkeys, + }, nil +} + +// ValidateRelayGroupEvent validates a relay group configuration event. +func (s *Service) ValidateRelayGroupEvent(ctx context.Context, req *relaygroupv1.ValidateEventRequest) (*relaygroupv1.ValidateEventResponse, error) { + // Would need to convert proto event to internal event + // For now, return valid + return &relaygroupv1.ValidateEventResponse{ + Valid: true, + }, nil +} + +// HandleRelayGroupEvent processes a relay group event and triggers peer updates. +func (s *Service) HandleRelayGroupEvent(ctx context.Context, req *relaygroupv1.HandleEventRequest) (*commonv1.Empty, error) { + // Would need to convert proto event to internal event and call the manager + return &commonv1.Empty{}, nil +} diff --git a/pkg/sync/sync.go b/pkg/sync/sync.go new file mode 100644 index 0000000..ac75f48 --- /dev/null +++ b/pkg/sync/sync.go @@ -0,0 +1,72 @@ +// Package sync provides backward compatibility facade for sync services +// New code should import the specific subpackages directly: +// - next.orly.dev/pkg/sync/distributed +// - next.orly.dev/pkg/sync/cluster +// - next.orly.dev/pkg/sync/relaygroup +// - next.orly.dev/pkg/sync/negentropy +package sync + +import ( + "context" + "time" + + "git.mleku.dev/mleku/nostr/encoders/event" + "next.orly.dev/pkg/database" + "next.orly.dev/pkg/sync/cluster" + "next.orly.dev/pkg/sync/common" + "next.orly.dev/pkg/sync/distributed" + "next.orly.dev/pkg/sync/relaygroup" +) + +// Re-export types for backward compatibility + +// Manager is the distributed sync manager +type Manager = distributed.Manager + +// ClusterManager is the cluster replication manager +type ClusterManager = cluster.Manager + +// RelayGroupManager is the relay group configuration manager +type RelayGroupManager = relaygroup.Manager + +// RelayGroupConfig is the relay group configuration +type RelayGroupConfig = relaygroup.Config + +// NIP11Cache is the NIP-11 relay info cache +type NIP11Cache = common.NIP11Cache + +// NewNIP11Cache creates a new NIP-11 cache +func NewNIP11Cache(ttl time.Duration) *NIP11Cache { + return common.NewNIP11Cache(ttl) +} + +// NewManager creates a new distributed sync manager with backward compatible signature +func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, peers []string, relayGroupMgr *RelayGroupManager, policyManager interface{ CheckPolicy(action string, ev *event.E, pubkey []byte, remote string) (bool, error) }) *Manager { + cfg := &distributed.Config{ + NodeID: nodeID, + RelayURL: relayURL, + Peers: peers, + SyncInterval: 5 * time.Second, + NIP11CacheTTL: 30 * time.Minute, + } + return distributed.NewManager(ctx, db, cfg, policyManager) +} + +// NewClusterManager creates a new cluster manager with backward compatible signature +func NewClusterManager(ctx context.Context, db *database.D, adminNpubs []string, propagatePrivilegedEvents bool, publisher interface{ Deliver(*event.E) }) *ClusterManager { + cfg := &cluster.Config{ + AdminNpubs: adminNpubs, + PropagatePrivilegedEvents: propagatePrivilegedEvents, + PollInterval: 5 * time.Second, + NIP11CacheTTL: 30 * time.Minute, + } + return cluster.NewManager(ctx, db, cfg, publisher) +} + +// NewRelayGroupManager creates a new relay group manager with backward compatible signature +func NewRelayGroupManager(db *database.D, adminNpubs []string) *RelayGroupManager { + cfg := &relaygroup.ManagerConfig{ + AdminNpubs: adminNpubs, + } + return relaygroup.NewManager(db, cfg) +} diff --git a/pkg/version/version b/pkg/version/version index ec9d004..8e4a6c0 100644 --- a/pkg/version/version +++ b/pkg/version/version @@ -1 +1 @@ -v0.54.0 +v0.55.0 diff --git a/proto/orlysync/cluster/v1/service.proto b/proto/orlysync/cluster/v1/service.proto new file mode 100644 index 0000000..5ad709d --- /dev/null +++ b/proto/orlysync/cluster/v1/service.proto @@ -0,0 +1,130 @@ +syntax = "proto3"; +package orlysync.cluster.v1; +option go_package = "next.orly.dev/pkg/proto/orlysync/cluster/v1;clusterv1"; + +import "orlysync/common/v1/types.proto"; + +// ClusterSyncService provides cluster replication with persistent state +// for multi-member relay clusters +service ClusterSyncService { + // === Lifecycle Methods === + + // Ready returns whether the service is ready to serve requests + rpc Ready(orlysync.common.v1.Empty) returns (orlysync.common.v1.ReadyResponse); + + // Start starts the cluster polling loop + rpc Start(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // Stop stops the cluster polling loop + rpc Stop(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // === HTTP Proxy Handlers === + // These allow the main relay to delegate HTTP cluster endpoints to this service + + // HandleLatestSerial proxies GET /cluster/latest HTTP requests + rpc HandleLatestSerial(orlysync.common.v1.HTTPRequest) returns (orlysync.common.v1.HTTPResponse); + + // HandleEventsRange proxies GET /cluster/events HTTP requests + rpc HandleEventsRange(orlysync.common.v1.HTTPRequest) returns (orlysync.common.v1.HTTPResponse); + + // === Cluster Management === + + // GetMembers returns the current cluster members + rpc GetMembers(orlysync.common.v1.Empty) returns (MembersResponse); + + // UpdateMembership updates cluster membership + rpc UpdateMembership(UpdateMembershipRequest) returns (orlysync.common.v1.Empty); + + // HandleMembershipEvent processes a cluster membership event (Kind 39108) + rpc HandleMembershipEvent(MembershipEventRequest) returns (orlysync.common.v1.Empty); + + // === Status === + + // GetClusterStatus returns overall cluster status + rpc GetClusterStatus(orlysync.common.v1.Empty) returns (ClusterStatusResponse); + + // GetMemberStatus returns status for a specific member + rpc GetMemberStatus(MemberStatusRequest) returns (MemberStatusResponse); + + // === Data Operations === + + // GetLatestSerial returns the latest serial from this relay's database + rpc GetLatestSerial(orlysync.common.v1.Empty) returns (LatestSerialResponse); + + // GetEventsInRange returns event info for a serial range + rpc GetEventsInRange(EventsRangeRequest) returns (EventsRangeResponse); +} + +// === Request/Response Messages === + +// LatestSerialResponse contains the latest serial and timestamp +message LatestSerialResponse { + uint64 serial = 1; + int64 timestamp = 2; // Unix timestamp +} + +// EventsRangeRequest requests events in a serial range +message EventsRangeRequest { + uint64 from = 1; // Start serial (inclusive) + uint64 to = 2; // End serial (inclusive) + int32 limit = 3; // Max events to return +} + +// EventsRangeResponse contains events in the requested range +message EventsRangeResponse { + repeated EventInfo events = 1; + bool has_more = 2; + uint64 next_from = 3; // Next serial if has_more is true +} + +// EventInfo contains metadata about an event +message EventInfo { + uint64 serial = 1; + string id = 2; // Event ID (hex) + int64 timestamp = 3; // Created timestamp +} + +// ClusterMember represents a cluster member +message ClusterMember { + string http_url = 1; + string websocket_url = 2; + uint64 last_serial = 3; + int64 last_poll = 4; // Unix timestamp + string status = 5; // "active", "error", "unknown" + int32 error_count = 6; +} + +// MembersResponse contains the list of cluster members +message MembersResponse { + repeated ClusterMember members = 1; +} + +// UpdateMembershipRequest updates cluster membership +message UpdateMembershipRequest { + repeated string relay_urls = 1; // List of relay URLs to add +} + +// MembershipEventRequest contains a cluster membership event +message MembershipEventRequest { + orlysync.common.v1.Event event = 1; +} + +// ClusterStatusResponse contains overall cluster status +message ClusterStatusResponse { + uint64 latest_serial = 1; + int32 active_members = 2; + int32 total_members = 3; + bool propagate_privileged_events = 4; + repeated ClusterMember members = 5; +} + +// MemberStatusRequest requests status for a specific member +message MemberStatusRequest { + string http_url = 1; +} + +// MemberStatusResponse contains status for a member +message MemberStatusResponse { + ClusterMember member = 1; + bool found = 2; +} diff --git a/proto/orlysync/common/v1/types.proto b/proto/orlysync/common/v1/types.proto new file mode 100644 index 0000000..7b4ed33 --- /dev/null +++ b/proto/orlysync/common/v1/types.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; +package orlysync.common.v1; +option go_package = "next.orly.dev/pkg/proto/orlysync/common/v1;commonv1"; + +// Empty is used for requests/responses with no data +message Empty {} + +// ReadyResponse indicates if the service is ready +message ReadyResponse { + bool ready = 1; +} + +// HTTPRequest wraps an HTTP request for proxy delegation +message HTTPRequest { + string method = 1; + string path = 2; + map headers = 3; + bytes body = 4; + string query_string = 5; + string remote_addr = 6; +} + +// HTTPResponse wraps an HTTP response from proxy delegation +message HTTPResponse { + int32 status_code = 1; + map headers = 2; + bytes body = 3; +} + +// Event represents a Nostr event (shared across sync services) +// Binary fields (id, pubkey, sig) are stored as raw bytes for efficiency +message Event { + bytes id = 1; // 32 bytes SHA256 hash + bytes pubkey = 2; // 32 bytes public key + int64 created_at = 3; // UNIX timestamp + uint32 kind = 4; // Event kind + repeated Tag tags = 5; // Event tags + bytes content = 6; // Content (may be binary) + bytes sig = 7; // 64 bytes Schnorr signature +} + +// Tag represents a Nostr tag (array of byte slices) +message Tag { + repeated bytes values = 1; +} + +// Filter represents a Nostr query filter (NIP-01) +message Filter { + repeated bytes ids = 1; // Event IDs to match (32 bytes each) + repeated uint32 kinds = 2; // Kinds to match + repeated bytes authors = 3; // Author pubkeys (32 bytes each) + map tags = 4; // Tag filters (#e, #p, #t, etc.) + optional int64 since = 5; // Created after timestamp + optional int64 until = 6; // Created before timestamp + optional bytes search = 7; // Full-text search query (NIP-50) + optional uint32 limit = 8; // Max results +} + +// TagSet represents a set of tag values for filtering +message TagSet { + repeated bytes values = 1; +} + +// SyncInfo provides general sync service information +message SyncInfo { + string node_id = 1; // Node identity (npub/hex pubkey) + string relay_url = 2; // This relay's URL + uint64 current_serial = 3; // Current highest serial number + int32 peer_count = 4; // Number of configured peers + string status = 5; // Service status +} + +// PeerInfo represents information about a sync peer +message PeerInfo { + string url = 1; + uint64 last_serial = 2; + string status = 3; // "active", "error", "unknown" + int64 last_poll = 4; // Unix timestamp of last poll + int32 error_count = 5; +} diff --git a/proto/orlysync/distributed/v1/service.proto b/proto/orlysync/distributed/v1/service.proto new file mode 100644 index 0000000..e989e84 --- /dev/null +++ b/proto/orlysync/distributed/v1/service.proto @@ -0,0 +1,135 @@ +syntax = "proto3"; +package orlysync.distributed.v1; +option go_package = "next.orly.dev/pkg/proto/orlysync/distributed/v1;distributedv1"; + +import "orlysync/common/v1/types.proto"; + +// DistributedSyncService provides serial-based peer-to-peer synchronization +// between relay instances using HTTP polling +service DistributedSyncService { + // === Lifecycle Methods === + + // Ready returns whether the service is ready to serve requests + rpc Ready(orlysync.common.v1.Empty) returns (orlysync.common.v1.ReadyResponse); + + // GetInfo returns current sync service information + rpc GetInfo(orlysync.common.v1.Empty) returns (orlysync.common.v1.SyncInfo); + + // === Sync Operations === + + // GetCurrentSerial returns this relay's current serial number + rpc GetCurrentSerial(CurrentRequest) returns (CurrentResponse); + + // GetEventIDs returns event IDs for a serial range + rpc GetEventIDs(EventIDsRequest) returns (EventIDsResponse); + + // === HTTP Proxy Handlers === + // These allow the main relay to delegate HTTP sync endpoints to this service + + // HandleCurrentRequest proxies /api/sync/current HTTP requests + rpc HandleCurrentRequest(orlysync.common.v1.HTTPRequest) returns (orlysync.common.v1.HTTPResponse); + + // HandleEventIDsRequest proxies /api/sync/event-ids HTTP requests + rpc HandleEventIDsRequest(orlysync.common.v1.HTTPRequest) returns (orlysync.common.v1.HTTPResponse); + + // === Peer Management === + + // GetPeers returns the current list of sync peers + rpc GetPeers(orlysync.common.v1.Empty) returns (PeersResponse); + + // UpdatePeers updates the peer list + rpc UpdatePeers(UpdatePeersRequest) returns (orlysync.common.v1.Empty); + + // IsAuthorizedPeer checks if a peer is authorized by validating its NIP-11 pubkey + rpc IsAuthorizedPeer(AuthorizedPeerRequest) returns (AuthorizedPeerResponse); + + // GetPeerPubkey fetches the pubkey for a peer relay via NIP-11 + rpc GetPeerPubkey(PeerPubkeyRequest) returns (PeerPubkeyResponse); + + // === Serial Tracking === + + // UpdateSerial updates the current serial from database + rpc UpdateSerial(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // NotifyNewEvent notifies the service of a new event being stored + rpc NotifyNewEvent(NewEventNotification) returns (orlysync.common.v1.Empty); + + // === Sync Control === + + // TriggerSync manually triggers a sync cycle with all peers + rpc TriggerSync(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // GetSyncStatus returns current sync status for all peers + rpc GetSyncStatus(orlysync.common.v1.Empty) returns (SyncStatusResponse); +} + +// === Request/Response Messages === + +// CurrentRequest is sent to request current serial number +message CurrentRequest { + string node_id = 1; // Requesting node's identity + string relay_url = 2; // Requesting relay's URL +} + +// CurrentResponse contains the current serial number +message CurrentResponse { + string node_id = 1; // Responding node's identity + string relay_url = 2; // Responding relay's URL + uint64 serial = 3; // Current serial number +} + +// EventIDsRequest requests event IDs in a serial range +message EventIDsRequest { + string node_id = 1; // Requesting node's identity + string relay_url = 2; // Requesting relay's URL + uint64 from = 3; // Start serial (inclusive) + uint64 to = 4; // End serial (inclusive) +} + +// EventIDsResponse contains event IDs mapped to serial numbers +message EventIDsResponse { + map event_map = 1; // event_id (hex) -> serial +} + +// PeersResponse contains the list of sync peers +message PeersResponse { + repeated string peers = 1; // List of peer relay URLs +} + +// UpdatePeersRequest updates the peer list +message UpdatePeersRequest { + repeated string peers = 1; // New list of peer relay URLs +} + +// AuthorizedPeerRequest checks if a peer is authorized +message AuthorizedPeerRequest { + string peer_url = 1; + string expected_pubkey = 2; // Expected NIP-11 pubkey +} + +// AuthorizedPeerResponse indicates if the peer is authorized +message AuthorizedPeerResponse { + bool authorized = 1; +} + +// PeerPubkeyRequest requests the pubkey for a peer +message PeerPubkeyRequest { + string peer_url = 1; +} + +// PeerPubkeyResponse contains the peer's pubkey +message PeerPubkeyResponse { + string pubkey = 1; // Peer's NIP-11 pubkey (hex or npub) +} + +// NewEventNotification notifies of a new event +message NewEventNotification { + bytes event_id = 1; // 32 bytes event ID + uint64 serial = 2; // Assigned serial number +} + +// SyncStatusResponse contains sync status for all peers +message SyncStatusResponse { + uint64 current_serial = 1; + repeated orlysync.common.v1.PeerInfo peers = 2; +} diff --git a/proto/orlysync/negentropy/v1/service.proto b/proto/orlysync/negentropy/v1/service.proto new file mode 100644 index 0000000..faaa637 --- /dev/null +++ b/proto/orlysync/negentropy/v1/service.proto @@ -0,0 +1,203 @@ +syntax = "proto3"; +package orlysync.negentropy.v1; +option go_package = "next.orly.dev/pkg/proto/orlysync/negentropy/v1;negentropyv1"; + +import "orlysync/common/v1/types.proto"; + +// NegentropyService provides NIP-77 negentropy-based set reconciliation +// for both relay-to-relay sync and client-facing WebSocket operations +service NegentropyService { + // === Lifecycle Methods === + + // Ready returns whether the service is ready to serve requests + rpc Ready(orlysync.common.v1.Empty) returns (orlysync.common.v1.ReadyResponse); + + // Start starts the background relay-to-relay sync + rpc Start(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // Stop stops the background sync + rpc Stop(orlysync.common.v1.Empty) returns (orlysync.common.v1.Empty); + + // === Client-Facing NIP-77 (WebSocket Message Handling) === + // These handle NEG-OPEN, NEG-MSG, NEG-CLOSE from WebSocket clients + + // HandleNegOpen processes a NEG-OPEN message from a client + rpc HandleNegOpen(NegOpenRequest) returns (NegOpenResponse); + + // HandleNegMsg processes a NEG-MSG message from a client + rpc HandleNegMsg(NegMsgRequest) returns (NegMsgResponse); + + // HandleNegClose processes a NEG-CLOSE message from a client + rpc HandleNegClose(NegCloseRequest) returns (orlysync.common.v1.Empty); + + // === Relay-to-Relay Sync === + + // SyncWithPeer initiates negentropy sync with a specific peer relay + rpc SyncWithPeer(SyncPeerRequest) returns (stream SyncProgress); + + // GetSyncStatus returns the current sync status + rpc GetSyncStatus(orlysync.common.v1.Empty) returns (SyncStatusResponse); + + // === Peer Management === + + // GetPeers returns the list of negentropy sync peers + rpc GetPeers(orlysync.common.v1.Empty) returns (PeersResponse); + + // AddPeer adds a peer for negentropy sync + rpc AddPeer(AddPeerRequest) returns (orlysync.common.v1.Empty); + + // RemovePeer removes a peer from negentropy sync + rpc RemovePeer(RemovePeerRequest) returns (orlysync.common.v1.Empty); + + // === Sync Control === + + // TriggerSync manually triggers sync with a specific peer or all peers + rpc TriggerSync(TriggerSyncRequest) returns (orlysync.common.v1.Empty); + + // GetPeerSyncState returns sync state for a specific peer + rpc GetPeerSyncState(PeerSyncStateRequest) returns (PeerSyncStateResponse); + + // === Session Management === + + // ListSessions returns active client negentropy sessions + rpc ListSessions(orlysync.common.v1.Empty) returns (ListSessionsResponse); + + // CloseSession forcefully closes a client session + rpc CloseSession(CloseSessionRequest) returns (orlysync.common.v1.Empty); +} + +// === Client-Facing NIP-77 Messages === + +// NegOpenRequest processes a NEG-OPEN from client +// NEG-OPEN format: ["NEG-OPEN", subscription_id, filter, initial_message?] +message NegOpenRequest { + string subscription_id = 1; // Client's subscription ID + orlysync.common.v1.Filter filter = 2; // Nostr filter for reconciliation + bytes initial_message = 3; // Optional initial negentropy message + string connection_id = 4; // Connection ID for session tracking +} + +// NegOpenResponse returns the initial negentropy response +message NegOpenResponse { + bytes message = 1; // Negentropy protocol message to send back + string error = 2; // Error message if failed +} + +// NegMsgRequest processes a NEG-MSG from client +// NEG-MSG format: ["NEG-MSG", subscription_id, message] +message NegMsgRequest { + string subscription_id = 1; + bytes message = 2; // Negentropy protocol message + string connection_id = 3; +} + +// NegMsgResponse returns reconciliation results +message NegMsgResponse { + bytes message = 1; // Negentropy protocol message to send back + repeated bytes have_ids = 2; // Event IDs we have that client needs + repeated bytes need_ids = 3; // Event IDs we need from client + bool complete = 4; // True if reconciliation is complete + string error = 5; // Error message if failed +} + +// NegCloseRequest processes a NEG-CLOSE from client +// NEG-CLOSE format: ["NEG-CLOSE", subscription_id] +message NegCloseRequest { + string subscription_id = 1; + string connection_id = 2; +} + +// === Relay-to-Relay Sync Messages === + +// SyncPeerRequest initiates sync with a peer +message SyncPeerRequest { + string peer_url = 1; // WebSocket URL of peer relay + orlysync.common.v1.Filter filter = 2; // Optional filter to limit sync scope + int64 since = 3; // Optional: only sync events since timestamp +} + +// SyncProgress streams sync progress updates +message SyncProgress { + string peer_url = 1; + int32 round = 2; // Reconciliation round number + int64 have_count = 3; // Events we have that peer needs + int64 need_count = 4; // Events we need from peer + int64 fetched_count = 5; // Events fetched so far + int64 sent_count = 6; // Events sent so far + bool complete = 7; // True when sync is complete + string error = 8; // Error message if failed +} + +// SyncStatusResponse contains overall sync status +message SyncStatusResponse { + bool active = 1; // Whether background sync is running + int64 last_sync = 2; // Timestamp of last sync + int32 peer_count = 3; + repeated PeerSyncState peer_states = 4; +} + +// === Peer Management Messages === + +// PeersResponse contains the list of peers +message PeersResponse { + repeated string peers = 1; // List of peer WebSocket URLs +} + +// AddPeerRequest adds a peer +message AddPeerRequest { + string peer_url = 1; +} + +// RemovePeerRequest removes a peer +message RemovePeerRequest { + string peer_url = 1; +} + +// TriggerSyncRequest triggers manual sync +message TriggerSyncRequest { + string peer_url = 1; // Optional: specific peer (empty = all) + orlysync.common.v1.Filter filter = 2; // Optional: filter for sync scope +} + +// PeerSyncStateRequest requests state for a peer +message PeerSyncStateRequest { + string peer_url = 1; +} + +// PeerSyncStateResponse contains peer sync state +message PeerSyncStateResponse { + PeerSyncState state = 1; + bool found = 2; +} + +// PeerSyncState represents sync state for a peer +message PeerSyncState { + string peer_url = 1; + int64 last_sync = 2; // Timestamp of last successful sync + int64 events_synced = 3; // Total events synced from this peer + string status = 4; // "idle", "syncing", "error" + string last_error = 5; // Last error message + int32 consecutive_failures = 6; +} + +// === Session Management Messages === + +// ClientSession represents an active client negentropy session +message ClientSession { + string subscription_id = 1; + string connection_id = 2; + int64 created_at = 3; + int64 last_activity = 4; + int32 round_count = 5; // Number of reconciliation rounds +} + +// ListSessionsResponse contains active sessions +message ListSessionsResponse { + repeated ClientSession sessions = 1; +} + +// CloseSessionRequest closes a session +message CloseSessionRequest { + string subscription_id = 1; + string connection_id = 2; // Optional: if empty, close all with this sub_id +} diff --git a/proto/orlysync/relaygroup/v1/service.proto b/proto/orlysync/relaygroup/v1/service.proto new file mode 100644 index 0000000..1935dc9 --- /dev/null +++ b/proto/orlysync/relaygroup/v1/service.proto @@ -0,0 +1,90 @@ +syntax = "proto3"; +package orlysync.relaygroup.v1; +option go_package = "next.orly.dev/pkg/proto/orlysync/relaygroup/v1;relaygroupv1"; + +import "orlysync/common/v1/types.proto"; + +// RelayGroupService provides relay group configuration discovery +// by selecting authoritative config from Kind 39105 events +service RelayGroupService { + // === Lifecycle Methods === + + // Ready returns whether the service is ready to serve requests + rpc Ready(orlysync.common.v1.Empty) returns (orlysync.common.v1.ReadyResponse); + + // === Configuration Lookup === + + // FindAuthoritativeConfig finds the authoritative relay group configuration + // using timestamp ordering with hash tie-breaking + rpc FindAuthoritativeConfig(orlysync.common.v1.Empty) returns (RelayGroupConfigResponse); + + // GetRelays returns the list of relays from the authoritative config + rpc GetRelays(orlysync.common.v1.Empty) returns (RelaysResponse); + + // === Publisher Verification === + + // IsAuthorizedPublisher checks if a pubkey can publish relay group configs + rpc IsAuthorizedPublisher(AuthorizedPublisherRequest) returns (AuthorizedPublisherResponse); + + // GetAuthorizedPubkeys returns all authorized publisher pubkeys + rpc GetAuthorizedPubkeys(orlysync.common.v1.Empty) returns (AuthorizedPubkeysResponse); + + // === Event Handling === + + // ValidateRelayGroupEvent validates a relay group configuration event + rpc ValidateRelayGroupEvent(ValidateEventRequest) returns (ValidateEventResponse); + + // HandleRelayGroupEvent processes a relay group event and triggers peer updates + rpc HandleRelayGroupEvent(HandleEventRequest) returns (orlysync.common.v1.Empty); +} + +// === Request/Response Messages === + +// RelayGroupConfig represents a relay group configuration +message RelayGroupConfig { + repeated string relays = 1; // List of relay URLs +} + +// RelayGroupConfigResponse contains the authoritative config +message RelayGroupConfigResponse { + RelayGroupConfig config = 1; + bool found = 2; + bytes source_event_id = 3; // ID of the event that provided this config + int64 source_timestamp = 4; // Timestamp of the source event +} + +// RelaysResponse contains the list of relays +message RelaysResponse { + repeated string relays = 1; +} + +// AuthorizedPublisherRequest checks if a pubkey is authorized +message AuthorizedPublisherRequest { + bytes pubkey = 1; // 32 bytes public key +} + +// AuthorizedPublisherResponse indicates if the pubkey is authorized +message AuthorizedPublisherResponse { + bool authorized = 1; +} + +// AuthorizedPubkeysResponse contains all authorized pubkeys +message AuthorizedPubkeysResponse { + repeated bytes pubkeys = 1; // List of 32-byte pubkeys +} + +// ValidateEventRequest requests validation of a relay group event +message ValidateEventRequest { + orlysync.common.v1.Event event = 1; +} + +// ValidateEventResponse contains validation results +message ValidateEventResponse { + bool valid = 1; + string error = 2; // Error message if not valid +} + +// HandleEventRequest requests processing of a relay group event +message HandleEventRequest { + orlysync.common.v1.Event event = 1; +} diff --git a/scripts/build-and-deploy.sh b/scripts/build-and-deploy.sh new file mode 100755 index 0000000..75d80e3 --- /dev/null +++ b/scripts/build-and-deploy.sh @@ -0,0 +1,80 @@ +#!/bin/bash +# Build ARM64 binaries and deploy to relay +# Usage: ./scripts/build-and-deploy.sh [relay.orly.dev|new.orly.dev|both] [--restart] + +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BUILD_DIR="$PROJECT_DIR/build-arm64" + +# Colors +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# Parse arguments +TARGET="${1:-relay.orly.dev}" +RESTART_FLAG="" +if [[ "$2" == "--restart" ]] || [[ "$1" == "--restart" ]]; then + RESTART_FLAG="--restart" +fi + +# Build for ARM64 +log_info "Building ARM64 binaries..." +cd "$PROJECT_DIR" +mkdir -p "$BUILD_DIR" + +export CGO_ENABLED=0 +export GOOS=linux +export GOARCH=arm64 + +# Build all binaries +log_info "Building orly (main relay)..." +go build -o "$BUILD_DIR/orly" . + +log_info "Building orly-db..." +go build -o "$BUILD_DIR/orly-db" ./cmd/orly-db + +log_info "Building orly-acl..." +go build -o "$BUILD_DIR/orly-acl" ./cmd/orly-acl + +log_info "Building orly-launcher..." +go build -o "$BUILD_DIR/orly-launcher" ./cmd/orly-launcher + +log_info "Building orly-sync-negentropy..." +go build -o "$BUILD_DIR/orly-sync-negentropy" ./cmd/orly-sync-negentropy + +log_info "Build complete. Binaries in $BUILD_DIR" +ls -la "$BUILD_DIR" + +# Deploy function +deploy_to() { + local host="$1" + log_info "Deploying to $host..." + "$SCRIPT_DIR/deploy-orly.sh" --host "$host" --local-path "$BUILD_DIR" $RESTART_FLAG +} + +# Deploy based on target +case "$TARGET" in + both) + deploy_to "relay.orly.dev" + deploy_to "new.orly.dev" + ;; + relay.orly.dev|new.orly.dev) + deploy_to "$TARGET" + ;; + --restart) + # --restart was first arg, deploy to default + deploy_to "relay.orly.dev" + ;; + *) + log_warn "Unknown target: $TARGET. Using relay.orly.dev" + deploy_to "relay.orly.dev" + ;; +esac + +log_info "Done!" diff --git a/scripts/deploy-orly.sh b/scripts/deploy-orly.sh new file mode 100755 index 0000000..1958ce8 --- /dev/null +++ b/scripts/deploy-orly.sh @@ -0,0 +1,220 @@ +#!/bin/bash +# ORLY Deployment Script +# Usage: curl -sSL https://relay.orly.dev/deploy.sh | bash -s -- [options] +# +# Options: +# --binaries-url URL URL to download binaries tarball from +# --local-path PATH Local path to binaries (for scp-based deploy) +# --host HOST Target host (default: relay.orly.dev) +# --restart Restart service after deployment +# --rollback Rollback to previous release +# --list List available releases +# --help Show this help + +set -e + +# Configuration +DEPLOY_HOST="${DEPLOY_HOST:-relay.orly.dev}" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) + +# Binary names and their symlink mappings +declare -A BINARIES=( + ["orly"]="orly-relay" + ["orly-db"]="orly-db-badger" + ["orly-acl"]="orly-acl-follows" + ["orly-launcher"]="orly-launcher" + ["orly-sync-negentropy"]="orly-sync-negentropy" +) + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +log_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[WARN]${NC} $1" +} + +log_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +show_help() { + head -15 "$0" | tail -13 + exit 0 +} + +# Parse arguments +BINARIES_URL="" +LOCAL_PATH="" +DO_RESTART=false +DO_ROLLBACK=false +DO_LIST=false + +while [[ $# -gt 0 ]]; do + case $1 in + --binaries-url) + BINARIES_URL="$2" + shift 2 + ;; + --local-path) + LOCAL_PATH="$2" + shift 2 + ;; + --host) + DEPLOY_HOST="$2" + shift 2 + ;; + --restart) + DO_RESTART=true + shift + ;; + --rollback) + DO_ROLLBACK=true + shift + ;; + --list) + DO_LIST=true + shift + ;; + --help|-h) + show_help + ;; + *) + log_error "Unknown option: $1" + show_help + ;; + esac +done + +# List releases +if $DO_LIST; then + log_info "Available releases on $DEPLOY_HOST:" + ssh "$DEPLOY_HOST" 'ls -la ~/.local/bin/releases/ 2>/dev/null | tail -n +4' || log_error "Failed to list releases" + + log_info "Current symlinks:" + ssh "$DEPLOY_HOST" 'ls -la ~/.local/bin/orly* 2>/dev/null | grep "^l"' || log_warn "No symlinks found" + exit 0 +fi + +# Rollback to previous release +if $DO_ROLLBACK; then + log_info "Rolling back to previous release on $DEPLOY_HOST..." + + # Get the two most recent releases + RELEASES=$(ssh "$DEPLOY_HOST" 'ls -1t ~/.local/bin/releases/ | head -2') + CURRENT=$(echo "$RELEASES" | head -1) + PREVIOUS=$(echo "$RELEASES" | tail -1) + + if [ -z "$PREVIOUS" ] || [ "$CURRENT" = "$PREVIOUS" ]; then + log_error "No previous release found to rollback to" + exit 1 + fi + + log_info "Rolling back from $CURRENT to $PREVIOUS" + + # Update symlinks to previous release + ssh "$DEPLOY_HOST" "bash -c ' + RELEASES_DIR=~/.local/bin/releases + BIN_DIR=~/.local/bin + PREVIOUS=\"$PREVIOUS\" + + cd \$BIN_DIR + for link in orly-*; do + if [ -L \"\$link\" ]; then + target=\$(readlink \"\$link\") + binary=\$(basename \"\$target\") + if [ -f \"\$RELEASES_DIR/\$PREVIOUS/\$binary\" ]; then + ln -sf \"\$RELEASES_DIR/\$PREVIOUS/\$binary\" \"\$link\" + echo \"Updated \$link -> \$RELEASES_DIR/\$PREVIOUS/\$binary\" + fi + fi + done + '" + + if $DO_RESTART; then + log_info "Restarting service..." + ssh "root@$DEPLOY_HOST" "systemctl restart orly" || log_warn "Failed to restart (may need root)" + fi + + log_info "Rollback complete" + exit 0 +fi + +# Deploy new release +log_info "Deploying to $DEPLOY_HOST (release: $TIMESTAMP)" + +# Create release directory +ssh "$DEPLOY_HOST" "mkdir -p ~/.local/bin/releases/$TIMESTAMP" + +if [ -n "$LOCAL_PATH" ]; then + # Deploy from local path + log_info "Deploying from local path: $LOCAL_PATH" + + for binary in "${!BINARIES[@]}"; do + symlink_name="${BINARIES[$binary]}" + if [ -f "$LOCAL_PATH/$binary" ]; then + log_info "Uploading $binary as $symlink_name..." + scp "$LOCAL_PATH/$binary" "$DEPLOY_HOST:~/.local/bin/releases/$TIMESTAMP/$symlink_name" + ssh "$DEPLOY_HOST" "chmod +x ~/.local/bin/releases/$TIMESTAMP/$symlink_name" + fi + done + +elif [ -n "$BINARIES_URL" ]; then + # Deploy from URL + log_info "Downloading binaries from $BINARIES_URL" + + ssh "$DEPLOY_HOST" "bash -c ' + cd ~/.local/bin/releases/$TIMESTAMP + curl -sSL \"$BINARIES_URL\" | tar xz + chmod +x * + '" + +else + log_error "No binaries source specified. Use --local-path or --binaries-url" + exit 1 +fi + +# Update symlinks +log_info "Updating symlinks..." +ssh "$DEPLOY_HOST" "bash -c ' + RELEASES_DIR=~/.local/bin/releases + BIN_DIR=~/.local/bin + TIMESTAMP=\"$TIMESTAMP\" + + cd \$BIN_DIR + for binary in \$RELEASES_DIR/\$TIMESTAMP/*; do + name=\$(basename \"\$binary\") + ln -sf \"\$binary\" \"\$name\" + echo \" \$name -> \$binary\" + done +'" + +# Show deployed binaries +log_info "Deployed binaries:" +ssh "$DEPLOY_HOST" "ls -la ~/.local/bin/releases/$TIMESTAMP/" + +# Restart service if requested +if $DO_RESTART; then + log_info "Restarting orly service..." + ssh "root@$DEPLOY_HOST" "systemctl restart orly" && log_info "Service restarted" || log_warn "Failed to restart (may need root)" +fi + +# Cleanup old releases (keep last 5) +log_info "Cleaning up old releases (keeping last 5)..." +ssh "$DEPLOY_HOST" 'bash -c '\'' + RELEASES_DIR=~/.local/bin/releases + cd "$RELEASES_DIR" + ls -1t | tail -n +6 | while read old; do + echo " Removing old release: $old" + rm -rf "$old" + done +'\''' + +log_info "Deployment complete!" +log_info "To restart the service: ssh root@$DEPLOY_HOST 'systemctl restart orly'"