Skip to content

Latest commit

 

History

History

README.md

@notemine/wrapper

npm License: MIT build docs

A TypeScript wrapper for @notemine/core that provides a high-level, user-friendly API for mining Nostr notes with proof-of-work.

Overview

@notemine/wrapper simplifies the process of mining Nostr notes by:

  • Managing Web Workers automatically for parallel mining
  • Providing RxJS observables for real-time progress tracking
  • Bundling WASM as inline base64 within inline workers for hassle-free deployment
  • Offering a clean, Promise-based API with TypeScript support
  • Tracking mining statistics and performance metrics

Features

  • Automatic Worker Management: Spawns and manages multiple Web Workers based on available CPU cores
  • Real-time Progress Tracking: RxJS observables for monitoring hash rate, best PoW, and mining progress
  • Pause & Resume: Pause mining operations and resume from the exact same state
  • State Persistence: Save and restore mining state across page refreshes or sessions
  • Dynamic Worker Scaling: Resume mining with a different number of workers
  • Zero Configuration: Works out of the box with modern bundlers (Vite, Webpack, Rollup, etc.)
  • TypeScript Support: Fully typed API with comprehensive interfaces
  • Cancellable Operations: Stop mining at any time with proper cleanup
  • Performance Metrics: Track hash rates, total hashes, and mining efficiency
  • Framework Agnostic: Works with React, Vue, Svelte, Angular, or vanilla JavaScript

Installation

npm install @notemine/wrapper @notemine/core rxjs
# or
pnpm install @notemine/wrapper @notemine/core rxjs
# or  
yarn add @notemine/wrapper @notemine/core rxjs

Peer Dependencies

  • @notemine/core: The WASM core mining module
  • rxjs: For reactive programming patterns

Basic Usage

import { Notemine } from '@notemine/wrapper';

// Create a new miner instance
const notemine = new Notemine({
  content: 'Hello, Nostr!',
  pubkey: 'your-public-key-here',
  difficulty: 21,
  numberOfWorkers: navigator.hardwareConcurrency || 4,
  tags: [['t', 'intro']]
});

// Subscribe to progress updates
const progressSub = notemine.progress$.subscribe(progress => {
  console.log(`Worker ${progress.workerId}: ${progress.hashRate} H/s`);
});

// Subscribe to success event
const successSub = notemine.success$.subscribe(({ result }) => {
  console.log('Mining completed!', result);
  console.log('Event ID:', result.event.id);
});

// Start mining
await notemine.mine();

// Cancel mining if needed
// notemine.cancel();

// Clean up subscriptions
progressSub.unsubscribe();
successSub.unsubscribe();

API Documentation

Constructor Options

interface MinerOptions {
  content?: string;          // The content to include in the mined event
  tags?: string[][];        // Tags for the event
  pubkey?: string;          // Public key for the event
  difficulty?: number;      // Target difficulty (default: 20)
  numberOfWorkers?: number; // Number of workers (default: CPU cores)
  kind?: number;            // Event kind (default: 1)
  debug?: boolean;          // Enable debug logging (default: false)
}

Properties

  • content: Get/set the event content
  • tags: Get/set the event tags
  • pubkey: Get/set the public key
  • difficulty: Get/set the mining difficulty
  • numberOfWorkers: Get/set the number of workers
  • totalHashRate: Get the combined hash rate of all workers

Methods

mine(): Promise<void>

Starts the mining process. Throws if pubkey or content is not set.

pause(): void

Pauses the mining process while preserving the current state (nonces, best PoW, etc.). Workers are terminated but state is maintained for resumption.

resume(workerNonces?: string[]): Promise<void>

Resumes mining from a paused state. Optionally accepts an array of worker nonces to resume from. If not provided, uses the tracked state from the last pause.

getState(): MiningState

Returns the current mining state as a serializable object. This can be saved to localStorage, IndexedDB, or any other storage mechanism for later restoration.

restoreState(state: MiningState): void

Restores the miner to a previously saved state. Must be called before resume(). Can be used to resume mining after a page refresh or across sessions.

cancel(): void

Stops the mining process and terminates all workers. State is not preserved.

stop(): void

Alias for cancel().

Observables

// Mining state
mining$: BehaviorSubject<boolean>
cancelled$: BehaviorSubject<boolean>
paused$: BehaviorSubject<boolean>

// Results
result$: BehaviorSubject<MinedResult | null>
success$: Observable<SuccessEvent>

// Progress tracking
progress$: Observable<ProgressEvent>
workersPow$: BehaviorSubject<Record<number, BestPowData>>
highestPow$: BehaviorSubject<WorkerPow | null>

// Errors
error$: Observable<ErrorEvent>
cancelledEvent$: Observable<CancelledEvent>

// Worker management
workers$: BehaviorSubject<Worker[]>

Type Definitions

interface ProgressEvent {
  workerId: number;
  hashRate?: number;
  bestPowData?: BestPowData;
}

interface BestPowData {
  bestPow: number;
  nonce: string;
  hash: string;
}

interface MinedResult {
  event: any;           // The mined Nostr event
  totalTime: number;    // Total mining time in milliseconds
  hashRate: number;     // Average hash rate achieved
}

interface SuccessEvent {
  result: MinedResult | null;
}

interface ErrorEvent {
  error: any;
  message?: string;
}

interface MiningState {
  event: {
    pubkey: string;
    kind: number;
    tags: string[][];
    content: string;
    created_at: number;
  };
  workerNonces: string[];      // Array of current nonces for each worker
  bestPow: BestPowData | null; // Best proof-of-work found so far
  difficulty: number;          // Target difficulty
  numberOfWorkers: number;     // Number of workers when state was saved
}

Advanced Features

Debug Mode

Enable detailed console logging for development and troubleshooting:

const notemine = new Notemine({
  content: 'Hello, Nostr!',
  pubkey: 'your-public-key-here',
  difficulty: 21,
  debug: true  // Enable debug logging
});

Debug output includes:

  • Session Management: RunId generation for each mining session
  • Worker Progress: Per-worker nonce updates (rate-limited to every 2s)
  • Hash Rate: Total hash rate with delta changes (every 1s)
  • Ghost Updates: Detection and blocking of stale worker messages
  • State Persistence: Nonce arrays being saved/restored

Example console output:

[Notemine] Starting new mining session, runId: f3008079-ef2e-44c7-a282-24e851ccfe0c
[Notemine] Worker 0 currentNonce: 123456
[Notemine] totalHashRate: 6000.50 KH/s (Δ 150.25)
[Notemine] 🚫 GHOST UPDATE BLOCKED - Ignoring message from old session

Protocol v2 - Worker Message Format

The wrapper uses Protocol v2 for communication with workers, which includes:

Key Features:

  • RunId Gating: Each mining session has a unique UUID to prevent ghost updates
  • Current Nonce Tracking: Workers report their current nonce for accurate resume
  • Backward Compatibility: Protocol v1 messages (without runId) still work

Message Structure:

{
  type: 'progress',
  workerId: 0,
  runId: 'uuid-v4-string',     // Session identifier
  currentNonce: '123456',      // Current position for resume
  bestPowData: {
    bestPow: 21,
    nonce: '123456',
    hash: '000000abc...'
  },
  hashRate: 5000
}

Custom Worker Implementation:

// In your custom mine.worker.ts
let sessionRunId: string;

self.onmessage = (e) => {
  const { event, runId, workerId } = e.data;
  sessionRunId = runId;

  // Your mining loop
  while (mining) {
    // ... mining logic ...

    // Send progress with Protocol v2 format
    self.postMessage({
      type: 'progress',
      workerId,
      runId: sessionRunId,           // Include runId
      currentNonce: nonce.toString(), // Include current nonce
      hashRate: calculatedHashRate,
      bestPowData: bestPow ? { bestPow, nonce, hash } : undefined
    });
  }
};

Guarded Persistence

The wrapper implements "guarded persistence" to avoid storing useless default state:

// Only real progress is persisted
const state = notemine.getState();

// If mining just started, workerNonces is empty
// state.workerNonces = []  (defaults like ["0", "1", "2"] are filtered out)

// After real progress
// state.workerNonces = ["123456", "789012"]  (actual progress is saved)

This prevents cluttering localStorage with meaningless initial state.

State Persistence Throttling

State updates are automatically throttled to ~500ms to reduce I/O overhead:

// Updates are batched and throttled
const notemine = new Notemine({
  /* ... */
  onMiningStateUpdate: (state) => {
    // This callback is called at most every 500ms
    // Instead of 4-8 times per second
    localStorage.setItem('mining-state', JSON.stringify(state));
  }
});

Performance Optimizations

The wrapper includes several performance improvements:

WASM SIMD: 15-35% hash rate improvement

Baseline: 5.2-5.5 MH/s
With SIMD: 6.0 MH/s sustained, 7.0 MH/s burst

Adaptive Progress Reporting: Automatically adjusts reporting frequency to maintain ~250ms cadence

Efficient Cancel: Workers respond to cancel requests within 100ms typical

Lifecycle Semantics

Enhanced pause/resume/cancel behavior:

Grace Period: Workers get 200ms to respond to cancel before termination

Idempotent Operations: Safe to call pause/resume/cancel multiple times

Progress Gating: Progress messages are ignored after mining stops (prevents race conditions)

// All of these are safe
notemine.pause();
notemine.pause();  // No-op, already paused

notemine.resume();
notemine.resume(); // No-op, already mining

Framework Examples

svelte
<script lang="ts">
  import { onMount } from 'svelte';
  import { type Writable, writable } from 'svelte/store';
  import { type ProgressEvent, Notemine } from '@notemine/wrapper';

  const numberOfMiners = 8
  let notemine: Notemine;
  let progress: Writable<ProgressEvent[]> = new writable(new Array(numberOfMiners))
  let success: Writeable<SuccessEvent> = new writable(null)

  onMount(() => {
    notemine = new Notemine({ content: 'Hello, Nostr!', numberOfMiners  });

    const progress$ = miner.progress$.subscribe(progress_ => {
      progress.update( _progress => {
        _progress[progress_.workerId] = progress_
        return _progress
      })
    });

    const success$ = miner.progress$.subscribe(success_ => {
      const {event, totalTime, hashRate}
      success.update( _success => {
        _success = success_
        return _success
      })
      miner.cancel();
    });

    notemine.mine();

    return () => {
      progress$.unsubscribe();
      success$.unsubscribe();
      miner.cancel();
    };
  });
  $: miners = $progress
</script>


<div>
{#each $miners as miner}
<span>Miner #{miner.workerId}: {miner.hashRate}kH/s [Best PoW: ${miner.bestPowData}]
{/each}

{#if($success !== null)}
  <pre>
  {$success.event}
  </pre>
{/if}

</div>
react
  import React, { useEffect } from 'react';
  import { Notemine } from '@notemine/wrapper';

  const MyComponent = () => {
    const notemine = new Notemine({ content: 'Hello, Nostr!' });

    useEffect(() => {
      const subscription = notemine.progress$.subscribe(progress => {
        // Update progress bar or display notemine's progress
      });

      notemine.mine();

      return () => {
        subscription.unsubscribe();
        notemine.cancel();
      };
    }, []);

    return (
      <div>
        {/* Your UI components */}
      </div>
    );
  };

vue
<template>
  <div>
    <!-- Your UI components -->
  </div>
</template>

<script lang="ts">
import { defineComponent, onMounted, onUnmounted } from 'vue';
import { Notemine } from '@notemine/wrapper';

export default defineComponent({
  name: 'MinerComponent',
  setup() {
    const notemine = new Notemine({ content: 'Hello, Nostr!' });

    onMounted(() => {
      const subscription = notemine.progress$.subscribe(progress => {
        // Update progress bar or display notemine's progress
      });

      notemine.mine();

      onUnmounted(() => {
        subscription.unsubscribe();
        notemine.cancel();
      });
    });

    return {};
  },
});
</script>
angular
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Notemine } from '@notemine/wrapper';
import { Subscription } from 'rxjs';

@Component({
  selector: 'app-notemine',
  templateUrl: './notemine.component.html',
})
export class MinerComponent implements OnInit, OnDestroy {
  notemine: Notemine;
  progressSubscription: Subscription;

  ngOnInit() {
    this.notemine = new Notemine({ content: 'Hello, Nostr!' });
    this.progressSubscription = this.notemine.progress$.subscribe(progress => {
      // Update progress bar or display notemine's progress
    });

    this.notemine.mine();
  }

  ngOnDestroy() {
    this.progressSubscription.unsubscribe();
    this.notemine.cancel();
  }
}

Advanced Usage

Pause and Resume Mining

import { Notemine } from '@notemine/wrapper';

const miner = new Notemine({
  content: 'Mining with pause/resume',
  pubkey: 'your-pubkey-here',
  difficulty: 21,
  numberOfWorkers: 4
});

// Start mining
await miner.mine();

// Pause after some time
setTimeout(() => {
  miner.pause();
  console.log('Mining paused');
}, 10000);

// Resume later
setTimeout(async () => {
  await miner.resume();
  console.log('Mining resumed');
}, 20000);

Persist State Across Page Refreshes

import { Notemine } from '@notemine/wrapper';

// Before page refresh - save state
const miner = new Notemine({
  content: 'Persistent mining',
  pubkey: 'your-pubkey-here',
  difficulty: 21
});

await miner.mine();

// User navigates away or refreshes
window.addEventListener('beforeunload', () => {
  miner.pause();
  const state = miner.getState();
  localStorage.setItem('mining_state', JSON.stringify(state));
});

// After page reload - restore state
const savedState = localStorage.getItem('mining_state');
if (savedState) {
  const state = JSON.parse(savedState);

  const miner = new Notemine({
    numberOfWorkers: state.numberOfWorkers
  });

  miner.restoreState(state);
  await miner.resume();

  console.log('Mining resumed from saved state!');
}

Resume with Different Worker Count

import { Notemine } from '@notemine/wrapper';

// Start mining with 4 workers
const miner = new Notemine({
  content: 'Scalable mining',
  pubkey: 'your-pubkey-here',
  difficulty: 21,
  numberOfWorkers: 4
});

await miner.mine();

// Pause and get state
miner.pause();
const state = miner.getState();

// Resume with 8 workers - nonces are automatically redistributed
const miner2 = new Notemine({
  numberOfWorkers: 8  // Different worker count!
});

miner2.restoreState(state);
await miner2.resume();

console.log('Mining resumed with 8 workers instead of 4!');

Track Pause/Resume State

import { Notemine } from '@notemine/wrapper';

const miner = new Notemine({ content: 'State tracking' });

// Subscribe to pause state
miner.paused$.subscribe(isPaused => {
  console.log(`Mining is ${isPaused ? 'paused' : 'active'}`);

  if (isPaused) {
    // Show resume button in UI
    // Display saved state info
  }
});

// Subscribe to mining state
miner.mining$.subscribe(isMining => {
  console.log(`Mining is ${isMining ? 'running' : 'stopped'}`);
});

Monitoring Mining Progress

import { Notemine } from '@notemine/wrapper';
import { combineLatest } from 'rxjs';

const miner = new Notemine({ content: 'Mining demo', difficulty: 25 });

// Combine multiple observables for comprehensive monitoring
combineLatest([
  miner.mining$,
  miner.highestPow$,
  miner.progress$
]).subscribe(([isMining, highestPow, progress]) => {
  if (isMining) {
    console.log('Mining in progress...');
    console.log(`Best PoW so far: ${highestPow?.bestPow || 0}`);
    console.log(`Worker ${progress.workerId} hash rate: ${progress.hashRate} H/s`);
  }
});

Custom Worker Configuration

// Use half of available CPU cores
const miner = new Notemine({
  content: 'Optimized mining',
  numberOfWorkers: Math.floor(navigator.hardwareConcurrency / 2)
});

// Monitor individual worker performance
miner.workersPow$.subscribe(workersPow => {
  Object.entries(workersPow).forEach(([workerId, powData]) => {
    console.log(`Worker ${workerId}: Best PoW = ${powData.bestPow}`);
  });
});

Error Handling

const miner = new Notemine({ content: 'Error handling demo' });

miner.error$.subscribe(error => {
  console.error('Mining error:', error);
  // Implement retry logic or user notification
});

try {
  await miner.mine();
} catch (error) {
  console.error('Failed to start mining:', error);
}

Building from Source

Prerequisites

  • Node.js 16+
  • pnpm (recommended) or npm
  • For WASM rebuilding: Rust toolchain and wasm-pack

Build Steps

# Clone the repository
git clone https://github.com/sandwichfarm/notemine.git
cd notemine/packages/wrapper

# Install dependencies
pnpm install

# Build the package
pnpm run build

# Run tests
pnpm test

Development Scripts

  • pnpm run clean - Remove build artifacts
  • pnpm run build - Build the package with TypeScript declarations
  • pnpm run build:types - Generate TypeScript declarations only
  • pnpm run dev - Run development build
  • pnpm test - Run test suite

Performance Tips

  1. Worker Count: More workers don't always mean better performance. Test different configurations for your use case.
  2. Difficulty: Higher difficulty exponentially increases mining time. Start with lower values for testing.
  3. Browser Considerations: Performance varies across browsers. Chrome and Firefox typically offer the best WASM performance.
  4. Memory Usage: Each worker maintains its own WASM instance. Monitor memory usage with many workers.

Troubleshooting

Common Issues

  1. "Public key is not set" error

    • Ensure you set the pubkey property before calling mine()
  2. Workers not starting

    • Check browser console for CSP (Content Security Policy) errors
    • Ensure your bundler properly handles Web Workers
  3. Low hash rates

    • Verify WASM is loading correctly
    • Check if browser throttling is active (background tabs)
    • Consider reducing the number of workers

Debug Mode

Enable detailed logging by checking the browser console for worker messages:

const miner = new Notemine({ content: 'Debug mode' });

// Monitor all worker messages
miner.workers$.subscribe(workers => {
  console.log(`Active workers: ${workers.length}`);
});

Related Packages

Demos

Contributing

Contributions are welcome! Please read our contributing guidelines and submit PRs to the main repository.

License

MIT License

See LICENSE for details.