A TypeScript wrapper for @notemine/core that provides a high-level, user-friendly API for mining Nostr notes with proof-of-work.
@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
- 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
npm install @notemine/wrapper @notemine/core rxjs
# or
pnpm install @notemine/wrapper @notemine/core rxjs
# or
yarn add @notemine/wrapper @notemine/core rxjs@notemine/core: The WASM core mining modulerxjs: For reactive programming patterns
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();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)
}content: Get/set the event contenttags: Get/set the event tagspubkey: Get/set the public keydifficulty: Get/set the mining difficultynumberOfWorkers: Get/set the number of workerstotalHashRate: Get the combined hash rate of all workers
Starts the mining process. Throws if pubkey or content is not set.
Pauses the mining process while preserving the current state (nonces, best PoW, etc.). Workers are terminated but state is maintained for resumption.
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.
Returns the current mining state as a serializable object. This can be saved to localStorage, IndexedDB, or any other storage mechanism for later restoration.
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.
Stops the mining process and terminates all workers. State is not preserved.
Alias for cancel().
// 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[]>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
}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
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
});
}
};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 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));
}
});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
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 miningsvelte
<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();
}
}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);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!');
}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!');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'}`);
});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`);
}
});// 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}`);
});
});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);
}- Node.js 16+
- pnpm (recommended) or npm
- For WASM rebuilding: Rust toolchain and wasm-pack
# 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 testpnpm run clean- Remove build artifactspnpm run build- Build the package with TypeScript declarationspnpm run build:types- Generate TypeScript declarations onlypnpm run dev- Run development buildpnpm test- Run test suite
- Worker Count: More workers don't always mean better performance. Test different configurations for your use case.
- Difficulty: Higher difficulty exponentially increases mining time. Start with lower values for testing.
- Browser Considerations: Performance varies across browsers. Chrome and Firefox typically offer the best WASM performance.
- Memory Usage: Each worker maintains its own WASM instance. Monitor memory usage with many workers.
-
"Public key is not set" error
- Ensure you set the
pubkeyproperty before callingmine()
- Ensure you set the
-
Workers not starting
- Check browser console for CSP (Content Security Policy) errors
- Ensure your bundler properly handles Web Workers
-
Low hash rates
- Verify WASM is loading correctly
- Check if browser throttling is active (background tabs)
- Consider reducing the number of workers
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}`);
});@notemine/core- Low-level WASM bindings@notemine/svelte- Svelte integration (planned)@notemine/reactjs- React integration (planned)
- Vanilla JS Demo - Basic implementation
- Svelte Demo - Svelte + Vite example
Contributions are welcome! Please read our contributing guidelines and submit PRs to the main repository.
MIT License
See LICENSE for details.