diff --git a/src/escrow/escrow.service.ts b/src/escrow/escrow.service.ts index 6d2dd289..c252993f 100644 --- a/src/escrow/escrow.service.ts +++ b/src/escrow/escrow.service.ts @@ -584,20 +584,14 @@ export class EscrowService { recipients: Array<[string, number]>, manager?: EntityManager, ): Promise { - return this.invokeOnLockedEscrow(escrow, operation, () => - this.soroban.invoke( - 'release', - // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). - [u64(this.onChainKeyFor(escrow)), recipients], - this.contractOpts(escrow), - ), return this.invokeOnLockedEscrow( escrow, operation, () => this.soroban.invoke( 'release', - [this.onChainKeyFor(escrow), recipients], + // `release(issue_id: u64, recipients)` — u64-typed on-chain (#301). + [u64(this.onChainKeyFor(escrow)), recipients], this.contractOpts(escrow), ), manager, diff --git a/src/github/github-webhooks.service.spec.ts b/src/github/github-webhooks.service.spec.ts index 45bb6fe8..a35ac3be 100644 --- a/src/github/github-webhooks.service.spec.ts +++ b/src/github/github-webhooks.service.spec.ts @@ -869,6 +869,9 @@ describe('GithubWebhooksService', () => { expect(bountiesService.markInReview).not.toHaveBeenCalled(); expect(bountiesService.markMergedAndRelease).not.toHaveBeenCalled(); + }); + }); + // #308: a redelivered X-GitHub-Delivery used to hit the unique constraint // on webhook_events.deliveryId and escape handleEvent as a 500, so every // redelivery of that event failed forever. diff --git a/src/reputation/reputation.service.ts b/src/reputation/reputation.service.ts index 0533cd2f..3dde7b38 100644 --- a/src/reputation/reputation.service.ts +++ b/src/reputation/reputation.service.ts @@ -33,7 +33,7 @@ export class ReputationService { this.paymentRepo, ); - const snapshot = this.snapshotRepo.create({ + const fields = { userId, totalEarnings: stats.lifetimeEarnings.toFixed(7), mergedPrCount: stats.mergedCount, @@ -43,8 +43,28 @@ export class ReputationService { onTimeDeliveryPercentage: stats.onTimeDeliveryPercentage.toFixed(2), languages: stats.languages, orgsContributedTo: stats.orgs, + }; + + // Skip the insert when nothing changed — an unthrottled recompute could + // otherwise append a row per call (~170k/day for one user, #584). If the + // stats match the latest snapshot, return it instead of a duplicate. + const latest = await this.snapshotRepo.findOne({ + where: { userId }, + order: { computedAt: 'DESC' }, }); - return this.snapshotRepo.save(snapshot); + if ( + latest && + latest.totalEarnings === fields.totalEarnings && + latest.mergedPrCount === fields.mergedPrCount && + latest.openBountiesClaimed === fields.openBountiesClaimed && + latest.completionRate === fields.completionRate && + latest.avgReviewTimeHours === fields.avgReviewTimeHours && + latest.onTimeDeliveryPercentage === fields.onTimeDeliveryPercentage + ) { + return latest; + } + + return this.snapshotRepo.save(this.snapshotRepo.create(fields)); } async getLatest(userId: string): Promise { diff --git a/src/sponsors/sponsors.controller.ts b/src/sponsors/sponsors.controller.ts index b7a48cca..c097bf1e 100644 --- a/src/sponsors/sponsors.controller.ts +++ b/src/sponsors/sponsors.controller.ts @@ -26,25 +26,6 @@ function validatePage(limit?: number, offset?: number) { throw new BadRequestException('offset must be >= 0'); } return { limit, offset }; -/** - * Parse optional limit/offset query params, rejecting NaN, negatives, - * non-integers, and limits above MAX_PAGE_LIMIT with a 400 (#280). - */ -function parsePagination(limit?: string, offset?: string) { - const parse = (name: string, raw: string | undefined, min: number, max: number) => { - if (raw === undefined || raw === '') return undefined; - const n = Number(raw); - if (!Number.isInteger(n) || n < min || n > max) { - throw new BadRequestException( - `${name} must be an integer between ${min} and ${max}`, - ); - } - return n; - }; - return { - limit: parse('limit', limit, 1, MAX_PAGE_LIMIT), - offset: parse('offset', offset, 0, Number.MAX_SAFE_INTEGER), - }; } @ApiTags('sponsors') @@ -73,7 +54,6 @@ export class SponsorsController { ) { this.assertOwnsSponsor(user, id); return this.sponsorsService.dashboard(id, validatePage(limit, offset)); - return this.sponsorsService.dashboard(id, parsePagination(limit, offset)); } @ApiBearerAuth() @@ -91,6 +71,5 @@ export class SponsorsController { id, validatePage(limit, offset), ); - return this.sponsorsService.milestoneProgress(id, parsePagination(limit, offset)); } }