Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Port nocapd to Deno and rename [bullmq->pqueue, lmdb -> sqlite] #774

Open
wants to merge 4 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/gui/src/lib/components/layout/PageHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
<span class="copy-message">click to copy</span>
</h1>
{#if subtitle}
<span class="ml-3 text-lg block">{subtitle}</span>
<span class="ml-3 text-lg block">{@html subtitle}</span>
{/if}

</div>
Expand Down
16 changes: 0 additions & 16 deletions apps/gui/src/lib/managers/ActivityManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ interface LifecycleMessage {
sourceId: string;
}

/** A simple delay helper. */
function delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
Expand All @@ -22,7 +21,6 @@ export class ActivityManager {
private currentState: ActivityState;
private transitioning: boolean = false;

// External handlers return Promise<void>
private externalHandlers: { active: () => Promise<void>; inactive: () => Promise<void> } = {
active: async () => {},
inactive: async () => {},
Expand All @@ -36,31 +34,24 @@ export class ActivityManager {
private leaderId: string | null = null;
private isLeader: boolean = false;
private releaseWaitTimeoutId: number | null = null;
// Reduced forced takeover timeout.
private releaseWaitTimeoutMs: number = 500;

// Reduced leadership confirmation delay.
private LEADERSHIP_CONFIRM_DELAY_MS: number = 500;
// Reduced hidden delay.
private HIDDEN_DELAY_MS: number = 3000;

// Heartbeat settings.
private heartbeatIntervalId: number | null = null;
private HEARTBEAT_INTERVAL_MS: number = 1000;
// Reduced stale threshold.
private STALE_THRESHOLD_MS: number = 5000;

private boundVisibilityHandler: () => void;
private boundIdleEventHandler: (e: Event) => void;
private boundChannelMessageHandler: (ev: MessageEvent) => void;
private boundBeforeUnloadHandler: () => void;

// Timeout ID for delayed hidden action.
private visibilityHiddenTimeoutId: number | null = null;

constructor(idleTimeoutMs: number = 5 * 60 * 1000) {
this.idleTimeoutMs = idleTimeoutMs;
// Initialize state: if visible, start as follower (not yet leader), otherwise inactive.
this.currentState =
document.visibilityState === 'visible' ? 'follower' : 'inactive';
this.updateTabState(this.currentState);
Expand All @@ -85,7 +76,6 @@ export class ActivityManager {

this.startIdleTimer();

// Delay the initial onActivity() to allow external handler registration.
if (document.visibilityState === 'visible') {
setTimeout(() => { this.onActivity(); }, 0);
}
Expand All @@ -98,15 +88,10 @@ export class ActivityManager {
}
}

// We define "active" as being the leader.
private isActiveState(state: ActivityState): boolean {
return state === 'leader';
}

/**
* Transition to a new state.
* Awaits the external active/inactive handler before updating the state.
*/
private async transitionState(newState: ActivityState) {
if (this.transitioning || this.currentState === newState) return;
this.transitioning = true;
Expand Down Expand Up @@ -272,7 +257,6 @@ export class ActivityManager {
localStorage.removeItem('leaderId');
}

// POLL: Wait if the stored leader is shutting down.
const MAX_SHUTDOWN_WAIT_MS = 2500;
let shutdownWaitTime = 0;
while (true) {
Expand Down
20 changes: 11 additions & 9 deletions apps/gui/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,17 @@
// });
// }

navigator.serviceWorker.getRegistrations().then(function(registrations) {
for (let registration of registrations) {
registration.unregister();
}
}).then(() => {
console.log("Service workers unregistered");
}).catch(error => {
console.error("Error unregistering service workers:", error);
});
if('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(function(registrations) {
for (let registration of registrations) {
registration.unregister();
}
}).then(() => {
console.log("Service workers unregistered");
}).catch(error => {
console.error("Error unregistering service workers:", error);
});
}


const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
Expand Down
3 changes: 3 additions & 0 deletions apps/relaymon/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
config.yaml
.env
node_modules
155 changes: 155 additions & 0 deletions apps/relaymon/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# Relaymon

Relaymon is a Nostr relay monitoring application written in Deno. It deduplicates, validates, and checks the liveness of relays, then publishes events (such as monitor announcements and relay lists) based on configurable settings. Relaymon leverages modular seeding from multiple sources (static configuration, files, database, API, events, subscriptions) and uses SQLite for caching relay statuses. It also implements configurable retry/backoff logic and job scheduling using p-queue.

## Features

- **Monitor Profile Announcement:**
Publishes the monitor’s profile (metadata) and relay list on startup using the npm:@nostrwatch/announce package.

- **Relay Checks:**
Supports configurable tests (e.g., "open", "read") with custom timeouts and expiration settings. Expired relay checks are requeued based on configurable polling intervals and limits.

- **Flexible Seeding:**
Aggregates relays from multiple sources:
- **Config:** A static list provided in the config.
- **Static File:** YAML or JSON seed file.
- **Cache:** Relay data stored in a SQLite database.
- **API:** Fetching relay information from a REST API.
- **Events:** Extracting relay URLs from Nostr events.
- **Subscription:** (Dummy implementation available, extendable for real-time updates.)

- **Retry & Backoff:**
Implements customizable backoff logic for failed relay checks.

- **Queue Concurrency:**
Uses p-queue with configurable concurrency for check jobs.

## Installation

Ensure Deno is installed.

Clone the repository:
```
git clone https://github.com/yourusername/relaymon.git
cd relaymon
```

## Configuration

Relaymon is fully configurable via a YAML file (config.yaml). Below is a sample configuration:

```
monitor:
slug: trawler.eighteen
info:
name: "trawler"
about: "Trrawls nostr for relays, dedupes, validates, checks liveness, and publishes events."
nip05: trawler@nostr.watch
owner: "9bbabc5e36297b6f7d15dd21b90ef85b2f1cb80e15c37fcc0c7f6c05acfd0019"
geo:
city: "Frankfurt am Main"
country: "Germany"
countryCode: "DE"
lat: 50.1169
lon: 8.6821
region: "Hesse"
continent: "Europe"

publisher:
relays:
- "wss://relay.nostr.watch"
- "wss://history.nostr.watch"
- "wss://relaypag.es"

relaymon:
networks:
- clearnet
- tor
- i2p
retry:
expiry:
- { max: 3, delay: "1m" }
- { max: 5, delay: "20m" }
- { max: 7, delay: "1h" }
- { max: 11, delay: "3h" }
- { max: 15, delay: "6h" }
- { max: 22, delay: "24h" }
- { max: 107, delay: "7d" }
seed:
interval: "1m" # How often to refresh the relay list from all sources
sources:
- config # Seed from a static list provided in the config
- static # Seed from a static file (YAML or JSON)
- cache # Seed from the SQLite database (relay cache)
- api # Seed from a REST API
- events # Seed from Nostr events
- subscription # Seed from relay subscriptions (if available)
options:
db:
path: "./relay.db"
static:
path: "./seed.yaml"
config: [] # Optional static relay list provided in the config
checks:
enabled:
- open
- read
options:
expires: "24h" # A relay's check is considered expired after 24 hours
interval: "15s" # Poll the database for expired relays every 15 seconds
timeout:
open: 30000 # 30 seconds timeout for the "open" check
read: 5000 # 5 seconds timeout for the "read" check
max: "200" # Enqueue up to 200 expired relays per polling iteration

queue:
workerConcurrency: 20 # p-queue concurrency for check jobs

```

## Usage

Run Relaymon with the following command:

deno run --allow-net --allow-env --allow-read --allow-write main.ts

Or, if you have a deno.json configured with tasks:

deno task start

## Project Structure

```
relaymon/
├── main.ts # Main entry point
├── config.ts # Loads configuration from config.yaml
├── announce.ts # Publishes monitor announcement at startup
├── daemon.ts # Initializes components and starts monitoring loops
├── queueManager.ts # Manages job queues (check & publish)
├── worker.ts # Processes relay checks
├── seeder.ts # Aggregates relay list from multiple seed sources
├── db.ts # SQLite database operations
├── retryManager.ts # Handles retry/backoff logic
├── logger.ts # Logging helper
└── deno.json # Deno configuration file
```

## Environment Variables

- DAEMON_PRIVKEY: The private key used to sign announcements and events.
- DAEMON_PUBKEY: The public key of the daemon for relay identification.

## Development

- Formatting: Run `deno fmt` to format the code.
- Linting: Run `deno lint` to lint the code.
- Testing: Run `deno test --allow-net --allow-env --allow-read --allow-write` to execute tests.

## Contributing

Contributions are welcome! Open an issue or submit a pull request with your changes.

## License

This project is licensed under the MIT License.
45 changes: 45 additions & 0 deletions apps/relaymon/announce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { getLogger } from "./logger.ts";
import { AnnounceMonitor } from "npm:@nostrwatch/announce";

export async function maybeAnnounce(config: any): Promise<void> {
const logger = getLogger("Announce");

if (!config.monitor || !config.monitor.info) {
logger.warn("Monitor metadata is missing; skipping announcement.");
return;
}
if (!config.publisher?.relays || !Array.isArray(config.publisher.relays) || config.publisher.relays.length === 0) {
logger.warn("Publisher relay list is missing; skipping announcement.");
return;
}

const announcer = new AnnounceMonitor({
slug: config.monitor.slug,
name: config.monitor.info.name,
about: config.monitor.info.about,
nip05: config.monitor.info.nip05,
owner: config.monitor.owner,
geo: config.monitor.geo,
}, 'pubkey');

const privkey = Deno.env.get("DAEMON_PRIVKEY");
if (!privkey) {
logger.error("Missing DAEMON_PRIVKEY; cannot sign announcement.");
return;
}

try {
announcer.signEvent(privkey);
} catch (error) {
logger.error("Error signing announcement: " + error.message);
return;
}

const event = announcer.getEvent();
try {
await announcer.publishEvent(event, config.publisher.relays);
logger.info("Monitor announcement published successfully.");
} catch (error: any) {
logger.error("Failed to publish monitor announcement: " + error.message);
}
}
Loading