@aiquants/daily-report 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiquants/daily-report",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Reusable daily-report feature package: shared types/schemas, React (Router v7) UI with virtual scrolling + optimistic updates + SSE sync, and a drizzle (mssql) server layer with DI ports (auth, user resolution, redis, external sources).",
5
5
  "sideEffects": false,
6
6
  "main": "dist/index.js",
@@ -75,8 +75,8 @@
75
75
  "react-dom": ">=18",
76
76
  "react-router": "^7.0.0",
77
77
  "zod": ">=3.25.0 <5.0.0",
78
- "@aiquants/virtualscroll": "^1.18.3",
79
- "@aiquants/swipe-overlay": "^1.2.3"
78
+ "@aiquants/swipe-overlay": "^1.2.3",
79
+ "@aiquants/virtualscroll": "^1.18.3"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "drizzle-orm": {
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it, vi } from "vitest"
2
+ import { createEpochStore, SqlResultCache } from "./cache"
3
+
4
+ describe("SqlResultCache Memory Cleanup", () => {
5
+ it("should clean up expired cache entries when map size exceeds 500", async () => {
6
+ // Mock epoch store
7
+ const epochStore = createEpochStore(undefined)
8
+ vi.spyOn(epochStore, "getEpoch").mockResolvedValue(0)
9
+
10
+ const cache = new SqlResultCache({ defaultTtlMs: 1000 }, epochStore)
11
+
12
+ // 1. 期限切れのキャッシュを 500 件作成 (expireAt を過去に設定)
13
+ // 直接プライベートフィールド buckets に流し込むことでモック状態を作る
14
+ const buckets = (cache as any).buckets
15
+ for (let i = 0; i < 505; i++) {
16
+ buckets.set(`expired-key-${i}`, {
17
+ records: [i],
18
+ expireAt: Date.now() - 1000, // 過去時間
19
+ epoch: 0,
20
+ })
21
+ }
22
+
23
+ // 2. 有効なキャッシュを 10 件作成 (expireAt を未来に設定)
24
+ for (let i = 0; i < 10; i++) {
25
+ buckets.set(`valid-key-${i}`, {
26
+ records: [i],
27
+ expireAt: Date.now() + 100000, // 未来時間
28
+ epoch: 0,
29
+ })
30
+ }
31
+
32
+ expect(buckets.size).toBe(515)
33
+
34
+ // 3. getOrFetch を実行して 500 件超えの閾値トリガーを引く
35
+ const fetcher = vi.fn().mockResolvedValue(["new-value"])
36
+ await cache.getOrFetch({
37
+ cacheKey: "trigger-key",
38
+ fetcher,
39
+ })
40
+
41
+ // 4. 検証: 期限切れの 505 件のデータが cleanExpired で削除され、有効な 10 件 + 新規の 1 件のみ残っていること
42
+ expect(buckets.size).toBe(11) // 10 (valid) + 1 (trigger-key)
43
+ expect(buckets.has("trigger-key")).toBe(true)
44
+ expect(buckets.has("valid-key-0")).toBe(true)
45
+ expect(buckets.has("expired-key-0")).toBe(false)
46
+ })
47
+ })
@@ -123,6 +123,10 @@ export class SqlResultCache {
123
123
  // キャッシュ保存時に現在の epoch を記録
124
124
  const epoch = opts.epochKey ? await this.epochStore.getEpoch(opts.epochKey) : 0
125
125
  this.buckets.set(cacheKey, { records, expireAt, epoch })
126
+ // 蓄積防止のため、サイズが一定値を超えたら期限切れキャッシュを一括クリーンアップ
127
+ if (this.buckets.size > 500) {
128
+ this.cleanExpired()
129
+ }
126
130
  }
127
131
  return records
128
132
  })()
@@ -162,4 +166,18 @@ export class SqlResultCache {
162
166
  }
163
167
  }
164
168
  }
169
+
170
+ /**
171
+ * Cleans up all expired cache buckets to prevent memory accumulation.
172
+ * メモリー蓄積を防ぐため、期限切れのキャッシュバケットをすべてクリーンアップする処理。
173
+ */
174
+ private cleanExpired(): void {
175
+ const now = Date.now()
176
+ // 期限切れのキーを削除
177
+ for (const [key, bucket] of this.buckets.entries()) {
178
+ if (bucket.expireAt <= now) {
179
+ this.buckets.delete(key)
180
+ }
181
+ }
182
+ }
165
183
  }