cyb/src/pages/Mining/wasmMiner.ts

export interface WasmMiningStatus {
  mining: boolean;
  total_hashes: number;
  elapsed_secs: number;
  hashrate: number;
  pending_proofs: number;
}

type WorkerMessage =
  | { type: 'ready' }
  | { type: 'progress'; totalHashes: number }
  | { type: 'proof'; hash: string; nonce: number; challenge: string; totalHashes: number };

export type FoundProof = { hash: string; nonce: number; challenge?: string };

type HashSnapshot = { time: number; hashes: number };

const ROLLING_WINDOW_MS = 30_000;

export class WasmMiner {
  private workers: Worker[] = [];
  private workerLastReported: number[] = [];
  private mining = false;
  private startTime = 0;
  private pendingProofs: FoundProof[] = [];
  private numThreads: number;
  private hashSnapshots: HashSnapshot[] = [];
  private currentChallenge = '';
  private totalHashes = 0;

  constructor(numThreads: number) {
    this.numThreads = numThreads;
  }

  async init(): Promise<void> {
    this.workers = [];
    this.workerLastReported = [];

    const readyPromises: Promise<void>[] = [];

    for (let i = 0; i < this.numThreads; i++) {
      const worker = new Worker(new URL('./miningWorker.ts', import.meta.url));
      this.workers.push(worker);
      this.workerLastReported.push(0);

      const readyPromise = new Promise<void>((resolve) => {
        const handler = (e: MessageEvent<WorkerMessage>) => {
          if (e.data.type === 'ready') {
            worker.removeEventListener('message', handler);
            resolve();
          }
        };
        worker.addEventListener('message', handler);
      });
      readyPromises.push(readyPromise);

      worker.postMessage({ type: 'init' });
    }

    await Promise.all(readyPromises);
    this.setupMessageHandlers();
  }

  private setupMessageHandlers(): void {
    this.workers.forEach((worker, index) => {
      worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
        const msg = e.data;

        if (msg.type === 'progress' || msg.type === 'proof') {
          // Incremental counting: only add positive deltas.
          // When a worker restarts (totalHashes drops to 0), delta < 0 โ†’ skip.
          // Next message after restart has a small positive delta โ†’ resumes counting.
          const delta = msg.totalHashes - this.workerLastReported[index];
          if (delta > 0) {
            this.totalHashes += delta;
          }
          this.workerLastReported[index] = msg.totalHashes;

          if (msg.type === 'proof') {
            this.pendingProofs.push({
              hash: msg.hash,
              nonce: msg.nonce,
              challenge: msg.challenge,
            });
          }
        }
      };
    });
  }

  start(challenge: string, difficulty: number): void {
    if (!this.mining) {
      // Fresh start โ€” reset everything
      this.startTime = Date.now();
      this.hashSnapshots = [];
      this.totalHashes = 0;
      this.workerLastReported = this.workers.map(() => 0);
      this.pendingProofs = [];
    }
    // Challenge change while mining: just send new start to workers.
    // workerLastReported stays โ€” delta tracking handles the restart automatically.
    this.mining = true;
    this.currentChallenge = challenge;

    this.workers.forEach((worker, i) => {
      worker.postMessage({
        type: 'start',
        threadId: i,
        numThreads: this.numThreads,
        challenge,
        difficulty,
      });
    });
  }

  stop(): void {
    this.stopWorkers();
  }

  private stopWorkers(): void {
    this.mining = false;
    this.workers.forEach((worker) => {
      worker.postMessage({ type: 'stop' });
    });
  }

  getStatus(): WasmMiningStatus {
    const now = Date.now();
    const elapsedSecs = this.mining || this.totalHashes > 0
      ? (now - this.startTime) / 1000
      : 0;

    // Update rolling window snapshots (called every ~500ms from poll)
    if (this.mining) {
      this.hashSnapshots.push({ time: now, hashes: this.totalHashes });
      const cutoff = now - ROLLING_WINDOW_MS;
      this.hashSnapshots = this.hashSnapshots.filter(
        (s) => s.time >= cutoff
      );
    }

    // 30s rolling hashrate from snapshots
    let hashrate = 0;
    if (this.hashSnapshots.length >= 2) {
      const oldest = this.hashSnapshots[0];
      const newest = this.hashSnapshots[this.hashSnapshots.length - 1];
      const dt = (newest.time - oldest.time) / 1000;
      if (dt > 0.5) {
        hashrate = (newest.hashes - oldest.hashes) / dt;
      }
    }

    // Fallback to lifetime average during warmup (<1s of data)
    if (hashrate === 0 && elapsedSecs > 0) {
      hashrate = this.totalHashes / elapsedSecs;
    }

    return {
      mining: this.mining,
      total_hashes: this.totalHashes,
      elapsed_secs: elapsedSecs,
      hashrate,
      pending_proofs: this.pendingProofs.length,
    };
  }

  takeProofs(): FoundProof[] {
    const proofs = this.pendingProofs;
    this.pendingProofs = [];
    return proofs;
  }

  destroy(): void {
    console.log('[WasmMiner] destroy: terminating', this.workers.length, 'workers');
    this.mining = false;
    this.workers.forEach((worker) => worker.terminate());
    this.workers = [];
    this.workerLastReported = [];
    this.totalHashes = 0;
    this.hashSnapshots = [];
  }
}

Neighbours