feat: steadier live updates and notices for public proposals
- The web pings the live connection, reconnects when it goes quiet and reloads what each screen shows after reconnecting. - Dropping a card on a column it cannot go to puts it back. - Owners and admins get a notice when a proposal arrives. - The proposal limit lives in Redis, so it holds across instances. - REST tells whether I voted; pasted images leave the attachment list. - Deleting a project removes its Git integration; no lint warnings left. - Reactions and the blocked badge are components of their own. - Docs: what is done and what is left (chapter 10), Word and PDF. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,33 +1,24 @@
|
||||
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
|
||||
import { RateLimitStore } from '@task-stream/api/tracker/application';
|
||||
|
||||
/** Proposals from one address within the window. */
|
||||
const LIMIT = 5;
|
||||
const WINDOW_MS = 15 * 60_000;
|
||||
|
||||
/**
|
||||
* Keeps one address from flooding a project with proposals. In memory:
|
||||
* enough for one instance, and an approximation with several (each counts
|
||||
* its own), which is fine for what it guards against.
|
||||
* Keeps one address from flooding a project with proposals. Counted in
|
||||
* Redis, so the limit holds across every instance of the API.
|
||||
*/
|
||||
@Injectable()
|
||||
export class ProposalLimiter {
|
||||
private readonly sent = new Map<string, number[]>();
|
||||
constructor(private readonly store: RateLimitStore) {}
|
||||
|
||||
take(address: string): void {
|
||||
const now = Date.now();
|
||||
const recent = (this.sent.get(address) ?? []).filter(
|
||||
(at) => now - at < WINDOW_MS,
|
||||
);
|
||||
if (recent.length >= LIMIT)
|
||||
async take(address: string): Promise<void> {
|
||||
const count = await this.store.hit(`proposals:${address}`, WINDOW_MS);
|
||||
if (count > LIMIT)
|
||||
throw new HttpException(
|
||||
'Too many proposals from here; try again in a few minutes',
|
||||
HttpStatus.TOO_MANY_REQUESTS,
|
||||
);
|
||||
recent.push(now);
|
||||
this.sent.set(address, recent);
|
||||
// Forget quiet addresses now and then, so the map does not grow forever.
|
||||
if (this.sent.size > 10_000)
|
||||
for (const [key, times] of this.sent)
|
||||
if (times.every((at) => now - at >= WINDOW_MS)) this.sent.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user