Skip to content

purplebase/purplebase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

purplebase

A powerful local-first nostr client framework written in Dart with built-in WebSocket pool management and background isolate support.

Reference implementation of models.

Features

  • 🔄 Local-First Architecture - SQLite-backed storage with background isolate processing
  • 🌐 Smart WebSocket Pool - Automatic reconnection with exponential backoff
  • 📡 Ping-Based Health Checks - Detects zombie connections via limit:0 requests
  • 🎯 Event Deduplication - Intelligent handling of events from multiple relays
  • 📊 Unified State - Single source of truth for subscriptions and relay state
  • ⚙️ Configurable Batching - Low-latency streaming with tunable flush windows

Quick Start

import 'package:purplebase/purplebase.dart';
import 'package:riverpod/riverpod.dart';

// Configure
final config = StorageConfiguration(
  databasePath: 'myapp.db',
  defaultRelays: {
    'default': {'wss://relay.damus.io', 'wss://relay.nostr.band'},
  },
  defaultQuerySource: const LocalAndRemoteSource(
    relays: 'default',
    stream: false,
  ),
);

// Initialize
final container = ProviderContainer();
await container.read(initializationProvider(config).future);

// Query
final notes = await container.read(storageNotifierProvider.notifier).query<Note>(
  RequestFilter(kinds: {1}, limit: 20).toRequest(),
  source: RemoteSource(relays: 'default'),
);

App Lifecycle (Recommended)

Call connect() when your app resumes and disconnect() when paused:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
  final storage = ref.read(storageNotifierProvider.notifier);
  if (state == AppLifecycleState.resumed) {
    storage.connect();
  } else if (state == AppLifecycleState.paused) {
    storage.disconnect();
  }
}
  • connect() resets backoff timers and triggers immediate reconnection for all non-connected relays (including failed ones)
  • disconnect() cleanly closes all connections and stops retry timers

Configuration

StorageConfiguration(
  databasePath: 'myapp.db',                          // null for in-memory
  defaultRelays: {'default': {'wss://relay.com'}},
  defaultQuerySource: LocalAndRemoteSource(relays: 'default', stream: false),
  responseTimeout: Duration(seconds: 15),            // EOSE and publish timeout
  streamingBufferDuration: Duration(milliseconds: 100), // Event batching window
)

Pool Constants

The pool uses these timing constants (not configurable):

Constant Value Description
relayTimeout 5s Connection and ping timeout
pingIdleThreshold 55s Ping if idle this long
healthCheckInterval 60s Heartbeat frequency
maxRetries 31 Total attempts before marking relay as failed

Backoff Schedule

Reconnection uses a tiered backoff: delay = 2^n seconds, repeated 2^n times:

Level Delay Attempts Cumulative
n=0 1s 1 1
n=1 2s 2 3
n=2 4s 4 7
n=3 8s 8 15
n=4 16s 16 31

After 31 attempts, the relay is marked as failed. Call connect() to restart failed relays.

Query Modes

Mode stream Returns Behavior
Blocking false Future<List<Event>> Waits for all relays, then returns
Streaming true [] immediately Events arrive via callbacks
// Blocking: waits for all relays to respond
final events = await storage.query<Note>(
  req,
  source: RemoteSource(relays: 'default', stream: false),
);

// Streaming: returns immediately, events come via notification
await storage.query<Note>(
  req,
  source: RemoteSource(relays: 'default', stream: true),
);

Timeout Handling

When relays don't respond within responseTimeout:

  • Blocking queries: Return partial results and auto-cleanup
  • Streaming queries: Continue with responding relays

Architecture

  • Background Isolate - SQLite + WebSocket pool run off the UI thread
  • Single Source of Truth - PoolState contains all subscription and relay state
  • Per-Subscription Relay Tracking - Each subscription tracks its relays independently
  • Auto-Reconnect - Tiered backoff (1s, 2s×2, 4s×4, 8s×8, 16s×16) with 31 total attempts
  • connect() / disconnect() - App lifecycle management for reconnection and clean shutdown
  • Ping Health Checks - limit:0 requests detect zombie connections (55s idle threshold)
  • Event Batching - Cross-relay deduplication with configurable flush window
  • No Resource Leaks - Timeouts always clean up blocking subscriptions

Testing

dart test

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •  

Languages