From 12f3983c8f89b2642cda902efaeea1e0fc1ce955 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 21 Jun 2026 17:30:06 -0400 Subject: [PATCH] Filter stale address workers --- src/ORM/client/client.service.ts | 28 ++++++++----- .../client/client.controller.spec.ts | 40 ++++++++++++++++++- src/controllers/client/client.controller.ts | 17 ++++++++ 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/src/ORM/client/client.service.ts b/src/ORM/client/client.service.ts index d3ef562..d3569d1 100644 --- a/src/ORM/client/client.service.ts +++ b/src/ORM/client/client.service.ts @@ -4,10 +4,11 @@ import { In, Repository } from 'typeorm'; import { ClientEntity } from './client.entity'; - +const DEFAULT_CLIENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000; @Injectable() export class ClientService { + private readonly activeWindowMs = this.readPositiveInt('CLIENT_REPORT_ACTIVE_WINDOW_MS', DEFAULT_CLIENT_ACTIVE_WINDOW_MS); constructor( @InjectRepository(ClientEntity) @@ -84,14 +85,15 @@ export class ClientService { } public async getByAddress(address: string): Promise { - return await this.clientRepository.find({ - where: { - address - }, - order: { - updatedAt: 'DESC', - }, - }) + const activeSince = new Date(Date.now() - this.activeWindowMs); + return await this.clientRepository + .createQueryBuilder('client') + .where('client.address = :address', { address }) + .andWhere('client.deletedAt IS NULL') + .andWhere('client.updatedAt > :activeSince', { activeSince }) + .andWhere('client.hashRate > 0') + .orderBy('client.updatedAt', 'DESC') + .getMany(); } @@ -135,4 +137,12 @@ export class ClientService { // return result; // } + private readPositiveInt(name: string, defaultValue: number): number { + const value = Number(process.env[name]); + if (Number.isInteger(value) && value > 0) { + return value; + } + return defaultValue; + } + } diff --git a/src/controllers/client/client.controller.spec.ts b/src/controllers/client/client.controller.spec.ts index 4cc1b2f..27b3c62 100644 --- a/src/controllers/client/client.controller.spec.ts +++ b/src/controllers/client/client.controller.spec.ts @@ -145,6 +145,8 @@ describe('ClientController', () => { }); it('should expose active database workers for an address', async () => { + const recent = new Date(Date.now() - 60 * 1000).toISOString(); + const stale = new Date(Date.now() - 60 * 60 * 1000).toISOString(); clientService.getByAddress.mockResolvedValue([ { id: 'active-client', @@ -155,7 +157,7 @@ describe('ClientController', () => { bestDifficulty: 64, hashRate: 1024, startTime: '2026-06-08T12:00:00.000Z', - updatedAt: '2026-06-08T12:10:00.000Z', + updatedAt: recent, }, { id: 'solo-client', @@ -166,7 +168,41 @@ describe('ClientController', () => { bestDifficulty: 128, hashRate: 2048, startTime: '2026-06-08T12:00:00.000Z', - updatedAt: '2026-06-08T12:10:00.000Z', + updatedAt: recent, + }, + { + id: 'stale-client', + address: 'bc1qtest', + sessionId: 'stale1', + clientName: 'stale-worker', + payoutMode: 'pplns', + bestDifficulty: 256, + hashRate: 4096, + startTime: '2026-06-08T12:00:00.000Z', + updatedAt: stale, + }, + { + id: 'idle-client', + address: 'bc1qtest', + sessionId: 'idle1', + clientName: 'idle-worker', + payoutMode: 'pplns', + bestDifficulty: 512, + hashRate: 0, + startTime: '2026-06-08T12:00:00.000Z', + updatedAt: recent, + }, + { + id: 'deleted-client', + address: 'bc1qtest', + sessionId: 'deleted1', + clientName: 'deleted-worker', + payoutMode: 'pplns', + bestDifficulty: 1024, + hashRate: 8192, + startTime: '2026-06-08T12:00:00.000Z', + updatedAt: recent, + deletedAt: recent, }, ]); addressSettingsService.getSettings.mockResolvedValue(null); diff --git a/src/controllers/client/client.controller.ts b/src/controllers/client/client.controller.ts index 5444484..dfeceda 100644 --- a/src/controllers/client/client.controller.ts +++ b/src/controllers/client/client.controller.ts @@ -7,9 +7,11 @@ import { PayoutSnapshotService } from '../../ORM/payout-snapshot/payout-snapshot import { ShareAccountingService } from '../../ORM/share-accounting/share-accounting.service'; import { normalizePayoutMode, PayoutMode } from '../../types/payout-mode'; +const DEFAULT_CLIENT_ACTIVE_WINDOW_MS = 30 * 60 * 1000; @Controller('client') export class ClientController { + private readonly activeWindowMs = this.readPositiveInt('CLIENT_REPORT_ACTIVE_WINDOW_MS', DEFAULT_CLIENT_ACTIVE_WINDOW_MS); constructor( private readonly clientService: ClientService, @@ -178,8 +180,15 @@ export class ClientController { private async getActiveAddressWorkers(address: string, payoutMode?: PayoutMode) { const workers = await this.clientService.getByAddress(address); + const activeSince = Date.now() - this.activeWindowMs; return workers .filter(worker => payoutMode == null || worker.payoutMode === payoutMode) + .filter(worker => worker.deletedAt == null) + .filter(worker => Number(worker.hashRate ?? 0) > 0) + .filter(worker => { + const updatedAt = worker.updatedAt == null ? 0 : new Date(worker.updatedAt).getTime(); + return Number.isFinite(updatedAt) && updatedAt > activeSince; + }) .map(worker => ({ clientId: worker.id, address: worker.address, @@ -193,4 +202,12 @@ export class ClientController { bestDifficulty: Number(worker.bestDifficulty ?? 0), })); } + + private readPositiveInt(name: string, defaultValue: number): number { + const value = Number(process.env[name]); + if (Number.isInteger(value) && value > 0) { + return value; + } + return defaultValue; + } }