From f27b9dc8e0cac8fdb3286011e0426dfe217ddc08 Mon Sep 17 00:00:00 2001 From: Yudhi Armyndharis Date: Tue, 23 Jun 2026 16:26:44 +0700 Subject: [PATCH] fix(after-hours): LRU eviction for the per-chat cooldown map (v0.1.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cooldown map evicted by first-seen order, so a continuously-active chat could be evicted ahead of idle ones once the 5000-entry cap was hit — resetting its cooldown and letting it bypass the throttle. Re-insert on each reply so eviction tracks recency. --- README.md | 2 +- after-hours/CHANGELOG.md | 8 ++++++++ after-hours/README.md | 2 +- after-hours/index.test.ts | 10 ++++++++++ after-hours/index.ts | 5 +++-- after-hours/manifest.json | 2 +- plugins.json | 4 ++-- 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 43be3fa..d5163d6 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ This repository provides: | Plugin | Description | Version | Status | | ------ | ----------- | ------- | ------ | -| [`after-hours`](./after-hours) | Auto-replies with a configurable away/closing message to messages received outside business hours. | 0.1.1 | stable | +| [`after-hours`](./after-hours) | Auto-replies with a configurable away/closing message to messages received outside business hours. | 0.1.2 | stable | | [`chat-flow`](./chat-flow) | Interactive, stateful auto-reply: a trigger word starts a greeting + numbered menu, replies traverse a configurable menu tree, and per-chat state expires after 15 minutes. | 1.0.2 | stable | | [`faq-bot`](./faq-bot) | Auto-replies to inbound WhatsApp messages from configurable FAQ keyword/regex rules. | 0.1.1 | stable | | [`group-translate`](./group-translate) | Auto-translates group messages between participants' languages via a LibreTranslate backend. Configure in-chat with /tr commands. Admin-gated; disabled until enabled. | 1.0.2 | stable | diff --git a/after-hours/CHANGELOG.md b/after-hours/CHANGELOG.md index 0313e93..293b505 100644 --- a/after-hours/CHANGELOG.md +++ b/after-hours/CHANGELOG.md @@ -8,6 +8,14 @@ The version here always matches `manifest.json`'s `version`. ## [Unreleased] +## [0.1.2] — 2026-06-23 + +### Changed + +- The per-chat cooldown map now evicts least-recently-used entries (re-inserting a chat on each reply) + instead of first-seen order, so a continuously-active chat keeps its cooldown when the map reaches its + cap rather than being evicted and allowed to bypass the throttle. + ## [0.1.1] — 2026-06-23 ### Added diff --git a/after-hours/README.md b/after-hours/README.md index b050dcb..8041748 100644 --- a/after-hours/README.md +++ b/after-hours/README.md @@ -12,7 +12,7 @@ | Field | Value | | ----- | ----- | | **Identifier** | `after-hours` | -| **Version** | 0.1.1 | +| **Version** | 0.1.2 | | **Released** | 2026-06-23 | | **Status** | stable | | **Author** | Yudhi Armyndharis | diff --git a/after-hours/index.test.ts b/after-hours/index.test.ts index f2d97e0..f262b1a 100644 --- a/after-hours/index.test.ts +++ b/after-hours/index.test.ts @@ -44,3 +44,13 @@ test('allowReply enforces the per-chat cooldown and caps the map', () => { assert.equal(big.size, 5000); assert.equal(big.has('k-0'), false); }); + +test('allowReply eviction is recency-aware: re-touching a key protects it', () => { + const map = new Map(); + for (let i = 0; i < 5000; i++) allowReply(map, `k-${i}`, i, 0); + allowReply(map, 'k-0', 10000, 0); // re-touch -> most recently used + allowReply(map, 'k-new', 10001, 0); // overflow -> evict genuinely-oldest + assert.equal(map.size, 5000); + assert.equal(map.has('k-0'), true); // protected by recent touch + assert.equal(map.has('k-1'), false); // now the oldest, evicted +}); diff --git a/after-hours/index.ts b/after-hours/index.ts index cad45d8..b012b72 100644 --- a/after-hours/index.ts +++ b/after-hours/index.ts @@ -41,12 +41,13 @@ export function parseConfig(raw: Record): { config: AfterHoursC } /** - * Decide whether an after-hours reply may go to `key` now. On allow, records `nowMs` and caps the map - * (drop oldest). `cooldownMs` of 0 always allows. + * Decide whether an after-hours reply may go to `key` now. On allow, records `nowMs` (re-inserting so + * the map evicts least-recently-used) and caps the map by dropping the LRU entry. `cooldownMs` of 0 always allows. */ export function allowReply(map: Map, key: string, nowMs: number, cooldownMs: number): boolean { const last = map.get(key); if (last !== undefined && nowMs - last < cooldownMs) return false; + map.delete(key); // re-insert so iteration order tracks recency (LRU by touch) map.set(key, nowMs); if (map.size > MAX_COOLDOWN_ENTRIES) { const oldest = map.keys().next().value as string | undefined; diff --git a/after-hours/manifest.json b/after-hours/manifest.json index 43123e0..4a17dab 100644 --- a/after-hours/manifest.json +++ b/after-hours/manifest.json @@ -1,7 +1,7 @@ { "id": "after-hours", "name": "After-Hours Auto-Reply", - "version": "0.1.1", + "version": "0.1.2", "type": "extension", "main": "dist/index.js", "description": "Auto-replies with a configurable away/closing message to messages received outside business hours.", diff --git a/plugins.json b/plugins.json index 7ab18fd..652702a 100644 --- a/plugins.json +++ b/plugins.json @@ -2,7 +2,7 @@ { "id": "after-hours", "name": "After-Hours Auto-Reply", - "version": "0.1.1", + "version": "0.1.2", "type": "extension", "status": "stable", "description": "Auto-replies with a configurable away/closing message to messages received outside business hours.", @@ -22,7 +22,7 @@ "repoPath": "after-hours", "repoUrl": "https://github.com/rmyndharis/OpenWA-plugins", "homepage": "https://github.com/rmyndharis/OpenWA-plugins/tree/main/after-hours", - "download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/after-hours-v0.1.1/after-hours.zip", + "download": "https://github.com/rmyndharis/OpenWA-plugins/releases/download/after-hours-v0.1.2/after-hours.zip", "i18n": { "es": { "name": "Respuesta Automática Fuera de Horario",