Compare commits

...

2 Commits

13 changed files with 1630 additions and 964 deletions

View File

@@ -1,223 +0,0 @@
# NOSTR Core Library - Automatic Versioning System
## Overview
The NOSTR Core Library now features an automatic version increment system that automatically increments the patch version (e.g., v0.2.0 → v0.2.1) with each build. This ensures consistent versioning and traceability across builds.
## How It Works
### Version Format
The library uses semantic versioning with the format: `vMAJOR.MINOR.PATCH`
- **MAJOR**: Incremented for breaking changes (manual)
- **MINOR**: Incremented for new features (manual)
- **PATCH**: Incremented automatically with each build
### Automatic Increment Process
1. **Version Detection**: The build system scans all git tags matching `v*.*.*` pattern
2. **Highest Version**: Uses `sort -V` to find the numerically highest version (not chronologically latest)
3. **Patch Increment**: Increments the patch number by 1
4. **Git Tag Creation**: Creates a new git tag for the incremented version
5. **File Generation**: Generates `nostr_core/version.h` and `nostr_core/version.c` with build metadata
### Generated Files
The system automatically generates two files during each build:
#### `nostr_core/version.h`
```c
#define VERSION_MAJOR 0
#define VERSION_MINOR 2
#define VERSION_PATCH 1
#define VERSION_STRING "0.2.1"
#define VERSION_TAG "v0.2.1"
#define BUILD_DATE "2025-08-09"
#define BUILD_TIME "10:42:45"
#define BUILD_TIMESTAMP "2025-08-09 10:42:45"
#define GIT_HASH "ca6b475"
#define GIT_BRANCH "master"
// API functions
const char* nostr_core_get_version(void);
const char* nostr_core_get_version_full(void);
const char* nostr_core_get_build_info(void);
```
#### `nostr_core/version.c`
Contains the implementation of the version API functions.
## Usage
### Building with Auto-Versioning
All major build targets automatically increment the version:
```bash
# Build static library (increments version)
./build.sh lib
# Build shared library (increments version)
./build.sh shared
# Build all libraries (increments version)
./build.sh all
# Build examples (increments version)
./build.sh examples
# Install to system (increments version)
./build.sh install
```
### Non-Versioned Builds
Some targets do not increment versions:
```bash
# Clean build artifacts (no version increment)
./build.sh clean
# Run tests (no version increment)
./build.sh test
```
### Using Version Information in Code
```c
#include "version.h"
// Get version string
printf("Version: %s\n", nostr_core_get_version());
// Get full version with timestamp and commit
printf("Full: %s\n", nostr_core_get_version_full());
// Get detailed build information
printf("Build: %s\n", nostr_core_get_build_info());
// Use version macros
#if VERSION_MAJOR >= 1
// Use new API
#else
// Use legacy API
#endif
```
### Testing Version System
A version test example is provided:
```bash
# Build and run version test
gcc -I. -Inostr_core examples/version_test.c -o examples/version_test ./libnostr_core.a ./secp256k1/.libs/libsecp256k1.a -lm
./examples/version_test
```
## Version History Tracking
### View All Versions
```bash
# List all version tags
git tag --list
# View version history
git log --oneline --decorate --graph
```
### Current Version
```bash
# Check current version
cat VERSION
# Or check the latest git tag
git describe --tags --abbrev=0
```
## Manual Version Management
### Major/Minor Version Bumps
For major or minor version changes, manually create the appropriate tag:
```bash
# For a minor version bump (new features)
git tag v0.3.0
# For a major version bump (breaking changes)
git tag v1.0.0
```
The next build will automatically increment from the new base version.
### Resetting Version
To reset or fix version numbering:
```bash
# Delete incorrect tags (if needed)
git tag -d v0.2.1
git push origin --delete v0.2.1
# Create correct base version
git tag v0.2.0
# Next build will create v0.2.1
```
## Integration Notes
### Makefile Integration
- The `version.c` file is automatically included in `LIB_SOURCES`
- Version files are compiled and linked with the library
- Clean targets remove generated version object files
### Git Integration
- Version files (`version.h`, `version.c`) are excluded from git via `.gitignore`
- Only version tags and the `VERSION` file are tracked
- Build system works in any git repository with version tags
### Build System Integration
- Version increment is integrated directly into `build.sh`
- No separate scripts or external dependencies required
- Self-contained and portable across systems
## Example Output
When building, you'll see output like:
```
[INFO] Incrementing version...
[INFO] Current version: v0.2.0
[INFO] New version: v0.2.1
[SUCCESS] Created new version tag: v0.2.1
[SUCCESS] Generated version.h and version.c
[SUCCESS] Updated VERSION file to 0.2.1
```
## Troubleshooting
### Version Not Incrementing
- Ensure you're in a git repository
- Check that git tags exist with `git tag --list`
- Verify tag format matches `v*.*.*` pattern
### Tag Already Exists
If a tag already exists, the build will continue with the existing version:
```
[WARNING] Tag v0.2.1 already exists - using existing version
```
### Missing Git Information
If git is not available, version files will show "unknown" for git hash and branch.
## Benefits
1. **Automatic Traceability**: Every build has a unique version
2. **Build Metadata**: Includes timestamp, git commit, and branch information
3. **API Integration**: Version information accessible via C API
4. **Zero Maintenance**: No manual version file editing required
5. **Git Integration**: Automatic git tag creation for version history

View File

@@ -302,20 +302,32 @@ publish_result_t* synchronous_publish_event_with_progress(const char** relay_url
### Relay Pools (Asynchronous)
```c
// Create and manage relay pool
nostr_relay_pool_t* nostr_relay_pool_create(void);
// Create and manage relay pool with reconnection
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
// Subscribe to events
// Subscribe to events (with auto-reconnection)
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool, const char** relay_urls, int relay_count, cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data), void* user_data);
void (*on_eose)(void* user_data), void* user_data, int close_on_eose);
// Run event loop
// Run event loop (handles reconnection automatically)
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
// Reconnection configuration
typedef struct {
int enable_auto_reconnect; // Enable automatic reconnection
int max_reconnect_attempts; // Maximum retry attempts
int initial_reconnect_delay_ms; // Initial delay between attempts
int max_reconnect_delay_ms; // Maximum delay cap
int reconnect_backoff_multiplier; // Exponential backoff factor
int ping_interval_seconds; // Health check ping interval
int pong_timeout_seconds; // Pong response timeout
} nostr_pool_reconnect_config_t;
```
### NIP-05 Identifier Verification
@@ -370,6 +382,9 @@ The library includes extensive tests:
# Individual test categories
cd tests && make test
# Interactive relay pool testing
cd tests && ./pool_test
```
**Test Categories:**
@@ -383,6 +398,7 @@ cd tests && make test
- **Relay Communication**: `relay_pool_test`, `sync_test`
- **HTTP/WebSocket**: `http_test`, `wss_test`
- **Proof of Work**: `test_pow_loop`
- **Interactive Pool Testing**: `pool_test` (menu-driven interface with reconnection testing)
## 🏗️ Integration

View File

@@ -1 +1 @@
0.2.1
0.4.3

View File

@@ -491,6 +491,7 @@ SOURCES="$SOURCES cjson/cJSON.c"
SOURCES="$SOURCES nostr_core/utils.c"
SOURCES="$SOURCES nostr_core/nostr_common.c"
SOURCES="$SOURCES nostr_core/core_relays.c"
SOURCES="$SOURCES nostr_core/core_relay_pool.c"
SOURCES="$SOURCES nostr_websocket/nostr_websocket_openssl.c"
SOURCES="$SOURCES nostr_core/request_validator.c"

View File

@@ -1,394 +0,0 @@
#!/bin/bash
# NOSTR Core Library Build Script
# Provides convenient build targets for the standalone library
# Automatically increments patch version with each build
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE}[INFO]${NC} $1"
}
print_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Function to automatically increment version
increment_version() {
print_status "Incrementing version..."
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_warning "Not in a git repository - skipping version increment"
return 0
fi
# Get the highest version tag (not necessarily the most recent chronologically)
LATEST_TAG=$(git tag -l 'v*.*.*' | sort -V | tail -n 1 || echo "v0.1.0")
if [[ -z "$LATEST_TAG" ]]; then
LATEST_TAG="v0.1.0"
fi
# Extract version components (remove 'v' prefix if present)
VERSION=${LATEST_TAG#v}
# Parse major.minor.patch
if [[ $VERSION =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
MAJOR=${BASH_REMATCH[1]}
MINOR=${BASH_REMATCH[2]}
PATCH=${BASH_REMATCH[3]}
else
print_error "Invalid version format in tag: $LATEST_TAG"
print_error "Expected format: v0.1.0"
return 1
fi
# Increment patch version
NEW_PATCH=$((PATCH + 1))
NEW_VERSION="v${MAJOR}.${MINOR}.${NEW_PATCH}"
print_status "Current version: $LATEST_TAG"
print_status "New version: $NEW_VERSION"
# Create new git tag
if git tag "$NEW_VERSION" 2>/dev/null; then
print_success "Created new version tag: $NEW_VERSION"
else
print_warning "Tag $NEW_VERSION already exists - using existing version"
NEW_VERSION=$LATEST_TAG
fi
# Update VERSION file for compatibility
echo "${NEW_VERSION#v}" > VERSION
print_success "Updated VERSION file to ${NEW_VERSION#v}"
}
# Function to perform git operations after successful build
perform_git_operations() {
local commit_message="$1"
if [[ -z "$commit_message" ]]; then
return 0 # No commit message provided, skip git operations
fi
print_status "Performing git operations..."
# Check if we're in a git repository
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_warning "Not in a git repository - skipping git operations"
return 0
fi
# Check if there are changes to commit
if git diff --quiet && git diff --cached --quiet; then
print_warning "No changes to commit"
return 0
fi
# Add all changes
print_status "Adding changes to git..."
if ! git add .; then
print_error "Failed to add changes to git"
return 1
fi
# Commit changes
print_status "Committing changes with message: '$commit_message'"
if ! git commit -m "$commit_message"; then
print_error "Failed to commit changes"
return 1
fi
# Push changes
print_status "Pushing changes to remote repository..."
if ! git push; then
print_error "Failed to push changes to remote repository"
print_warning "Changes have been committed locally but not pushed"
return 1
fi
print_success "Git operations completed successfully!"
return 0
}
# Function to show usage
show_usage() {
echo "NOSTR Core Library Build Script"
echo "==============================="
echo ""
echo "Usage: $0 [target] [-m \"commit message\"]"
echo ""
echo "Available targets:"
echo " clean - Clean all build artifacts"
echo " lib - Build static libraries for both x64 and ARM64 (default)"
echo " x64 - Build x64 static library only"
echo " arm64 - Build ARM64 static library only"
echo " all - Build both architectures and examples"
echo " examples - Build example programs"
echo " test - Run tests"
echo " install - Install library to system"
echo " uninstall - Remove library from system"
echo " help - Show this help message"
echo ""
echo "Options:"
echo " -m \"message\" - Git commit message (triggers automatic git add, commit, push after successful build)"
echo ""
echo "Examples:"
echo " $0 lib -m \"Add new proof-of-work parameters\""
echo " $0 x64 -m \"Fix OpenSSL minimal build configuration\""
echo " $0 lib # Build without git operations"
echo ""
echo "Library outputs (both self-contained with secp256k1):"
echo " libnostr_core.a - x86_64 static library"
echo " libnostr_core_arm64.a - ARM64 static library"
echo " examples/* - Example programs"
echo ""
echo "Both libraries include secp256k1 objects internally."
echo "Users only need to link with the library + -lm."
}
# Parse command line arguments
TARGET=""
COMMIT_MESSAGE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
-m)
COMMIT_MESSAGE="$2"
shift 2
;;
-*)
print_error "Unknown option: $1"
show_usage
exit 1
;;
*)
if [[ -z "$TARGET" ]]; then
TARGET="$1"
else
print_error "Multiple targets specified: $TARGET and $1"
show_usage
exit 1
fi
shift
;;
esac
done
# Set default target if none specified
TARGET=${TARGET:-lib}
case "$TARGET" in
clean)
print_status "Cleaning build artifacts..."
make clean
print_success "Clean completed"
;;
lib|library)
increment_version
print_status "Building both x64 and ARM64 static libraries..."
make clean
make
# Check both libraries were built
SUCCESS=0
if [ -f "libnostr_core.a" ]; then
SIZE_X64=$(stat -c%s "libnostr_core.a")
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
SUCCESS=$((SUCCESS + 1))
else
print_error "Failed to build x64 static library"
fi
if [ -f "libnostr_core_arm64.a" ]; then
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
SUCCESS=$((SUCCESS + 1))
else
print_error "Failed to build ARM64 static library"
fi
if [ $SUCCESS -eq 2 ]; then
print_success "Both architectures built successfully!"
ls -la libnostr_core*.a
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build all libraries"
exit 1
fi
;;
x64|x64-only)
increment_version
print_status "Building x64 static library only..."
make clean
make x64
if [ -f "libnostr_core.a" ]; then
SIZE=$(stat -c%s "libnostr_core.a")
print_success "x64 static library built successfully (${SIZE} bytes)"
ls -la libnostr_core.a
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build x64 static library"
exit 1
fi
;;
arm64|arm64-only)
increment_version
print_status "Building ARM64 static library only..."
make clean
make arm64
if [ -f "libnostr_core_arm64.a" ]; then
SIZE=$(stat -c%s "libnostr_core_arm64.a")
print_success "ARM64 static library built successfully (${SIZE} bytes)"
ls -la libnostr_core_arm64.a
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build ARM64 static library"
exit 1
fi
;;
shared)
increment_version
print_status "Building shared library..."
make clean
make libnostr_core.so
if [ -f "libnostr_core.so" ]; then
SIZE=$(stat -c%s "libnostr_core.so")
print_success "Shared library built successfully (${SIZE} bytes)"
ls -la libnostr_core.so
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build shared library"
exit 1
fi
;;
all)
increment_version
print_status "Building all libraries and examples..."
make clean
make all
# Check both libraries and examples were built
SUCCESS=0
if [ -f "libnostr_core.a" ]; then
SIZE_X64=$(stat -c%s "libnostr_core.a")
print_success "x64 static library built successfully (${SIZE_X64} bytes)"
SUCCESS=$((SUCCESS + 1))
else
print_error "Failed to build x64 static library"
fi
if [ -f "libnostr_core_arm64.a" ]; then
SIZE_ARM64=$(stat -c%s "libnostr_core_arm64.a")
print_success "ARM64 static library built successfully (${SIZE_ARM64} bytes)"
SUCCESS=$((SUCCESS + 1))
else
print_error "Failed to build ARM64 static library"
fi
if [ $SUCCESS -eq 2 ]; then
print_success "All libraries and examples built successfully!"
ls -la libnostr_core*.a
ls -la examples/
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build all components"
exit 1
fi
;;
examples)
increment_version
print_status "Building both libraries and examples..."
make clean
make
make examples
# Verify libraries were built
if [ -f "libnostr_core.a" ] && [ -f "libnostr_core_arm64.a" ]; then
print_success "Both libraries and examples built successfully"
ls -la libnostr_core*.a
ls -la examples/
perform_git_operations "$COMMIT_MESSAGE"
else
print_error "Failed to build libraries for examples"
exit 1
fi
;;
test)
print_status "Running tests..."
make clean
make
if make test-crypto 2>/dev/null; then
print_success "All tests passed"
else
print_warning "Running simple test instead..."
make test
print_success "Basic test completed"
fi
;;
tests)
print_status "Running tests..."
make clean
make
if make test-crypto 2>/dev/null; then
print_success "All tests passed"
else
print_warning "Running simple test instead..."
make test
print_success "Basic test completed"
fi
;;
install)
increment_version
print_status "Installing library to system..."
make clean
make all
sudo make install
print_success "Library installed to /usr/local"
perform_git_operations "$COMMIT_MESSAGE"
;;
uninstall)
print_status "Uninstalling library from system..."
sudo make uninstall
print_success "Library uninstalled"
;;
help|--help|-h)
show_usage
;;
*)
print_error "Unknown target: $TARGET"
echo ""
show_usage
exit 1
;;
esac

150
increment_and_push.sh Executable file
View File

@@ -0,0 +1,150 @@
#!/bin/bash
# increment_and_push.sh - Version increment and git automation script
# Usage: ./increment_and_push.sh "meaningful git comment"
set -e # Exit on error
# Color constants
RED='\033[31m'
GREEN='\033[32m'
YELLOW='\033[33m'
BLUE='\033[34m'
BOLD='\033[1m'
RESET='\033[0m'
# Function to print output with colors
print_info() {
if [ "$USE_COLORS" = true ]; then
echo -e "${BLUE}[INFO]${RESET} $1"
else
echo "[INFO] $1"
fi
}
print_success() {
if [ "$USE_COLORS" = true ]; then
echo -e "${GREEN}${BOLD}[SUCCESS]${RESET} $1"
else
echo "[SUCCESS] $1"
fi
}
print_warning() {
if [ "$USE_COLORS" = true ]; then
echo -e "${YELLOW}[WARNING]${RESET} $1"
else
echo "[WARNING] $1"
fi
}
print_error() {
if [ "$USE_COLORS" = true ]; then
echo -e "${RED}${BOLD}[ERROR]${RESET} $1"
else
echo "[ERROR] $1"
fi
}
# Check if we're in the correct directory
CURRENT_DIR=$(basename "$(pwd)")
if [ "$CURRENT_DIR" != "nostr_core_lib" ]; then
print_error "Script must be run from the nostr_core_lib directory"
echo ""
echo "Current directory: $CURRENT_DIR"
echo "Expected directory: nostr_core_lib"
echo ""
echo "Please change to the nostr_core_lib directory first."
echo ""
exit 1
fi
# Check if git repository exists
if ! git rev-parse --git-dir > /dev/null 2>&1; then
print_error "Not a git repository. Please initialize git first."
exit 1
fi
# Check if we have a commit message
if [ $# -eq 0 ]; then
print_error "Usage: $0 \"meaningful git comment\""
echo ""
echo "Example: $0 \"Add enhanced subscription functionality\""
echo ""
exit 1
fi
COMMIT_MESSAGE="$1"
# Check if nostr_core.h exists
if [ ! -f "nostr_core/nostr_core.h" ]; then
print_error "nostr_core/nostr_core.h not found"
exit 1
fi
print_info "Starting version increment and push process..."
# Extract current version from nostr_core.h
CURRENT_VERSION=$(grep '#define VERSION ' nostr_core/nostr_core.h | cut -d'"' -f2)
if [ -z "$CURRENT_VERSION" ]; then
print_error "Could not find VERSION define in nostr_core.h"
exit 1
fi
# Extract version components
VERSION_MAJOR=$(grep '#define VERSION_MAJOR ' nostr_core/nostr_core.h | awk '{print $3}')
VERSION_MINOR=$(grep '#define VERSION_MINOR ' nostr_core/nostr_core.h | awk '{print $3}')
VERSION_PATCH=$(grep '#define VERSION_PATCH ' nostr_core/nostr_core.h | awk '{print $3}')
if [ -z "$VERSION_MAJOR" ] || [ -z "$VERSION_MINOR" ] || [ -z "$VERSION_PATCH" ]; then
print_error "Could not extract version components from nostr_core.h"
exit 1
fi
print_info "Current version: $CURRENT_VERSION (Major: $VERSION_MAJOR, Minor: $VERSION_MINOR, Patch: $VERSION_PATCH)"
# Increment patch version
NEW_PATCH=$((VERSION_PATCH + 1))
NEW_VERSION="v$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH"
print_info "New version will be: $NEW_VERSION"
# Update version in nostr_core.h
sed -i "s/#define VERSION .*/#define VERSION \"$NEW_VERSION\"/" nostr_core/nostr_core.h
sed -i "s/#define VERSION_PATCH .*/#define VERSION_PATCH $NEW_PATCH/" nostr_core/nostr_core.h
print_success "Updated version in nostr_core.h"
# Check if VERSION file exists and update it
if [ -f "VERSION" ]; then
echo "$VERSION_MAJOR.$VERSION_MINOR.$NEW_PATCH" > VERSION
print_success "Updated VERSION file"
fi
# Check git status
if ! git diff --quiet; then
print_info "Adding changes to git..."
git add .
print_info "Committing changes..."
git commit -m "$COMMIT_MESSAGE"
print_success "Changes committed"
else
print_warning "No changes to commit"
fi
# Create and push git tag
print_info "Creating git tag: $NEW_VERSION"
git tag "$NEW_VERSION"
print_info "Pushing commits and tags..."
git push origin main
git push origin "$NEW_VERSION"
print_success "Version $NEW_VERSION successfully released!"
print_info "Git commit: $COMMIT_MESSAGE"
print_info "Tag: $NEW_VERSION"
echo ""
echo "🎉 Release complete! Version $NEW_VERSION is now live."

View File

@@ -65,22 +65,26 @@ typedef struct relay_connection {
char* url;
nostr_ws_client_t* ws_client;
nostr_pool_relay_status_t status;
// Connection management
time_t last_ping;
time_t connect_time;
nostr_relay_pool_t* pool; // Back reference to pool for config access
// Reconnection management
int reconnect_attempts;
// Ping management for latency measurement
time_t last_reconnect_attempt;
time_t next_reconnect_time;
// Connection health monitoring (ping/pong)
time_t last_ping_sent;
time_t next_ping_time; // last_ping_sent + NOSTR_POOL_PING_INTERVAL
time_t last_pong_received;
int ping_pending;
double pending_ping_start_ms; // High-resolution timestamp for ping measurement
int ping_pending; // Flag to track if ping response is expected
// Multi-subscription latency tracking (REQ->first EVENT/EOSE)
subscription_timing_t pending_subscriptions[NOSTR_POOL_MAX_PENDING_SUBSCRIPTIONS];
int pending_subscription_count;
// Statistics
nostr_relay_stats_t stats;
} relay_connection_t;
@@ -88,34 +92,53 @@ typedef struct relay_connection {
struct nostr_pool_subscription {
char subscription_id[NOSTR_POOL_SUBSCRIPTION_ID_SIZE];
cJSON* filter;
// Relay-specific subscription tracking
char** relay_urls;
int relay_count;
int* eose_received; // Track EOSE from each relay
// Callbacks
void (*on_event)(cJSON* event, const char* relay_url, void* user_data);
void (*on_eose)(void* user_data);
void (*on_eose)(cJSON** events, int event_count, void* user_data);
void* user_data;
int closed;
int close_on_eose; // Auto-close subscription when all relays send EOSE
nostr_relay_pool_t* pool; // Back reference to pool
// New subscription control parameters
int enable_deduplication; // Per-subscription deduplication control
nostr_pool_eose_result_mode_t result_mode; // EOSE result selection mode
int relay_timeout_seconds; // Timeout for individual relay operations
int eose_timeout_seconds; // Timeout for waiting for EOSE completion
time_t subscription_start_time; // When subscription was created
// Event collection for EOSE result modes
cJSON** collected_events;
int collected_event_count;
int collected_events_capacity;
// Per-relay timeout tracking
time_t* relay_last_activity; // Last activity time per relay
};
struct nostr_relay_pool {
relay_connection_t* relays[NOSTR_POOL_MAX_RELAYS];
int relay_count;
// Reconnection configuration
nostr_pool_reconnect_config_t reconnect_config;
// Event deduplication - simple hash table with linear probing
char seen_event_ids[NOSTR_POOL_MAX_SEEN_EVENTS][65]; // 64 hex chars + null terminator
int seen_count;
int seen_next_index;
// Active subscriptions
nostr_pool_subscription_t* subscriptions[NOSTR_POOL_MAX_SUBSCRIPTIONS];
int subscription_count;
// Pool-wide settings
int default_timeout_ms;
};
@@ -163,33 +186,211 @@ static int add_subscription_timing(relay_connection_t* relay, const char* subscr
// Helper function to find and remove subscription timing
static double remove_subscription_timing(relay_connection_t* relay, const char* subscription_id) {
if (!relay || !subscription_id) return -1.0;
for (int i = 0; i < relay->pending_subscription_count; i++) {
if (relay->pending_subscriptions[i].active &&
if (relay->pending_subscriptions[i].active &&
strcmp(relay->pending_subscriptions[i].subscription_id, subscription_id) == 0) {
// Calculate latency
double current_time_ms = get_current_time_ms();
double latency_ms = current_time_ms - relay->pending_subscriptions[i].start_time_ms;
// Mark as inactive and remove by shifting remaining entries
relay->pending_subscriptions[i].active = 0;
for (int j = i; j < relay->pending_subscription_count - 1; j++) {
relay->pending_subscriptions[j] = relay->pending_subscriptions[j + 1];
}
relay->pending_subscription_count--;
return latency_ms;
}
}
return -1.0; // Not found
}
// Helper function to check if event ID has been seen
// Helper function to ensure relay connection
static int ensure_relay_connection(relay_connection_t* relay) {
if (!relay) {
return -1;
}
if (relay->ws_client && nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED) {
relay->status = NOSTR_POOL_RELAY_CONNECTED;
return 0; // Already connected
}
// Close existing connection if any
if (relay->ws_client) {
nostr_ws_close(relay->ws_client);
relay->ws_client = NULL;
}
// Attempt connection
relay->status = NOSTR_POOL_RELAY_CONNECTING;
relay->stats.connection_attempts++;
relay->ws_client = nostr_ws_connect(relay->url);
if (!relay->ws_client) {
relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++;
relay->stats.connection_failures++;
return -1;
}
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
if (state == NOSTR_WS_CONNECTED) {
relay->status = NOSTR_POOL_RELAY_CONNECTED;
relay->connect_time = time(NULL);
relay->reconnect_attempts = 0;
// Initialize ping/pong monitoring on new connection
relay->last_ping_sent = time(NULL);
relay->last_pong_received = time(NULL);
relay->ping_pending = 0;
return 0;
} else {
relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++;
relay->stats.connection_failures++;
// Close the failed connection
nostr_ws_close(relay->ws_client);
relay->ws_client = NULL;
return -1;
}
}
// Reconnection helper functions
static int should_attempt_reconnect(relay_connection_t* relay) {
if (!relay->pool->reconnect_config.enable_auto_reconnect) return 0;
if (relay->status == NOSTR_POOL_RELAY_CONNECTED) return 0;
if (relay->reconnect_attempts >= relay->pool->reconnect_config.max_reconnect_attempts) return 0;
time_t now = time(NULL);
return (now >= relay->next_reconnect_time);
}
static int calculate_reconnect_delay(relay_connection_t* relay) {
int delay = relay->pool->reconnect_config.initial_reconnect_delay_ms;
// Apply exponential backoff
for (int i = 1; i < relay->reconnect_attempts; i++) {
delay *= relay->pool->reconnect_config.reconnect_backoff_multiplier;
if (delay > relay->pool->reconnect_config.max_reconnect_delay_ms) {
delay = relay->pool->reconnect_config.max_reconnect_delay_ms;
break;
}
}
return delay;
}
static void restore_subscriptions_on_reconnect(relay_connection_t* relay) {
// Find subscriptions that should be active on this relay
for (int i = 0; i < relay->pool->subscription_count; i++) {
nostr_pool_subscription_t* sub = relay->pool->subscriptions[i];
if (!sub->closed) {
// Check if this subscription should be on this relay
for (int j = 0; j < sub->relay_count; j++) {
if (strcmp(sub->relay_urls[j], relay->url) == 0) {
// Re-send the subscription
if (nostr_relay_send_req(relay->ws_client, sub->subscription_id, sub->filter) >= 0) {
// Add timing for latency measurement
add_subscription_timing(relay, sub->subscription_id);
}
break;
}
}
}
}
}
static void attempt_reconnect(relay_connection_t* relay) {
relay->status = NOSTR_POOL_RELAY_CONNECTING;
relay->last_reconnect_attempt = time(NULL);
relay->reconnect_attempts++;
if (ensure_relay_connection(relay) == 0) {
// Success! Reset reconnection state
relay->reconnect_attempts = 0;
relay->next_reconnect_time = 0;
// Restore subscriptions on reconnect
restore_subscriptions_on_reconnect(relay);
} else {
// Failed - schedule next attempt with backoff
int delay_ms = calculate_reconnect_delay(relay);
relay->next_reconnect_time = time(NULL) + (delay_ms / 1000);
}
}
// Connection health monitoring (ping/pong)
static void check_connection_health(relay_connection_t* relay) {
time_t now = time(NULL);
// Send ping if interval elapsed and ping is enabled
if (relay->pool->reconnect_config.ping_interval_seconds > 0 &&
now - relay->last_ping_sent >= relay->pool->reconnect_config.ping_interval_seconds &&
!relay->ping_pending) {
if (nostr_ws_ping(relay->ws_client) == 0) {
relay->last_ping_sent = now;
relay->ping_pending = 1;
// Store high-resolution start time for latency measurement
relay->pending_ping_start_ms = get_current_time_ms();
}
}
// Check for pong timeout
if (relay->ping_pending &&
now - relay->last_ping_sent > relay->pool->reconnect_config.pong_timeout_seconds) {
// No pong received - connection is dead
relay->status = NOSTR_POOL_RELAY_DISCONNECTED;
relay->ping_pending = 0;
}
}
static void handle_pong_response(relay_connection_t* relay) {
relay->last_pong_received = time(NULL);
if (relay->ping_pending) {
// Calculate ping latency
double current_time_ms = get_current_time_ms();
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
// Update ping statistics
if (relay->stats.ping_samples == 0) {
relay->stats.ping_latency_avg = ping_latency;
relay->stats.ping_latency_min = ping_latency;
relay->stats.ping_latency_max = ping_latency;
} else {
relay->stats.ping_latency_avg =
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
(relay->stats.ping_samples + 1);
if (ping_latency < relay->stats.ping_latency_min) {
relay->stats.ping_latency_min = ping_latency;
}
if (ping_latency > relay->stats.ping_latency_max) {
relay->stats.ping_latency_max = ping_latency;
}
}
relay->stats.ping_latency_current = ping_latency;
relay->stats.ping_samples++;
relay->ping_pending = 0;
}
}
static int is_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
if (!pool || !event_id) return 0;
for (int i = 0; i < pool->seen_count; i++) {
if (strcmp(pool->seen_event_ids[i], event_id) == 0) {
return 1;
@@ -201,28 +402,49 @@ static int is_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
// Helper function to mark event as seen
static void mark_event_seen(nostr_relay_pool_t* pool, const char* event_id) {
if (!pool || !event_id) return;
// Don't add duplicates
if (is_event_seen(pool, event_id)) return;
// Use circular buffer for seen events
strncpy(pool->seen_event_ids[pool->seen_next_index], event_id, 64);
pool->seen_event_ids[pool->seen_next_index][64] = '\0';
pool->seen_next_index = (pool->seen_next_index + 1) % NOSTR_POOL_MAX_SEEN_EVENTS;
if (pool->seen_count < NOSTR_POOL_MAX_SEEN_EVENTS) {
pool->seen_count++;
}
}
// Default configuration helper
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void) {
static nostr_pool_reconnect_config_t config = {
.enable_auto_reconnect = 1,
.max_reconnect_attempts = 10,
.initial_reconnect_delay_ms = 1000,
.max_reconnect_delay_ms = 30000,
.reconnect_backoff_multiplier = 2,
.ping_interval_seconds = 30,
.pong_timeout_seconds = 10
};
return &config;
}
// Pool management functions
nostr_relay_pool_t* nostr_relay_pool_create(void) {
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config) {
if (!config) {
config = nostr_pool_reconnect_config_default();
}
nostr_relay_pool_t* pool = calloc(1, sizeof(nostr_relay_pool_t));
if (!pool) {
return NULL;
}
// Copy configuration
pool->reconnect_config = *config;
pool->default_timeout_ms = NOSTR_POOL_DEFAULT_TIMEOUT;
return pool;
}
@@ -250,15 +472,19 @@ int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url)
relay->status = NOSTR_POOL_RELAY_DISCONNECTED;
relay->ws_client = NULL;
relay->last_ping = 0;
relay->connect_time = 0;
relay->pool = pool; // Set back reference
// Initialize reconnection state
relay->reconnect_attempts = 0;
// Initialize ping management
relay->last_reconnect_attempt = 0;
relay->next_reconnect_time = 0;
// Initialize ping/pong monitoring
relay->last_ping_sent = 0;
relay->next_ping_time = 0;
relay->pending_ping_start_ms = 0.0;
relay->last_pong_received = 0;
relay->ping_pending = 0;
relay->pending_ping_start_ms = 0.0;
// Initialize statistics
memset(&relay->stats, 0, sizeof(relay->stats));
@@ -325,85 +551,21 @@ void nostr_relay_pool_destroy(nostr_relay_pool_t* pool) {
free(pool);
}
// Helper function to ensure relay connection
static int ensure_relay_connection(relay_connection_t* relay) {
if (!relay) {
return -1;
}
if (relay->ws_client && nostr_ws_get_state(relay->ws_client) == NOSTR_WS_CONNECTED) {
relay->status = NOSTR_POOL_RELAY_CONNECTED;
return 0; // Already connected
}
// Close existing connection if any
if (relay->ws_client) {
nostr_ws_close(relay->ws_client);
relay->ws_client = NULL;
}
// Attempt connection
relay->status = NOSTR_POOL_RELAY_CONNECTING;
relay->stats.connection_attempts++;
relay->ws_client = nostr_ws_connect(relay->url);
if (!relay->ws_client) {
relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++;
relay->stats.connection_failures++;
return -1;
}
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
if (state == NOSTR_WS_CONNECTED) {
relay->status = NOSTR_POOL_RELAY_CONNECTED;
relay->connect_time = time(NULL);
relay->reconnect_attempts = 0;
// PING FUNCTIONALITY DISABLED - Initial ping on connection establishment
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
// Trigger immediate ping on new connection
time_t current_time = time(NULL);
relay->pending_ping_start_ms = get_current_time_ms();
relay->ping_pending = 1;
relay->last_ping_sent = current_time;
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
relay->ping_pending = 0;
}
*/
return 0;
} else {
relay->status = NOSTR_POOL_RELAY_ERROR;
relay->reconnect_attempts++;
relay->stats.connection_failures++;
// Close the failed connection
nostr_ws_close(relay->ws_client);
relay->ws_client = NULL;
return -1;
}
}
// Subscription management
nostr_pool_subscription_t* nostr_relay_pool_subscribe(
nostr_relay_pool_t* pool,
const char** relay_urls,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data) {
void (*on_eose)(cJSON** events, int event_count, void* user_data),
void* user_data,
int close_on_eose,
int enable_deduplication,
nostr_pool_eose_result_mode_t result_mode,
int relay_timeout_seconds,
int eose_timeout_seconds) {
if (!pool || !relay_urls || relay_count <= 0 || !filter ||
pool->subscription_count >= NOSTR_POOL_MAX_SUBSCRIPTIONS) {
@@ -458,7 +620,57 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe(
sub->on_eose = on_eose;
sub->user_data = user_data;
sub->closed = 0;
sub->close_on_eose = close_on_eose;
sub->pool = pool;
// Set new subscription control parameters
sub->enable_deduplication = enable_deduplication;
sub->result_mode = result_mode;
sub->relay_timeout_seconds = relay_timeout_seconds;
sub->eose_timeout_seconds = eose_timeout_seconds;
sub->subscription_start_time = time(NULL);
// Initialize event collection arrays (only for EOSE result modes)
if (result_mode != NOSTR_POOL_EOSE_FIRST) {
sub->collected_events_capacity = 10; // Initial capacity
sub->collected_events = calloc(sub->collected_events_capacity, sizeof(cJSON*));
if (!sub->collected_events) {
// Cleanup on failure
cJSON_Delete(sub->filter);
for (int j = 0; j < relay_count; j++) {
free(sub->relay_urls[j]);
}
free(sub->relay_urls);
free(sub->eose_received);
free(sub);
return NULL;
}
sub->collected_event_count = 0;
} else {
sub->collected_events = NULL;
sub->collected_event_count = 0;
sub->collected_events_capacity = 0;
}
// Initialize per-relay activity tracking
sub->relay_last_activity = calloc(relay_count, sizeof(time_t));
if (!sub->relay_last_activity) {
// Cleanup on failure
if (sub->collected_events) free(sub->collected_events);
cJSON_Delete(sub->filter);
for (int j = 0; j < relay_count; j++) {
free(sub->relay_urls[j]);
}
free(sub->relay_urls);
free(sub->eose_received);
free(sub);
return NULL;
}
// Initialize all relay activity times to current time
time_t now = time(NULL);
for (int i = 0; i < relay_count; i++) {
sub->relay_last_activity[i] = now;
}
// Add to pool
pool->subscriptions[pool->subscription_count++] = sub;
@@ -556,43 +768,56 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
if (event_id_json && cJSON_IsString(event_id_json)) {
const char* event_id = cJSON_GetStringValue(event_id_json);
// Check for duplicate
if (!is_event_seen(pool, event_id)) {
mark_event_seen(pool, event_id);
relay->stats.events_received++;
// Measure query latency (first event response)
double latency_ms = remove_subscription_timing(relay, subscription_id);
if (latency_ms > 0.0) {
// Update query latency statistics
if (relay->stats.query_samples == 0) {
relay->stats.query_latency_avg = latency_ms;
relay->stats.query_latency_min = latency_ms;
relay->stats.query_latency_max = latency_ms;
} else {
relay->stats.query_latency_avg =
(relay->stats.query_latency_avg * relay->stats.query_samples + latency_ms) /
(relay->stats.query_samples + 1);
if (latency_ms < relay->stats.query_latency_min) {
relay->stats.query_latency_min = latency_ms;
}
if (latency_ms > relay->stats.query_latency_max) {
relay->stats.query_latency_max = latency_ms;
}
}
relay->stats.query_samples++;
// Find subscription first
nostr_pool_subscription_t* sub = NULL;
for (int i = 0; i < pool->subscription_count; i++) {
if (pool->subscriptions[i] && !pool->subscriptions[i]->closed &&
strcmp(pool->subscriptions[i]->subscription_id, subscription_id) == 0) {
sub = pool->subscriptions[i];
break;
}
// Find subscription and call callback
for (int i = 0; i < pool->subscription_count; i++) {
nostr_pool_subscription_t* sub = pool->subscriptions[i];
if (sub && !sub->closed &&
strcmp(sub->subscription_id, subscription_id) == 0) {
if (sub->on_event) {
sub->on_event(event, relay->url, sub->user_data);
}
if (sub) {
// Check for duplicate (per-subscription deduplication)
int is_duplicate = 0;
if (sub->enable_deduplication) {
if (is_event_seen(pool, event_id)) {
is_duplicate = 1;
} else {
mark_event_seen(pool, event_id);
}
}
if (!is_duplicate) {
relay->stats.events_received++;
// Measure query latency (first event response)
double latency_ms = remove_subscription_timing(relay, subscription_id);
if (latency_ms > 0.0) {
// Update query latency statistics
if (relay->stats.query_samples == 0) {
relay->stats.query_latency_avg = latency_ms;
relay->stats.query_latency_min = latency_ms;
relay->stats.query_latency_max = latency_ms;
} else {
relay->stats.query_latency_avg =
(relay->stats.query_latency_avg * relay->stats.query_samples + latency_ms) /
(relay->stats.query_samples + 1);
if (latency_ms < relay->stats.query_latency_min) {
relay->stats.query_latency_min = latency_ms;
}
if (latency_ms > relay->stats.query_latency_max) {
relay->stats.query_latency_max = latency_ms;
}
}
break;
relay->stats.query_samples++;
}
// Call event callback
if (sub->on_event) {
sub->on_event(event, relay->url, sub->user_data);
}
}
}
@@ -630,8 +855,22 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
}
}
if (all_eose && sub->on_eose) {
sub->on_eose(sub->user_data);
if (all_eose) {
if (sub->on_eose) {
// Pass collected events based on result mode
if (sub->result_mode == NOSTR_POOL_EOSE_FIRST) {
// FIRST mode: no events collected, pass NULL/0
sub->on_eose(NULL, 0, sub->user_data);
} else {
// FULL_SET or MOST_RECENT: pass collected events
sub->on_eose(sub->collected_events, sub->collected_event_count, sub->user_data);
}
}
// Auto-close subscription if close_on_eose is enabled
if (sub->close_on_eose && !sub->closed) {
nostr_pool_subscription_close(sub);
}
}
break;
}
@@ -652,39 +891,8 @@ static void process_relay_message(nostr_relay_pool_t* pool, relay_connection_t*
}
}
} else if (strcmp(msg_type, "PONG") == 0) {
// PING FUNCTIONALITY DISABLED - Handle PONG response
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
if (relay->ping_pending) {
double current_time_ms = get_current_time_ms();
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
// Update ping statistics
if (relay->stats.ping_samples == 0) {
relay->stats.ping_latency_avg = ping_latency;
relay->stats.ping_latency_min = ping_latency;
relay->stats.ping_latency_max = ping_latency;
} else {
relay->stats.ping_latency_avg =
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
(relay->stats.ping_samples + 1);
if (ping_latency < relay->stats.ping_latency_min) {
relay->stats.ping_latency_min = ping_latency;
}
if (ping_latency > relay->stats.ping_latency_max) {
relay->stats.ping_latency_max = ping_latency;
}
}
relay->stats.ping_latency_current = ping_latency;
relay->stats.ping_samples++;
relay->ping_pending = 0;
#ifdef NOSTR_DEBUG_ENABLED
printf("🏓 DEBUG: PONG from %s - latency: %.2f ms\n", relay->url, ping_latency);
#endif
}
*/
// Handle PONG response for connection health monitoring
handle_pong_response(relay);
}
if (msg_type) free(msg_type);
@@ -757,11 +965,17 @@ cJSON** nostr_relay_pool_query_sync(
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 100);
if (len > 0) {
buffer[len] = '\0';
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
// Check if this is a pong message (WebSocket library prefixes pong messages)
if (strncmp(buffer, "__PONG__", 8) == 0) {
// Handle pong response for connection health monitoring
handle_pong_response(relay);
} else {
// Process as regular NOSTR message
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
if (msg_type && strcmp(msg_type, "EVENT") == 0) {
// Handle EVENT message
if (cJSON_IsArray(parsed) && cJSON_GetArraySize(parsed) >= 3) {
cJSON* sub_id_json = cJSON_GetArrayItem(parsed, 1);
@@ -806,6 +1020,7 @@ cJSON** nostr_relay_pool_query_sync(
}
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
}
}
}
}
@@ -1059,76 +1274,78 @@ double nostr_relay_pool_get_relay_query_latency(
}
int nostr_relay_pool_ping_relay(
nostr_relay_pool_t* pool,
nostr_relay_pool_t* pool,
const char* relay_url) {
// PING FUNCTIONALITY DISABLED
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
if (!pool || !relay_url) {
return NOSTR_ERROR_INVALID_INPUT;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay || !relay->ws_client) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (ensure_relay_connection(relay) != 0) {
return NOSTR_ERROR_NETWORK_FAILED;
}
time_t current_time = time(NULL);
relay->pending_ping_start_ms = get_current_time_ms();
relay->ping_pending = 1;
relay->last_ping_sent = current_time;
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
relay->ping_pending = 0;
return NOSTR_ERROR_NETWORK_FAILED;
}
return NOSTR_SUCCESS;
*/
(void)pool; // Suppress unused parameter warning
(void)relay_url; // Suppress unused parameter warning
return NOSTR_ERROR_INVALID_INPUT; // Function disabled
}
int nostr_relay_pool_ping_relay_sync(
nostr_relay_pool_t* pool,
const char* relay_url,
nostr_relay_pool_t* pool,
const char* relay_url,
int timeout_seconds) {
// PING FUNCTIONALITY DISABLED
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
if (!pool || !relay_url) {
return NOSTR_ERROR_INVALID_INPUT;
}
relay_connection_t* relay = find_relay_by_url(pool, relay_url);
if (!relay || !relay->ws_client) {
return NOSTR_ERROR_INVALID_INPUT;
}
if (ensure_relay_connection(relay) != 0) {
return NOSTR_ERROR_NETWORK_FAILED;
}
if (timeout_seconds <= 0) {
timeout_seconds = 5;
}
time_t current_time = time(NULL);
relay->pending_ping_start_ms = get_current_time_ms();
relay->ping_pending = 1;
relay->last_ping_sent = current_time;
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
relay->ping_pending = 0;
return NOSTR_ERROR_NETWORK_FAILED;
}
// Wait for PONG response
time_t wait_start = time(NULL);
while (time(NULL) - wait_start < timeout_seconds && relay->ping_pending) {
@@ -1136,7 +1353,7 @@ int nostr_relay_pool_ping_relay_sync(
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, 1000);
if (len > 0) {
buffer[len] = '\0';
char* msg_type = NULL;
cJSON* parsed = NULL;
if (nostr_parse_relay_message(buffer, &msg_type, &parsed) == 0) {
@@ -1146,17 +1363,17 @@ int nostr_relay_pool_ping_relay_sync(
if (relay->ping_pending) {
double current_time_ms = get_current_time_ms();
double ping_latency = current_time_ms - relay->pending_ping_start_ms;
// Update ping statistics
if (relay->stats.ping_samples == 0) {
relay->stats.ping_latency_avg = ping_latency;
relay->stats.ping_latency_min = ping_latency;
relay->stats.ping_latency_max = ping_latency;
} else {
relay->stats.ping_latency_avg =
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
relay->stats.ping_latency_avg =
(relay->stats.ping_latency_avg * relay->stats.ping_samples + ping_latency) /
(relay->stats.ping_samples + 1);
if (ping_latency < relay->stats.ping_latency_min) {
relay->stats.ping_latency_min = ping_latency;
}
@@ -1164,11 +1381,11 @@ int nostr_relay_pool_ping_relay_sync(
relay->stats.ping_latency_max = ping_latency;
}
}
relay->stats.ping_latency_current = ping_latency;
relay->stats.ping_samples++;
relay->ping_pending = 0;
if (msg_type) free(msg_type);
if (parsed) cJSON_Delete(parsed);
return NOSTR_SUCCESS;
@@ -1179,12 +1396,15 @@ int nostr_relay_pool_ping_relay_sync(
}
}
}
// Timeout
relay->ping_pending = 0;
return NOSTR_ERROR_NETWORK_FAILED;
*/
(void)pool; // Suppress unused parameter warning
(void)relay_url; // Suppress unused parameter warning
(void)timeout_seconds; // Suppress unused parameter warning
return NOSTR_ERROR_INVALID_INPUT; // Function disabled
}
@@ -1233,52 +1453,58 @@ int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms) {
if (!pool) {
return -1;
}
int events_processed = 0;
for (int i = 0; i < pool->relay_count; i++) {
relay_connection_t* relay = pool->relays[i];
if (!relay || !relay->ws_client) {
if (!relay) {
continue;
}
// Check if reconnection is needed
if (should_attempt_reconnect(relay)) {
attempt_reconnect(relay);
}
// Skip if no WebSocket client
if (!relay->ws_client) {
continue;
}
// Check connection state
nostr_ws_state_t state = nostr_ws_get_state(relay->ws_client);
if (state != NOSTR_WS_CONNECTED) {
relay->status = (state == NOSTR_WS_ERROR) ? NOSTR_POOL_RELAY_ERROR : NOSTR_POOL_RELAY_DISCONNECTED;
continue;
}
relay->status = NOSTR_POOL_RELAY_CONNECTED;
// PING FUNCTIONALITY DISABLED - Automatic ping management
/* COMMENTED OUT - PING FUNCTIONALITY DISABLED
// Check if we need to send a ping to keep the connection alive
if (current_time >= relay->next_ping_time && !relay->ping_pending) {
relay->pending_ping_start_ms = get_current_time_ms();
relay->ping_pending = 1;
relay->last_ping_sent = current_time;
relay->next_ping_time = current_time + NOSTR_POOL_PING_INTERVAL;
if (nostr_ws_send_ping(relay->ws_client, "ping", 4) < 0) {
relay->ping_pending = 0;
}
}
*/
// Connection health monitoring (ping/pong)
check_connection_health(relay);
// Process incoming messages
char buffer[8192];
int timeout_per_relay = timeout_ms / pool->relay_count;
int len = nostr_ws_receive(relay->ws_client, buffer, sizeof(buffer) - 1, timeout_per_relay);
if (len > 0) {
buffer[len] = '\0';
process_relay_message(pool, relay, buffer);
// Check if this is a pong message (WebSocket library prefixes pong messages)
if (strncmp(buffer, "__PONG__", 8) == 0) {
// Handle pong response for connection health monitoring
handle_pong_response(relay);
} else {
// Process as regular NOSTR message
process_relay_message(pool, relay, buffer);
}
events_processed++;
}
}
return events_processed;
}

View File

@@ -1,6 +1,12 @@
#ifndef NOSTR_CORE_H
#define NOSTR_CORE_H
// Version information (auto-updated by increment_and_push.sh)
#define VERSION "v0.4.3"
#define VERSION_MAJOR 0
#define VERSION_MINOR 4
#define VERSION_PATCH 3
/*
* NOSTR Core Library - Complete API Reference
*
@@ -159,6 +165,13 @@ typedef enum {
NOSTR_POOL_RELAY_ERROR = -1
} nostr_pool_relay_status_t;
// EOSE result mode for subscriptions
typedef enum {
NOSTR_POOL_EOSE_FULL_SET, // Wait for all relays, return all events
NOSTR_POOL_EOSE_MOST_RECENT, // Wait for all relays, return most recent event
NOSTR_POOL_EOSE_FIRST // Return results on first EOSE (fastest response)
} nostr_pool_eose_result_mode_t;
typedef struct {
int connection_attempts;
int connection_failures;
@@ -184,8 +197,20 @@ typedef struct {
typedef struct nostr_relay_pool nostr_relay_pool_t;
typedef struct nostr_pool_subscription nostr_pool_subscription_t;
// Reconnection configuration
typedef struct {
int enable_auto_reconnect; // 1 = enable, 0 = disable
int max_reconnect_attempts; // Max attempts per relay
int initial_reconnect_delay_ms; // Initial delay between attempts
int max_reconnect_delay_ms; // Max delay (cap exponential backoff)
int reconnect_backoff_multiplier; // Delay multiplier
int ping_interval_seconds; // How often to ping (0 = disable)
int pong_timeout_seconds; // How long to wait for pong before reconnecting
} nostr_pool_reconnect_config_t;
// Relay pool management functions
nostr_relay_pool_t* nostr_relay_pool_create(void);
nostr_relay_pool_t* nostr_relay_pool_create(nostr_pool_reconnect_config_t* config);
nostr_pool_reconnect_config_t* nostr_pool_reconnect_config_default(void);
int nostr_relay_pool_add_relay(nostr_relay_pool_t* pool, const char* relay_url);
int nostr_relay_pool_remove_relay(nostr_relay_pool_t* pool, const char* relay_url);
void nostr_relay_pool_destroy(nostr_relay_pool_t* pool);
@@ -197,10 +222,26 @@ nostr_pool_subscription_t* nostr_relay_pool_subscribe(
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data);
void (*on_eose)(cJSON** events, int event_count, void* user_data),
void* user_data,
int close_on_eose,
int enable_deduplication,
nostr_pool_eose_result_mode_t result_mode,
int relay_timeout_seconds,
int eose_timeout_seconds);
int nostr_pool_subscription_close(nostr_pool_subscription_t* subscription);
// Backward compatibility wrapper
nostr_pool_subscription_t* nostr_relay_pool_subscribe_compat(
nostr_relay_pool_t* pool,
const char** relay_urls,
int relay_count,
cJSON* filter,
void (*on_event)(cJSON* event, const char* relay_url, void* user_data),
void (*on_eose)(void* user_data),
void* user_data,
int close_on_eose);
// Event loop functions
int nostr_relay_pool_run(nostr_relay_pool_t* pool, int timeout_ms);
int nostr_relay_pool_poll(nostr_relay_pool_t* pool, int timeout_ms);
@@ -242,6 +283,9 @@ int nostr_relay_pool_reset_relay_stats(
double nostr_relay_pool_get_relay_query_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
double nostr_relay_pool_get_relay_ping_latency(
nostr_relay_pool_t* pool,
const char* relay_url);
// Synchronous relay operations (one-off queries/publishes)
typedef enum {

2
pool.log Normal file
View File

@@ -0,0 +1,2 @@
[Thu Oct 2 14:40:34 2025] 🚀 Pool test started

692
tests/pool_test.c Normal file
View File

@@ -0,0 +1,692 @@
/*
* Interactive Relay Pool Test Program
*
* Interactive command-line interface for testing nostr_relay_pool functionality.
* All output is logged to pool.log while the menu runs in the terminal.
*
* Usage: ./pool_test
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/stat.h>
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
// Global variables
volatile sig_atomic_t running = 1;
nostr_relay_pool_t* pool = NULL;
nostr_pool_subscription_t** subscriptions = NULL;
int subscription_count = 0;
int subscription_capacity = 0;
pthread_t poll_thread;
int log_fd = -1;
// Signal handler for clean shutdown
void signal_handler(int signum) {
(void)signum;
running = 0;
}
// Event callback - called when an event is received
void on_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data;
// Extract basic event information
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
cJSON* content = cJSON_GetObjectItem(event, "content");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0'; // Remove newline
dprintf(log_fd, "[%s] 📨 EVENT from %s\n", timestamp, relay_url);
dprintf(log_fd, "├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
dprintf(log_fd, "├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
dprintf(log_fd, "├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
dprintf(log_fd, "├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
// Truncate content if too long
if (content && cJSON_IsString(content)) {
const char* content_str = cJSON_GetStringValue(content);
size_t content_len = strlen(content_str);
if (content_len > 100) {
dprintf(log_fd, "└── Content: %.97s...\n", content_str);
} else {
dprintf(log_fd, "└── Content: %s\n", content_str);
}
} else {
dprintf(log_fd, "└── Content: (empty)\n");
}
dprintf(log_fd, "\n");
}
// EOSE callback - called when End of Stored Events is received
void on_eose(cJSON** events, int event_count, void* user_data) {
(void)user_data;
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 📋 EOSE received - %d events collected\n", timestamp, event_count);
// Log collected events if any
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItem(events[i], "id");
if (id && cJSON_IsString(id)) {
dprintf(log_fd, " Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
}
}
dprintf(log_fd, "\n");
}
// Background polling thread
void* poll_thread_func(void* arg) {
(void)arg;
while (running) {
if (pool) {
nostr_relay_pool_poll(pool, 100);
}
struct timespec ts = {0, 10000000}; // 10ms
nanosleep(&ts, NULL);
}
return NULL;
}
// Print menu
void print_menu() {
printf("\n=== NOSTR Relay Pool Test Menu ===\n");
printf("1. Start Pool (wss://relay.laantungir.net)\n");
printf("2. Stop Pool\n");
printf("3. Add relay to pool\n");
printf("4. Remove relay from pool\n");
printf("5. Add subscription\n");
printf("6. Remove subscription\n");
printf("7. Show pool status\n");
printf("8. Test reconnection (simulate disconnect)\n");
printf("9. Exit\n");
printf("Choice: ");
}
// Get user input with default
char* get_input(const char* prompt, const char* default_value) {
static char buffer[1024];
printf("%s", prompt);
if (default_value) {
printf(" [%s]", default_value);
}
printf(": ");
if (!fgets(buffer, sizeof(buffer), stdin)) {
return NULL;
}
// Remove newline
size_t len = strlen(buffer);
if (len > 0 && buffer[len-1] == '\n') {
buffer[len-1] = '\0';
}
// Return default if empty
if (strlen(buffer) == 0 && default_value) {
return strdup(default_value);
}
return strdup(buffer);
}
// Parse comma-separated list into cJSON array
cJSON* parse_comma_list(const char* input, int is_number) {
if (!input || strlen(input) == 0) {
return NULL;
}
cJSON* array = cJSON_CreateArray();
if (!array) return NULL;
char* input_copy = strdup(input);
char* token = strtok(input_copy, ",");
while (token) {
// Trim whitespace
while (*token == ' ') token++;
char* end = token + strlen(token) - 1;
while (end > token && *end == ' ') *end-- = '\0';
if (is_number) {
int num = atoi(token);
cJSON_AddItemToArray(array, cJSON_CreateNumber(num));
} else {
cJSON_AddItemToArray(array, cJSON_CreateString(token));
}
token = strtok(NULL, ",");
}
free(input_copy);
return array;
}
// Add subscription interactively
void add_subscription() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
printf("\n--- Add Subscription ---\n");
printf("Enter filter values (press Enter for no value):\n");
cJSON* filter = cJSON_CreateObject();
// ids
char* ids_input = get_input("ids (comma-separated event ids)", NULL);
if (ids_input && strlen(ids_input) > 0) {
cJSON* ids = parse_comma_list(ids_input, 0);
if (ids) cJSON_AddItemToObject(filter, "ids", ids);
}
free(ids_input);
// authors
char* authors_input = get_input("authors (comma-separated pubkeys)", NULL);
if (authors_input && strlen(authors_input) > 0) {
cJSON* authors = parse_comma_list(authors_input, 0);
if (authors) cJSON_AddItemToObject(filter, "authors", authors);
}
free(authors_input);
// kinds
char* kinds_input = get_input("kinds (comma-separated numbers)", NULL);
if (kinds_input && strlen(kinds_input) > 0) {
cJSON* kinds = parse_comma_list(kinds_input, 1);
if (kinds) cJSON_AddItemToObject(filter, "kinds", kinds);
}
free(kinds_input);
// #e tag
char* e_input = get_input("#e (comma-separated event ids)", NULL);
if (e_input && strlen(e_input) > 0) {
cJSON* e_array = parse_comma_list(e_input, 0);
if (e_array) cJSON_AddItemToObject(filter, "#e", e_array);
}
free(e_input);
// #p tag
char* p_input = get_input("#p (comma-separated pubkeys)", NULL);
if (p_input && strlen(p_input) > 0) {
cJSON* p_array = parse_comma_list(p_input, 0);
if (p_array) cJSON_AddItemToObject(filter, "#p", p_array);
}
free(p_input);
// since
char* since_input = get_input("since (unix timestamp or 'n' for now)", NULL);
if (since_input && strlen(since_input) > 0) {
if (strcmp(since_input, "n") == 0) {
// Use current timestamp
time_t now = time(NULL);
cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber((int)now));
printf("Using current timestamp: %ld\n", now);
} else {
int since = atoi(since_input);
if (since > 0) cJSON_AddItemToObject(filter, "since", cJSON_CreateNumber(since));
}
}
free(since_input);
// until
char* until_input = get_input("until (unix timestamp)", NULL);
if (until_input && strlen(until_input) > 0) {
int until = atoi(until_input);
if (until > 0) cJSON_AddItemToObject(filter, "until", cJSON_CreateNumber(until));
}
free(until_input);
// limit
char* limit_input = get_input("limit (max events)", "10");
if (limit_input && strlen(limit_input) > 0) {
int limit = atoi(limit_input);
if (limit > 0) cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(limit));
}
free(limit_input);
// Get relay URLs from pool
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
cJSON_Delete(filter);
return;
}
// Ask about close_on_eose behavior
char* close_input = get_input("Close subscription on EOSE? (y/n)", "n");
int close_on_eose = (close_input && strcmp(close_input, "y") == 0) ? 1 : 0;
free(close_input);
// Create subscription with new parameters
nostr_pool_subscription_t* sub = nostr_relay_pool_subscribe(
pool,
(const char**)relay_urls,
relay_count,
filter,
on_event,
on_eose,
NULL,
close_on_eose,
1, // enable_deduplication
NOSTR_POOL_EOSE_FULL_SET, // result_mode
30, // relay_timeout_seconds
60 // eose_timeout_seconds
);
// Free relay URLs
for (int i = 0; i < relay_count; i++) {
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
if (!sub) {
printf("❌ Failed to create subscription\n");
cJSON_Delete(filter);
return;
}
// Store subscription
if (subscription_count >= subscription_capacity) {
subscription_capacity = subscription_capacity == 0 ? 10 : subscription_capacity * 2;
subscriptions = realloc(subscriptions, subscription_capacity * sizeof(nostr_pool_subscription_t*));
}
subscriptions[subscription_count++] = sub;
printf("✅ Subscription created (ID: %d)\n", subscription_count);
// Log the filter
char* filter_json = cJSON_Print(filter);
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔍 New subscription created (ID: %d)\n", timestamp, subscription_count);
dprintf(log_fd, "Filter: %s\n\n", filter_json);
free(filter_json);
}
// Remove subscription
void remove_subscription() {
if (subscription_count == 0) {
printf("❌ No subscriptions to remove\n");
return;
}
printf("\n--- Remove Subscription ---\n");
printf("Available subscriptions:\n");
for (int i = 0; i < subscription_count; i++) {
printf("%d. Subscription %d\n", i + 1, i + 1);
}
char* choice_input = get_input("Enter subscription number to remove", NULL);
if (!choice_input || strlen(choice_input) == 0) {
free(choice_input);
return;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= subscription_count) {
printf("❌ Invalid subscription number\n");
return;
}
nostr_pool_subscription_close(subscriptions[choice]);
// Shift remaining subscriptions
for (int i = choice; i < subscription_count - 1; i++) {
subscriptions[i] = subscriptions[i + 1];
}
subscription_count--;
printf("✅ Subscription removed\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🗑️ Subscription removed (was ID: %d)\n\n", timestamp, choice + 1);
}
// Show pool status
void show_pool_status() {
if (!pool) {
printf("❌ Pool not started\n");
return;
}
// Give polling thread time to establish connections
printf("⏳ Waiting for connections to establish...\n");
sleep(3);
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
printf("\n📊 POOL STATUS\n");
printf("Relays: %d\n", relay_count);
printf("Subscriptions: %d\n", subscription_count);
if (relay_count > 0) {
printf("\nRelay Details:\n");
for (int i = 0; i < relay_count; i++) {
const char* status_str;
switch (statuses[i]) {
case NOSTR_POOL_RELAY_CONNECTED: status_str = "🟢 CONNECTED"; break;
case NOSTR_POOL_RELAY_CONNECTING: status_str = "🟡 CONNECTING"; break;
case NOSTR_POOL_RELAY_DISCONNECTED: status_str = "⚪ DISCONNECTED"; break;
case NOSTR_POOL_RELAY_ERROR: status_str = "🔴 ERROR"; break;
default: status_str = "❓ UNKNOWN"; break;
}
printf("├── %s: %s\n", relay_urls[i], status_str);
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
if (stats) {
printf("│ ├── Events received: %d\n", stats->events_received);
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
printf("│ ├── Connection failures: %d\n", stats->connection_failures);
printf("│ ├── Ping latency: %.2f ms\n", stats->ping_latency_current);
printf("│ └── Query latency: %.2f ms\n", stats->query_latency_avg);
}
free(relay_urls[i]);
}
free(relay_urls);
free(statuses);
}
printf("\n");
}
int main() {
// Setup logging to file
log_fd = open("pool.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (log_fd == -1) {
fprintf(stderr, "❌ Failed to open pool.log for writing\n");
return 1;
}
// Initialize NOSTR library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
close(log_fd);
return 1;
}
// Setup signal handler
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Start polling thread
if (pthread_create(&poll_thread, NULL, poll_thread_func, NULL) != 0) {
fprintf(stderr, "❌ Failed to create polling thread\n");
nostr_cleanup();
close(log_fd);
return 1;
}
printf("🔗 NOSTR Relay Pool Interactive Test\n");
printf("=====================================\n");
printf("All event output is logged to pool.log\n");
printf("Press Ctrl+C to exit\n\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🚀 Pool test started\n\n", timestamp);
// Main menu loop
while (running) {
print_menu();
char choice;
if (scanf("%c", &choice) != 1) {
break;
}
// Consume newline
int c;
while ((c = getchar()) != '\n' && c != EOF);
switch (choice) {
case '1': { // Start Pool
if (pool) {
printf("❌ Pool already started\n");
break;
}
// Create pool with custom reconnection configuration for faster testing
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
config->ping_interval_seconds = 5; // Ping every 5 seconds for testing
pool = nostr_relay_pool_create(config);
if (!pool) {
printf("❌ Failed to create pool\n");
break;
}
if (nostr_relay_pool_add_relay(pool, "wss://relay.laantungir.net") != NOSTR_SUCCESS) {
printf("❌ Failed to add default relay\n");
nostr_relay_pool_destroy(pool);
pool = NULL;
break;
}
printf("✅ Pool started with wss://relay.laantungir.net\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🏊 Pool started with default relay\n\n", timestamp);
break;
}
case '2': { // Stop Pool
if (!pool) {
printf("❌ Pool not started\n");
break;
}
// Close all subscriptions
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
subscriptions = NULL;
subscription_count = 0;
subscription_capacity = 0;
nostr_relay_pool_destroy(pool);
pool = NULL;
printf("✅ Pool stopped\n");
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🛑 Pool stopped\n\n", timestamp);
break;
}
case '3': { // Add relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL", "wss://relay.example.com");
if (url && strlen(url) > 0) {
if (nostr_relay_pool_add_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay added: %s\n", url);
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay added: %s\n\n", timestamp, url);
} else {
printf("❌ Failed to add relay\n");
}
}
free(url);
break;
}
case '4': { // Remove relay
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char* url = get_input("Enter relay URL to remove", NULL);
if (url && strlen(url) > 0) {
if (nostr_relay_pool_remove_relay(pool, url) == NOSTR_SUCCESS) {
printf("✅ Relay removed: %s\n", url);
now = time(NULL);
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] Relay removed: %s\n\n", timestamp, url);
} else {
printf("❌ Failed to remove relay\n");
}
}
free(url);
break;
}
case '5': // Add subscription
add_subscription();
break;
case '6': // Remove subscription
remove_subscription();
break;
case '7': // Show status
show_pool_status();
break;
case '8': { // Test reconnection
if (!pool) {
printf("❌ Pool not started\n");
break;
}
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
break;
}
printf("\n--- Test Reconnection ---\n");
printf("Available relays:\n");
for (int i = 0; i < relay_count; i++) {
printf("%d. %s (%s)\n", i + 1, relay_urls[i],
statuses[i] == NOSTR_POOL_RELAY_CONNECTED ? "CONNECTED" : "NOT CONNECTED");
}
char* choice_input = get_input("Enter relay number to test reconnection with", NULL);
if (!choice_input || strlen(choice_input) == 0) {
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
free(choice_input);
break;
}
int choice = atoi(choice_input) - 1;
free(choice_input);
if (choice < 0 || choice >= relay_count) {
printf("❌ Invalid relay number\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
printf("🔄 Testing reconnection with %s...\n", relay_urls[choice]);
printf(" The pool is configured with automatic reconnection enabled.\n");
printf(" If the connection drops, it will automatically attempt to reconnect\n");
printf(" with exponential backoff (1s → 2s → 4s → 8s → 16s → 30s max).\n");
printf(" Connection health is monitored with ping/pong every 30 seconds.\n");
time_t now = time(NULL);
char timestamp[26];
ctime_r(&now, timestamp);
timestamp[24] = '\0';
dprintf(log_fd, "[%s] 🔄 TEST: Testing reconnection behavior with %s\n", timestamp, relay_urls[choice]);
dprintf(log_fd, " Pool configured with: auto-reconnect=ON, max_attempts=10, ping_interval=30s\n\n");
printf("✅ Reconnection test initiated. Monitor the status and logs for reconnection activity.\n");
for (int i = 0; i < relay_count; i++) free(relay_urls[i]);
free(relay_urls);
free(statuses);
break;
}
case '9': // Exit
running = 0;
break;
default:
printf("❌ Invalid choice\n");
break;
}
}
printf("\n🧹 Cleaning up...\n");
// Stop polling thread
running = 0;
pthread_join(poll_thread, NULL);
// Clean up pool and subscriptions
if (pool) {
for (int i = 0; i < subscription_count; i++) {
if (subscriptions[i]) {
nostr_pool_subscription_close(subscriptions[i]);
}
}
free(subscriptions);
nostr_relay_pool_destroy(pool);
printf("✅ Pool destroyed\n");
}
// Cleanup
nostr_cleanup();
close(log_fd);
printf("👋 Test completed\n");
return 0;
}

View File

@@ -0,0 +1,254 @@
/*
* Relay Pool Test Program
*
* Tests the nostr_relay_pool functionality with persistent connections
* and subscriptions. Prints events as they arrive and shows connection status.
*
* Usage: ./pool_test
* Press Ctrl+C to exit
*/
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
// Global variables for signal handling
volatile sig_atomic_t running = 1;
time_t last_status_time = 0;
// Signal handler for clean shutdown
void signal_handler(int signum) {
(void)signum; // Suppress unused parameter warning
printf("\n🛑 Received signal, shutting down...\n");
running = 0;
}
// Event callback - called when an event is received
void on_event(cJSON* event, const char* relay_url, void* user_data) {
(void)user_data; // Suppress unused parameter warning
// Extract basic event information
cJSON* id = cJSON_GetObjectItem(event, "id");
cJSON* pubkey = cJSON_GetObjectItem(event, "pubkey");
cJSON* created_at = cJSON_GetObjectItem(event, "created_at");
cJSON* kind = cJSON_GetObjectItem(event, "kind");
cJSON* content = cJSON_GetObjectItem(event, "content");
printf("\n📨 EVENT from %s\n", relay_url);
printf("├── ID: %.12s...\n", id && cJSON_IsString(id) ? cJSON_GetStringValue(id) : "unknown");
printf("├── Pubkey: %.12s...\n", pubkey && cJSON_IsString(pubkey) ? cJSON_GetStringValue(pubkey) : "unknown");
printf("├── Kind: %d\n", kind && cJSON_IsNumber(kind) ? (int)cJSON_GetNumberValue(kind) : -1);
printf("├── Created: %lld\n", created_at && cJSON_IsNumber(created_at) ? (long long)cJSON_GetNumberValue(created_at) : 0);
// Truncate content if too long
if (content && cJSON_IsString(content)) {
const char* content_str = cJSON_GetStringValue(content);
size_t content_len = strlen(content_str);
if (content_len > 100) {
printf("└── Content: %.97s...\n", content_str);
} else {
printf("└── Content: %s\n", content_str);
}
} else {
printf("└── Content: (empty)\n");
}
fflush(stdout);
}
// EOSE callback - called when End of Stored Events is received
void on_eose(cJSON** events, int event_count, void* user_data) {
(void)user_data; // Suppress unused parameter warning
printf("📋 EOSE received - %d events collected\n", event_count);
// Log collected events if any
for (int i = 0; i < event_count; i++) {
cJSON* id = cJSON_GetObjectItem(events[i], "id");
if (id && cJSON_IsString(id)) {
printf(" Event %d: %.12s...\n", i + 1, cJSON_GetStringValue(id));
}
}
fflush(stdout);
}
// Print connection status for all relays
void print_relay_status(nostr_relay_pool_t* pool) {
char** relay_urls = NULL;
nostr_pool_relay_status_t* statuses = NULL;
int relay_count = nostr_relay_pool_list_relays(pool, &relay_urls, &statuses);
if (relay_count <= 0) {
printf("❌ No relays in pool\n");
return;
}
printf("\n📊 RELAY STATUS (%d relays):\n", relay_count);
for (int i = 0; i < relay_count; i++) {
const char* status_str;
switch (statuses[i]) {
case NOSTR_POOL_RELAY_CONNECTED:
status_str = "🟢 CONNECTED";
break;
case NOSTR_POOL_RELAY_CONNECTING:
status_str = "🟡 CONNECTING";
break;
case NOSTR_POOL_RELAY_DISCONNECTED:
status_str = "⚪ DISCONNECTED";
break;
case NOSTR_POOL_RELAY_ERROR:
status_str = "🔴 ERROR";
break;
default:
status_str = "❓ UNKNOWN";
break;
}
printf("├── %s: %s\n", relay_urls[i], status_str);
// Show additional stats if available
const nostr_relay_stats_t* stats = nostr_relay_pool_get_relay_stats(pool, relay_urls[i]);
if (stats) {
printf("│ ├── Events received: %d\n", stats->events_received);
printf("│ ├── Connection attempts: %d\n", stats->connection_attempts);
printf("│ └── Connection failures: %d\n", stats->connection_failures);
}
free(relay_urls[i]);
}
printf("\n");
free(relay_urls);
free(statuses);
fflush(stdout);
}
int main() {
printf("🔗 NOSTR Relay Pool Test\n");
printf("========================\n");
printf("Testing persistent relay connections with subscriptions.\n");
printf("Press Ctrl+C to exit.\n\n");
// Initialize NOSTR library
if (nostr_init() != NOSTR_SUCCESS) {
fprintf(stderr, "❌ Failed to initialize NOSTR library\n");
return 1;
}
// Setup signal handler for clean shutdown
signal(SIGINT, signal_handler);
signal(SIGTERM, signal_handler);
// Create relay pool with default configuration
nostr_pool_reconnect_config_t* config = nostr_pool_reconnect_config_default();
nostr_relay_pool_t* pool = nostr_relay_pool_create(config);
if (!pool) {
fprintf(stderr, "❌ Failed to create relay pool\n");
nostr_cleanup();
return 1;
}
// Add relays to the pool
const char* relay_urls[] = {
"wss://nostr.mom",
"wss://relay.laantungir.net",
"wss://nos.lol"
};
int relay_count = 3;
printf("📡 Adding %d relays to pool:\n", relay_count);
for (int i = 0; i < relay_count; i++) {
printf("├── %s\n", relay_urls[i]);
if (nostr_relay_pool_add_relay(pool, relay_urls[i]) != NOSTR_SUCCESS) {
printf("│ ❌ Failed to add relay\n");
} else {
printf("│ ✅ Added successfully\n");
}
}
printf("\n");
// Create filter for subscription (kind 1 events - text notes)
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(10)); // Limit to 10 events per relay
printf("🔍 Creating subscription with filter:\n");
char* filter_json = cJSON_Print(filter);
printf("%s\n\n", filter_json);
free(filter_json);
// Create subscription with new parameters
nostr_pool_subscription_t* subscription = nostr_relay_pool_subscribe(
pool,
relay_urls,
relay_count,
filter,
on_event, // Event callback
on_eose, // EOSE callback
NULL, // User data (not used)
0, // close_on_eose (false - keep subscription open)
1, // enable_deduplication
NOSTR_POOL_EOSE_FULL_SET, // result_mode
30, // relay_timeout_seconds
60 // eose_timeout_seconds
);
if (!subscription) {
fprintf(stderr, "❌ Failed to create subscription\n");
cJSON_Delete(filter);
nostr_relay_pool_destroy(pool);
nostr_cleanup();
return 1;
}
printf("✅ Subscription created successfully\n");
printf("🎯 Listening for events... (Ctrl+C to exit)\n\n");
// Record start time for status updates
last_status_time = time(NULL);
// Main event loop
while (running) {
// Poll for events (100ms timeout)
int events_processed = nostr_relay_pool_poll(pool, 100);
// Check if we should print status (every 30 seconds)
time_t current_time = time(NULL);
if (current_time - last_status_time >= 30) {
print_relay_status(pool);
last_status_time = current_time;
}
// Small delay to prevent busy waiting
if (events_processed == 0) {
struct timespec ts = {0, 10000000}; // 10ms
nanosleep(&ts, NULL);
}
}
printf("\n🧹 Cleaning up...\n");
// Close subscription
if (subscription) {
nostr_pool_subscription_close(subscription);
printf("✅ Subscription closed\n");
}
// Destroy pool
nostr_relay_pool_destroy(pool);
printf("✅ Relay pool destroyed\n");
// Cleanup JSON
cJSON_Delete(filter);
// Cleanup library
nostr_cleanup();
printf("👋 Test completed successfully\n");
return 0;
}

View File

@@ -10,7 +10,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "../nostr_core/nostr_common.h"
#include "../nostr_core/nostr_core.h"
#include "../cjson/cJSON.h"
@@ -80,7 +80,7 @@ int main() {
const char* filter_json =
"{"
" \"kinds\": [1],"
" \"limit\": 1"
" \"limit\": 4"
"}";
// Alternative filter examples (comment out the one above, uncomment one below):
@@ -133,7 +133,8 @@ int main() {
cJSON** results = synchronous_query_relays_with_progress(
test_relays, relay_count, filter, test_mode,
&result_count, 5, progress_callback, NULL
&result_count, 5, progress_callback, NULL,
1, NULL // nip42_enabled = true, private_key = NULL (no auth)
);
time_t end_time = time(NULL);

View File

@@ -1,103 +0,0 @@
/*
* WebSocket SSL Test - Test OpenSSL WebSocket implementation
* Connect to a NOSTR relay and fetch one type 1 event
*/
#include <stdio.h>
#include <stdlib.h>
#include "../cjson/cJSON.h"
#include "../nostr_core/nostr_common.h"
// Progress callback to show connection status
static void progress_callback(
const char* relay_url,
const char* status,
const char* event_id,
int events_received,
int total_relays,
int completed_relays,
void* user_data)
{
printf("Progress: %s - %s", relay_url ? relay_url : "Summary", status);
if (event_id) {
printf(" (Event: %.12s...)", event_id);
}
printf(" [%d/%d events, %d/%d relays]\n",
events_received, *(int*)user_data, completed_relays, total_relays);
}
int main() {
printf("WebSocket SSL Test - Testing OpenSSL WebSocket with NOSTR relay\n");
printf("================================================================\n");
// Initialize NOSTR library
if (nostr_init() != NOSTR_SUCCESS) {
printf("❌ Failed to initialize NOSTR library\n");
return 1;
}
printf("✅ NOSTR library initialized\n");
// Setup relay and filter
const char* relay_urls[] = {"wss://nostr.mom"};
int relay_count = 1;
// Create filter for type 1 events (text notes), limit to 1 event
cJSON* filter = cJSON_CreateObject();
cJSON* kinds = cJSON_CreateArray();
cJSON_AddItemToArray(kinds, cJSON_CreateNumber(1));
cJSON_AddItemToObject(filter, "kinds", kinds);
cJSON_AddItemToObject(filter, "limit", cJSON_CreateNumber(1));
printf("📡 Connecting to %s...\n", relay_urls[0]);
printf("🔍 Requesting 1 type 1 event (text note)...\n\n");
// Query the relay
int result_count = 0;
int expected_events = 1;
cJSON** events = synchronous_query_relays_with_progress(
relay_urls,
relay_count,
filter,
RELAY_QUERY_FIRST_RESULT, // Return as soon as we get the first event
&result_count,
10, // 10 second timeout
progress_callback,
&expected_events
);
// Process results
if (events && result_count > 0) {
printf("\n✅ Successfully received %d event(s)!\n", result_count);
printf("📄 Raw JSON event data:\n");
printf("========================\n");
for (int i = 0; i < result_count; i++) {
char* json_string = cJSON_Print(events[i]);
if (json_string) {
printf("%s\n\n", json_string);
free(json_string);
}
cJSON_Delete(events[i]);
}
free(events);
printf("🎉 WebSocket SSL Test PASSED - OpenSSL WebSocket working correctly!\n");
} else {
printf("\n❌ No events received or query failed\n");
printf("❌ WebSocket SSL Test FAILED\n");
// Cleanup and return error
cJSON_Delete(filter);
nostr_cleanup();
return 1;
}
// Cleanup
cJSON_Delete(filter);
nostr_cleanup();
printf("✅ WebSocket connection and TLS working with OpenSSL\n");
return 0;
}