Browse Source

Add IPC sync services and NIP-77 negentropy synchronization (v0.55.0)

- Add four independent gRPC sync service binaries:
  - orly-sync-distributed: Serial-based peer-to-peer sync
  - orly-sync-cluster: Cluster replication with persistent state
  - orly-sync-relaygroup: Relay group config discovery (Kind 39105)
  - orly-sync-negentropy: NIP-77 efficient set reconciliation
- Implement NIP-77 negentropy protocol for relay-to-relay sync
- Add push-based event transfer for negentropy sync
- Add client-facing NIP-77 WebSocket support (NEG-OPEN/MSG/CLOSE)
- Add deployment scripts with symlink-based versioning
- Add Makefile targets for quick deployment and rollback
- Refactor existing sync code into modular service packages
- Add proto definitions for all sync services
- Integrate negentropy service into orly-launcher supervisor
- Advertise NIP-77 support in relay info document

Files modified:
- Makefile: Add sync service build targets and deployment commands
- README.md: Document NIP-77 support
- app/config/config.go: Add negentropy configuration options
- app/handle-message.go: Route NEG-* envelopes to negentropy handler
- app/handle-negentropy.go: New file for NIP-77 WebSocket handling
- app/handle-relayinfo.go: Advertise NIP-77 support
- cmd/orly-launcher/: Add negentropy service management
- cmd/orly-sync-*/: New service binaries
- main.go: Add negentropy gRPC client initialization
- pkg/proto/orlysync/: New proto definitions and generated code
- pkg/sync/*/: Refactored sync packages with gRPC services
- scripts/deploy-orly.sh: New symlink-based deployment script
- scripts/build-and-deploy.sh: New build and deploy workflow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
main v0.55.0
woikos 4 months ago
parent
commit
769f9d2daa
No known key found for this signature in database
  1. 62
      Makefile
  2. 24
      README.md
  3. 30
      app/config/config.go
  4. 22
      app/handle-message.go
  5. 347
      app/handle-negentropy.go
  6. 8
      app/handle-relayinfo.go
  7. 51
      cmd/orly-launcher/config.go
  8. 383
      cmd/orly-launcher/supervisor.go
  9. 55
      cmd/orly-sync-cluster/config.go
  10. 130
      cmd/orly-sync-cluster/main.go
  11. 56
      cmd/orly-sync-distributed/config.go
  12. 144
      cmd/orly-sync-distributed/main.go
  13. 56
      cmd/orly-sync-negentropy/config.go
  14. 140
      cmd/orly-sync-negentropy/main.go
  15. 49
      cmd/orly-sync-relaygroup/config.go
  16. 127
      cmd/orly-sync-relaygroup/main.go
  17. 371
      docs/IPC_ARCHITECTURE.md
  18. 316
      docs/IPC_SYNC_SERVICES.md
  19. 2
      go.mod
  20. 2
      go.sum
  21. 25
      main.go
  22. 808
      pkg/proto/orlysync/cluster/v1/service.pb.go
  23. 570
      pkg/proto/orlysync/cluster/v1/service_grpc.pb.go
  24. 827
      pkg/proto/orlysync/common/v1/types.pb.go
  25. 790
      pkg/proto/orlysync/distributed/v1/service.pb.go
  26. 651
      pkg/proto/orlysync/distributed/v1/service_grpc.pb.go
  27. 1323
      pkg/proto/orlysync/negentropy/v1/service.pb.go
  28. 694
      pkg/proto/orlysync/negentropy/v1/service_grpc.pb.go
  29. 574
      pkg/proto/orlysync/relaygroup/v1/service.pb.go
  30. 372
      pkg/proto/orlysync/relaygroup/v1/service_grpc.pb.go
  31. 187
      pkg/sync/cluster/grpc/client.go
  32. 250
      pkg/sync/cluster/manager.go
  33. 209
      pkg/sync/cluster/server/service.go
  34. 4
      pkg/sync/common/nip11.go
  35. 219
      pkg/sync/distributed/grpc/client.go
  36. 138
      pkg/sync/distributed/manager.go
  37. 268
      pkg/sync/distributed/server/service.go
  38. 331
      pkg/sync/negentropy/grpc/client.go
  39. 778
      pkg/sync/negentropy/manager.go
  40. 360
      pkg/sync/negentropy/server/service.go
  41. 142
      pkg/sync/relaygroup/grpc/client.go
  42. 79
      pkg/sync/relaygroup/manager.go
  43. 99
      pkg/sync/relaygroup/server/service.go
  44. 72
      pkg/sync/sync.go
  45. 2
      pkg/version/version
  46. 130
      proto/orlysync/cluster/v1/service.proto
  47. 80
      proto/orlysync/common/v1/types.proto
  48. 135
      proto/orlysync/distributed/v1/service.proto
  49. 203
      proto/orlysync/negentropy/v1/service.proto
  50. 90
      proto/orlysync/relaygroup/v1/service.proto
  51. 80
      scripts/build-and-deploy.sh
  52. 220
      scripts/deploy-orly.sh

62
Makefile

@ -2,6 +2,8 @@ @@ -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: @@ -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 @@ -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: @@ -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"

24
README.md

@ -12,8 +12,32 @@ zap me: <EFBFBD>mlekudev@getalby.com @@ -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)

30
app/config/config.go

@ -128,6 +128,15 @@ type C struct { @@ -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() ( @@ -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
}

22
app/handle-message.go

@ -92,6 +92,28 @@ func (l *Listener) HandleMessage(msg []byte, remote string) { @@ -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(

347
app/handle-negentropy.go

@ -0,0 +1,347 @@ @@ -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)
}
}
}

8
app/handle-relayinfo.go

@ -73,6 +73,10 @@ func (s *Server) HandleRelayInfo(w http.ResponseWriter, r *http.Request) { @@ -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) { @@ -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)

51
cmd/orly-launcher/config.go

@ -50,6 +50,38 @@ type Config struct { @@ -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) { @@ -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

383
cmd/orly-launcher/supervisor.go

@ -14,7 +14,7 @@ import ( @@ -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 { @@ -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) @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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() @@ -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() @@ -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
}

55
cmd/orly-sync-cluster/config.go

@ -0,0 +1,55 @@ @@ -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
}

130
cmd/orly-sync-cluster/main.go

@ -0,0 +1,130 @@ @@ -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)
}
}

56
cmd/orly-sync-distributed/config.go

@ -0,0 +1,56 @@ @@ -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
}

144
cmd/orly-sync-distributed/main.go

@ -0,0 +1,144 @@ @@ -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)
}
}

56
cmd/orly-sync-negentropy/config.go

@ -0,0 +1,56 @@ @@ -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
}

140
cmd/orly-sync-negentropy/main.go

@ -0,0 +1,140 @@ @@ -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)
}
}

49
cmd/orly-sync-relaygroup/config.go

@ -0,0 +1,49 @@ @@ -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
}

127
cmd/orly-sync-relaygroup/main.go

@ -0,0 +1,127 @@ @@ -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)
}
}

371
docs/IPC_ARCHITECTURE.md

@ -0,0 +1,371 @@ @@ -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`

316
docs/IPC_SYNC_SERVICES.md

@ -0,0 +1,316 @@ @@ -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`.

2
go.mod

@ -3,7 +3,7 @@ module next.orly.dev @@ -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

2
go.sum

@ -1,5 +1,7 @@ @@ -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=

25
main.go

@ -26,6 +26,7 @@ import ( @@ -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() { @@ -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,

808
pkg/proto/orlysync/cluster/v1/service.pb.go

@ -0,0 +1,808 @@ @@ -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
}

570
pkg/proto/orlysync/cluster/v1/service_grpc.pb.go

@ -0,0 +1,570 @@ @@ -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",
}

827
pkg/proto/orlysync/common/v1/types.pb.go

@ -0,0 +1,827 @@ @@ -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
}

790
pkg/proto/orlysync/distributed/v1/service.pb.go

@ -0,0 +1,790 @@ @@ -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
}

651
pkg/proto/orlysync/distributed/v1/service_grpc.pb.go

@ -0,0 +1,651 @@ @@ -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",
}

1323
pkg/proto/orlysync/negentropy/v1/service.pb.go

File diff suppressed because it is too large Load Diff

694
pkg/proto/orlysync/negentropy/v1/service_grpc.pb.go

@ -0,0 +1,694 @@ @@ -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",
}

574
pkg/proto/orlysync/relaygroup/v1/service.pb.go

@ -0,0 +1,574 @@ @@ -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
}

372
pkg/proto/orlysync/relaygroup/v1/service_grpc.pb.go

@ -0,0 +1,372 @@ @@ -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",
}

187
pkg/sync/cluster/grpc/client.go

@ -0,0 +1,187 @@ @@ -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
}

250
pkg/sync/cluster.go → pkg/sync/cluster/manager.go

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
package sync
// Package cluster provides cluster replication with persistent state
package cluster
import (
"context"
@ -6,37 +7,45 @@ import ( @@ -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 {
// 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]*ClusterMember // keyed by relay URL
membersMux sync.RWMutex
members map[string]*Member // keyed by relay URL
membersMux gosync.RWMutex
pollTicker *time.Ticker
pollDone chan struct{}
httpClient *http.Client
propagatePrivilegedEvents bool
publisher interface{ Deliver(*event.E) }
nip11Cache *NIP11Cache
publisher EventPublisher
nip11Cache *common.NIP11Cache
}
type ClusterMember struct {
// Member represents a cluster member
type Member struct {
HTTPURL string
WebSocketURL string
LastSerial uint64
@ -45,26 +54,51 @@ type ClusterMember struct { @@ -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, @@ -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() { @@ -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() { @@ -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() { @@ -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() { @@ -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 {
@ -189,7 +291,7 @@ func (cm *ClusterManager) pollMember(member *ClusterMember) { @@ -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 @@ -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 @@ -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 @@ -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) { @@ -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) { @@ -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 { @@ -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 { @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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) { @@ -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) ([] @@ -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) ([] @@ -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) ([] @@ -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) ([] @@ -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) ([] @@ -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) ([] @@ -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 ( @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {

209
pkg/sync/cluster/server/service.go

@ -0,0 +1,209 @@ @@ -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
}

4
pkg/sync/nip11.go → pkg/sync/common/nip11.go

@ -1,5 +1,5 @@ @@ -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"

219
pkg/sync/distributed/grpc/client.go

@ -0,0 +1,219 @@ @@ -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
}

138
pkg/sync/manager.go → pkg/sync/distributed/manager.go

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
package sync
// Package distributed provides serial-based peer-to-peer synchronization
package distributed
import (
"bytes"
@ -7,17 +8,28 @@ import ( @@ -7,17 +8,28 @@ 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
@ -29,10 +41,9 @@ type Manager struct { @@ -29,10 +41,9 @@ type Manager struct {
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
nip11Cache *common.NIP11Cache
policyManager PolicyChecker
mutex gosync.RWMutex
}
// CurrentRequest represents a request for the current serial number
@ -48,7 +59,6 @@ type CurrentResponse struct { @@ -48,7 +59,6 @@ type CurrentResponse struct {
Serial uint64 `json:"serial"`
}
// EventIDsRequest represents a request for event IDs with serials
type EventIDsRequest struct {
NodeID string `json:"node_id"`
@ -62,22 +72,42 @@ type EventIDsResponse struct { @@ -62,22 +72,42 @@ 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,
nodeID: cfg.NodeID,
relayURL: cfg.RelayURL,
peers: cfg.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
nip11Cache: common.NewNIP11Cache(cfg.NIP11CacheTTL),
policyManager: policyManager,
}
@ -97,9 +127,9 @@ func NewManager(ctx context.Context, db *database.D, nodeID, relayURL string, pe @@ -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 { @@ -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() { @@ -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) { @@ -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) { @@ -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) { @@ -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) @@ -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

268
pkg/sync/distributed/server/service.go

@ -0,0 +1,268 @@ @@ -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, &currentReq); 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
}

331
pkg/sync/negentropy/grpc/client.go

@ -0,0 +1,331 @@ @@ -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
}

778
pkg/sync/negentropy/manager.go

@ -0,0 +1,778 @@ @@ -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], &notice)
}
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

360
pkg/sync/negentropy/server/service.go

@ -0,0 +1,360 @@ @@ -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
}

142
pkg/sync/relaygroup/grpc/client.go

@ -0,0 +1,142 @@ @@ -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
}

79
pkg/sync/relaygroup.go → pkg/sync/relaygroup/manager.go

@ -1,44 +1,56 @@ @@ -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 {
// 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 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 @@ -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 @@ -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 @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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 @@ -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)
}
}

99
pkg/sync/relaygroup/server/service.go

@ -0,0 +1,99 @@ @@ -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
}

72
pkg/sync/sync.go

@ -0,0 +1,72 @@ @@ -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)
}

2
pkg/version/version

@ -1 +1 @@ @@ -1 +1 @@
v0.54.0
v0.55.0

130
proto/orlysync/cluster/v1/service.proto

@ -0,0 +1,130 @@ @@ -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;
}

80
proto/orlysync/common/v1/types.proto

@ -0,0 +1,80 @@ @@ -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<string, string> 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<string, string> 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<string, TagSet> 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;
}

135
proto/orlysync/distributed/v1/service.proto

@ -0,0 +1,135 @@ @@ -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<string, uint64> 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;
}

203
proto/orlysync/negentropy/v1/service.proto

@ -0,0 +1,203 @@ @@ -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
}

90
proto/orlysync/relaygroup/v1/service.proto

@ -0,0 +1,90 @@ @@ -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;
}

80
scripts/build-and-deploy.sh

@ -0,0 +1,80 @@ @@ -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!"

220
scripts/deploy-orly.sh

@ -0,0 +1,220 @@ @@ -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'"
Loading…
Cancel
Save